diff --git a/app/javascript/application.js b/app/javascript/application.js index a08c858a4..5db6cce6e 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -228,6 +228,7 @@ safeRegisterElement('template-builder', class extends HTMLElement { template, customFields: reactive(JSON.parse(this.dataset.customFields || '[]')), dateFormats: JSON.parse(this.dataset.dateFormats || '[]'), + autosave: this.dataset.autosave !== 'false', dynamicDocuments: reactive(JSON.parse(this.dataset.dynamicDocuments || '[]')), backgroundColor: '#faf7f5', locale: this.dataset.locale, diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index f8c2580bc..8d118b00c 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -640,6 +640,7 @@ :draw-field-type="drawFieldType" :draw-custom-field="drawCustomField" :draw-option="drawOption" + :autosave="autosave" @update="onDynamicDocumentUpdate" @draw="clearDrawField" @add-custom-field="addCustomField" @@ -1379,6 +1380,7 @@ export default { isPreviewLoading: false, isLoadingBlankPage: false, isSaving: false, + hasUnsavedChanges: false, isDetectingPageFields: false, detectFieldsQueue: [], detectingAnalyzingProgress: null, @@ -1662,6 +1664,11 @@ export default { window.addEventListener('dragleave', this.onWindowDragLeave) + if (!this.autosave) { + window.addEventListener('beforeunload', this.onBeforeUnload) + document.addEventListener('turbo:before-visit', this.onBeforeVisit) + } + this.$nextTick(() => { if (document.location.search?.includes('stripe_connect_success')) { document.querySelector('form[action="/auth/stripe_connect"]')?.closest('.dropdown')?.querySelector('label')?.focus() @@ -1685,6 +1692,11 @@ export default { this.resizeObserver.disconnect() window.removeEventListener('dragleave', this.onWindowDragLeave) + + if (!this.autosave) { + window.removeEventListener('beforeunload', this.onBeforeUnload) + document.removeEventListener('turbo:before-visit', this.onBeforeVisit) + } }, beforeUpdate () { this.documentRefs = [] @@ -2241,6 +2253,20 @@ export default { this.insertField(field) } }, + onBeforeUnload (event) { + if (this.hasUnsavedChanges) { + event.preventDefault() + + event.returnValue = '' + + return '' + } + }, + onBeforeVisit (event) { + if (this.hasUnsavedChanges && !window.confirm(this.t('close_without_saving_changes'))) { + event.preventDefault() + } + }, closeDropdown () { document.activeElement.blur() }, @@ -4036,6 +4062,8 @@ export default { } if (!this.autosave && !force) { + this.hasUnsavedChanges = true + return Promise.resolve({}) } @@ -4058,6 +4086,8 @@ export default { }), headers: { 'Content-Type': 'application/json' } }).then(() => { + this.hasUnsavedChanges = false + if (this.onSave) { this.onSave(this.template) } diff --git a/app/javascript/template_builder/dynamic_document.vue b/app/javascript/template_builder/dynamic_document.vue index 3391e8223..eabebe8be 100644 --- a/app/javascript/template_builder/dynamic_document.vue +++ b/app/javascript/template_builder/dynamic_document.vue @@ -84,6 +84,11 @@ export default { required: false, default: true }, + autosave: { + type: Boolean, + required: false, + default: true + }, selectedSubmitter: { type: Object, required: false, @@ -279,7 +284,7 @@ export default { this.updatedSectionEditors.set(section.id, editor) - this.saveTimer = setTimeout(() => this.updateSectionsAndSave(), 1000) + this.saveTimer = setTimeout(() => this.autosave ? this.updateSectionsAndSave() : this.updateSections(), 1000) }, loadListsStyle () { this.listsStyle = this.bodyDom.querySelector('body > style[data-lists]')?.textContent || null @@ -299,7 +304,9 @@ export default { updateVariablesSchema () { this.document.variables_schema = buildVariablesSchema(this.bodyDom.body) }, - updateSectionsAndSave () { + updateSections () { + delete this.saveTimer + this.updatedSectionEditors.forEach((editor, sectionId) => { const target = this.bodyDom.getElementById(sectionId) @@ -315,6 +322,9 @@ export default { this.updateVariablesSchema() this.$emit('update', this.document) + }, + updateSectionsAndSave () { + this.updateSections() return this.saveBody() }, diff --git a/app/javascript/template_builder/i18n.js b/app/javascript/template_builder/i18n.js index a377ba2f3..6a0466340 100644 --- a/app/javascript/template_builder/i18n.js +++ b/app/javascript/template_builder/i18n.js @@ -70,6 +70,7 @@ const en = { remove_condition: 'Remove condition', add_condition: 'Add condition', are_you_sure_: 'Are you sure?', + close_without_saving_changes: 'Close without saving changes?', sign_yourself: 'Sign Yourself', set_signing_date: 'Set signing date', signing_date: 'Signing Date', @@ -371,6 +372,7 @@ const es = { down: 'Abajo', set_signing_date: 'Establecer fecha de firma', are_you_sure_: '¿Estás seguro?', + close_without_saving_changes: '¿Cerrar sin guardar los cambios?', sign_yourself: 'Firma tú mismo', checked: 'Marcado', current_date: 'Fecha actual', @@ -652,6 +654,7 @@ const it = { remove_condition: 'Rimuovi condizione', add_condition: 'Aggiungi condizione', are_you_sure_: 'Sei sicuro?', + close_without_saving_changes: 'Chiudere senza salvare le modifiche?', sign_yourself: 'Firma te stesso', set_signing_date: 'Imposta data di firma', signing_date: 'Data di firma', @@ -953,6 +956,7 @@ const pt = { down: 'Para baixo', set_signing_date: 'Definir data de assinatura', are_you_sure_: 'Tem certeza?', + close_without_saving_changes: 'Fechar sem salvar as alterações?', sign_yourself: 'Assine você mesmo', checked: 'Marcado', current_date: 'Data atual', @@ -1234,6 +1238,7 @@ const fr = { remove_condition: 'Supprimer la condition', add_condition: 'Ajouter une condition', are_you_sure_: 'Êtes-vous sûr ?', + close_without_saving_changes: 'Fermer sans enregistrer les modifications ?', sign_yourself: 'Signer vous-même', set_signing_date: 'Définir la date de signature', signing_date: 'Date de signature', @@ -1525,6 +1530,7 @@ const de = { remove_condition: 'Bedingung entfernen', add_condition: 'Bedingung hinzufügen', are_you_sure_: 'Sind Sie sicher?', + close_without_saving_changes: 'Ohne Speichern der Änderungen schließen?', sign_yourself: 'Selbst unterschreiben', set_signing_date: 'Unterzeichnungsdatum festlegen', signing_date: 'Unterzeichnungsdatum', @@ -1816,6 +1822,7 @@ const nl = { remove_condition: 'Voorwaarde verwijderen', add_condition: 'Voorwaarde toevoegen', are_you_sure_: 'Weet u het zeker?', + close_without_saving_changes: 'Sluiten zonder wijzigingen op te slaan?', sign_yourself: 'Zelf ondertekenen', set_signing_date: 'Ondertekeningsdatum instellen', signing_date: 'Ondertekeningsdatum', diff --git a/app/models/account_config.rb b/app/models/account_config.rb index fa6d89b7a..fbc092646 100644 --- a/app/models/account_config.rb +++ b/app/models/account_config.rb @@ -61,6 +61,7 @@ class AccountConfig < ApplicationRecord DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format' TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields' TEMPLATE_DATE_FORMATS_KEY = 'template_date_formats' + TEMPLATE_BUILDER_AUTOSAVE_KEY = 'template_builder_autosave' POLICY_LINKS_KEY = 'policy_links' ENABLE_MCP_KEY = 'enable_mcp' diff --git a/app/views/templates/edit.html.erb b/app/views/templates/edit.html.erb index 65e5739e0..ba269b276 100644 --- a/app/views/templates/edit.html.erb +++ b/app/views/templates/edit.html.erb @@ -7,7 +7,8 @@ <%= button_to nil, user_configs_path, method: :post, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'hidden', id: 'start_tour_button' %> <% end %> <% end %> -<% account_configs = current_account.account_configs.where(key: [AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY, AccountConfig::TEMPLATE_DATE_FORMATS_KEY]).to_a %> +<% account_configs = current_account.account_configs.where(key: [AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY, AccountConfig::TEMPLATE_DATE_FORMATS_KEY, AccountConfig::TEMPLATE_BUILDER_AUTOSAVE_KEY]).to_a %> <% custom_fields = account_configs.find { |e| e.key == AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY }&.value || [] %> <% date_formats = account_configs.find { |e| e.key == AccountConfig::TEMPLATE_DATE_FORMATS_KEY }&.value || [] %> - +<% autosave = account_configs.find { |e| e.key == AccountConfig::TEMPLATE_BUILDER_AUTOSAVE_KEY }&.value != false %> +