feat: Make stepper draggable and take up more width
This commit is contained in:
@@ -1,83 +1,149 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col w-full lg:max-w-4xl mx-auto">
|
<div class="flex flex-col w-full">
|
||||||
<slot />
|
<div class="w-full p-4">
|
||||||
|
<div class="flex flex-col gap-4 sm:gap-6 w-full">
|
||||||
|
<slot />
|
||||||
|
|
||||||
<UStepper ref="stepper" v-model="activeStepperItemIndex" :items="stepperItems" class="w-full" />
|
<div class="lch-stepper relative">
|
||||||
|
<div
|
||||||
|
ref="stepperScrollEl"
|
||||||
|
:class="['lch-stepper-scroll overflow-x-auto overflow-y-visible scroll-smooth', cursorClass]"
|
||||||
|
>
|
||||||
|
<UStepper
|
||||||
|
ref="stepper"
|
||||||
|
v-model="activeStepperItemIndex"
|
||||||
|
:items="stepperItems"
|
||||||
|
:ui="{
|
||||||
|
header: 'flex w-full flex-nowrap',
|
||||||
|
item: 'w-auto shrink-0 flex-[0_0_calc(100%/6)] min-w-[14rem]'
|
||||||
|
}"
|
||||||
|
:linear="false"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h1 v-if="currentFormElementSection?.title" class="text-xl text-pretty font-bold text-highlighted">
|
<div
|
||||||
{{ currentFormElementSection.title }}
|
v-if="canScrollLeft"
|
||||||
</h1>
|
class="pointer-events-none absolute inset-y-0 left-0 w-20 bg-linear-to-r from-default to-transparent"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="canScrollRight"
|
||||||
|
class="pointer-events-none absolute inset-y-0 right-0 w-20 bg-linear-to-l from-default to-transparent"
|
||||||
|
/>
|
||||||
|
|
||||||
<UCard
|
<UButton
|
||||||
v-for="{ subsection, sectionIndex } in visibleSubsections"
|
v-if="canScrollLeft"
|
||||||
:key="getSubsectionKey(currentFormElementSection, sectionIndex, subsection)"
|
icon="i-lucide-chevron-left"
|
||||||
variant="subtle"
|
color="neutral"
|
||||||
class="mb-6"
|
variant="ghost"
|
||||||
>
|
size="md"
|
||||||
<div class="mb-4">
|
class="absolute left-1 top-1/2 -translate-y-1/2"
|
||||||
<h2 class="text-lg font-semibold text-highlighted">{{ subsection.title }}</h2>
|
aria-label="Scroll steps left"
|
||||||
<p v-if="subsection.subtitle" class="text-sm text-dimmed">{{ subsection.subtitle }}</p>
|
@click="scrollStepperBy(-1)"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
v-if="canScrollRight"
|
||||||
|
icon="i-lucide-chevron-right"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
size="md"
|
||||||
|
class="absolute right-1 top-1/2 -translate-y-1/2"
|
||||||
|
aria-label="Scroll steps right"
|
||||||
|
@click="scrollStepperBy(1)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<FormEngine
|
</div>
|
||||||
:model-value="subsection.formElements"
|
|
||||||
:visibility-map="visibilityMap"
|
|
||||||
:application-form-id="applicationFormId"
|
|
||||||
:disabled="disabled"
|
|
||||||
:all-form-elements="allFormElements"
|
|
||||||
@update:model-value="
|
|
||||||
(elements) =>
|
|
||||||
handleFormElementUpdate(elements, getSubsectionKey(currentFormElementSection, sectionIndex, subsection))
|
|
||||||
"
|
|
||||||
@add:input-form="
|
|
||||||
(position) =>
|
|
||||||
handleAddInputForm(position, getSubsectionKey(currentFormElementSection, sectionIndex, subsection))
|
|
||||||
"
|
|
||||||
@clone:element="
|
|
||||||
(element, position) =>
|
|
||||||
handleCloneElement(element, position, getSubsectionKey(currentFormElementSection, sectionIndex, subsection))
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</UCard>
|
|
||||||
|
|
||||||
<UCard v-if="visibleSubsections.length === 0" variant="subtle" class="mb-6">
|
<div class="w-full p-4">
|
||||||
<div class="text-center py-8 text-dimmed">
|
<div class="flex flex-col gap-4 sm:gap-6 w-full lg:max-w-4xl mx-auto">
|
||||||
<UIcon name="i-lucide-eye-off" class="w-12 h-12 mx-auto mb-3 opacity-50" />
|
<h1 v-if="currentFormElementSection?.title" class="text-xl text-pretty font-bold text-highlighted">
|
||||||
<p>{{ $t('applicationForms.noVisibleElements') }}</p>
|
{{ currentFormElementSection.title }}
|
||||||
</div>
|
</h1>
|
||||||
</UCard>
|
|
||||||
|
|
||||||
<div class="flex gap-2 justify-between">
|
<UCard
|
||||||
<UButton leading-icon="i-lucide-arrow-left" :disabled="!stepper?.hasPrev" @click="handleNavigate('backward')">
|
v-for="{ subsection, sectionIndex } in visibleSubsections"
|
||||||
{{ $t('applicationForms.navigation.previous') }}
|
:key="getSubsectionKey(currentFormElementSection, sectionIndex, subsection)"
|
||||||
</UButton>
|
variant="subtle"
|
||||||
|
class="mb-6"
|
||||||
<div class="flex flex-wrap items-center gap-1.5">
|
|
||||||
<UButton trailing-icon="i-lucide-save" :disabled="disabled" variant="outline" size="lg" @click="emit('save')">
|
|
||||||
{{ $t('applicationForms.navigation.save') }}
|
|
||||||
</UButton>
|
|
||||||
<UButton
|
|
||||||
v-if="stepper?.hasNext"
|
|
||||||
trailing-icon="i-lucide-arrow-right"
|
|
||||||
size="lg"
|
|
||||||
@click="handleNavigate('forward')"
|
|
||||||
>
|
>
|
||||||
{{ $t('applicationForms.navigation.next') }}
|
<div class="mb-4">
|
||||||
</UButton>
|
<h2 class="text-lg font-semibold text-highlighted">{{ subsection.title }}</h2>
|
||||||
<UButton
|
<p v-if="subsection.subtitle" class="text-sm text-dimmed">{{ subsection.subtitle }}</p>
|
||||||
v-if="!stepper?.hasNext"
|
</div>
|
||||||
trailing-icon="i-lucide-send-horizontal"
|
<FormEngine
|
||||||
:disabled="disabled"
|
:model-value="subsection.formElements"
|
||||||
size="lg"
|
:visibility-map="visibilityMap"
|
||||||
@click="emit('submit')"
|
:application-form-id="applicationFormId"
|
||||||
>
|
:disabled="disabled"
|
||||||
{{ $t('applicationForms.navigation.submit') }}
|
:all-form-elements="allFormElements"
|
||||||
</UButton>
|
@update:model-value="
|
||||||
|
(elements) =>
|
||||||
|
handleFormElementUpdate(elements, getSubsectionKey(currentFormElementSection, sectionIndex, subsection))
|
||||||
|
"
|
||||||
|
@add:input-form="
|
||||||
|
(position) =>
|
||||||
|
handleAddInputForm(position, getSubsectionKey(currentFormElementSection, sectionIndex, subsection))
|
||||||
|
"
|
||||||
|
@clone:element="
|
||||||
|
(element, position) =>
|
||||||
|
handleCloneElement(
|
||||||
|
element,
|
||||||
|
position,
|
||||||
|
getSubsectionKey(currentFormElementSection, sectionIndex, subsection)
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard v-if="visibleSubsections.length === 0" variant="subtle" class="mb-6">
|
||||||
|
<div class="text-center py-8 text-dimmed">
|
||||||
|
<UIcon name="i-lucide-eye-off" class="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||||
|
<p>{{ $t('applicationForms.noVisibleElements') }}</p>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<div class="flex gap-2 justify-between">
|
||||||
|
<UButton leading-icon="i-lucide-arrow-left" :disabled="!stepper?.hasPrev" @click="handleNavigate('backward')">
|
||||||
|
{{ $t('applicationForms.navigation.previous') }}
|
||||||
|
</UButton>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-1.5">
|
||||||
|
<UButton
|
||||||
|
trailing-icon="i-lucide-save"
|
||||||
|
:disabled="disabled"
|
||||||
|
variant="outline"
|
||||||
|
size="lg"
|
||||||
|
@click="emit('save')"
|
||||||
|
>
|
||||||
|
{{ $t('applicationForms.navigation.save') }}
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
v-if="stepper?.hasNext"
|
||||||
|
trailing-icon="i-lucide-arrow-right"
|
||||||
|
size="lg"
|
||||||
|
@click="handleNavigate('forward')"
|
||||||
|
>
|
||||||
|
{{ $t('applicationForms.navigation.next') }}
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
v-if="!stepper?.hasNext"
|
||||||
|
trailing-icon="i-lucide-send-horizontal"
|
||||||
|
:disabled="disabled"
|
||||||
|
size="lg"
|
||||||
|
@click="emit('submit')"
|
||||||
|
>
|
||||||
|
{{ $t('applicationForms.navigation.submit') }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePointerSwipe } from '@vueuse/core'
|
||||||
import type {
|
import type {
|
||||||
ApplicationFormDto,
|
ApplicationFormDto,
|
||||||
FormElementSectionDto,
|
FormElementSectionDto,
|
||||||
@@ -104,11 +170,39 @@ const { stepper, activeStepperItemIndex, stepperItems, currentFormElementSection
|
|||||||
computed(() => props.formElementSections)
|
computed(() => props.formElementSections)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const stepperScrollEl = ref<HTMLElement | null>(null)
|
||||||
|
const initialScrollLeft = ref(0)
|
||||||
|
const initialPointerX = ref(0)
|
||||||
|
const canScrollLeft = ref(false)
|
||||||
|
const canScrollRight = ref(false)
|
||||||
|
|
||||||
const { evaluateFormElementVisibility } = useFormElementVisibility()
|
const { evaluateFormElementVisibility } = useFormElementVisibility()
|
||||||
const { clearHiddenFormElementValues } = useFormElementValueClearing()
|
const { clearHiddenFormElementValues } = useFormElementValueClearing()
|
||||||
const { processSpawnTriggers } = useSectionSpawning()
|
const { processSpawnTriggers } = useSectionSpawning()
|
||||||
const { cloneElement } = useClonableElements()
|
const { cloneElement } = useClonableElements()
|
||||||
|
|
||||||
|
const { isSwiping } = usePointerSwipe(stepperScrollEl, {
|
||||||
|
threshold: 0,
|
||||||
|
disableTextSelect: true,
|
||||||
|
|
||||||
|
onSwipeStart: (e: PointerEvent) => {
|
||||||
|
// Capture initial scroll position and pointer position when drag starts
|
||||||
|
initialScrollLeft.value = stepperScrollEl.value?.scrollLeft ?? 0
|
||||||
|
initialPointerX.value = e.clientX
|
||||||
|
},
|
||||||
|
|
||||||
|
onSwipe: (e: PointerEvent) => {
|
||||||
|
// Calculate how far the pointer has moved from initial position
|
||||||
|
const deltaX = initialPointerX.value - e.clientX
|
||||||
|
|
||||||
|
// Update scroll position with direct 1:1 tracking
|
||||||
|
const el = stepperScrollEl.value
|
||||||
|
if (el) {
|
||||||
|
el.scrollLeft = initialScrollLeft.value + deltaX
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const previousVisibilityMap = ref<Map<string, boolean>>(new Map())
|
const previousVisibilityMap = ref<Map<string, boolean>>(new Map())
|
||||||
|
|
||||||
const allFormElements = computed(() => {
|
const allFormElements = computed(() => {
|
||||||
@@ -121,6 +215,13 @@ const visibilityMap = computed(() => {
|
|||||||
return evaluateFormElementVisibility(allFormElements.value)
|
return evaluateFormElementVisibility(allFormElements.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const cursorClass = computed(
|
||||||
|
() =>
|
||||||
|
isSwiping.value
|
||||||
|
? 'cursor-grabbing select-none' // During drag: closed fist + no selection
|
||||||
|
: 'cursor-grab' // Ready to drag: open hand
|
||||||
|
)
|
||||||
|
|
||||||
const visibleSubsections = computed(() => {
|
const visibleSubsections = computed(() => {
|
||||||
if (!currentFormElementSection.value?.formElementSubSections) {
|
if (!currentFormElementSection.value?.formElementSubSections) {
|
||||||
return []
|
return []
|
||||||
@@ -146,8 +247,105 @@ onMounted(() => {
|
|||||||
activeStepperItemIndex.value = props.initialSectionIndex
|
activeStepperItemIndex.value = props.initialSectionIndex
|
||||||
}
|
}
|
||||||
previousVisibilityMap.value = visibilityMap.value
|
previousVisibilityMap.value = visibilityMap.value
|
||||||
|
|
||||||
|
stepperScrollEl.value?.addEventListener('scroll', updateStepperOverflowIndicators, { passive: true })
|
||||||
|
updateStepperOverflowIndicators()
|
||||||
|
|
||||||
|
window.addEventListener('resize', scrollToActiveStep)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stepperScrollEl.value?.removeEventListener('scroll', updateStepperOverflowIndicators)
|
||||||
|
window.removeEventListener('resize', scrollToActiveStep)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => activeStepperItemIndex.value,
|
||||||
|
async () => {
|
||||||
|
await nextTick()
|
||||||
|
scrollToActiveStep()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => stepperItems.value.length,
|
||||||
|
async () => {
|
||||||
|
await nextTick()
|
||||||
|
updateStepperOverflowIndicators()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function getActiveStepElement(): HTMLElement | null {
|
||||||
|
const root = stepperScrollEl.value
|
||||||
|
if (!root) return null
|
||||||
|
|
||||||
|
const selectors = [
|
||||||
|
// Nuxt UI / Reka often exposes state via data attributes
|
||||||
|
'[data-state="active"]',
|
||||||
|
'[data-active="true"]',
|
||||||
|
// ARIA patterns
|
||||||
|
'[aria-current="step"]',
|
||||||
|
'[aria-selected="true"]'
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const selector of selectors) {
|
||||||
|
const el = root.querySelector(selector) as HTMLElement | null
|
||||||
|
if (!el) continue
|
||||||
|
|
||||||
|
// Prefer the list item (stable width) if present
|
||||||
|
const li = el.closest('li') as HTMLElement | null
|
||||||
|
return li ?? el
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToActiveStep() {
|
||||||
|
const scroller = stepperScrollEl.value
|
||||||
|
if (!scroller) return
|
||||||
|
|
||||||
|
const activeIndex = activeStepperItemIndex.value
|
||||||
|
if (activeIndex < 3) {
|
||||||
|
scroller.scrollTo({ left: 0, behavior: 'smooth' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeEl = getActiveStepElement()
|
||||||
|
if (!activeEl) return
|
||||||
|
|
||||||
|
const scrollerRect = scroller.getBoundingClientRect()
|
||||||
|
const activeRect = activeEl.getBoundingClientRect()
|
||||||
|
|
||||||
|
const activeCenterInScroller = activeRect.left - scrollerRect.left + activeRect.width / 2 + scroller.scrollLeft
|
||||||
|
const targetScrollLeft = activeCenterInScroller - scroller.clientWidth / 2
|
||||||
|
|
||||||
|
const maxScrollLeft = Math.max(0, scroller.scrollWidth - scroller.clientWidth)
|
||||||
|
const clamped = Math.max(0, Math.min(maxScrollLeft, targetScrollLeft))
|
||||||
|
|
||||||
|
scroller.scrollTo({ left: clamped, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStepperOverflowIndicators() {
|
||||||
|
const scroller = stepperScrollEl.value
|
||||||
|
if (!scroller) {
|
||||||
|
canScrollLeft.value = false
|
||||||
|
canScrollRight.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxScrollLeft = Math.max(0, scroller.scrollWidth - scroller.clientWidth)
|
||||||
|
canScrollLeft.value = scroller.scrollLeft > 1
|
||||||
|
canScrollRight.value = scroller.scrollLeft < maxScrollLeft - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollStepperBy(direction: -1 | 1) {
|
||||||
|
const scroller = stepperScrollEl.value
|
||||||
|
if (!scroller) return
|
||||||
|
|
||||||
|
const delta = Math.max(200, Math.round(scroller.clientWidth * 0.6))
|
||||||
|
scroller.scrollBy({ left: direction * delta, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
|
||||||
async function handleAddInputForm(position: number, subsectionKey: string) {
|
async function handleAddInputForm(position: number, subsectionKey: string) {
|
||||||
const foundSubsection = findSubsectionByKey(subsectionKey)
|
const foundSubsection = findSubsectionByKey(subsectionKey)
|
||||||
if (!foundSubsection) return
|
if (!foundSubsection) return
|
||||||
@@ -248,3 +446,17 @@ function updateSubsectionElements(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.lch-stepper-scroll {
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lch-stepper-scroll::-webkit-scrollbar {
|
||||||
|
height: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lch-stepper-scroll {
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import type { FormElementDto, FormElementVisibilityCondition, VisibilityConditionOperator } from '~~/.api-client'
|
import type { FormElementDto, FormElementVisibilityCondition, VisibilityConditionOperator } from '~~/.api-client'
|
||||||
import { VisibilityConditionOperator as VCOperator, VisibilityConditionType as VCType, FormElementType } from '~~/.api-client'
|
import {
|
||||||
|
VisibilityConditionOperator as VCOperator,
|
||||||
|
VisibilityConditionType as VCType,
|
||||||
|
FormElementType
|
||||||
|
} from '~~/.api-client'
|
||||||
|
|
||||||
export function useFormElementVisibility() {
|
export function useFormElementVisibility() {
|
||||||
/**
|
/**
|
||||||
@@ -60,13 +64,13 @@ export function useFormElementVisibility() {
|
|||||||
// Special handling for CHECKBOX with multiple options
|
// Special handling for CHECKBOX with multiple options
|
||||||
if (sourceElement.type === FormElementType.Checkbox && sourceElement.options.length > 1) {
|
if (sourceElement.type === FormElementType.Checkbox && sourceElement.options.length > 1) {
|
||||||
const operator = condition.formElementOperator || VCOperator.Equals
|
const operator = condition.formElementOperator || VCOperator.Equals
|
||||||
const conditionMet = evaluateCheckboxCondition(sourceElement, condition.formElementExpectedValue, operator)
|
const conditionMet = evaluateCheckboxCondition(sourceElement, condition.formElementExpectedValue || '', operator)
|
||||||
return condition.formElementConditionType === VCType.Show ? conditionMet : !conditionMet
|
return condition.formElementConditionType === VCType.Show ? conditionMet : !conditionMet
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceValue = getFormElementValue(sourceElement)
|
const sourceValue = getFormElementValue(sourceElement)
|
||||||
const operator = condition.formElementOperator || VCOperator.Equals
|
const operator = condition.formElementOperator || VCOperator.Equals
|
||||||
const conditionMet = evaluateCondition(sourceValue, condition.formElementExpectedValue, operator)
|
const conditionMet = evaluateCondition(sourceValue, condition.formElementExpectedValue || '', operator)
|
||||||
|
|
||||||
return condition.formElementConditionType === VCType.Show ? conditionMet : !conditionMet
|
return condition.formElementConditionType === VCType.Show ? conditionMet : !conditionMet
|
||||||
}
|
}
|
||||||
@@ -91,9 +95,7 @@ export function useFormElementVisibility() {
|
|||||||
expectedValue: string,
|
expectedValue: string,
|
||||||
operator: VisibilityConditionOperator
|
operator: VisibilityConditionOperator
|
||||||
): boolean {
|
): boolean {
|
||||||
const selectedLabels = element.options
|
const selectedLabels = element.options.filter((option) => option.value === 'true').map((option) => option.label)
|
||||||
.filter((option) => option.value === 'true')
|
|
||||||
.map((option) => option.label)
|
|
||||||
|
|
||||||
switch (operator) {
|
switch (operator) {
|
||||||
case VCOperator.Equals:
|
case VCOperator.Equals:
|
||||||
|
|||||||
@@ -30,19 +30,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex flex-col gap-4 sm:gap-6 lg:gap-12 w-full lg:max-w-4xl mx-auto">
|
<FormStepperWithNavigation
|
||||||
<FormStepperWithNavigation
|
:form-element-sections="applicationForm.formElementSections"
|
||||||
:form-element-sections="applicationForm.formElementSections"
|
:initial-section-index="sectionIndex"
|
||||||
:initial-section-index="sectionIndex"
|
:application-form-id="applicationForm.id ?? undefined"
|
||||||
:application-form-id="applicationForm.id ?? undefined"
|
:disabled="isReadOnly"
|
||||||
:disabled="isReadOnly"
|
@save="onSave"
|
||||||
@save="onSave"
|
@submit="onSubmit"
|
||||||
@submit="onSubmit"
|
@navigate="handleNavigate"
|
||||||
@navigate="handleNavigate"
|
@add-input-form="handleAddInputForm"
|
||||||
@add-input-form="handleAddInputForm"
|
@update:form-element-sections="handleFormElementSectionsUpdate"
|
||||||
@update:form-element-sections="handleFormElementSectionsUpdate"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</UDashboardPanel>
|
</UDashboardPanel>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -15,26 +15,24 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex flex-col gap-4 sm:gap-6 lg:gap-12 w-full lg:max-w-4xl mx-auto">
|
<div v-if="!canWriteApplicationForms" class="text-center py-12">
|
||||||
<div v-if="!canWriteApplicationForms" class="text-center py-12">
|
<UIcon name="i-lucide-shield-x" class="w-16 h-16 mx-auto text-red-400 mb-4" />
|
||||||
<UIcon name="i-lucide-shield-x" class="w-16 h-16 mx-auto text-red-400 mb-4" />
|
<h2 class="text-2xl font-semibold text-gray-700 mb-2">{{ $t('applicationForms.noPermission') }}</h2>
|
||||||
<h2 class="text-2xl font-semibold text-gray-700 mb-2">{{ $t('applicationForms.noPermission') }}</h2>
|
<p class="text-gray-500 mb-4">{{ $t('applicationForms.noPermissionDescription') }}</p>
|
||||||
<p class="text-gray-500 mb-4">{{ $t('applicationForms.noPermissionDescription') }}</p>
|
<UButton to="/" class="mt-4"> {{ $t('applicationForms.backToOverview') }} </UButton>
|
||||||
<UButton to="/" class="mt-4"> {{ $t('applicationForms.backToOverview') }} </UButton>
|
</div>
|
||||||
</div>
|
<div v-else-if="applicationFormTemplate">
|
||||||
<div v-else-if="applicationFormTemplate">
|
<FormStepperWithNavigation
|
||||||
<FormStepperWithNavigation
|
:form-element-sections="applicationFormTemplate.formElementSections"
|
||||||
:form-element-sections="applicationFormTemplate.formElementSections"
|
@save="onSave"
|
||||||
@save="onSave"
|
@submit="onSubmit"
|
||||||
@submit="onSubmit"
|
@add-input-form="handleAddInputForm"
|
||||||
@add-input-form="handleAddInputForm"
|
@update:form-element-sections="handleFormElementSectionsUpdate"
|
||||||
@update:form-element-sections="handleFormElementSectionsUpdate"
|
>
|
||||||
>
|
<UFormField :label="$t('common.name')" class="mb-4">
|
||||||
<UFormField :label="$t('common.name')" class="mb-4">
|
<UInput v-model="applicationFormTemplate.name" class="w-full" />
|
||||||
<UInput v-model="applicationFormTemplate.name" class="w-full" />
|
</UFormField>
|
||||||
</UFormField>
|
</FormStepperWithNavigation>
|
||||||
</FormStepperWithNavigation>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UDashboardPanel>
|
</UDashboardPanel>
|
||||||
|
|||||||
Reference in New Issue
Block a user