feat(#5): Add title-body control element that can be added dynamically, refactored sectionIndex/create
This commit is contained in:
8
CHANGELOG.md
Normal file
8
CHANGELOG.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-11-02 - Form Element Management API
|
||||||
|
|
||||||
|
- New input field with title and text needed to be added to application forms
|
||||||
|
- Adding new form elements to existing application forms caused type conflicts: frontend needed to add `CreateFormElementDto` (without ID) to arrays of `FormElementDto` (with ID)
|
||||||
|
- Implemented separate endpoint for adding form elements: `POST /application-forms/{applicationFormId}/sections/{sectionId}/form-elements`
|
||||||
|
|
||||||
@@ -224,6 +224,57 @@ paths:
|
|||||||
"503":
|
"503":
|
||||||
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable"
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable"
|
||||||
|
|
||||||
|
/application-forms/{applicationFormId}/sections/{sectionId}/form-elements:
|
||||||
|
post:
|
||||||
|
summary: Add a new form element to a specific section
|
||||||
|
operationId: addFormElementToSection
|
||||||
|
tags:
|
||||||
|
- application-form
|
||||||
|
parameters:
|
||||||
|
- name: applicationFormId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
description: The ID of the application form
|
||||||
|
- name: sectionId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
description: The ID of the form element section
|
||||||
|
- name: position
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
description: The position to insert the form element
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/CreateFormElementDto"
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: Form element successfully added
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ApplicationFormDto"
|
||||||
|
"400":
|
||||||
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/BadRequest"
|
||||||
|
"401":
|
||||||
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized"
|
||||||
|
"404":
|
||||||
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/NotFound"
|
||||||
|
"500":
|
||||||
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError"
|
||||||
|
"503":
|
||||||
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable"
|
||||||
|
|
||||||
####### Application Form Templates #######
|
####### Application Form Templates #######
|
||||||
/application-form-templates:
|
/application-form-templates:
|
||||||
get:
|
get:
|
||||||
@@ -1080,6 +1131,7 @@ components:
|
|||||||
- RADIOBUTTON
|
- RADIOBUTTON
|
||||||
- TEXTFIELD
|
- TEXTFIELD
|
||||||
- SWITCH
|
- SWITCH
|
||||||
|
- TITLE_BODY_TEXTFIELDS
|
||||||
|
|
||||||
####### UserDto #######
|
####### UserDto #######
|
||||||
UserDto:
|
UserDto:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.betriebsratkanzlei.legalconsenthub.application_form
|
|||||||
import com.betriebsratkanzlei.legalconsenthub_api.api.ApplicationFormApi
|
import com.betriebsratkanzlei.legalconsenthub_api.api.ApplicationFormApi
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto
|
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto
|
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateFormElementDto
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.PagedApplicationFormDto
|
import com.betriebsratkanzlei.legalconsenthub_api.model.PagedApplicationFormDto
|
||||||
import org.springframework.core.io.ByteArrayResource
|
import org.springframework.core.io.ByteArrayResource
|
||||||
import org.springframework.core.io.Resource
|
import org.springframework.core.io.Resource
|
||||||
@@ -108,4 +109,24 @@ class ApplicationFormController(
|
|||||||
applicationFormService.submitApplicationForm(id),
|
applicationFormService.submitApplicationForm(id),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@PreAuthorize(
|
||||||
|
"hasAnyRole('CHIEF_EXECUTIVE_OFFICER', 'BUSINESS_DEPARTMENT', 'IT_DEPARTMENT', 'HUMAN_RESOURCES', 'HEAD_OF_WORKS_COUNCIL', 'WORKS_COUNCIL')",
|
||||||
|
)
|
||||||
|
override fun addFormElementToSection(
|
||||||
|
applicationFormId: UUID,
|
||||||
|
sectionId: UUID,
|
||||||
|
position: Int,
|
||||||
|
createFormElementDto: CreateFormElementDto,
|
||||||
|
): ResponseEntity<ApplicationFormDto> =
|
||||||
|
ResponseEntity.status(201).body(
|
||||||
|
applicationFormMapper.toApplicationFormDto(
|
||||||
|
applicationFormService.addFormElementToSection(
|
||||||
|
applicationFormId,
|
||||||
|
sectionId,
|
||||||
|
createFormElementDto,
|
||||||
|
position,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotCreatedExc
|
|||||||
import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotDeletedException
|
import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotDeletedException
|
||||||
import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotFoundException
|
import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotFoundException
|
||||||
import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotUpdatedException
|
import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotUpdatedException
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub.error.FormElementSectionNotFoundException
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementMapper
|
||||||
import com.betriebsratkanzlei.legalconsenthub.notification.NotificationService
|
import com.betriebsratkanzlei.legalconsenthub.notification.NotificationService
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto
|
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormStatus
|
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormStatus
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto
|
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateFormElementDto
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto
|
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationType
|
import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationType
|
||||||
import org.springframework.data.domain.Page
|
import org.springframework.data.domain.Page
|
||||||
@@ -20,6 +23,7 @@ import java.util.UUID
|
|||||||
class ApplicationFormService(
|
class ApplicationFormService(
|
||||||
private val applicationFormRepository: ApplicationFormRepository,
|
private val applicationFormRepository: ApplicationFormRepository,
|
||||||
private val applicationFormMapper: ApplicationFormMapper,
|
private val applicationFormMapper: ApplicationFormMapper,
|
||||||
|
private val formElementMapper: FormElementMapper,
|
||||||
private val notificationService: NotificationService,
|
private val notificationService: NotificationService,
|
||||||
) {
|
) {
|
||||||
fun createApplicationForm(createApplicationFormDto: CreateApplicationFormDto): ApplicationForm {
|
fun createApplicationForm(createApplicationFormDto: CreateApplicationFormDto): ApplicationForm {
|
||||||
@@ -45,7 +49,6 @@ class ApplicationFormService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateApplicationForm(applicationFormDto: ApplicationFormDto): ApplicationForm {
|
fun updateApplicationForm(applicationFormDto: ApplicationFormDto): ApplicationForm {
|
||||||
// TODO find statt mappen?
|
|
||||||
val applicationForm = applicationFormMapper.toApplicationForm(applicationFormDto)
|
val applicationForm = applicationFormMapper.toApplicationForm(applicationFormDto)
|
||||||
val updatedApplicationForm: ApplicationForm
|
val updatedApplicationForm: ApplicationForm
|
||||||
|
|
||||||
@@ -112,4 +115,35 @@ class ApplicationFormService(
|
|||||||
|
|
||||||
notificationService.createNotificationForOrganization(createNotificationDto)
|
notificationService.createNotificationForOrganization(createNotificationDto)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addFormElementToSection(
|
||||||
|
applicationFormId: UUID,
|
||||||
|
sectionId: UUID,
|
||||||
|
createFormElementDto: CreateFormElementDto,
|
||||||
|
position: Int,
|
||||||
|
): ApplicationForm {
|
||||||
|
val applicationForm = getApplicationFormById(applicationFormId)
|
||||||
|
|
||||||
|
val section =
|
||||||
|
applicationForm.formElementSections
|
||||||
|
.find { it.id == sectionId }
|
||||||
|
?: throw FormElementSectionNotFoundException(sectionId)
|
||||||
|
|
||||||
|
val newFormElement = formElementMapper.toFormElement(createFormElementDto, section)
|
||||||
|
|
||||||
|
if (position >= 0 && position < section.formElements.size) {
|
||||||
|
section.formElements.add(position, newFormElement)
|
||||||
|
} else {
|
||||||
|
section.formElements.add(newFormElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
val updatedApplicationForm =
|
||||||
|
try {
|
||||||
|
applicationFormRepository.save(applicationForm)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw ApplicationFormNotUpdatedException(e, applicationFormId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedApplicationForm
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<template v-for="(formElement, index) in props.modelValue" :key="formElement.id">
|
<template v-for="(formElement, index) in props.modelValue" :key="formElement.id">
|
||||||
<div class="group py-3 lg:py-4">
|
<div class="flex py-3 lg:py-4">
|
||||||
|
<div class="group flex-auto">
|
||||||
<p v-if="formElement.title" class="font-semibold">{{ formElement.title }}</p>
|
<p v-if="formElement.title" class="font-semibold">{{ formElement.title }}</p>
|
||||||
<p v-if="formElement.description" class="text-dimmed pb-3">{{ formElement.description }}</p>
|
<p v-if="formElement.description" class="text-dimmed pb-3">{{ formElement.description }}</p>
|
||||||
<div class="flex justify-between">
|
|
||||||
<component
|
<component
|
||||||
:is="getResolvedComponent(formElement)"
|
:is="getResolvedComponent(formElement)"
|
||||||
:form-options="formElement.options"
|
:form-options="formElement.options"
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
@update:form-options="updateFormOptions($event, index)"
|
@update:form-options="updateFormOptions($event, index)"
|
||||||
/>
|
/>
|
||||||
<UIcon
|
|
||||||
name="i-lucide-message-square-more"
|
|
||||||
class="size-5 cursor-pointer hidden group-hover:block"
|
|
||||||
@click="toggleComments(formElement.id)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<TheComment
|
<TheComment
|
||||||
v-if="applicationFormId && activeFormElement === formElement.id"
|
v-if="applicationFormId && activeFormElement === formElement.id"
|
||||||
:form-element-id="formElement.id"
|
:form-element-id="formElement.id"
|
||||||
:application-form-id="applicationFormId"
|
:application-form-id="applicationFormId"
|
||||||
:comments="comments?.[formElement.id]"
|
:comments="comments?.[formElement.id]"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<UDropdownMenu :items="getDropdownItems(formElement.id, index)" :content="{ align: 'end' }">
|
||||||
|
<UButton icon="i-lucide-ellipsis-vertical" color="neutral" variant="ghost" />
|
||||||
|
</UDropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<USeparator />
|
<USeparator />
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
import type { FormElementDto, FormOptionDto } from '~/.api-client'
|
import type { FormElementDto, FormOptionDto } from '~/.api-client'
|
||||||
import { resolveComponent } from 'vue'
|
import { resolveComponent } from 'vue'
|
||||||
import TheComment from '~/components/TheComment.vue'
|
import TheComment from '~/components/TheComment.vue'
|
||||||
|
import type { DropdownMenuItem } from '@nuxt/ui'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: FormElementDto[]
|
modelValue: FormElementDto[]
|
||||||
@@ -41,6 +42,7 @@ const props = defineProps<{
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', formElementDto: FormElementDto[]): void
|
(e: 'update:modelValue', formElementDto: FormElementDto[]): void
|
||||||
(e: 'click:comments', formElementId: string): void
|
(e: 'click:comments', formElementId: string): void
|
||||||
|
(e: 'add:input-form', position: number): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const commentStore = useCommentStore()
|
const commentStore = useCommentStore()
|
||||||
@@ -66,11 +68,30 @@ function getResolvedComponent(formElement: FormElementDto) {
|
|||||||
return resolveComponent('TheSwitch')
|
return resolveComponent('TheSwitch')
|
||||||
case 'TEXTFIELD':
|
case 'TEXTFIELD':
|
||||||
return resolveComponent('TheInput')
|
return resolveComponent('TheInput')
|
||||||
|
case 'TITLE_BODY_TEXTFIELDS':
|
||||||
|
return resolveComponent('TheTitleBodyInput')
|
||||||
default:
|
default:
|
||||||
return resolveComponent('Unimplemented')
|
return resolveComponent('Unimplemented')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDropdownItems(formElementId: string, formElementPosition: number): DropdownMenuItem[] {
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: 'Comments',
|
||||||
|
icon: 'i-lucide-message-square-more',
|
||||||
|
onClick: () => toggleComments(formElementId)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Add input field below',
|
||||||
|
icon: 'i-lucide-list-plus',
|
||||||
|
onClick: () => emit('add:input-form', formElementPosition)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
function updateFormOptions(formOptions: FormOptionDto[], formElementIndex: number) {
|
function updateFormOptions(formOptions: FormOptionDto[], formElementIndex: number) {
|
||||||
const updatedModelValue = [...props.modelValue]
|
const updatedModelValue = [...props.modelValue]
|
||||||
updatedModelValue[formElementIndex] = { ...updatedModelValue[formElementIndex], options: formOptions }
|
updatedModelValue[formElementIndex] = { ...updatedModelValue[formElementIndex], options: formOptions }
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<UFormField label="Titel">
|
||||||
|
<UInput v-model="title" class="w-full" :disabled="props.disabled" />
|
||||||
|
</UFormField>
|
||||||
|
<UFormField label="Text">
|
||||||
|
<UTextarea v-model="body" class="w-full" autoresize :disabled="props.disabled" />
|
||||||
|
</UFormField>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormOptionDto } from '~/.api-client'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
formOptions: FormOptionDto[]
|
||||||
|
disabled?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:formOptions', value: FormOptionDto[]): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const SEPARATOR = '|||'
|
||||||
|
|
||||||
|
const title = computed({
|
||||||
|
get: () => {
|
||||||
|
const currentValue = props.formOptions?.[0]?.value ?? ''
|
||||||
|
return splitValue(currentValue).title
|
||||||
|
},
|
||||||
|
set: (newTitle: string) => {
|
||||||
|
const currentValue = props.formOptions?.[0]?.value ?? ''
|
||||||
|
const { body: currentBody } = splitValue(currentValue)
|
||||||
|
const combinedValue = joinValue(newTitle, currentBody)
|
||||||
|
|
||||||
|
const updatedModelValue = [...props.formOptions]
|
||||||
|
updatedModelValue[0] = { ...updatedModelValue[0], value: combinedValue }
|
||||||
|
emit('update:formOptions', updatedModelValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const body = computed({
|
||||||
|
get: () => {
|
||||||
|
const currentValue = props.formOptions?.[0]?.value ?? ''
|
||||||
|
return splitValue(currentValue).body
|
||||||
|
},
|
||||||
|
set: (newBody: string) => {
|
||||||
|
const currentValue = props.formOptions?.[0]?.value ?? ''
|
||||||
|
const { title: currentTitle } = splitValue(currentValue)
|
||||||
|
const combinedValue = joinValue(currentTitle, newBody)
|
||||||
|
|
||||||
|
const updatedModelValue = [...props.formOptions]
|
||||||
|
updatedModelValue[0] = { ...updatedModelValue[0], value: combinedValue }
|
||||||
|
emit('update:formOptions', updatedModelValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function splitValue(value: string): { title: string; body: string } {
|
||||||
|
const parts = value.split(SEPARATOR)
|
||||||
|
return {
|
||||||
|
title: parts[0] || '',
|
||||||
|
body: parts[1] || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function joinValue(title: string, body: string): string {
|
||||||
|
return `${title}${SEPARATOR}${body}`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
import { type CreateApplicationFormDto, type ApplicationFormDto, type PagedApplicationFormDto } from '~/.api-client'
|
import type {
|
||||||
|
CreateApplicationFormDto,
|
||||||
|
CreateFormElementDto,
|
||||||
|
ApplicationFormDto,
|
||||||
|
PagedApplicationFormDto
|
||||||
|
} from '~/.api-client'
|
||||||
import { useApplicationFormApi } from './useApplicationFormApi'
|
import { useApplicationFormApi } from './useApplicationFormApi'
|
||||||
|
|
||||||
export function useApplicationForm() {
|
export function useApplicationForm() {
|
||||||
@@ -71,12 +76,36 @@ export function useApplicationForm() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addFormElementToSection(
|
||||||
|
applicationFormId: string,
|
||||||
|
sectionId: string,
|
||||||
|
createFormElementDto: CreateFormElementDto,
|
||||||
|
position: number
|
||||||
|
): Promise<ApplicationFormDto> {
|
||||||
|
if (!applicationFormId || !sectionId) {
|
||||||
|
return Promise.reject(new Error('Application form ID or section ID missing'))
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await applicationFormApi.addFormElementToSection(
|
||||||
|
applicationFormId,
|
||||||
|
sectionId,
|
||||||
|
createFormElementDto,
|
||||||
|
position
|
||||||
|
)
|
||||||
|
} catch (e: unknown) {
|
||||||
|
console.error(`Failed adding form element to section ${sectionId}:`, e)
|
||||||
|
return Promise.reject(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createApplicationForm,
|
createApplicationForm,
|
||||||
getAllApplicationForms,
|
getAllApplicationForms,
|
||||||
getApplicationFormById,
|
getApplicationFormById,
|
||||||
updateApplicationForm,
|
updateApplicationForm,
|
||||||
deleteApplicationFormById,
|
deleteApplicationFormById,
|
||||||
submitApplicationForm
|
submitApplicationForm,
|
||||||
|
addFormElementToSection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
ApplicationFormApi,
|
ApplicationFormApi,
|
||||||
Configuration,
|
Configuration,
|
||||||
type CreateApplicationFormDto,
|
type CreateApplicationFormDto,
|
||||||
|
type CreateFormElementDto,
|
||||||
type ApplicationFormDto,
|
type ApplicationFormDto,
|
||||||
type PagedApplicationFormDto
|
type PagedApplicationFormDto
|
||||||
} from '~/.api-client'
|
} from '~/.api-client'
|
||||||
@@ -53,12 +54,27 @@ export function useApplicationFormApi() {
|
|||||||
return applicationFormApiClient.submitApplicationForm({ id })
|
return applicationFormApiClient.submitApplicationForm({ id })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addFormElementToSection(
|
||||||
|
applicationFormId: string,
|
||||||
|
sectionId: string,
|
||||||
|
createFormElementDto: CreateFormElementDto,
|
||||||
|
position: number
|
||||||
|
): Promise<ApplicationFormDto> {
|
||||||
|
return applicationFormApiClient.addFormElementToSection({
|
||||||
|
applicationFormId,
|
||||||
|
sectionId,
|
||||||
|
createFormElementDto,
|
||||||
|
position
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createApplicationForm,
|
createApplicationForm,
|
||||||
getAllApplicationForms,
|
getAllApplicationForms,
|
||||||
getApplicationFormById,
|
getApplicationFormById,
|
||||||
updateApplicationForm,
|
updateApplicationForm,
|
||||||
deleteApplicationFormById,
|
deleteApplicationFormById,
|
||||||
submitApplicationForm
|
submitApplicationForm,
|
||||||
|
addFormElementToSection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
48
legalconsenthub/composables/useFormElementManagement.ts
Normal file
48
legalconsenthub/composables/useFormElementManagement.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import type { ApplicationFormDto, CreateFormElementDto, FormElementSectionDto } from '~/.api-client'
|
||||||
|
import type { MaybeRefOrGetter } from 'vue'
|
||||||
|
|
||||||
|
export function useFormElementManagement(
|
||||||
|
currentFormElementSection: MaybeRefOrGetter<FormElementSectionDto | undefined>,
|
||||||
|
applicationFormId?: string
|
||||||
|
) {
|
||||||
|
const { addFormElementToSection } = useApplicationForm()
|
||||||
|
|
||||||
|
async function addInputFormToApplicationForm(position: number): Promise<ApplicationFormDto | undefined> {
|
||||||
|
const section = toValue(currentFormElementSection)
|
||||||
|
if (!section) return
|
||||||
|
|
||||||
|
const { formElements } = section
|
||||||
|
|
||||||
|
const inputFormElement: CreateFormElementDto = {
|
||||||
|
title: 'Formular ergänzen',
|
||||||
|
description: 'Bitte fügen Sie hier Ihre Ergänzungen ein.',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: '|||',
|
||||||
|
label: '',
|
||||||
|
processingPurpose: 'NONE',
|
||||||
|
employeeDataCategory: 'NONE'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
type: 'TITLE_BODY_TEXTFIELDS'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (applicationFormId) {
|
||||||
|
try {
|
||||||
|
return await addFormElementToSection(applicationFormId, section.id, inputFormElement, position + 1)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to add form element:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// @ts-expect-error Add CreateFormElementDto to formElements array. ID will be generated by the backend.
|
||||||
|
formElements.splice(position + 1, 0, inputFormElement)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
addInputFormToApplicationForm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
58
legalconsenthub/composables/useFormStepper.ts
Normal file
58
legalconsenthub/composables/useFormStepper.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import type { FormElementSectionDto } from '~/.api-client'
|
||||||
|
import type { StepperItem } from '@nuxt/ui'
|
||||||
|
import type { MaybeRefOrGetter } from 'vue'
|
||||||
|
|
||||||
|
interface Stepper {
|
||||||
|
hasPrev: boolean
|
||||||
|
hasNext: boolean
|
||||||
|
next: () => void
|
||||||
|
prev: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFormStepper(
|
||||||
|
formElementSections: MaybeRefOrGetter<FormElementSectionDto[] | undefined>,
|
||||||
|
options?: {
|
||||||
|
onNavigate?: (direction: 'forward' | 'backward', newIndex: number) => void | Promise<void>
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const stepper = useTemplateRef<Stepper>('stepper')
|
||||||
|
const activeStepperItemIndex = ref<number>(0)
|
||||||
|
|
||||||
|
const sections = computed(() => toValue(formElementSections) ?? [])
|
||||||
|
|
||||||
|
const stepperItems = computed(() => {
|
||||||
|
const items: StepperItem[] = []
|
||||||
|
sections.value.forEach((section: FormElementSectionDto) => {
|
||||||
|
items.push({
|
||||||
|
title: section.shortTitle,
|
||||||
|
description: section.description
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return items
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentFormElementSection = computed<FormElementSectionDto | undefined>(
|
||||||
|
() => sections.value[activeStepperItemIndex.value]
|
||||||
|
)
|
||||||
|
|
||||||
|
async function navigateStepper(direction: 'forward' | 'backward') {
|
||||||
|
if (direction === 'forward') {
|
||||||
|
stepper.value?.next()
|
||||||
|
} else {
|
||||||
|
stepper.value?.prev()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.onNavigate) {
|
||||||
|
await options.onNavigate(direction, activeStepperItemIndex.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
stepper,
|
||||||
|
activeStepperItemIndex,
|
||||||
|
stepperItems,
|
||||||
|
currentFormElementSection,
|
||||||
|
navigateStepper
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -31,15 +31,16 @@
|
|||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex flex-col w-full lg:max-w-4xl mx-auto">
|
<div class="flex flex-col w-full lg:max-w-4xl mx-auto">
|
||||||
<UStepper ref="stepper" v-model="activeStepperItemIndex" :items="stepperItems" class="w-full" />
|
<UStepper ref="stepper" v-model="activeStepperItemIndex" :items="stepperItems" class="w-full" />
|
||||||
<h1 class="text-xl text-pretty font-bold text-highlighted">
|
<h1 v-if="currentFormElementSection?.title" class="text-xl text-pretty font-bold text-highlighted">
|
||||||
{{ currentFormElementSection.title }}
|
{{ currentFormElementSection.title }}
|
||||||
</h1>
|
</h1>
|
||||||
<UCard variant="subtle">
|
<UCard variant="subtle">
|
||||||
<FormEngine
|
<FormEngine
|
||||||
v-if="applicationForm"
|
v-if="applicationForm && currentFormElementSection?.formElements"
|
||||||
v-model="currentFormElementSection.formElements"
|
v-model="currentFormElementSection.formElements"
|
||||||
:application-form-id="applicationForm.id"
|
:application-form-id="applicationForm.id"
|
||||||
:disabled="isReadOnly"
|
:disabled="isReadOnly"
|
||||||
|
@add:input-form="handleAddInputForm"
|
||||||
/>
|
/>
|
||||||
<div class="flex gap-2 justify-between mt-4">
|
<div class="flex gap-2 justify-between mt-4">
|
||||||
<UButton
|
<UButton
|
||||||
@@ -75,8 +76,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ApplicationFormDto, FormElementSectionDto } from '~/.api-client'
|
import type { ApplicationFormDto } from '~/.api-client'
|
||||||
import type { StepperItem } from '@nuxt/ui'
|
|
||||||
|
|
||||||
const { getApplicationFormById, updateApplicationForm, submitApplicationForm } = useApplicationForm()
|
const { getApplicationFormById, updateApplicationForm, submitApplicationForm } = useApplicationForm()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -90,11 +90,6 @@ definePageMeta({
|
|||||||
key: (route) => `${route.params.id}`
|
key: (route) => `${route.params.id}`
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const sectionIndex = parseInt(route.params.sectionIndex[0])
|
|
||||||
activeStepperItemIndex.value = !isNaN(sectionIndex) ? sectionIndex : 0
|
|
||||||
})
|
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -105,13 +100,6 @@ const items = [
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
const stepper = useTemplateRef('stepper')
|
|
||||||
const activeStepperItemIndex = ref<number>(0)
|
|
||||||
|
|
||||||
const currentFormElementSection = computed<FormElementSectionDto>(
|
|
||||||
() => applicationForm.value?.formElementSections[activeStepperItemIndex.value]
|
|
||||||
)
|
|
||||||
|
|
||||||
const { data, error } = await useAsyncData<ApplicationFormDto>(`application-form-${route.params.id}`, async () => {
|
const { data, error } = await useAsyncData<ApplicationFormDto>(`application-form-${route.params.id}`, async () => {
|
||||||
console.log('Fetching application form with ID:', route.params.id)
|
console.log('Fetching application form with ID:', route.params.id)
|
||||||
return await getApplicationFormById(Array.isArray(route.params.id) ? route.params.id[0] : route.params.id)
|
return await getApplicationFormById(Array.isArray(route.params.id) ? route.params.id[0] : route.params.id)
|
||||||
@@ -124,29 +112,35 @@ if (error.value) {
|
|||||||
const applicationForm = computed<ApplicationFormDto>(() => data?.value as ApplicationFormDto)
|
const applicationForm = computed<ApplicationFormDto>(() => data?.value as ApplicationFormDto)
|
||||||
|
|
||||||
const isReadOnly = computed(() => {
|
const isReadOnly = computed(() => {
|
||||||
return applicationForm.value?.createdBy.id !== user.value?.id
|
return applicationForm.value?.createdBy.keycloakId !== user.value?.keycloakId
|
||||||
})
|
})
|
||||||
|
|
||||||
const stepperItems = computed(() => {
|
const { stepper, activeStepperItemIndex, stepperItems, currentFormElementSection, navigateStepper } = useFormStepper(
|
||||||
const stepperItems: StepperItem[] = []
|
computed(() => applicationForm.value?.formElementSections),
|
||||||
applicationForm.value.formElementSections.forEach((section: FormElementSectionDto) => {
|
{
|
||||||
stepperItems.push({
|
onNavigate: async () => {
|
||||||
title: section.shortTitle,
|
|
||||||
description: section.description
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return stepperItems
|
|
||||||
})
|
|
||||||
|
|
||||||
async function navigateStepper(direction: 'forward' | 'backward') {
|
|
||||||
if (direction === 'forward') {
|
|
||||||
stepper.value?.next()
|
|
||||||
} else {
|
|
||||||
stepper.value?.prev()
|
|
||||||
}
|
|
||||||
await navigateTo(`/application-forms/${route.params.id}/${activeStepperItemIndex.value}`)
|
await navigateTo(`/application-forms/${route.params.id}/${activeStepperItemIndex.value}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const { addInputFormToApplicationForm } = useFormElementManagement(
|
||||||
|
currentFormElementSection,
|
||||||
|
applicationForm.value?.id
|
||||||
|
)
|
||||||
|
|
||||||
|
async function handleAddInputForm(position: number) {
|
||||||
|
const updatedForm = await addInputFormToApplicationForm(position)
|
||||||
|
if (updatedForm) {
|
||||||
|
data.value = updatedForm
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const sectionIndex = parseInt(route.params.sectionIndex[0])
|
||||||
|
activeStepperItemIndex.value = !isNaN(sectionIndex) ? sectionIndex : 0
|
||||||
|
})
|
||||||
|
|
||||||
async function onSave() {
|
async function onSave() {
|
||||||
if (data?.value) {
|
if (data?.value) {
|
||||||
await updateApplicationForm(data.value.id, data.value)
|
await updateApplicationForm(data.value.id, data.value)
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
<FormEngine
|
<FormEngine
|
||||||
v-if="currentFormElementSection?.formElements"
|
v-if="currentFormElementSection?.formElements"
|
||||||
v-model="currentFormElementSection.formElements"
|
v-model="currentFormElementSection.formElements"
|
||||||
|
@add:input-form="addInputFormToApplicationForm"
|
||||||
/>
|
/>
|
||||||
<div class="flex gap-2 justify-between mt-4">
|
<div class="flex gap-2 justify-between mt-4">
|
||||||
<UButton
|
<UButton
|
||||||
@@ -71,10 +72,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ComplianceStatus, type FormElementSectionDto, type PagedApplicationFormDto } from '~/.api-client'
|
import { ComplianceStatus, type PagedApplicationFormDto } from '~/.api-client'
|
||||||
import { useApplicationFormValidator } from '~/composables/useApplicationFormValidator'
|
import { useApplicationFormValidator } from '~/composables/useApplicationFormValidator'
|
||||||
import type { FormElementId } from '~/types/formElement'
|
import type { FormElementId } from '~/types/formElement'
|
||||||
import type { StepperItem } from '@nuxt/ui'
|
|
||||||
|
|
||||||
const { getAllApplicationFormTemplates } = await useApplicationFormTemplate()
|
const { getAllApplicationFormTemplates } = await useApplicationFormTemplate()
|
||||||
const { createApplicationForm, submitApplicationForm } = useApplicationForm()
|
const { createApplicationForm, submitApplicationForm } = useApplicationForm()
|
||||||
@@ -84,17 +84,6 @@ const userStore = useUserStore()
|
|||||||
const { selectedOrganization } = storeToRefs(userStore)
|
const { selectedOrganization } = storeToRefs(userStore)
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const stepper = useTemplateRef('stepper')
|
|
||||||
const activeStepperItemIndex = ref<number>(0)
|
|
||||||
|
|
||||||
const currentFormElementSection = computed(
|
|
||||||
() => applicationFormTemplate.value?.formElementSections[activeStepperItemIndex.value]
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(activeStepperItemIndex, async (newActiveStepperItem: number) => {
|
|
||||||
activeStepperItemIndex.value = newActiveStepperItem
|
|
||||||
})
|
|
||||||
|
|
||||||
const { data, error } = await useAsyncData<PagedApplicationFormDto>(async () => {
|
const { data, error } = await useAsyncData<PagedApplicationFormDto>(async () => {
|
||||||
return await getAllApplicationFormTemplates()
|
return await getAllApplicationFormTemplates()
|
||||||
})
|
})
|
||||||
@@ -103,34 +92,17 @@ if (error.value) {
|
|||||||
throw createError({ statusText: error.value.message })
|
throw createError({ statusText: error.value.message })
|
||||||
}
|
}
|
||||||
|
|
||||||
const stepperItems = computed(() => {
|
|
||||||
const stepperItems: StepperItem[] = []
|
|
||||||
if (!applicationFormTemplate.value) {
|
|
||||||
return stepperItems
|
|
||||||
}
|
|
||||||
|
|
||||||
applicationFormTemplate.value.formElementSections.forEach((section: FormElementSectionDto) => {
|
|
||||||
stepperItems.push({
|
|
||||||
title: section.shortTitle,
|
|
||||||
description: section.description
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return stepperItems
|
|
||||||
})
|
|
||||||
|
|
||||||
async function navigateStepper(direction: 'forward' | 'backward') {
|
|
||||||
if (direction === 'forward') {
|
|
||||||
stepper.value?.next()
|
|
||||||
} else {
|
|
||||||
stepper.value?.prev()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const applicationFormTemplate = computed(
|
const applicationFormTemplate = computed(
|
||||||
// TODO: Don't select always the first item, allow user to select a template
|
// TODO: Don't select always the first item, allow user to select a template
|
||||||
() => data?.value?.content[0] ?? undefined
|
() => data?.value?.content[0] ?? undefined
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const { stepper, activeStepperItemIndex, stepperItems, currentFormElementSection, navigateStepper } = useFormStepper(
|
||||||
|
computed(() => applicationFormTemplate.value?.formElementSections)
|
||||||
|
)
|
||||||
|
|
||||||
|
const { addInputFormToApplicationForm } = useFormElementManagement(currentFormElementSection)
|
||||||
|
|
||||||
const formElements = computed({
|
const formElements = computed({
|
||||||
get: () => currentFormElementSection?.value?.formElements ?? [],
|
get: () => currentFormElementSection?.value?.formElements ?? [],
|
||||||
set: (val) => {
|
set: (val) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user