major: Rename legalconsenthub to gremiumhub
This commit is contained in:
@@ -1,294 +0,0 @@
|
||||
<template>
|
||||
<UDashboardPanel id="home">
|
||||
<template #header>
|
||||
<UDashboardNavbar :title="$t('common.home')" :ui="{ right: 'gap-3' }">
|
||||
<template #leading>
|
||||
<UDashboardSidebarCollapse />
|
||||
</template>
|
||||
|
||||
<template #right>
|
||||
<UButton
|
||||
icon="i-lucide-circle-plus"
|
||||
:label="$t('applicationForms.createNew')"
|
||||
to="/create"
|
||||
:disabled="!canWriteApplicationForms"
|
||||
size="xl"
|
||||
class="bg-gradient-to-br from-teal-500 to-cyan-500 text-white font-semibold rounded-xl shadow-lg shadow-teal-500/25 hover:from-cyan-500 hover:to-violet-500 hover:shadow-xl hover:shadow-violet-500/30 transition-all duration-200"
|
||||
/>
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
|
||||
<UDashboardToolbar>
|
||||
<UNavigationMenu :items="links" highlight class="-mx-1 flex-1" />
|
||||
<USeparator orientation="vertical" class="h-8 mx-2" />
|
||||
<FormValidationIndicator :status="validationStatus" />
|
||||
</UDashboardToolbar>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<FormStepperWithNavigation
|
||||
:form-element-sections="applicationForm.formElementSections"
|
||||
:initial-section-index="sectionIndex"
|
||||
:application-form-id="applicationForm.id ?? undefined"
|
||||
:disabled="isReadOnly"
|
||||
@save="onSave"
|
||||
@submit="onSubmit"
|
||||
@navigate="handleNavigate"
|
||||
@add-input-form="handleAddInputForm"
|
||||
@update:form-element-sections="handleFormElementSectionsUpdate"
|
||||
>
|
||||
<UFormField :label="$t('common.name')" class="mb-4">
|
||||
<UInput v-model="applicationForm.name" class="w-full" :disabled="isReadOnly" />
|
||||
</UFormField>
|
||||
</FormStepperWithNavigation>
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
|
||||
<UModal
|
||||
:open="showRecoveryModal"
|
||||
:title="$t('applicationForms.recovery.title')"
|
||||
@update:open="showRecoveryModal = $event"
|
||||
>
|
||||
<template #body>
|
||||
<p class="text-sm text-(--ui-text-muted)">
|
||||
{{
|
||||
$t('applicationForms.recovery.message', {
|
||||
timestamp: backupTimestamp?.toLocaleString()
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<p
|
||||
v-if="backupSectionIndex !== null && backupSectionIndex !== sectionIndex"
|
||||
class="text-sm text-(--ui-text-muted) mt-2"
|
||||
>
|
||||
{{
|
||||
$t('applicationForms.recovery.sectionNote', {
|
||||
section: (backupSectionIndex + 1).toString()
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<UButton color="neutral" variant="outline" @click="handleDiscardBackup">
|
||||
{{ $t('applicationForms.recovery.discard') }}
|
||||
</UButton>
|
||||
<UButton color="primary" @click="handleRestoreBackup">
|
||||
{{ $t('applicationForms.recovery.restore') }}
|
||||
</UButton>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watchDebounced } from '@vueuse/core'
|
||||
import {
|
||||
ComplianceStatus,
|
||||
type ApplicationFormDto,
|
||||
type FormElementDto,
|
||||
type FormElementSectionDto
|
||||
} from '~~/.api-client'
|
||||
import type { FormElementId } from '~~/types/formElement'
|
||||
import { useApplicationFormValidator } from '~/composables/useApplicationFormValidator'
|
||||
import { useLocalStorageBackup } from '~/composables/useLocalStorageBackup'
|
||||
import { useUserStore } from '~~/stores/useUserStore'
|
||||
import { useCommentStore } from '~~/stores/useCommentStore'
|
||||
|
||||
const route = useRoute()
|
||||
const toast = useToast()
|
||||
const { t: $t } = useI18n()
|
||||
const commentStore = useCommentStore()
|
||||
|
||||
definePageMeta({
|
||||
// Prevent whole page from re-rendering when navigating between sections to keep state
|
||||
key: (route) => `${route.params.id}`
|
||||
})
|
||||
|
||||
const applicationFormId = Array.isArray(route.params.id) ? route.params.id[0] : route.params.id
|
||||
const {
|
||||
applicationForm,
|
||||
navigationLinks: links,
|
||||
updateApplicationForm
|
||||
} = await useApplicationFormNavigation(applicationFormId!)
|
||||
|
||||
if (applicationFormId) {
|
||||
await commentStore.loadCounts(applicationFormId)
|
||||
}
|
||||
|
||||
const { updateApplicationForm: updateForm, submitApplicationForm } = useApplicationForm()
|
||||
const { validateFormElements, getHighestComplianceStatus } = useApplicationFormValidator()
|
||||
const { evaluateFormElementVisibility } = useFormElementVisibility()
|
||||
const { canWriteApplicationForms } = usePermissions()
|
||||
const userStore = useUserStore()
|
||||
const { user } = storeToRefs(userStore)
|
||||
|
||||
const sectionIndex = computed(() => {
|
||||
const param = route.params.sectionIndex
|
||||
const index = parseInt(Array.isArray(param) ? param[0]! : (param ?? '0'))
|
||||
return !isNaN(index) ? index : 0
|
||||
})
|
||||
|
||||
// LocalStorage backup for auto-save
|
||||
const { hasBackup, backupTimestamp, backupSectionIndex, saveBackup, loadBackup, clearBackup } = useLocalStorageBackup(
|
||||
applicationFormId!
|
||||
)
|
||||
|
||||
const showRecoveryModal = ref(false)
|
||||
|
||||
const isReadOnly = computed(() => {
|
||||
return applicationForm.value?.createdBy?.keycloakId !== user.value?.keycloakId
|
||||
})
|
||||
|
||||
// Unsaved changes tracking
|
||||
const originalFormJson = ref<string>('')
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
originalFormJson.value = JSON.stringify(applicationForm.value)
|
||||
window.addEventListener('beforeunload', handleBeforeUnload)
|
||||
|
||||
// Show recovery modal if backup exists
|
||||
if (hasBackup.value) {
|
||||
showRecoveryModal.value = true
|
||||
}
|
||||
})
|
||||
|
||||
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<Map<FormElementId, ComplianceStatus> | undefined>()
|
||||
const validationStatus = ref<ComplianceStatus>(ComplianceStatus.NonCritical)
|
||||
|
||||
const allFormElements = computed(() => {
|
||||
return (
|
||||
applicationForm.value?.formElementSections?.flatMap((section: FormElementSectionDto) =>
|
||||
section.formElementSubSections.flatMap((subsection) => subsection.formElements)
|
||||
) ?? []
|
||||
)
|
||||
})
|
||||
|
||||
const visibilityMap = computed(() => {
|
||||
return evaluateFormElementVisibility(allFormElements.value)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => allFormElements.value,
|
||||
(updatedFormElements: FormElementDto[]) => {
|
||||
validationMap.value = validateFormElements(updatedFormElements, visibilityMap.value)
|
||||
validationStatus.value = getHighestComplianceStatus()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// Re-capture baseline after section navigation to absorb initialization effects
|
||||
// (e.g. TheTable applying row presets, TheEditor normalizing content on mount)
|
||||
watch(sectionIndex, async () => {
|
||||
await nextTick()
|
||||
originalFormJson.value = JSON.stringify(applicationForm.value)
|
||||
})
|
||||
|
||||
// Auto-save to localStorage with 5 second debounce
|
||||
watchDebounced(
|
||||
() => applicationForm.value,
|
||||
(form) => {
|
||||
if (form && hasUnsavedChanges.value && !isReadOnly.value) {
|
||||
saveBackup(form, sectionIndex.value)
|
||||
}
|
||||
},
|
||||
{ debounce: 5000, deep: true }
|
||||
)
|
||||
|
||||
function handleRestoreBackup() {
|
||||
const backupData = loadBackup()
|
||||
if (backupData && applicationForm.value) {
|
||||
updateApplicationForm(backupData)
|
||||
originalFormJson.value = JSON.stringify(backupData)
|
||||
showRecoveryModal.value = false
|
||||
clearBackup()
|
||||
|
||||
// Navigate to the section user was editing
|
||||
if (backupSectionIndex.value !== null && backupSectionIndex.value !== sectionIndex.value) {
|
||||
navigateTo(`/application-forms/${applicationFormId}/${backupSectionIndex.value}`)
|
||||
}
|
||||
|
||||
toast.add({
|
||||
title: $t('common.success'),
|
||||
description: $t('applicationForms.recovery.restore'),
|
||||
color: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function handleDiscardBackup() {
|
||||
clearBackup()
|
||||
showRecoveryModal.value = false
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
if (applicationForm.value?.id) {
|
||||
const updated = await updateForm(applicationForm.value.id, applicationForm.value)
|
||||
if (updated) {
|
||||
updateApplicationForm(updated)
|
||||
originalFormJson.value = JSON.stringify(updated)
|
||||
clearBackup()
|
||||
toast.add({ title: $t('common.success'), description: $t('applicationForms.saved'), color: 'success' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
if (applicationForm.value?.id) {
|
||||
// Save the form first to persist any unsaved changes before submitting
|
||||
const updated = await updateForm(applicationForm.value.id, applicationForm.value)
|
||||
if (updated) {
|
||||
updateApplicationForm(updated)
|
||||
}
|
||||
await submitApplicationForm(applicationForm.value.id)
|
||||
await navigateTo('/')
|
||||
toast.add({ title: $t('common.success'), description: $t('applicationForms.submitted'), color: 'success' })
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNavigate({ index }: { direction: 'forward' | 'backward'; index: number }) {
|
||||
await navigateTo(`/application-forms/${applicationFormId}/${index}`)
|
||||
}
|
||||
|
||||
function handleAddInputForm(updatedForm: ApplicationFormDto | undefined) {
|
||||
if (updatedForm) {
|
||||
updateApplicationForm(updatedForm)
|
||||
}
|
||||
}
|
||||
|
||||
function handleFormElementSectionsUpdate(sections: FormElementSectionDto[]) {
|
||||
if (applicationForm.value) {
|
||||
applicationForm.value.formElementSections = sections
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,54 +0,0 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<template>
|
||||
<UDashboardPanel id="versions">
|
||||
<template #header>
|
||||
<UDashboardNavbar :title="$t('versions.pageTitle', { name: applicationForm?.name })">
|
||||
<template #leading>
|
||||
<UDashboardSidebarCollapse />
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
|
||||
<UDashboardToolbar>
|
||||
<UNavigationMenu :items="links" highlight class="-mx-1 flex-1" />
|
||||
</UDashboardToolbar>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div class="p-6">
|
||||
<VersionHistory
|
||||
v-if="applicationForm?.id"
|
||||
:application-form-id="applicationForm.id"
|
||||
:current-form="applicationForm"
|
||||
@restored="handleRestored"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const { t: $t } = useI18n()
|
||||
|
||||
definePageMeta({
|
||||
key: (route) => `${route.params.id}-versions`
|
||||
})
|
||||
|
||||
const applicationFormId = Array.isArray(route.params.id) ? route.params.id[0] : route.params.id
|
||||
const { applicationForm, navigationLinks: links, refresh } = await useApplicationFormNavigation(applicationFormId!)
|
||||
|
||||
async function handleRestored() {
|
||||
await refresh()
|
||||
toast.add({
|
||||
title: $t('versions.restored'),
|
||||
description: $t('versions.restoredDescription'),
|
||||
color: 'success'
|
||||
})
|
||||
if (!applicationForm.value?.id) {
|
||||
return
|
||||
}
|
||||
router.push(`/application-forms/${applicationForm.value.id}/0`)
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user