From 753d90dc64ceff698f6dc68921356e22cbd04f46 Mon Sep 17 00:00:00 2001 From: Denis Lugowski Date: Mon, 19 Jan 2026 17:28:22 +0100 Subject: [PATCH] feat(#37): Add prompt when leaving with unsaved changes --- .../application-forms/[id]/[sectionIndex].vue | 39 +++++++++++++++++++ legalconsenthub/app/pages/create.vue | 39 ++++++++++++++++++- legalconsenthub/i18n/locales/de.json | 3 +- legalconsenthub/i18n/locales/en.json | 3 +- 4 files changed, 81 insertions(+), 3 deletions(-) diff --git a/legalconsenthub/app/pages/application-forms/[id]/[sectionIndex].vue b/legalconsenthub/app/pages/application-forms/[id]/[sectionIndex].vue index 7b109bc..b8283b8 100644 --- a/legalconsenthub/app/pages/application-forms/[id]/[sectionIndex].vue +++ b/legalconsenthub/app/pages/application-forms/[id]/[sectionIndex].vue @@ -97,6 +97,44 @@ const isReadOnly = computed(() => { return applicationForm.value?.createdBy?.keycloakId !== user.value?.keycloakId }) +// Unsaved changes tracking +const originalFormJson = ref('') + +onMounted(() => { + originalFormJson.value = JSON.stringify(applicationForm.value) + window.addEventListener('beforeunload', handleBeforeUnload) +}) + +onUnmounted(() => { + window.removeEventListener('beforeunload', handleBeforeUnload) +}) + +const hasUnsavedChanges = computed(() => { + if (!originalFormJson.value) return false + return JSON.stringify(applicationForm.value) !== originalFormJson.value +}) + +function handleBeforeUnload(e: BeforeUnloadEvent) { + if (hasUnsavedChanges.value) { + e.preventDefault() + } +} + +onBeforeRouteLeave((to, from, next) => { + // Allow navigation between sections of the same form + if (to.params.id === from.params.id) { + next() + return + } + + if (hasUnsavedChanges.value) { + const answer = window.confirm($t('applicationForms.unsavedWarning')) + next(answer) + } else { + next() + } +}) + const validationMap = ref | undefined>() const validationStatus = ref(ComplianceStatus.NonCritical) @@ -126,6 +164,7 @@ async function onSave() { const updated = await updateForm(applicationForm.value.id, applicationForm.value) if (updated) { updateApplicationForm(updated) + originalFormJson.value = JSON.stringify(updated) toast.add({ title: $t('common.success'), description: $t('applicationForms.saved'), color: 'success' }) } } diff --git a/legalconsenthub/app/pages/create.vue b/legalconsenthub/app/pages/create.vue index 3bf6204..2cdf7e4 100644 --- a/legalconsenthub/app/pages/create.vue +++ b/legalconsenthub/app/pages/create.vue @@ -82,6 +82,38 @@ const applicationFormTemplate = computed( const validationMap = ref | undefined>() const validationStatus = ref(ComplianceStatus.NonCritical) +// Unsaved changes tracking +const originalFormJson = ref('') + +onMounted(() => { + originalFormJson.value = JSON.stringify(applicationFormTemplate.value) + window.addEventListener('beforeunload', handleBeforeUnload) +}) + +onUnmounted(() => { + window.removeEventListener('beforeunload', handleBeforeUnload) +}) + +const hasUnsavedChanges = computed(() => { + if (!originalFormJson.value) return false + return JSON.stringify(applicationFormTemplate.value) !== originalFormJson.value +}) + +function handleBeforeUnload(e: BeforeUnloadEvent) { + if (hasUnsavedChanges.value) { + e.preventDefault() + } +} + +onBeforeRouteLeave((_to, _from, next) => { + if (hasUnsavedChanges.value) { + const answer = window.confirm($t('applicationForms.unsavedWarning')) + next(answer) + } else { + next() + } +}) + const allFormElements = computed(() => { return ( applicationFormTemplate.value?.formElementSections?.flatMap((section: FormElementSectionDto) => @@ -105,8 +137,11 @@ watch( async function onSave() { const applicationForm = await prepareAndCreateApplicationForm() - if (applicationForm) { + if (applicationForm?.id) { + // Reset to prevent unsaved changes warning when navigating + originalFormJson.value = JSON.stringify(applicationFormTemplate.value) toast.add({ title: $t('common.success'), description: $t('applicationForms.saved'), color: 'success' }) + await navigateTo(`/application-forms/${applicationForm.id}/0`) } } @@ -114,6 +149,8 @@ async function onSubmit() { const applicationForm = await prepareAndCreateApplicationForm() if (applicationForm?.id) { await submitApplicationForm(applicationForm.id) + // Reset to prevent unsaved changes warning when navigating + originalFormJson.value = JSON.stringify(applicationFormTemplate.value) await navigateTo('/') toast.add({ title: $t('common.success'), description: $t('applicationForms.submitted'), color: 'success' }) } diff --git a/legalconsenthub/i18n/locales/de.json b/legalconsenthub/i18n/locales/de.json index 0af5f96..fd57caa 100644 --- a/legalconsenthub/i18n/locales/de.json +++ b/legalconsenthub/i18n/locales/de.json @@ -51,7 +51,8 @@ "form": "Formular", "versions": "Versionen", "preview": "Vorschau" - } + }, + "unsavedWarning": "Sie haben ungespeicherte Änderungen. Möchten Sie die Seite wirklich verlassen?" }, "templates": { "title": "Vorlagen", diff --git a/legalconsenthub/i18n/locales/en.json b/legalconsenthub/i18n/locales/en.json index 612dc3a..b52e009 100644 --- a/legalconsenthub/i18n/locales/en.json +++ b/legalconsenthub/i18n/locales/en.json @@ -51,7 +51,8 @@ "form": "Form", "versions": "Versions", "preview": "Preview" - } + }, + "unsavedWarning": "You have unsaved changes. Do you really want to leave this page?" }, "templates": { "title": "Templates",