diff --git a/backend/apps/dev/build.gradle b/backend/apps/dev/build.gradle index 29475cd733..f0c25087ab 100644 --- a/backend/apps/dev/build.gradle +++ b/backend/apps/dev/build.gradle @@ -79,9 +79,9 @@ apply from: "${rootProject.projectDir}/backend/gradle/valtimo-app.gradle" // Generate TypeScript types for the frontend from the backend DTOs. Lives in the // dev module because it needs the full runtime classpath and is a dev-only tool. -def generatedDir = rootProject.file("../frontend/projects/valtimo/shared/src/lib/generated") -def outDtsFile = rootProject.file("../frontend/projects/valtimo/shared/src/lib/generated/generated-backend-types.d.ts") -def outTsFile = rootProject.file("../frontend/projects/valtimo/shared/src/lib/generated/generated-backend-types.ts") +def generatedDir = rootProject.file("frontend/projects/valtimo/shared/src/lib/generated") +def outDtsFile = rootProject.file("frontend/projects/valtimo/shared/src/lib/generated/generated-backend-types.d.ts") +def outTsFile = rootProject.file("frontend/projects/valtimo/shared/src/lib/generated/generated-backend-types.ts") tasks.named("generateTypeScript").configure { dependsOn tasks.named("classes") @@ -93,7 +93,17 @@ tasks.named("generateTypeScript").configure { excludeClasses = ["com.ritense.search.domain.DisplayType"] customTypeMappings = [ - "com.ritense.search.domain.DisplayType:any" + "com.ritense.search.domain.DisplayType:any", + // Types with custom Jackson serializers that write plain strings. + "java.net.URI:string", + "org.semver4j.Semver:string", + 'com.ritense.document.domain.Document$Id:string', + ] + // Distinct TypeScript names for classes whose simple names collide. + customTypeNaming = [ + 'com.ritense.dashboard.domain.WidgetLayout:DashboardWidgetLayout', + 'com.ritense.tab.domain.WidgetLayout:TabWidgetLayout', + 'com.ritense.document.domain.DocumentDefinition$Id:DocumentDefinitionId', ] doFirst { diff --git a/backend/form-flow/src/main/kotlin/com/ritense/formflow/AbstractFormFlowLinkTaskProvider.kt b/backend/form-flow/src/main/kotlin/com/ritense/formflow/AbstractFormFlowLinkTaskProvider.kt index 93756ecb56..f5b9a56cfe 100644 --- a/backend/form-flow/src/main/kotlin/com/ritense/formflow/AbstractFormFlowLinkTaskProvider.kt +++ b/backend/form-flow/src/main/kotlin/com/ritense/formflow/AbstractFormFlowLinkTaskProvider.kt @@ -35,15 +35,15 @@ abstract class AbstractFormFlowLinkTaskProvider( .singleResult() val additionalProperties = mutableMapOf( - "processInstanceId" to task.getProcessInstanceId(), - "processInstanceBusinessKey" to processInstance.businessKey, - "taskInstanceId" to task.id + PROCESS_INSTANCE_ID to task.getProcessInstanceId(), + PROCESS_INSTANCE_BUSINESS_KEY to processInstance.businessKey, + TASK_INSTANCE_ID to task.id ) try { val document = AuthorizationContext.runWithoutAuthorization { documentService[processInstance.businessKey] } if (document != null) { - additionalProperties["documentId"] = processInstance.businessKey + additionalProperties[DOCUMENT_ID] = processInstance.businessKey } } catch (e: DocumentNotFoundException) { // we do nothing here, intentional @@ -55,6 +55,16 @@ abstract class AbstractFormFlowLinkTaskProvider( companion object { const val FORM_FLOW_TASK_TYPE_KEY = "form-flow" + + // The keys of the additional properties that are available to SpEL expressions in a form + // flow. These are also published through the form flow registry, so the editor can show + // which context data a definition can rely on. + const val PROCESS_INSTANCE_ID = "processInstanceId" + const val PROCESS_INSTANCE_BUSINESS_KEY = "processInstanceBusinessKey" + const val TASK_INSTANCE_ID = "taskInstanceId" + const val DOCUMENT_ID = "documentId" + const val PROCESS_DEFINITION_KEY = "processDefinitionKey" + const val DOCUMENT_DEFINITION_NAME = "documentDefinitionName" } } diff --git a/backend/form-flow/src/main/kotlin/com/ritense/formflow/FormFlowProcessLinkActivityHandler.kt b/backend/form-flow/src/main/kotlin/com/ritense/formflow/FormFlowProcessLinkActivityHandler.kt index d20fb1660e..5ef407dc46 100644 --- a/backend/form-flow/src/main/kotlin/com/ritense/formflow/FormFlowProcessLinkActivityHandler.kt +++ b/backend/form-flow/src/main/kotlin/com/ritense/formflow/FormFlowProcessLinkActivityHandler.kt @@ -103,9 +103,9 @@ class FormFlowProcessLinkActivityHandler( repositoryService.findProcessDefinitionById(processDefinitionId)!! } - val additionalProperties = mutableMapOf("processDefinitionKey" to processDefinition.key) - documentId?.let { additionalProperties["documentId"] = it } - documentDefinitionName?.let { additionalProperties["documentDefinitionName"] = it } + val additionalProperties = mutableMapOf(PROCESS_DEFINITION_KEY to processDefinition.key) + documentId?.let { additionalProperties[DOCUMENT_ID] = it } + documentDefinitionName?.let { additionalProperties[DOCUMENT_DEFINITION_NAME] = it } ProcessLinkActivityResult( processLink.id, diff --git a/backend/form-flow/src/main/kotlin/com/ritense/formflow/autoconfigure/FormFlowAutoConfiguration.kt b/backend/form-flow/src/main/kotlin/com/ritense/formflow/autoconfigure/FormFlowAutoConfiguration.kt index b46f7819cb..8af0c3841f 100644 --- a/backend/form-flow/src/main/kotlin/com/ritense/formflow/autoconfigure/FormFlowAutoConfiguration.kt +++ b/backend/form-flow/src/main/kotlin/com/ritense/formflow/autoconfigure/FormFlowAutoConfiguration.kt @@ -45,11 +45,13 @@ import com.ritense.formflow.repository.FormFlowStepRepository import com.ritense.formflow.repository.MySqlFormFlowAdditionalPropertiesSearchRepository import com.ritense.formflow.repository.PostgresFormFlowAdditionalPropertiesSearchRepository import com.ritense.formflow.security.ValtimoFormFlowHttpSecurityConfigurer +import com.ritense.formflow.service.FormFlowRegistryService import com.ritense.formflow.service.FormFlowService import com.ritense.formflow.service.FormFlowSupportedProcessLinksHandler import com.ritense.formflow.service.FormFlowValtimoService import com.ritense.formflow.service.ObjectMapperConfigurer import com.ritense.formflow.web.rest.FormFlowManagementResource +import com.ritense.formflow.web.rest.FormFlowRegistryResource import com.ritense.formflow.web.rest.FormFlowResource import com.ritense.formflow.web.rest.ProcessLinkFormFlowDefinitionResource import com.ritense.outbox.OutboxService @@ -213,6 +215,28 @@ class FormFlowAutoConfiguration { ) } + @Bean + @ConditionalOnMissingBean(FormFlowRegistryService::class) + fun formFlowRegistryService( + formFlowStepTypeHandlers: List, + stepPropertiesTypes: Collection, + applicationContext: ApplicationContext, + ): FormFlowRegistryService { + return FormFlowRegistryService( + formFlowStepTypeHandlers, + stepPropertiesTypes, + applicationContext, + ) + } + + @Bean + @ConditionalOnMissingBean(FormFlowRegistryResource::class) + fun formFlowRegistryResource( + formFlowRegistryService: FormFlowRegistryService, + ): FormFlowRegistryResource { + return FormFlowRegistryResource(formFlowRegistryService) + } + @Bean @Order(270) @ConditionalOnMissingBean(ValtimoFormFlowHttpSecurityConfigurer::class) diff --git a/backend/form-flow/src/main/kotlin/com/ritense/formflow/security/ValtimoFormFlowHttpSecurityConfigurer.kt b/backend/form-flow/src/main/kotlin/com/ritense/formflow/security/ValtimoFormFlowHttpSecurityConfigurer.kt index db07ac3606..87c2208b6d 100644 --- a/backend/form-flow/src/main/kotlin/com/ritense/formflow/security/ValtimoFormFlowHttpSecurityConfigurer.kt +++ b/backend/form-flow/src/main/kotlin/com/ritense/formflow/security/ValtimoFormFlowHttpSecurityConfigurer.kt @@ -42,6 +42,7 @@ class ValtimoFormFlowHttpSecurityConfigurer : HttpSecurityConfigurer { .requestMatchers(antMatcher(POST, "/api/v1/form-flow/instance/{formFlowId}/step/instance/{stepInstanceId}/to/step/instance/{targetStepInstanceId}")).authenticated() .requestMatchers(antMatcher(GET, "/api/v1/form-flow/instance/{formFlowId}/breadcrumbs")).authenticated() .requestMatchers(antMatcher(GET, "/api/management/v1/form-flow-definition/schema")).hasAuthority(ADMIN) + .requestMatchers(antMatcher(GET, "/api/management/v1/form-flow/registry")).hasAuthority(ADMIN) .requestMatchers(antMatcher(GET, "/api/management/v1/case-definition/{caseDefinitionKey}/version/{versionTag}/form-flow-definition/process-link-option")).hasAuthority(ADMIN) .requestMatchers(antMatcher(GET, "/api/management/v1/case-definition/{caseDefinitionKey}/version/{versionTag}/form-flow-definition")).hasAuthority(ADMIN) .requestMatchers(antMatcher(GET, "/api/management/v1/case-definition/{caseDefinitionKey}/version/{versionTag}/form-flow-definition/{definitionKey}")).hasAuthority(ADMIN) diff --git a/backend/form-flow/src/main/kotlin/com/ritense/formflow/service/FormFlowRegistryService.kt b/backend/form-flow/src/main/kotlin/com/ritense/formflow/service/FormFlowRegistryService.kt new file mode 100644 index 0000000000..7247be061d --- /dev/null +++ b/backend/form-flow/src/main/kotlin/com/ritense/formflow/service/FormFlowRegistryService.kt @@ -0,0 +1,203 @@ +/* + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ritense.formflow.service + +import com.fasterxml.jackson.databind.jsontype.NamedType +import com.ritense.formflow.AbstractFormFlowLinkTaskProvider +import com.ritense.formflow.domain.definition.configuration.step.StepTypeProperties +import com.ritense.formflow.expression.FormFlowBean +import com.ritense.formflow.handler.FormFlowStepTypeHandler +import com.ritense.formflow.web.rest.dto.FormFlowAdditionalPropertyDto +import com.ritense.formflow.web.rest.dto.FormFlowExpressionBeanDto +import com.ritense.formflow.web.rest.dto.FormFlowExpressionMethodDto +import com.ritense.formflow.web.rest.dto.FormFlowExpressionParameterDto +import com.ritense.formflow.web.rest.dto.FormFlowRegistryDto +import com.ritense.formflow.web.rest.dto.FormFlowStepTypeDto +import com.ritense.formflow.web.rest.dto.FormFlowStepTypePropertyDto +import org.springframework.boot.context.event.ApplicationReadyEvent +import org.springframework.context.ApplicationContext +import org.springframework.context.event.EventListener +import org.springframework.core.DefaultParameterNameDiscoverer +import org.springframework.util.ClassUtils +import java.lang.reflect.Method +import java.lang.reflect.Modifier + +class FormFlowRegistryService( + private val stepTypeHandlers: List, + private val stepPropertiesTypes: Collection, + private val applicationContext: ApplicationContext, +) { + private val parameterNameDiscoverer = DefaultParameterNameDiscoverer() + private val cacheLock = Any() + + @Volatile + private var cachedRegistry: FormFlowRegistryDto? = null + + /** + * Builds the registry once during startup, so requests never pay the discovery cost. All + * expression beans exist by the time [ApplicationReadyEvent] fires, matching when the SpEL + * expression beans are collected for form flow execution. + */ + @EventListener(ApplicationReadyEvent::class) + fun warmUp() { + getRegistry() + } + + /** + * Returns the registry that describes what can be used in a form flow definition. + * + * Step types and expression beans are determined by the application classpath and Spring + * configuration, so they do not change while the application is running. The result is + * therefore built once and reused for subsequent calls. + */ + fun getRegistry(): FormFlowRegistryDto { + cachedRegistry?.let { return it } + + return synchronized(cacheLock) { + cachedRegistry ?: createRegistry().also { + cachedRegistry = it + } + } + } + + private fun createRegistry(): FormFlowRegistryDto { + return FormFlowRegistryDto( + stepTypes = createStepTypes(), + expressionBeans = createExpressionBeans(), + additionalProperties = ADDITIONAL_PROPERTIES, + ) + } + + private fun createStepTypes(): List { + val propertiesByTypeName = stepPropertiesTypes + .filter { StepTypeProperties::class.java.isAssignableFrom(it.type) } + .associate { it.name to it.type } + + return stepTypeHandlers + .map { it.getType() } + .distinct() + .sorted() + .map { typeName -> + FormFlowStepTypeDto( + name = typeName, + properties = propertiesByTypeName[typeName] + ?.let(::extractStepTypeProperties) + ?: emptyList(), + ) + } + } + + private fun extractStepTypeProperties(clazz: Class<*>): List { + return clazz.declaredFields + .filterNot { it.isSynthetic || Modifier.isStatic(it.modifiers) } + .map { + FormFlowStepTypePropertyDto( + name = it.name, + type = it.type.simpleName, + ) + } + } + + private fun createExpressionBeans(): List { + return applicationContext.getBeansWithAnnotation(FormFlowBean::class.java) + .map { (beanName, bean) -> + FormFlowExpressionBeanDto( + name = beanName, + methods = extractMethods(ClassUtils.getUserClass(bean)), + ) + } + .sortedBy(FormFlowExpressionBeanDto::name) + } + + private fun extractMethods(clazz: Class<*>): List { + return clazz.methods + .filterNot { it.isSynthetic || it.isBridge } + .filterNot { it.declaringClass == Any::class.java } + .filterNot { Modifier.isStatic(it.modifiers) } + .map { method -> + FormFlowExpressionMethodDto( + name = method.name, + parameters = extractParameters(method), + returnType = method.returnType.simpleName, + ) + } + .sortedWith( + compareBy( + { it.name }, + { it.parameters.size }, + ) + ) + } + + private fun extractParameters(method: Method): List { + val parameterNames = parameterNameDiscoverer.getParameterNames(method) + + return method.parameters.mapIndexed { index, parameter -> + FormFlowExpressionParameterDto( + name = parameterNames?.getOrNull(index) ?: parameter.name, + type = parameter.type.simpleName, + ) + } + } + + companion object { + private const val CONTEXT_USER_TASK = "userTask" + private const val CONTEXT_START_EVENT = "startEvent" + + /** + * The `additionalProperties` entries that form flow instances receive, as populated by + * [AbstractFormFlowLinkTaskProvider] and its process link activity handler. + */ + private val ADDITIONAL_PROPERTIES = listOf( + FormFlowAdditionalPropertyDto( + name = AbstractFormFlowLinkTaskProvider.PROCESS_INSTANCE_ID, + context = CONTEXT_USER_TASK, + alwaysPresent = true, + ), + FormFlowAdditionalPropertyDto( + name = AbstractFormFlowLinkTaskProvider.PROCESS_INSTANCE_BUSINESS_KEY, + context = CONTEXT_USER_TASK, + alwaysPresent = true, + ), + FormFlowAdditionalPropertyDto( + name = AbstractFormFlowLinkTaskProvider.TASK_INSTANCE_ID, + context = CONTEXT_USER_TASK, + alwaysPresent = true, + ), + FormFlowAdditionalPropertyDto( + name = AbstractFormFlowLinkTaskProvider.DOCUMENT_ID, + context = CONTEXT_USER_TASK, + alwaysPresent = false, + ), + FormFlowAdditionalPropertyDto( + name = AbstractFormFlowLinkTaskProvider.PROCESS_DEFINITION_KEY, + context = CONTEXT_START_EVENT, + alwaysPresent = true, + ), + FormFlowAdditionalPropertyDto( + name = AbstractFormFlowLinkTaskProvider.DOCUMENT_ID, + context = CONTEXT_START_EVENT, + alwaysPresent = false, + ), + FormFlowAdditionalPropertyDto( + name = AbstractFormFlowLinkTaskProvider.DOCUMENT_DEFINITION_NAME, + context = CONTEXT_START_EVENT, + alwaysPresent = false, + ), + ) + } +} diff --git a/backend/form-flow/src/main/kotlin/com/ritense/formflow/web/rest/FormFlowRegistryResource.kt b/backend/form-flow/src/main/kotlin/com/ritense/formflow/web/rest/FormFlowRegistryResource.kt new file mode 100644 index 0000000000..c2754aeb50 --- /dev/null +++ b/backend/form-flow/src/main/kotlin/com/ritense/formflow/web/rest/FormFlowRegistryResource.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ritense.formflow.web.rest + +import com.ritense.formflow.service.FormFlowRegistryService +import com.ritense.formflow.web.rest.dto.FormFlowRegistryDto +import com.ritense.valtimo.contract.annotation.SkipComponentScan +import com.ritense.valtimo.contract.domain.ValtimoMediaType.APPLICATION_JSON_UTF8_VALUE +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@SkipComponentScan +@RequestMapping("/api/management", produces = [APPLICATION_JSON_UTF8_VALUE]) +class FormFlowRegistryResource( + private val formFlowRegistryService: FormFlowRegistryService, +) { + @GetMapping("/v1/form-flow/registry") + fun getRegistry(): ResponseEntity { + return ResponseEntity.ok(formFlowRegistryService.getRegistry()) + } +} diff --git a/backend/form-flow/src/main/kotlin/com/ritense/formflow/web/rest/dto/FormFlowRegistryDto.kt b/backend/form-flow/src/main/kotlin/com/ritense/formflow/web/rest/dto/FormFlowRegistryDto.kt new file mode 100644 index 0000000000..269c564287 --- /dev/null +++ b/backend/form-flow/src/main/kotlin/com/ritense/formflow/web/rest/dto/FormFlowRegistryDto.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ritense.formflow.web.rest.dto + +data class FormFlowRegistryDto( + val stepTypes: List, + val expressionBeans: List, + val additionalProperties: List, +) + +/** + * Describes one entry of the `additionalProperties` map that form flow expressions can access. + * [context] tells how the form flow must be linked for the property to exist (`userTask` or + * `startEvent`) and [alwaysPresent] whether it is guaranteed within that context. + */ +data class FormFlowAdditionalPropertyDto( + val name: String, + val context: String, + val alwaysPresent: Boolean, +) + +data class FormFlowStepTypeDto( + val name: String, + val properties: List, +) + +data class FormFlowStepTypePropertyDto( + val name: String, + val type: String, +) + +data class FormFlowExpressionBeanDto( + val name: String, + val methods: List, +) + +data class FormFlowExpressionMethodDto( + val name: String, + val parameters: List, + val returnType: String, +) + +data class FormFlowExpressionParameterDto( + val name: String, + val type: String, +) diff --git a/backend/form-flow/src/test/kotlin/com/ritense/formflow/FormFlowProcessLinkActivityHandlerIntTest.kt b/backend/form-flow/src/test/kotlin/com/ritense/formflow/FormFlowProcessLinkActivityHandlerIntTest.kt index 4ea5055e58..19fd292de1 100644 --- a/backend/form-flow/src/test/kotlin/com/ritense/formflow/FormFlowProcessLinkActivityHandlerIntTest.kt +++ b/backend/form-flow/src/test/kotlin/com/ritense/formflow/FormFlowProcessLinkActivityHandlerIntTest.kt @@ -32,7 +32,9 @@ import com.ritense.formflow.web.rest.dto.FormFlowProcessLinkCreateRequestDto import com.ritense.valtimo.contract.case_.CaseDefinitionId import com.ritense.valtimo.service.OperatonProcessService import com.ritense.valtimo.service.OperatonTaskService +import com.ritense.formflow.service.FormFlowRegistryService import java.util.UUID +import org.assertj.core.api.Assertions.assertThat import org.operaton.bpm.engine.RepositoryService import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test @@ -67,6 +69,9 @@ internal class FormFlowProcessLinkActivityHandlerIntTest: BaseIntegrationTest() @Autowired lateinit var formFlowService: FormFlowService + @Autowired + lateinit var formFlowRegistryService: FormFlowRegistryService + @Test fun `should not create form flow instance when Operaton user task is created`() { val caseDefinitionId = CaseDefinitionId("profile", "1.0.0") @@ -173,6 +178,91 @@ internal class FormFlowProcessLinkActivityHandlerIntTest: BaseIntegrationTest() assertEquals(additionalProperties["processDefinitionKey"], "formflow-one-task-process") } + @Test + @WithMockUser(username = TEST_USER, authorities = [USER]) + fun `should only put registry-declared additional properties on task-opened instances`() { + val caseDefinitionId = CaseDefinitionId("profile", "1.0.0") + val processDefinition = repositoryService.createProcessDefinitionQuery() + .latestVersion() + .processDefinitionKey("formflow-one-task-process") + .singleResult() + + processLinkService.createProcessLink( + FormFlowProcessLinkCreateRequestDto( + processDefinitionId = processDefinition.id, + activityId = "do-something", + activityType = ActivityTypeWithEventName.USER_TASK_START, + formFlowDefinitionKey = "inkomens_loket_alternate" + ), + caseDefinitionId + ) + val processInstance = runWithoutAuthorization { + operatonProcessService.startProcess( + processDefinition.key, + UUID.randomUUID().toString(), + caseDefinitionId, + mapOf() + ) + } + val task = taskService.findTask(byProcessInstanceId(processInstance.processInstanceDto.id)) + + processLinkActivityService.openTask(UUID.fromString(task.id)) + + // Guards against drift between what the handler populates and what the registry documents + // for the editor. + val declaredUserTaskProperties = formFlowRegistryService.getRegistry() + .additionalProperties + .filter { it.context == "userTask" } + .map { it.name } + val instanceProperties = formFlowInstanceRepository.findAll().single().getAdditionalProperties().keys + + assertThat(instanceProperties).isNotEmpty + assertThat(declaredUserTaskProperties).containsAll(instanceProperties) + } + + @Test + fun `should only put registry-declared additional properties on start-event instances`() { + val processDefinition = repositoryService.createProcessDefinitionQuery() + .latestVersion() + .processDefinitionKey("formflow-one-task-process") + .singleResult() + val formFlowDefinition = formFlowService.findDefinition( + "inkomens_loket_alternate", + CaseDefinitionId("profile", "1.0.0") + ) + val processLink: ProcessLink = FormFlowProcessLink( + id = UUID.randomUUID(), + processDefinitionId = processDefinition.id, + activityId = "some_activity_id", + activityType = ActivityTypeWithEventName.START_EVENT_START, + formFlowDefinitionKey = formFlowDefinition?.id?.key!!, + formDisplayType = FormDisplayType.modal, + formSize = FormSizes.large, + subtitles = listOf() + ) + + processLinkActivityHandler.getStartEventObject( + processDefinition.id, + null, + "some-document", + processLink + ) + + // Guards against drift between what the handler populates and what the registry documents + // for the editor. + val declaredStartEventProperties = formFlowRegistryService.getRegistry() + .additionalProperties + .filter { it.context == "startEvent" } + .map { it.name } + val instanceProperties = formFlowInstanceRepository.findAll() + .single { it.formFlowDefinition.id.toString() == "inkomens_loket_alternate" } + .getAdditionalProperties() + .keys + + assertThat(instanceProperties).isNotEmpty + assertThat(declaredStartEventProperties).containsAll(instanceProperties) + } + companion object { private const val TEST_USER = "user@valtimo.nl" } diff --git a/backend/form-flow/src/test/kotlin/com/ritense/formflow/service/FormFlowRegistryServiceTest.kt b/backend/form-flow/src/test/kotlin/com/ritense/formflow/service/FormFlowRegistryServiceTest.kt new file mode 100644 index 0000000000..22b30a8e8b --- /dev/null +++ b/backend/form-flow/src/test/kotlin/com/ritense/formflow/service/FormFlowRegistryServiceTest.kt @@ -0,0 +1,163 @@ +/* + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ritense.formflow.service + +import com.fasterxml.jackson.databind.jsontype.NamedType +import com.ritense.formflow.domain.definition.configuration.step.CustomComponentStepTypeProperties +import com.ritense.formflow.domain.definition.configuration.step.FormStepTypeProperties +import com.ritense.formflow.expression.FormFlowBean +import com.ritense.formflow.handler.FormFlowStepTypeHandler +import com.ritense.formflow.web.rest.dto.FormFlowExpressionParameterDto +import com.ritense.formflow.web.rest.dto.FormFlowStepTypePropertyDto +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.springframework.context.ApplicationContext + +internal class FormFlowRegistryServiceTest { + + lateinit var applicationContext: ApplicationContext + lateinit var formFlowRegistryService: FormFlowRegistryService + + @BeforeEach + fun beforeEach() { + applicationContext = mock() + whenever(applicationContext.getBeansWithAnnotation(FormFlowBean::class.java)) + .thenReturn(mapOf("testExpressionBean" to TestExpressionBean())) + + formFlowRegistryService = FormFlowRegistryService( + stepTypeHandlers = listOf( + stepTypeHandler("form"), + stepTypeHandler("custom-component"), + stepTypeHandler("no-properties"), + ), + stepPropertiesTypes = listOf( + NamedType(FormStepTypeProperties::class.java, "form"), + NamedType(CustomComponentStepTypeProperties::class.java, "custom-component"), + NamedType(String::class.java, "no-properties"), + ), + applicationContext = applicationContext, + ) + } + + @Test + fun `should return step types with their properties`() { + val registry = formFlowRegistryService.getRegistry() + + assertThat(registry.stepTypes.map { it.name }) + .containsExactly("custom-component", "form", "no-properties") + + val formStepType = registry.stepTypes.single { it.name == "form" } + assertThat(formStepType.properties) + .containsExactly(FormFlowStepTypePropertyDto(name = "definition", type = "String")) + + val customComponentStepType = registry.stepTypes.single { it.name == "custom-component" } + assertThat(customComponentStepType.properties) + .containsExactly(FormFlowStepTypePropertyDto(name = "componentId", type = "String")) + } + + @Test + fun `should ignore step properties types that do not implement StepTypeProperties`() { + val registry = formFlowRegistryService.getRegistry() + + val noPropertiesStepType = registry.stepTypes.single { it.name == "no-properties" } + assertThat(noPropertiesStepType.properties).isEmpty() + } + + @Test + fun `should return expression beans with public methods only`() { + val registry = formFlowRegistryService.getRegistry() + + val bean = registry.expressionBeans.single() + assertThat(bean.name).isEqualTo("testExpressionBean") + assertThat(bean.methods.map { it.name }) + .containsExactly("doSomething", "doSomething", "startFlow") + } + + @Test + fun `should return expression method parameter names and types`() { + val registry = formFlowRegistryService.getRegistry() + + val bean = registry.expressionBeans.single() + val method = bean.methods.single { it.name == "doSomething" && it.parameters.size == 2 } + assertThat(method.parameters).containsExactly( + FormFlowExpressionParameterDto(name = "input", type = "String"), + FormFlowExpressionParameterDto(name = "count", type = "int"), + ) + assertThat(method.returnType).isEqualTo("boolean") + } + + @Test + fun `should cache registry after first call`() { + val firstRegistry = formFlowRegistryService.getRegistry() + val secondRegistry = formFlowRegistryService.getRegistry() + + assertThat(secondRegistry).isSameAs(firstRegistry) + verify(applicationContext, times(1)).getBeansWithAnnotation(FormFlowBean::class.java) + } + + @Test + fun `should return the additional properties available to expressions`() { + val registry = formFlowRegistryService.getRegistry() + + assertThat(registry.additionalProperties).isNotEmpty + assertThat(registry.additionalProperties.map { it.name }).contains( + "processInstanceId", + "processInstanceBusinessKey", + "taskInstanceId", + "documentId", + "processDefinitionKey", + "documentDefinitionName", + ) + assertThat(registry.additionalProperties.map { it.context }.distinct()) + .containsExactlyInAnyOrder("userTask", "startEvent") + } + + @Test + fun `should build registry on warm up so requests reuse the cached registry`() { + formFlowRegistryService.warmUp() + + formFlowRegistryService.getRegistry() + + verify(applicationContext, times(1)).getBeansWithAnnotation(FormFlowBean::class.java) + } + + private fun stepTypeHandler(type: String): FormFlowStepTypeHandler { + val handler = mock() + whenever(handler.getType()).thenReturn(type) + return handler + } + + @FormFlowBean + class TestExpressionBean { + + fun doSomething(input: String): Boolean = input.isNotEmpty() + + fun doSomething(input: String, count: Int): Boolean = input.length == count + + fun startFlow(properties: Map) { + properties.isEmpty() + } + + @Suppress("unused") + private fun hidden(): String = "hidden" + } +} diff --git a/backend/form-flow/src/test/kotlin/com/ritense/formflow/web/rest/FormFlowRegistryResourceIntTest.kt b/backend/form-flow/src/test/kotlin/com/ritense/formflow/web/rest/FormFlowRegistryResourceIntTest.kt new file mode 100644 index 0000000000..7d2540c976 --- /dev/null +++ b/backend/form-flow/src/test/kotlin/com/ritense/formflow/web/rest/FormFlowRegistryResourceIntTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ritense.formflow.web.rest + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import com.ritense.formflow.BaseIntegrationTest +import com.ritense.formflow.service.FormFlowRegistryService +import com.ritense.formflow.web.rest.dto.FormFlowRegistryDto +import com.ritense.formflow.web.rest.dto.FormFlowStepTypePropertyDto +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.web.context.WebApplicationContext + +class FormFlowRegistryResourceIntTest : BaseIntegrationTest() { + + @Autowired + lateinit var webApplicationContext: WebApplicationContext + + @Autowired + lateinit var objectMapper: ObjectMapper + + @Autowired + lateinit var formFlowRegistryService: FormFlowRegistryService + + lateinit var mockMvc: MockMvc + + @BeforeEach + fun init() { + mockMvc = MockMvcBuilders + .webAppContextSetup(this.webApplicationContext) + .build() + } + + @Test + fun `should return step types with their properties`() { + val registry = getRegistry() + + assertThat(registry.stepTypes.map { it.name }) + .contains("form", "custom-component") + + val formStepType = registry.stepTypes.single { it.name == "form" } + assertThat(formStepType.properties) + .containsExactly(FormFlowStepTypePropertyDto(name = "definition", type = "String")) + + val customComponentStepType = registry.stepTypes.single { it.name == "custom-component" } + assertThat(customComponentStepType.properties) + .containsExactly(FormFlowStepTypePropertyDto(name = "componentId", type = "String")) + } + + @Test + fun `should return expression beans with their methods`() { + val registry = getRegistry() + + val valtimoFormFlow = registry.expressionBeans.single { it.name == "valtimoFormFlow" } + assertThat(valtimoFormFlow.methods.map { it.name }) + .containsExactly( + "completeTask", + "completeTask", + "completeTask", + "startCase", + "startSupportingProcess", + ) + + val completeTask = valtimoFormFlow.methods + .single { it.name == "completeTask" && it.parameters.size == 3 } + assertThat(completeTask.parameters.map { it.name }) + .containsExactly("additionalProperties", "submissionData", "submissionSavePath") + assertThat(completeTask.parameters.map { it.type }) + .containsExactly("Map", "JsonNode", "Map") + } + + @Test + fun `should have built the registry during application startup`() { + // Proves the ApplicationReadyEvent listener is wired by Spring: the cache is populated by + // the startup warm-up, before this test calls the service or the endpoint. (When other + // tests in the shared context ran first, the cache is populated either way; a fresh + // context is the case that would expose a broken listener.) + val cachedRegistry = FormFlowRegistryService::class.java + .getDeclaredField("cachedRegistry") + .apply { isAccessible = true } + .get(formFlowRegistryService) + + assertThat(cachedRegistry).isNotNull + } + + @Test + fun `should return the additional properties available to expressions`() { + val registry = getRegistry() + + assertThat(registry.additionalProperties.map { it.name }).contains( + "processInstanceId", + "taskInstanceId", + "documentId", + "processDefinitionKey", + ) + } + + private fun getRegistry(): FormFlowRegistryDto { + val response = mockMvc + .perform(get("/api/management/v1/form-flow/registry")) + .andDo(print()) + .andExpect(status().isOk) + .andReturn() + .response + .contentAsString + + return objectMapper.readValue(response) + } +} diff --git a/backend/form-flow/src/test/kotlin/com/ritense/formflow/web/rest/FormFlowRegistrySecurityIntTest.kt b/backend/form-flow/src/test/kotlin/com/ritense/formflow/web/rest/FormFlowRegistrySecurityIntTest.kt new file mode 100644 index 0000000000..eb87abcce6 --- /dev/null +++ b/backend/form-flow/src/test/kotlin/com/ritense/formflow/web/rest/FormFlowRegistrySecurityIntTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ritense.formflow.web.rest + +import com.ritense.formflow.BaseIntegrationTest +import com.ritense.valtimo.contract.authentication.AuthoritiesConstants.ADMIN +import com.ritense.valtimo.contract.authentication.AuthoritiesConstants.USER +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.web.context.WebApplicationContext + +/** + * Verifies the authorization rule of the registry endpoint through the real security filter + * chain: the smoke test only proves that anonymous access is blocked, not which authority is + * required. + */ +class FormFlowRegistrySecurityIntTest : BaseIntegrationTest() { + + @Autowired + lateinit var webApplicationContext: WebApplicationContext + + lateinit var mockMvc: MockMvc + + @BeforeEach + fun init() { + mockMvc = MockMvcBuilders + .webAppContextSetup(this.webApplicationContext) + .apply(springSecurity()) + .build() + } + + @Test + @WithMockUser(authorities = [ADMIN]) + fun `should return the registry to admins`() { + mockMvc + .perform(get("/api/management/v1/form-flow/registry")) + .andExpect(status().isOk) + } + + @Test + @WithMockUser(authorities = [USER]) + fun `should forbid the registry for users without the admin authority`() { + mockMvc + .perform(get("/api/management/v1/form-flow/registry")) + .andExpect(status().isForbidden) + } +} diff --git a/documentation/features/building-blocks/form-flows.md b/documentation/features/building-blocks/form-flows.md index cf4eb556db..5c84357cc0 100644 --- a/documentation/features/building-blocks/form-flows.md +++ b/documentation/features/building-blocks/form-flows.md @@ -5,6 +5,11 @@ are scoped to a specific building block version and can be used in user tasks wi ## Managing form flows +Form flows of a building block are edited in the same form flow editor as case form flows, with a visual +**Editor** tab and a **JSON editor** tab. The **Form** dropdown of a step lists the forms of the building +block. See the [form flow editor documentation](../case/form-flow.md#creating-a-form-flow-definition) for how +to configure steps, transitions and actions. + ### Creating a form flow * Open a building block definition. diff --git a/documentation/features/case/form-flow.md b/documentation/features/case/form-flow.md index 7b76eba53a..9392c9e199 100644 --- a/documentation/features/case/form-flow.md +++ b/documentation/features/case/form-flow.md @@ -12,44 +12,36 @@ For information on how to link a form flow definition to a task, see the [form f {% tabs %} {% tab title="Via UI" %} -
- * Go to the `Admin` menu. * Go to the `Cases` menu and select the case to configure form flows for. -* Select the `Form Flows` tab +* Select the `Form Flows` tab. -Form flows can be added to draft case definitions via the **Add new form flow** button. A modal will be shown where the form flow key can be set and the form flow can be created. +Form flows can be added to draft case definitions via the **Create new form flow** button. A modal will be shown where the form flow key can be set and the form flow can be created. -
+After creating a form flow, it opens in the form flow editor. The editor has two tabs that work on the same definition: a visual **Editor** and a **JSON editor**. -After creating a form flow, the contents of it can be edited. The steps and different step types are described below. +**The visual editor** -
+
-Add each individual step to the form flow definition, e.g.: +The left panel lists the steps of the flow; **Add step** adds a new one. Selecting a step shows its configuration on the right: -``` -{ - "key": "my-form-flow", - "startStep": "personalDetailsStep", - "steps": [ - { - "key": "personalDetailsStep" - }, - { - "key": "loanApprovedStep" - }, - { - "key": "loanDeniedStep" - }, - { - "key": "summaryStep" - } - ] -} -``` +* **Step details.** The key identifies the step; renaming it automatically updates the start step and every transition that references it. The optional title is shown in the [breadcrumb trail](form-flow.md#bread-crumbs). The type determines what the step shows: for a `form` step, the **Form** dropdown lists the forms of this case definition; for a `custom-component` step, the **Component ID** dropdown lists the custom components registered by the implementation. See the [step types section](form-flow.md#step-types) for more information. +* **Start step.** The step where the form flow begins carries a *Start step* tag. Any other step can be made the start step with the **Make start step** button. +* **Navigation.** Transitions define where the user can go after completing the step. Each transition points to another step and can have a SpEL condition. Transitions are evaluated from top to bottom — the first one whose condition holds is taken, and a transition without a condition is the default. The order can be changed with the arrow buttons. +* **Actions.** Expressions that run when the step opens, when it is completed, or when the user navigates back. The **Add action** menu lists the registered form flow functions with their parameters, next to the option to write a blank expression. See the [expressions section](form-flow.md#expressions) for more information. + +
+ +The **How do expressions work?** button opens a help dialog explaining conditions and actions, including exactly which data is available in `additionalProperties` for this application. + +
+ +The editor validates the definition while editing — duplicate step keys, a missing start step, transitions to unknown steps, and multiple default transitions are reported — and warns when leaving the page with unsaved changes. + +**The JSON editor** -More details can be found in the **Via IDE** tab. +The **JSON editor** tab shows the definition as JSON with schema-based validation and autocompletion. This is useful for copying definitions between environments or for editing properties in bulk. The JSON format is described in the **Via IDE** tab. {% endtab %} {% tab title="Via IDE" %} diff --git a/documentation/features/case/images/form-flow-editor-actions.png b/documentation/features/case/images/form-flow-editor-actions.png new file mode 100644 index 0000000000..b656172fa5 Binary files /dev/null and b/documentation/features/case/images/form-flow-editor-actions.png differ diff --git a/documentation/features/case/images/form-flow-editor-help.png b/documentation/features/case/images/form-flow-editor-help.png new file mode 100644 index 0000000000..df0dc2790a Binary files /dev/null and b/documentation/features/case/images/form-flow-editor-help.png differ diff --git a/documentation/features/case/images/form-flow-editor.png b/documentation/features/case/images/form-flow-editor.png new file mode 100644 index 0000000000..ede14eac02 Binary files /dev/null and b/documentation/features/case/images/form-flow-editor.png differ diff --git a/documentation/release-notes/13.x.x/13.41.0/README.md b/documentation/release-notes/13.x.x/13.41.0/README.md index e096e76aa3..6dc060e5e0 100644 --- a/documentation/release-notes/13.x.x/13.41.0/README.md +++ b/documentation/release-notes/13.x.x/13.41.0/README.md @@ -6,16 +6,34 @@ ## New Features -* **New feature title** +* **Visual form flow editor** - New feature explanation. + Form flows can now be built in a visual editor instead of writing JSON by hand. The editor opens on a new + **Editor** tab when a form flow is opened in case management or building block management; the existing JSON + editor remains available on a separate **JSON editor** tab, and both work on the same definition. -## Enhancements + The visual editor shows the steps of the flow in a sidebar and the configuration of the selected step next to + it. Per step, the key, title and step type can be set, and the type-specific configuration is offered as a + choice: the **Form** dropdown lists the forms of the surrounding case definition or building block, and the + **Component ID** dropdown lists the custom components registered by the implementation (the + `custom-component` type is unavailable when none are registered). Any step can be marked as the start step, + and renaming a step key automatically updates the start step and every transition that referenced it. -* **New enhancement title** + Transitions to next steps are configured per step, including their SpEL conditions and evaluation order. + Actions that run when a step opens, completes, or when the user navigates back can be added from a menu that + lists the registered form flow functions — such as `valtimoFormFlow.completeTask` — with their parameters. + Inline help explains how conditions and actions work, and a help dialog documents exactly which data is + available in `additionalProperties`, based on what the application provides. - New enhancement explanation. + The editor validates the definition while editing (duplicate step keys, missing start step, transitions to + unknown steps, at most one default transition) and warns when leaving the page with unsaved changes. See the + [form flow documentation](../../../features/case/form-flow.md#creating-a-form-flow-definition) for details. ## Bugfixes -* New bugfix. +* **Changing the form flow of an existing form flow process link is now saved** + + When editing an existing form flow process link and selecting a different form flow definition, the change + was silently ignored on save: the process link kept its previous form flow. Changes to the display type and + size were saved correctly, which made this easy to miss. Selecting a different form flow definition is now + saved as expected. diff --git a/e2e/tests/case-details-management-form-flows/case-details-management-form-flows.spec.ts b/e2e/tests/case-details-management-form-flows/case-details-management-form-flows.spec.ts index 21e00345d0..8209e21081 100644 --- a/e2e/tests/case-details-management-form-flows/case-details-management-form-flows.spec.ts +++ b/e2e/tests/case-details-management-form-flows/case-details-management-form-flows.spec.ts @@ -65,7 +65,7 @@ test.describe('Case details management — Form Flows', () => { await formFlowsPage.createFormFlow(formFlowTestData.key); // Assert — creation navigates directly to the form flow editor - await formFlowsPage.assertEditorVisible(); + await formFlowsPage.assertEditorPageVisible(); }); }); }); @@ -77,8 +77,9 @@ test.describe('Case details management — Form Flows', () => { // Navigate back to list first (creation left us on the editor) await formFlowsPage.navigateBackToFormFlowsList(); - // Act + // Act — open the form flow and switch to the JSON editor tab await formFlowsPage.openFormFlow(formFlowTestData.key); + await formFlowsPage.openJsonEditorTab(); // Assert — Monaco editor is rendered await formFlowsPage.assertEditorVisible(); @@ -148,6 +149,58 @@ test.describe('Case details management — Form Flows', () => { }); }); + // ─── 6.62 Visual editor ─────────────────────────────────────────── + + test.describe('6.62 — Visual editor', () => { + test('Visual editor shows the saved steps', async () => { + // Act — switch from the JSON tab to the visual editor tab + await formFlowsPage.openVisualEditorTab(); + + // Assert — the step saved through the JSON editor is listed and selected; its form key is + // preserved in the form dropdown even though no form with that name exists + await expect(formFlowsPage.visualStepListItems).toHaveCount(1); + await expect(formFlowsPage.visualStepListItems.first()).toContainText('step1'); + await expect(formFlowsPage.visualStepKeyInput).toHaveValue('step1'); + await expect(formFlowsPage.visualFormDefinitionDropdown).toContainText('test-form'); + }); + + test('Edit a step title in the visual editor and save', async () => { + // Act + await formFlowsPage.visualStepTitleInput.fill('First step'); + await expect(formFlowsPage.saveButton).toBeEnabled(); + const response = await formFlowsPage.saveFormFlow(formFlowTestData.key, CASE_IDENTIFIER); + + // Assert + expect(response.ok()).toBeTruthy(); + await formFlowsPage.assertSaveSuccessNotification(formFlowTestData.key); + }); + + test('Add a step with a transition in the visual editor and save', async () => { + // Act — add a second step and pick the first available form of the case definition + await formFlowsPage.addVisualStep(); + await expect(formFlowsPage.visualStepKeyInput).toHaveValue('step-2'); + // The new step is not the start step, so it offers the make-start action + await expect(formFlowsPage.visualMakeStartStepButton).toBeVisible(); + await formFlowsPage.selectFirstVisualFormDefinition(); + + // Act — connect the first step to the new step + await formFlowsPage.selectVisualStep(0); + await formFlowsPage.visualAddTransitionButton.click(); + await formFlowsPage.selectVisualTransitionTarget(0, 'step-2'); + + await expect(formFlowsPage.saveButton).toBeEnabled(); + const response = await formFlowsPage.saveFormFlow(formFlowTestData.key, CASE_IDENTIFIER); + + // Assert + expect(response.ok()).toBeTruthy(); + await formFlowsPage.assertSaveSuccessNotification(formFlowTestData.key); + + // Assert — the transition is part of the persisted definition shown in the JSON editor + await formFlowsPage.openJsonEditorTab(); + await expect(formFlowsPage.monacoEditor).toContainText('step-2'); + }); + }); + // ─── Delete form flow (cleanup) ─────────────────────────────────── test.describe('Delete form flow', () => { diff --git a/e2e/tests/case-details-management-form-flows/page.ts b/e2e/tests/case-details-management-form-flows/page.ts index 64bac6dfab..7a843ed763 100644 --- a/e2e/tests/case-details-management-form-flows/page.ts +++ b/e2e/tests/case-details-management-form-flows/page.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {APIRequestContext, expect, Page} from '@playwright/test'; +import {APIRequestContext, expect, Locator, Page} from '@playwright/test'; import {CarbonList, CarbonListRow} from '../../shared/carbon-list/carbon-list.utils'; import * as ApiUtils from '../../utils/api.utils'; import {ensureDraftVersionSelected, getVersionFromUrl} from '../../utils/version.utils'; @@ -63,7 +63,11 @@ export class CaseDetailsManagementFormFlowsPage { // Create modal: only a key field, no data-test-ids get formFlowKeyInput() { - return this.page.locator('cds-modal').locator('cds-label').filter({hasText: 'Key'}).locator('input'); + return this.page + .locator('cds-modal') + .locator('cds-label') + .filter({hasText: 'Key'}) + .locator('input'); } get createFormFlowButton() { @@ -131,6 +135,91 @@ export class CaseDetailsManagementFormFlowsPage { return this.page.locator('.monaco-editor').first(); } + // The editor page opens on the visual editor tab; the JSON (Monaco) editor lives in its own tab. + async openJsonEditorTab() { + await this.page.getByRole('tab', {name: 'JSON editor'}).click(); + await expect(this.monacoEditor).toBeVisible(); + } + + // ─── Visual Editor Elements ──────────────────────────────────────── + + get visualStepList() { + return this.page.locator('[data-test-id="formFlowEditorStepList"]'); + } + + get visualStepListItems() { + return this.page.locator('[data-test-id="formFlowEditorStepListItem"]'); + } + + get visualAddStepButton() { + return this.page.locator('[data-test-id="formFlowEditorAddStepButton"]'); + } + + get visualStepKeyInput() { + return this.page.locator('[data-test-id="formFlowEditorStepKeyInput"]'); + } + + get visualStepTitleInput() { + return this.page.locator('[data-test-id="formFlowEditorStepTitleInput"]'); + } + + get visualStepPropertyInput() { + return this.page.locator('[data-test-id="formFlowEditorStepPropertyInput"]'); + } + + get visualFormDefinitionDropdown() { + return this.page.locator('[data-test-id="formFlowEditorStepPropertyDropdown"]'); + } + + get visualMakeStartStepButton() { + return this.page.locator('[data-test-id="formFlowEditorMakeStartStepButton"]'); + } + + get visualAddTransitionButton() { + return this.page.locator('[data-test-id="formFlowEditorAddTransitionButton"]'); + } + + get visualTransitionRow() { + return this.page.locator('[data-test-id="formFlowEditorTransitionRow"]'); + } + + // ─── Visual Editor Actions ───────────────────────────────────────── + + async openVisualEditorTab() { + await this.page.getByRole('tab', {name: 'Editor', exact: true}).click(); + await expect(this.visualStepList).toBeVisible(); + } + + async selectVisualStep(index: number) { + await this.visualStepListItems.nth(index).click(); + } + + async addVisualStep() { + await this.visualAddStepButton.click(); + await expect(this.visualStepKeyInput).toBeVisible(); + } + + async selectVisualTransitionTarget(rowIndex: number, targetStepKey: string) { + await this.selectCarbonDropdownOption(this.visualTransitionRow.nth(rowIndex), targetStepKey); + } + + // Picks the first available form of the case definition and returns its name. + async selectFirstVisualFormDefinition(): Promise { + return this.selectCarbonDropdownOption(this.visualFormDefinitionDropdown); + } + + // Opens the Carbon dropdown inside `root` and picks the named option, or the first option when + // no name is given. Returns the picked option's text. + private async selectCarbonDropdownOption(root: Locator, optionName?: string): Promise { + await root.locator('cds-dropdown button').first().click(); + const option = optionName + ? this.page.getByRole('option', {name: optionName}) + : this.page.getByRole('option').first(); + const name = (await option.textContent())?.trim() ?? ''; + await option.click(); + return name; + } + // ─── Save Actions ───────────────────────────────────────────────── async editFormFlowJson(json: object) { @@ -157,9 +246,9 @@ export class CaseDetailsManagementFormFlowsPage { } async assertSaveSuccessNotification(key: string) { - await expect( - this.page.getByText(`${key} was saved successfully`).first() - ).toBeVisible({timeout: 15_000}); + await expect(this.page.getByText(`${key} was saved successfully`).first()).toBeVisible({ + timeout: 15_000, + }); } // ─── Assertions ─────────────────────────────────────────────────── @@ -176,6 +265,10 @@ export class CaseDetailsManagementFormFlowsPage { await expect(this.page.locator('valtimo-editor')).toBeVisible(); } + async assertEditorPageVisible() { + await expect(this.page.getByRole('tab', {name: 'JSON editor'})).toBeVisible(); + } + // ─── API Cleanup ────────────────────────────────────────────────── async deleteFormFlowViaApi(key: string) { diff --git a/frontend/projects/valtimo/admin-settings/src/lib/models/accent-colors.model.ts b/frontend/projects/valtimo/admin-settings/src/lib/models/accent-colors.model.ts index a93fb63dbb..c22e47c80f 100644 --- a/frontend/projects/valtimo/admin-settings/src/lib/models/accent-colors.model.ts +++ b/frontend/projects/valtimo/admin-settings/src/lib/models/accent-colors.model.ts @@ -14,13 +14,10 @@ * limitations under the License. */ -interface AccentColorsDto { - colors: {[key: string]: string}; -} - interface AccentColorDefinition { cssVar: string; labelTranslationKey: string; } -export {AccentColorsDto, AccentColorDefinition}; +export {AccentColorsDto} from '@valtimo/shared'; +export {AccentColorDefinition}; diff --git a/frontend/projects/valtimo/admin-settings/src/lib/models/feature-toggle.model.ts b/frontend/projects/valtimo/admin-settings/src/lib/models/feature-toggle.model.ts index 886ee4830b..8e8bdf2961 100644 --- a/frontend/projects/valtimo/admin-settings/src/lib/models/feature-toggle.model.ts +++ b/frontend/projects/valtimo/admin-settings/src/lib/models/feature-toggle.model.ts @@ -16,17 +16,9 @@ import {ValtimoConfigFeatureToggles} from '@valtimo/shared'; -interface FeatureToggleOverridesDto { - overrides: {[key: string]: boolean}; -} - -interface UpdateFeatureToggleDto { - key: string; - enabled: boolean; -} - interface FeatureToggleDefinition { key: keyof ValtimoConfigFeatureToggles; } -export {FeatureToggleOverridesDto, UpdateFeatureToggleDto, FeatureToggleDefinition}; +export {FeatureToggleOverridesDto, UpdateFeatureToggleDto} from '@valtimo/shared'; +export {FeatureToggleDefinition}; diff --git a/frontend/projects/valtimo/building-block-management/src/lib/building-block-management-routing.ts b/frontend/projects/valtimo/building-block-management/src/lib/building-block-management-routing.ts index bb597d9d66..878ce971fe 100644 --- a/frontend/projects/valtimo/building-block-management/src/lib/building-block-management-routing.ts +++ b/frontend/projects/valtimo/building-block-management/src/lib/building-block-management-routing.ts @@ -17,6 +17,7 @@ import {NgModule} from '@angular/core'; import {RouterModule, Routes} from '@angular/router'; import {CommonModule} from '@angular/common'; +import {pendingChangesGuard} from '@valtimo/components'; import {AuthGuardService} from '@valtimo/security'; import {ROLE_ADMIN} from '@valtimo/shared'; import {BuildingBlockManagementListComponent} from './components/building-block-management-list/building-block-management-list.component'; @@ -84,6 +85,19 @@ const routes: Routes = [ path: `building-block-management/building-block/:buildingBlockDefinitionKey/version/:buildingBlockDefinitionVersionTag/${BUILDING_BLOCK_MANAGEMENT_TABS.FORM_FLOWS}/:formFlowDefinitionKey`, component: FormFlowEditorComponent, canActivate: [AuthGuardService], + canDeactivate: [pendingChangesGuard], + data: { + title: 'formFlow.title', + roles: [ROLE_ADMIN], + customPageTitle: true, + context: 'buildingBlock', + }, + }, + { + path: `building-block-management/building-block/:buildingBlockDefinitionKey/version/:buildingBlockDefinitionVersionTag/${BUILDING_BLOCK_MANAGEMENT_TABS.FORM_FLOWS}/:formFlowDefinitionKey/json-editor`, + component: FormFlowEditorComponent, + canActivate: [AuthGuardService], + canDeactivate: [pendingChangesGuard], data: { title: 'formFlow.title', roles: [ROLE_ADMIN], diff --git a/frontend/projects/valtimo/case-management/src/lib/case-management-routing.module.ts b/frontend/projects/valtimo/case-management/src/lib/case-management-routing.module.ts index 50ab94b743..7b71d5484c 100644 --- a/frontend/projects/valtimo/case-management/src/lib/case-management-routing.module.ts +++ b/frontend/projects/valtimo/case-management/src/lib/case-management-routing.module.ts @@ -162,6 +162,14 @@ const routes: Routes = [ path: `case-management/case/:caseDefinitionKey/version/:caseDefinitionVersionTag/${TabEnum.FORM_FLOWS}/:formFlowDefinitionKey`, component: FormFlowEditorComponent, canActivate: [AuthGuardService], + canDeactivate: [pendingChangesGuard], + data: {title: 'Form flow details', roles: [ROLE_ADMIN], customPageTitle: true}, + }, + { + path: `case-management/case/:caseDefinitionKey/version/:caseDefinitionVersionTag/${TabEnum.FORM_FLOWS}/:formFlowDefinitionKey/json-editor`, + component: FormFlowEditorComponent, + canActivate: [AuthGuardService], + canDeactivate: [pendingChangesGuard], data: {title: 'Form flow details', roles: [ROLE_ADMIN], customPageTitle: true}, }, ]; diff --git a/frontend/projects/valtimo/case-management/src/lib/models/startable-item.model.ts b/frontend/projects/valtimo/case-management/src/lib/models/startable-item.model.ts index 983cc6c1ff..d13a9d8a12 100644 --- a/frontend/projects/valtimo/case-management/src/lib/models/startable-item.model.ts +++ b/frontend/projects/valtimo/case-management/src/lib/models/startable-item.model.ts @@ -16,6 +16,8 @@ import {BuildingBlockInputMapping, BuildingBlockOutputMapping} from '@valtimo/process-link'; +export {StartableItemOrderEntry, UpdateStartableItemOrderRequest} from '@valtimo/shared'; + export enum StartableItemType { PROCESS = 'PROCESS', BUILDING_BLOCK = 'BUILDING_BLOCK', @@ -30,17 +32,6 @@ export interface ManagementStartableItem { sortOrder: number | null; } -export interface StartableItemOrderEntry { - key: string; - type: StartableItemType; - versionTag: string | null; - sortOrder: number; -} - -export interface UpdateStartableItemOrderRequest { - items: StartableItemOrderEntry[]; -} - export interface CreateStartableItemRequest { type: StartableItemType; properties: CreateStartableItemProcessProperties | CreateStartableItemBuildingBlockProperties; diff --git a/frontend/projects/valtimo/case/src/lib/models/case-inspection.models.ts b/frontend/projects/valtimo/case/src/lib/models/case-inspection.models.ts index 136693b944..fbcce52375 100644 --- a/frontend/projects/valtimo/case/src/lib/models/case-inspection.models.ts +++ b/frontend/projects/valtimo/case/src/lib/models/case-inspection.models.ts @@ -15,6 +15,12 @@ */ import {CaseTag, Document, DocumentDefinitionId, RelatedFile} from '@valtimo/document'; +import { + BuildingBlockInstanceDto as BuildingBlockInstance, + BuildingBlockProcessReference, + ProcessVariableMutationRequest, + ProcessVariableType, +} from '@valtimo/shared'; interface DocumentInspection { id: string; @@ -57,14 +63,6 @@ interface ProcessVariable { value: unknown; } -type ProcessVariableType = 'STRING' | 'INTEGER' | 'LONG' | 'DOUBLE' | 'BOOLEAN' | 'JSON'; - -interface ProcessVariableMutationRequest { - name: string; - type: ProcessVariableType; - value: unknown; -} - type ProcessJobType = 'TIMER' | 'ASYNC_CONTINUATION' | 'MESSAGE' | 'BATCH' | 'OTHER'; interface ProcessJob { @@ -88,13 +86,6 @@ interface ProcessTask { taskDefinitionKey: string | null; } -interface BuildingBlockProcessReference { - instanceId: string; - definitionKey: string; - definitionVersionTag: string; - documentId: string; -} - interface ProcessInstanceInspection { processInstanceId: string; processDefinitionId: string | null; @@ -113,19 +104,6 @@ interface ProcessInstanceInspection { buildingBlock: BuildingBlockProcessReference | null; } -interface BuildingBlockInstance { - id: string; - documentId: string; - caseDocumentId: string | null; - definitionKey: string; - definitionVersionTag: string; - activityId: string | null; - callerProcessDefinitionId: string | null; - processInstanceId: string | null; - parentBuildingBlockInstanceId: string | null; - rootBuildingBlockInstanceId: string | null; -} - interface ModifyDocumentRequest { documentId: string; content: object; diff --git a/frontend/projects/valtimo/components/src/lib/models/choice-field.model.ts b/frontend/projects/valtimo/components/src/lib/models/choice-field.model.ts index a6e567bc48..4344ac0837 100644 --- a/frontend/projects/valtimo/components/src/lib/models/choice-field.model.ts +++ b/frontend/projects/valtimo/components/src/lib/models/choice-field.model.ts @@ -14,18 +14,10 @@ * limitations under the License. */ -export interface ChoiceField { - id: number; - keyName: string; - title: string; -} +import {ChoiceFieldValue as ChoiceFieldValueDto} from '@valtimo/shared'; + +export {ChoiceField} from '@valtimo/shared'; -export interface ChoiceFieldValue { - id: number; - choiceField: ChoiceField; - deprecated: boolean; - name: string; - sortOrder: number; - value: string; - deprecatedDisplayString: string; +export interface ChoiceFieldValue extends ChoiceFieldValueDto { + deprecatedDisplayString?: string; } diff --git a/frontend/projects/valtimo/document/src/lib/models/document.model.ts b/frontend/projects/valtimo/document/src/lib/models/document.model.ts index 3ef402b710..edb623b516 100644 --- a/frontend/projects/valtimo/document/src/lib/models/document.model.ts +++ b/frontend/projects/valtimo/document/src/lib/models/document.model.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {DisplayType} from '@valtimo/shared'; +import {DisplayType, StartableItemType} from '@valtimo/shared'; import {CaseTag} from './case-tags.model'; interface SortResult { @@ -148,8 +148,6 @@ interface ProcessDefinitionCaseDefinition { draft?: boolean; } -type StartableItemType = 'PROCESS' | 'BUILDING_BLOCK'; - interface StartableItem { type: StartableItemType; name: string | null; diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/form-flow-editor.component.html b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/form-flow-editor.component.html index 3a58c60d4e..85050fb5bc 100644 --- a/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/form-flow-editor.component.html +++ b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/form-flow-editor.component.html @@ -14,21 +14,48 @@ ~ limitations under the License. --> -@if ({model: model$ | async, schema: formFlowSchemaJson$ | async}; as obs) { - @if (obs.model && obs.schema) { - - } @else { - - } -} + + + + + + + + + +
+ @if ($activeTab() === FormFlowEditorTab.EDITOR) { + + } + + @if ($activeTab() === FormFlowEditorTab.JSON_EDITOR) { + + } +
+
@@ -57,7 +84,12 @@ - + + diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-expression-help-modal/form-flow-expression-help-modal.component.scss b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-expression-help-modal/form-flow-expression-help-modal.component.scss new file mode 100644 index 0000000000..e266522e9e --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-expression-help-modal/form-flow-expression-help-modal.component.scss @@ -0,0 +1,44 @@ +/*! + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.expression-help { + display: flex; + flex-direction: column; + gap: var(--cds-spacing-04); + color: var(--cds-text-primary); + + h5 { + margin-top: var(--cds-spacing-03); + } + + p { + color: var(--cds-text-secondary); + } + + ul { + display: flex; + flex-direction: column; + gap: var(--cds-spacing-02); + list-style: disc; + padding-left: var(--cds-spacing-05); + color: var(--cds-text-secondary); + } + + code { + font-family: 'IBM Plex Mono', monospace; + color: var(--cds-text-primary); + } +} diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-expression-help-modal/form-flow-expression-help-modal.component.ts b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-expression-help-modal/form-flow-expression-help-modal.component.ts new file mode 100644 index 0000000000..2e22dafeb3 --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-expression-help-modal/form-flow-expression-help-modal.component.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {CommonModule} from '@angular/common'; +import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from '@angular/core'; +import {TranslateModule} from '@ngx-translate/core'; +import {ValtimoCdsModalDirective} from '@valtimo/components'; +import {FormFlowRegistryDto} from '@valtimo/shared'; +import {ButtonModule, ModalModule} from 'carbon-components-angular'; +import {FormFlowContextPropertiesComponent} from '../form-flow-context-properties/form-flow-context-properties.component'; + +/** + * Explains how SpEL expressions work in a form flow — conditions, action hooks and the data they + * can access — in a modal, so the editor itself only needs a compact pointer to this help. + */ +@Component({ + standalone: true, + selector: 'valtimo-form-flow-expression-help-modal', + templateUrl: './form-flow-expression-help-modal.component.html', + styleUrls: ['./form-flow-expression-help-modal.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + TranslateModule, + ButtonModule, + ModalModule, + ValtimoCdsModalDirective, + FormFlowContextPropertiesComponent, + ], +}) +export class FormFlowExpressionHelpModalComponent { + @Input() public open = false; + @Input() public registry: FormFlowRegistryDto | null = null; + + @Output() public closeEvent = new EventEmitter(); +} diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-expression-list/form-flow-expression-list.component.html b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-expression-list/form-flow-expression-list.component.html new file mode 100644 index 0000000000..59928f3824 --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-expression-list/form-flow-expression-list.component.html @@ -0,0 +1,76 @@ + + +
+
+
{{ label }}
+ +

{{ description }}

+
+ + @for (control of expressionControls; track control; let index = $index) { +
+ + + @if (!readOnly) { + + } +
+ } @empty { +

{{ 'formFlow.uiEditor.noExpressions' | translate }}

+ } + + @if (!readOnly) { +
+ + + + {{ + 'formFlow.uiEditor.blankExpression' | translate + }} + + @for (suggestion of suggestions; track suggestion.label) { + {{ + suggestion.label + }} + } + +
+ } +
diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-expression-list/form-flow-expression-list.component.scss b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-expression-list/form-flow-expression-list.component.scss new file mode 100644 index 0000000000..52a9239374 --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-expression-list/form-flow-expression-list.component.scss @@ -0,0 +1,55 @@ +/*! + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.expression-list { + display: flex; + flex-direction: column; + gap: var(--cds-spacing-03); + + &__header { + h6 { + margin: 0; + } + } + + &__description { + color: var(--cds-text-secondary); + font-size: var(--cds-label-01-font-size, 0.75rem); + margin: var(--cds-spacing-02) 0 0 0; + } + + &__row { + display: flex; + align-items: center; + gap: var(--cds-spacing-03); + + input { + flex: 1; + font-family: 'IBM Plex Mono', monospace; + } + } + + &__empty { + color: var(--cds-text-helper); + font-size: var(--cds-body-compact-01-font-size, 0.875rem); + margin: 0; + } + + // On top of the list gap, so the add action stands slightly apart from the rows. + &__add { + margin-top: var(--cds-spacing-03); + } +} diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-expression-list/form-flow-expression-list.component.ts b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-expression-list/form-flow-expression-list.component.ts new file mode 100644 index 0000000000..d669b63f11 --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-expression-list/form-flow-expression-list.component.ts @@ -0,0 +1,103 @@ +/* + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {CommonModule} from '@angular/common'; +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {FormArray, FormControl, ReactiveFormsModule} from '@angular/forms'; +import {TranslateModule} from '@ngx-translate/core'; +import {Add16, TrashCan16} from '@carbon/icons'; +import { + OverflowMenuComponent, + OverflowMenuOptionComponent, + OverflowMenuTriggerComponent, +} from '@valtimo/components'; +import {FormFlowExpressionMethodDto, FormFlowRegistryDto} from '@valtimo/shared'; +import {ButtonModule, IconModule, IconService, InputModule} from 'carbon-components-angular'; +import {FORM_FLOW_EDITOR_TEST_IDS} from '../../../../../constants'; +import {FormFlowEditorFormService} from '../../../../../services/form-flow-editor-form.service'; + +interface ExpressionSuggestion { + label: string; + expression: string; +} + +/** + * Editable list of SpEL expressions for one of a step's lifecycle hooks (on open, on complete, + * on back). The add-menu offers the expression beans from the form flow registry as ready-made + * templates next to a blank expression. + */ +@Component({ + standalone: true, + selector: 'valtimo-form-flow-expression-list', + templateUrl: './form-flow-expression-list.component.html', + styleUrls: ['./form-flow-expression-list.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + ReactiveFormsModule, + TranslateModule, + ButtonModule, + IconModule, + InputModule, + OverflowMenuComponent, + OverflowMenuOptionComponent, + OverflowMenuTriggerComponent, + ], +}) +export class FormFlowExpressionListComponent { + @Input() public expressions!: FormArray; + @Input() public label = ''; + @Input() public description = ''; + @Input() public readOnly: boolean | null = false; + + @Input() public set registry(registry: FormFlowRegistryDto | null) { + this.suggestions = (registry?.expressionBeans ?? []).flatMap(bean => + bean.methods.map(method => { + const call = `${bean.name}.${this.formatMethod(method)}`; + return {label: call, expression: `\${${call}}`}; + }) + ); + } + + public suggestions: ExpressionSuggestion[] = []; + + protected readonly testIds = FORM_FLOW_EDITOR_TEST_IDS; + + constructor( + private readonly formService: FormFlowEditorFormService, + private readonly iconService: IconService + ) { + this.iconService.registerAll([Add16, TrashCan16]); + } + + public get expressionControls(): FormControl[] { + return this.expressions.controls as FormControl[]; + } + + public addExpression(expression = ''): void { + this.expressions.push(this.formService.buildExpressionControl(expression)); + this.expressions.markAsDirty(); + } + + public removeExpression(index: number): void { + this.expressions.removeAt(index); + this.expressions.markAsDirty(); + } + + private formatMethod(method: FormFlowExpressionMethodDto): string { + return `${method.name}(${method.parameters.map(parameter => parameter.name).join(', ')})`; + } +} diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-step-detail/form-flow-step-detail.component.html b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-step-detail/form-flow-step-detail.component.html new file mode 100644 index 0000000000..93609a2bfd --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-step-detail/form-flow-step-detail.component.html @@ -0,0 +1,273 @@ + + +
+
+
+
{{ 'formFlow.uiEditor.stepDetails' | translate }}
+ + @if (isStartStep) { + {{ 'formFlow.uiEditor.startStep' | translate }} + } +
+ + @if (!readOnly) { +
+ @if (!isStartStep) { + + } + + +
+ } +
+ +
+ + + {{ 'formFlow.key' | translate }} + + + + + + + + + + {{ 'formFlow.uiEditor.title' | translate }} + + + + + + + +
+ + + + + + + {{ 'formFlow.uiEditor.stepType' | translate }} + + + + +
+ + + @for (propertyName of propertyNames; track propertyName) { + @if (isFormDefinitionProperty(propertyName)) { +
+ + + + + + + {{ getPropertyLabel(propertyName) }} + + + + +
+ } @else if (isCustomComponentProperty(propertyName)) { +
+ + + + + + + {{ getPropertyLabel(propertyName) }} + + + + +
+ } @else { + + {{ getPropertyLabel(propertyName) }} + + + + } + } +
+
+ +
+ +
+
+
{{ 'formFlow.uiEditor.navigation' | translate }}
+ +

+ {{ 'formFlow.uiEditor.navigationDescription' | translate }} +

+
+ + + + +
+ +
+ +
+
+
{{ 'formFlow.uiEditor.actions' | translate }}
+ +

+ {{ 'formFlow.uiEditor.actionsDescription' | translate }} +

+
+ + + + + + + + +
+ + +
diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-step-detail/form-flow-step-detail.component.scss b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-step-detail/form-flow-step-detail.component.scss new file mode 100644 index 0000000000..8b84167954 --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-step-detail/form-flow-step-detail.component.scss @@ -0,0 +1,97 @@ +/*! + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Spacing is gap-driven on a single scale: 24px between the top-level blocks, 16px between the +// blocks inside a section, 12px inside lists and 4px for heading/label clusters. Elements carry +// no individual margins, so gaps never compound. +.step-detail { + display: flex; + flex-direction: column; + gap: var(--cds-spacing-06); + + &__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--cds-spacing-05); + } + + &__heading { + display: flex; + align-items: center; + gap: var(--cds-spacing-03); + + cds-tag { + margin: 0; + } + } + + &__header-actions { + display: flex; + align-items: center; + gap: var(--cds-spacing-03); + } + + &__grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--cds-spacing-05); + + cds-label { + margin-bottom: 0; + } + } + + &__divider { + border: none; + border-top: 1px solid var(--cds-border-subtle); + margin: 0; + width: 100%; + } + + &__section { + display: flex; + flex-direction: column; + gap: var(--cds-spacing-05); + } + + &__section-header { + h5 { + margin: 0 0 var(--cds-spacing-02) 0; + } + } + + &__section-description { + color: var(--cds-text-secondary); + font-size: var(--cds-body-compact-01-font-size, 0.875rem); + margin: 0; + } + + // Carbon puts the cds--actionable-notification class (with its max-inline-size cap and margin) + // on the host element itself, so the override targets the host directly. + &__help-notification { + display: block; + margin: 0; + max-inline-size: none; + max-width: none; + } + + &__label { + display: inline-flex; + align-items: center; + gap: var(--cds-spacing-02); + } +} diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-step-detail/form-flow-step-detail.component.ts b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-step-detail/form-flow-step-detail.component.ts new file mode 100644 index 0000000000..be155b4ed8 --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-step-detail/form-flow-step-detail.component.ts @@ -0,0 +1,256 @@ +/* + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {CommonModule} from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + signal, + SimpleChanges, +} from '@angular/core'; +import {FormArray, FormGroup, ReactiveFormsModule} from '@angular/forms'; +import {TranslateModule, TranslateService} from '@ngx-translate/core'; +import {Flag16, TrashCan16} from '@carbon/icons'; +import {FormFlowRegistryDto} from '@valtimo/shared'; +import {TooltipIconModule} from '@valtimo/components'; +import { + ButtonModule, + DropdownModule, + IconModule, + IconService, + InputModule, + LayerModule, + ListItem, + NotificationAction, + NotificationModule, + TagModule, +} from 'carbon-components-angular'; +import {Subscription} from 'rxjs'; +import {FORM_FLOW_EDITOR_TEST_IDS} from '../../../../../constants'; +import {FormFlowEditorFormService} from '../../../../../services/form-flow-editor-form.service'; +import {translateWithFallback} from '../../../../../utils'; +import {FormFlowExpressionHelpModalComponent} from '../form-flow-expression-help-modal/form-flow-expression-help-modal.component'; +import {FormFlowExpressionListComponent} from '../form-flow-expression-list/form-flow-expression-list.component'; +import {FormFlowTransitionListComponent} from '../form-flow-transition-list/form-flow-transition-list.component'; + +/** + * Detail editor for a single form flow step: its key, title, type with type-specific properties, + * outgoing transitions and the three lifecycle expression lists. + */ +@Component({ + standalone: true, + selector: 'valtimo-form-flow-step-detail', + templateUrl: './form-flow-step-detail.component.html', + styleUrls: ['./form-flow-step-detail.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + ReactiveFormsModule, + TranslateModule, + ButtonModule, + DropdownModule, + IconModule, + InputModule, + LayerModule, + NotificationModule, + TagModule, + TooltipIconModule, + FormFlowExpressionHelpModalComponent, + FormFlowExpressionListComponent, + FormFlowTransitionListComponent, + ], +}) +export class FormFlowStepDetailComponent implements OnInit, OnChanges, OnDestroy { + @Input() public stepGroup!: FormGroup; + @Input() public registry: FormFlowRegistryDto | null = null; + @Input() public stepKeys: string[] = []; + @Input() public formOptions: string[] = []; + @Input() public componentOptions: string[] = []; + @Input() public isStartStep = false; + @Input() public readOnly: boolean | null = false; + @Input() public duplicateKey = false; + + @Output() public deleteStepEvent = new EventEmitter(); + @Output() public makeStartEvent = new EventEmitter(); + + public stepTypeItems: ListItem[] = []; + + public readonly $showHelpModal = signal(false); + + // The "how do expressions work" action on the inline help notifications, opening the modal that + // holds the full explanation. + public helpActions: NotificationAction[] = []; + + protected readonly testIds = FORM_FLOW_EDITOR_TEST_IDS; + + // Cached per property: the Carbon dropdown resets its visual selection whenever its items array + // is swapped, so an array is only rebuilt when its options or its selection change. + private readonly _selectItemsCache = new Map< + string, + {options: string[]; value: string; items: ListItem[]} + >(); + + private _typeSubscription = new Subscription(); + + constructor( + private readonly formService: FormFlowEditorFormService, + private readonly iconService: IconService, + private readonly translateService: TranslateService + ) { + this.iconService.registerAll([Flag16, TrashCan16]); + } + + public ngOnInit(): void { + this.helpActions = [ + { + text: this.translateService.instant('formFlow.uiEditor.help.button'), + click: () => this.$showHelpModal.set(true), + }, + ]; + } + + public ngOnChanges(changes: SimpleChanges): void { + if (changes['stepGroup'] || changes['registry'] || changes['componentOptions']) { + this.buildStepTypeItems(); + } + + if (changes['stepGroup'] || changes['registry']) { + this.openTypeSubscription(); + } + } + + public ngOnDestroy(): void { + this._typeSubscription.unsubscribe(); + } + + public get propertiesGroup(): FormGroup { + return this.stepGroup.get('properties') as FormGroup; + } + + public get propertyNames(): string[] { + return Object.keys(this.propertiesGroup.controls); + } + + public get transitions(): FormArray { + return this.stepGroup.get('nextSteps') as FormArray; + } + + public getExpressions(hook: 'onOpen' | 'onComplete' | 'onBack'): FormArray { + return this.stepGroup.get(hook) as FormArray; + } + + public isControlInvalid(name: string): boolean { + const control = this.stepGroup.get(name); + return !!control && control.invalid && control.touched; + } + + public getPropertyLabel(name: string): string { + return translateWithFallback( + this.translateService, + `formFlow.uiEditor.properties.${name}`, + name + ); + } + + // The `definition` property of the built-in `form` step type references a form of the + // surrounding case definition or building block, so it is offered as a choice list. + public isFormDefinitionProperty(name: string): boolean { + return name === 'definition' && this.formOptions.length > 0; + } + + public getFormDefinitionItems(): ListItem[] { + return this.getSelectItems('definition', this.formOptions); + } + + // The `componentId` property of the built-in `custom-component` step type references an Angular + // component registered by the implementation, so the registered ids are offered as a choice list. + public isCustomComponentProperty(name: string): boolean { + return name === 'componentId' && this.componentOptions.length > 0; + } + + public getCustomComponentItems(): ListItem[] { + return this.getSelectItems('componentId', this.componentOptions); + } + + private getSelectItems(propertyName: string, options: string[]): ListItem[] { + const value = this.propertiesGroup.get(propertyName)?.value ?? ''; + const cached = this._selectItemsCache.get(propertyName); + if (cached && cached.options === options && cached.value === value) { + return cached.items; + } + + // A value that is no longer available stays selectable, so opening an older definition never + // silently changes it. + const names = options.includes(value) || !value ? options : [...options, value]; + const items = names.map(name => ({content: name, id: name, selected: name === value})); + this._selectItemsCache.set(propertyName, {options, value, items}); + return items; + } + + private buildStepTypeItems(): void { + const registryTypeNames = (this.registry?.stepTypes ?? []).map(stepType => stepType.name); + const currentTypeName = this.stepGroup?.get('typeName')?.value; + const typeNames = + currentTypeName && !registryTypeNames.includes(currentTypeName) + ? [...registryTypeNames, currentTypeName] + : registryTypeNames; + + this.stepTypeItems = typeNames.map(name => ({ + content: name, + id: name, + selected: name === currentTypeName, + // `custom-component` steps need a component registered by the implementation; without any, + // the type cannot be configured (unless the step already uses it). + disabled: + name === 'custom-component' && + this.componentOptions.length === 0 && + currentTypeName !== 'custom-component', + })); + } + + public getTypeTooltip(): string { + const tooltip = this.translateService.instant('formFlow.uiEditor.fieldTooltips.type'); + + return this.componentOptions.length === 0 + ? `${tooltip} ${this.translateService.instant('formFlow.uiEditor.fieldTooltips.typeNoComponents')}` + : tooltip; + } + + // Selecting another step type swaps the properties group for the controls that type needs, + // keeping the values of properties both types share. + private openTypeSubscription(): void { + this._typeSubscription.unsubscribe(); + this._typeSubscription = new Subscription(); + + if (!this.stepGroup || !this.registry) return; + + this._typeSubscription.add( + this.stepGroup.get('typeName')?.valueChanges.subscribe(typeName => { + const currentValues = this.propertiesGroup.getRawValue() as Record; + this.stepGroup.setControl( + 'properties', + this.formService.buildPropertiesGroup(typeName, currentValues, this.registry!) + ); + }) + ); + } +} diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-step-list/form-flow-step-list.component.html b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-step-list/form-flow-step-list.component.html new file mode 100644 index 0000000000..def2d6aa2e --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-step-list/form-flow-step-list.component.html @@ -0,0 +1,73 @@ + + +
+ {{ 'formFlow.uiEditor.stepsTitle' | translate }} + + @if (!readOnly) { + + } +
+ +
    + @for (step of steps; track $index) { +
  • + +
  • + } +
diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-step-list/form-flow-step-list.component.scss b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-step-list/form-flow-step-list.component.scss new file mode 100644 index 0000000000..1fe7a2774b --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-step-list/form-flow-step-list.component.scss @@ -0,0 +1,130 @@ +/*! + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// The component fills the editor sidebar: a fixed header with the add-step action and a list that +// scrolls on its own. +:host { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-height: 0; +} + +.step-list { + &__header { + align-items: center; + border-bottom: 1px solid var(--cds-border-subtle, #e0e0e0); + display: flex; + flex: 0 0 auto; + justify-content: space-between; + padding: 12px; + } + + &__title { + color: var(--cds-text-secondary, #525252); + font-size: 14px; + line-height: 24px; + font-weight: 600; + } + + &__list { + flex: 1 1 auto; + list-style: none; + margin: 0; + min-height: 0; + overflow-y: auto; + padding: 0; + } + + &__list-item { + margin: 0; + } + + &__item { + background: transparent; + border: 0; + border-left: 3px solid transparent; + cursor: pointer; + display: flex; + flex-direction: column; + gap: 0.375rem; + padding: 0.75rem 1rem; + text-align: left; + width: 100%; + + &:hover { + background: var(--cds-layer-hover-01, #e8e8e8); + } + + &:focus-visible { + outline: 2px solid var(--cds-focus, #0f62fe); + outline-offset: -2px; + } + + &--selected { + background: var(--cds-layer-selected-01, #e0e0e0); + border-left-color: var(--cds-border-interactive, #0f62fe); + + &:hover { + background: var(--cds-layer-selected-hover-01, #d1d1d1); + } + } + } + + &__item-heading { + align-items: center; + display: flex; + gap: 0.5rem; + justify-content: space-between; + max-width: 100%; + } + + // The step key is truncated with an ellipsis when it does not fit the fixed sidebar width. + &__item-key { + color: var(--cds-text-primary, #161616); + font-size: 0.875rem; + font-weight: 600; + line-height: 1.29; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &__item-warning { + fill: var(--cds-support-error, #da1e28); + flex-shrink: 0; + } + + &__item-title { + color: var(--cds-text-secondary, #525252); + font-size: 0.75rem; + line-height: 1.34; + } + + // The subtle ring makes the tags stand out from the hover and selected backgrounds. + &__item-tags { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 0.25rem; + + ::ng-deep .cds--tag { + margin: 0; + max-width: none; + box-shadow: 0 0 0 1px var(--cds-layer-01); + } + } +} diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-step-list/form-flow-step-list.component.ts b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-step-list/form-flow-step-list.component.ts new file mode 100644 index 0000000000..603c063eae --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-step-list/form-flow-step-list.component.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {CommonModule} from '@angular/common'; +import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from '@angular/core'; +import {TranslateModule} from '@ngx-translate/core'; +import {Add16, WarningFilled16} from '@carbon/icons'; +import {ButtonModule, IconModule, IconService, TagModule} from 'carbon-components-angular'; +import {FORM_FLOW_EDITOR_TEST_IDS} from '../../../../../constants'; + +interface FormFlowStepListItem { + key: string; + title: string; + typeName: string; + isStart: boolean; + invalid: boolean; +} + +/** Clickable overview of all steps in the flow, ordered as defined, with the start step first. */ +@Component({ + standalone: true, + selector: 'valtimo-form-flow-step-list', + templateUrl: './form-flow-step-list.component.html', + styleUrls: ['./form-flow-step-list.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CommonModule, TranslateModule, ButtonModule, IconModule, TagModule], +}) +export class FormFlowStepListComponent { + @Input() public steps: FormFlowStepListItem[] = []; + @Input() public selectedIndex: number | null = null; + @Input() public readOnly: boolean | null = false; + + @Output() public selectEvent = new EventEmitter(); + @Output() public addEvent = new EventEmitter(); + + protected readonly testIds = FORM_FLOW_EDITOR_TEST_IDS; + + constructor(private readonly iconService: IconService) { + this.iconService.registerAll([Add16, WarningFilled16]); + } +} + +export {FormFlowStepListItem}; diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-transition-list/form-flow-transition-list.component.html b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-transition-list/form-flow-transition-list.component.html new file mode 100644 index 0000000000..cc99b6fb9a --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-transition-list/form-flow-transition-list.component.html @@ -0,0 +1,122 @@ + + +
+ @for (transitionGroup of transitionGroups; track transitionGroup; let index = $index) { +
+
+ + + + + + + {{ 'formFlow.uiEditor.targetStep' | translate }} + + + + +
+ + + + {{ 'formFlow.uiEditor.condition' | translate }} + + + + + + + + @if (!readOnly) { +
+ + + + + +
+ } +
+ } @empty { +

{{ 'formFlow.uiEditor.noTransitions' | translate }}

+ } + + @if (transitions.hasError('multipleDefaultTransitions')) { +

+ {{ 'formFlow.uiEditor.multipleDefaultTransitions' | translate }} +

+ } + + @if (!readOnly) { +
+ +
+ } +
diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-transition-list/form-flow-transition-list.component.scss b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-transition-list/form-flow-transition-list.component.scss new file mode 100644 index 0000000000..33221aba8a --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-transition-list/form-flow-transition-list.component.scss @@ -0,0 +1,65 @@ +/*! + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.transition-list { + display: flex; + flex-direction: column; + gap: var(--cds-spacing-04); + + &__row { + display: flex; + align-items: flex-end; + gap: var(--cds-spacing-04); + padding: var(--cds-spacing-04); + border: 1px solid var(--cds-border-subtle); + } + + &__target { + flex: 0 0 220px; + } + + &__condition { + flex: 1; + margin-bottom: 0; + + input { + font-family: 'IBM Plex Mono', monospace; + } + } + + &__actions { + display: flex; + align-items: center; + } + + &__label { + display: inline-flex; + align-items: center; + gap: var(--cds-spacing-02); + } + + &__empty { + color: var(--cds-text-helper); + font-size: var(--cds-body-compact-01-font-size, 0.875rem); + margin: 0; + } + + &__error { + color: var(--cds-text-error); + font-size: var(--cds-label-01-font-size, 0.75rem); + margin: 0; + } +} diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-transition-list/form-flow-transition-list.component.ts b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-transition-list/form-flow-transition-list.component.ts new file mode 100644 index 0000000000..a3587c577b --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-transition-list/form-flow-transition-list.component.ts @@ -0,0 +1,120 @@ +/* + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {CommonModule} from '@angular/common'; +import {ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges} from '@angular/core'; +import {FormArray, FormGroup, ReactiveFormsModule} from '@angular/forms'; +import {TranslateModule} from '@ngx-translate/core'; +import {Add16, ArrowDown16, ArrowUp16, TrashCan16} from '@carbon/icons'; +import {TooltipIconModule} from '@valtimo/components'; +import { + ButtonModule, + DropdownModule, + IconModule, + IconService, + InputModule, + ListItem, +} from 'carbon-components-angular'; +import {FORM_FLOW_EDITOR_TEST_IDS} from '../../../../../constants'; +import {FormFlowEditorFormService} from '../../../../../services/form-flow-editor-form.service'; + +/** + * Editable list of a step's outgoing transitions. Transitions are evaluated top to bottom — the + * first entry whose condition holds is taken and a condition-less entry acts as the default — so + * the rows can be reordered. + */ +@Component({ + standalone: true, + selector: 'valtimo-form-flow-transition-list', + templateUrl: './form-flow-transition-list.component.html', + styleUrls: ['./form-flow-transition-list.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + ReactiveFormsModule, + TranslateModule, + ButtonModule, + DropdownModule, + IconModule, + InputModule, + TooltipIconModule, + ], +}) +export class FormFlowTransitionListComponent implements OnChanges { + @Input() public transitions!: FormArray; + @Input() public stepKeys: string[] = []; + @Input() public readOnly: boolean | null = false; + + protected readonly testIds = FORM_FLOW_EDITOR_TEST_IDS; + + // Per-row dropdown items, cached per transition group. The Carbon dropdown resets its visual + // selection whenever its items array is swapped, so the array is only rebuilt when the step keys + // or the row's own selection actually change. + private readonly _rowItems = new WeakMap< + FormGroup, + {keys: string[]; value: string; items: ListItem[]} + >(); + + constructor( + private readonly formService: FormFlowEditorFormService, + private readonly iconService: IconService + ) { + this.iconService.registerAll([Add16, ArrowDown16, ArrowUp16, TrashCan16]); + } + + public ngOnChanges(changes: SimpleChanges): void { + if (changes['stepKeys']) { + // A new keys reference invalidates every cached row; rows lazily rebuild on the next read. + this.transitionGroups.forEach(group => this._rowItems.delete(group)); + } + } + + public get transitionGroups(): FormGroup[] { + return this.transitions.controls as FormGroup[]; + } + + public getRowItems(transitionGroup: FormGroup): ListItem[] { + const value = transitionGroup.get('step')?.value ?? ''; + const cached = this._rowItems.get(transitionGroup); + if (cached && cached.keys === this.stepKeys && cached.value === value) { + return cached.items; + } + + const items = this.stepKeys.map(key => ({content: key, id: key, selected: key === value})); + this._rowItems.set(transitionGroup, {keys: this.stepKeys, value, items}); + return items; + } + + public addTransition(): void { + this.transitions.push(this.formService.buildTransitionGroup()); + this.transitions.markAsDirty(); + } + + public removeTransition(index: number): void { + this.transitions.removeAt(index); + this.transitions.markAsDirty(); + } + + public moveTransition(index: number, offset: -1 | 1): void { + const target = index + offset; + if (target < 0 || target >= this.transitions.length) return; + + const control = this.transitions.at(index); + this.transitions.removeAt(index); + this.transitions.insert(target, control); + this.transitions.markAsDirty(); + } +} diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-ui-editor-tab.component.html b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-ui-editor-tab.component.html new file mode 100644 index 0000000000..2ea4b364d1 --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-ui-editor-tab.component.html @@ -0,0 +1,90 @@ + + +@if ($parseFailed()) { +
+ +
+} @else if (form && registry) { +
+ @if ($definitionErrors().length) { +
+ +
+ } + + +
+ + +
+ @if (selectedStepGroup; as stepGroup) { + + } @else { +
+
+ {{ 'formFlow.uiEditor.noStepsTitle' | translate }} +
+ +

+ {{ 'formFlow.uiEditor.noStepsDescription' | translate }} +

+
+ } +
+
+
+} @else { +
+ +
+} diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-ui-editor-tab.component.scss b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-ui-editor-tab.component.scss new file mode 100644 index 0000000000..eba1e2fbdd --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-ui-editor-tab.component.scss @@ -0,0 +1,77 @@ +/*! + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.ui-editor { + // fitPage gives this container an explicit height; the sidebar and panel scroll independently + // inside it, so the container itself never scrolls. + display: flex; + border: 1px solid var(--cds-border-subtle, #e0e0e0); + overflow: hidden; + + &__sidebar { + background: var(--cds-layer-01, #f4f4f4); + border-right: 1px solid var(--cds-border-subtle, #e0e0e0); + display: flex; + flex: 0 0 22rem; + flex-direction: column; + min-height: 0; + } + + &__panel { + flex: 1 1 auto; + min-width: 0; + overflow-y: auto; + padding: 1rem 1.5rem 1.5rem; + } + + &__notification { + padding: 0 0 var(--cds-spacing-03) 0; + + cds-inline-notification { + display: block; + max-width: none; + margin: 0; + } + } + + &__empty { + align-items: center; + border: 1px dashed var(--cds-border-strong, #8d8d8d); + display: flex; + flex-direction: column; + gap: 0.5rem; + margin: 2rem auto; + max-width: 30rem; + padding: 2rem; + text-align: center; + } + + &__empty-title { + margin: 0; + } + + &__empty-message { + color: var(--cds-text-secondary, #525252); + margin: 0 0 0.5rem; + } + + &__loading { + display: flex; + width: 100%; + justify-content: center; + padding: var(--cds-spacing-07); + } +} diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-ui-editor-tab.component.ts b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-ui-editor-tab.component.ts new file mode 100644 index 0000000000..bf84e52d2c --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-ui-editor-tab.component.ts @@ -0,0 +1,417 @@ +/* + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {CommonModule} from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnChanges, + OnDestroy, + Output, + signal, + SimpleChanges, +} from '@angular/core'; +import {FormArray, FormGroup, ReactiveFormsModule} from '@angular/forms'; +import {ActivatedRoute} from '@angular/router'; +import {TranslateModule, TranslateService} from '@ngx-translate/core'; +import {EditorModel, FitPageDirective} from '@valtimo/components'; +import {FormDefinition, FormManagementService} from '@valtimo/form-management'; +import { + FormFlowRegistryDto, + getBuildingBlockManagementRouteParams, + getCaseManagementRouteParams, + getContextObservable, +} from '@valtimo/shared'; +import {LoadingModule, NotificationModule} from 'carbon-components-angular'; +import {catchError, map, of, Subscription, switchMap, take} from 'rxjs'; +import {FORM_FLOW_EDITOR_TEST_IDS} from '../../../../constants'; +import {FormFlowDefinition} from '../../../../models'; +import {FormFlowService} from '../../../../services'; +import {FormFlowComponentService} from '../../../../services/form-flow-component.service'; +import {FormFlowEditorFormService} from '../../../../services/form-flow-editor-form.service'; +import {FormFlowStepDetailComponent} from './form-flow-step-detail/form-flow-step-detail.component'; +import { + FormFlowStepListComponent, + FormFlowStepListItem, +} from './form-flow-step-list/form-flow-step-list.component'; + +/** + * The visual form flow editor. Renders the definition as a step list with a detail panel and emits + * the modified definition as JSON through the same contract as the JSON editor tab, so the + * surrounding editor page treats both tabs identically. + */ +@Component({ + standalone: true, + selector: 'valtimo-form-flow-ui-editor-tab', + templateUrl: './form-flow-ui-editor-tab.component.html', + styleUrls: ['./form-flow-ui-editor-tab.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [FormFlowEditorFormService], + imports: [ + CommonModule, + ReactiveFormsModule, + TranslateModule, + FitPageDirective, + LoadingModule, + NotificationModule, + FormFlowStepDetailComponent, + FormFlowStepListComponent, + ], +}) +export class FormFlowUiEditorTabComponent implements OnChanges, OnDestroy { + @Input() public model: EditorModel | null = null; + @Input() public readOnly: boolean | null = false; + + @Output() public validEvent = new EventEmitter(); + @Output() public valueChangeEvent = new EventEmitter(); + + public form: FormGroup | null = null; + public registry: FormFlowRegistryDto | null = null; + + public readonly $selectedIndex = signal(null); + public readonly $stepListItems = signal([]); + // Only replaced when the keys themselves change: the Carbon dropdown resets its visual + // selection whenever its items array is swapped, so a stable reference prevents flicker while + // typing in unrelated fields. + public readonly $stepKeys = signal([]); + public readonly $formOptions = signal([]); + public readonly $componentOptions = signal([]); + public readonly $definitionErrors = signal([]); + public readonly $parseFailed = signal(false); + + protected readonly testIds = FORM_FLOW_EDITOR_TEST_IDS; + + private _definition: FormFlowDefinition | null = null; + private _definitionKey = ''; + private _previousStepKeys: string[] = []; + private _formSubscriptions = new Subscription(); + private readonly _subscriptions = new Subscription(); + + constructor( + private readonly formService: FormFlowEditorFormService, + private readonly formFlowService: FormFlowService, + private readonly formFlowComponentService: FormFlowComponentService, + private readonly formManagementService: FormManagementService, + private readonly route: ActivatedRoute, + private readonly translateService: TranslateService + ) { + this._subscriptions.add( + this.formFlowService.getFormFlowRegistry().subscribe(registry => { + this.registry = registry; + this.buildForm(); + }) + ); + + // The custom Angular components an implementation has registered for `custom-component` + // steps. The editor offers their ids as choices. + this._subscriptions.add( + this.formFlowComponentService.supportedComponents$ + .pipe(take(1)) + .subscribe(components => + this.$componentOptions.set(components.map(component => component.id).sort()) + ) + ); + + this.loadFormOptions(); + } + + public ngOnChanges(changes: SimpleChanges): void { + if (changes['model']) { + this.parseModel(); + this.buildForm(); + } + + if (changes['readOnly'] && !changes['readOnly'].firstChange) { + this.applyReadOnly(); + } + } + + public ngOnDestroy(): void { + this._subscriptions.unsubscribe(); + this._formSubscriptions.unsubscribe(); + } + + public get selectedStepGroup(): FormGroup | null { + const index = this.$selectedIndex(); + if (this.form === null || index === null) return null; + + return (this.stepsArray.controls[index] as FormGroup) ?? null; + } + + public isSelectedKeyDuplicate(): boolean { + const index = this.$selectedIndex(); + if (index === null) return false; + + const keys = this.stepsArray.controls.map(stepGroup => stepGroup.get('key')?.value); + return keys.filter(key => key === keys[index]).length > 1; + } + + public isSelectedStepStart(): boolean { + return ( + !!this.selectedStepGroup && + this.selectedStepGroup.get('key')?.value === this.form?.get('startStep')?.value + ); + } + + public onMakeStartStep(): void { + const key = this.selectedStepGroup?.get('key')?.value; + if (key) { + this.form?.get('startStep')?.setValue(key); + this.form?.get('startStep')?.markAsDirty(); + } + } + + public onSelectStep(index: number): void { + this.$selectedIndex.set(index); + } + + public onAddStep(): void { + if (!this.form || !this.registry) return; + + const existingKeys = this.stepsArray.controls.map(stepGroup => stepGroup.get('key')?.value); + this.stepsArray.push(this.formService.buildNewStepGroup(existingKeys, this.registry)); + this.stepsArray.markAsDirty(); + this.$selectedIndex.set(this.stepsArray.length - 1); + } + + public onDeleteStep(): void { + const index = this.$selectedIndex(); + if (index === null || !this.form) return; + + const deletedKey = this.stepsArray.at(index).get('key')?.value; + this.stepsArray.removeAt(index); + this.stepsArray.markAsDirty(); + + // Deleting the start step promotes the first remaining step, so the definition stays valid. + const startStepControl = this.form.get('startStep'); + if (startStepControl?.value === deletedKey) { + startStepControl.setValue(this.stepsArray.at(0)?.get('key')?.value ?? ''); + } + + if (this.stepsArray.length === 0) { + this.$selectedIndex.set(null); + } else { + this.$selectedIndex.set(Math.min(index, this.stepsArray.length - 1)); + } + } + + private get stepsArray(): FormArray { + return this.formService.getStepsArray(this.form!); + } + + private parseModel(): void { + this._definition = null; + this.$parseFailed.set(false); + + if (!this.model?.value) return; + + try { + this._definition = JSON.parse(this.model.value) as FormFlowDefinition; + this._definitionKey = this._definition.key; + } catch { + this.$parseFailed.set(true); + } + } + + private buildForm(): void { + if (!this._definition || !this.registry) return; + + this._formSubscriptions.unsubscribe(); + this._formSubscriptions = new Subscription(); + + this.form = this.formService.buildDefinitionForm(this._definition, this.registry); + this._previousStepKeys = this.currentStepKeys(); + this.applyReadOnly(); + this.selectInitialStep(); + this.refreshDerivedState(); + + // The initial emission is the container's clean baseline; every change after that re-emits the + // serialized definition and its validity, mirroring the JSON editor tab. + this.emitState(); + + // Validity only changes together with values (validators run on value changes and the + // readOnly enable/disable path suppresses events), so valueChanges alone keeps every derived + // signal fresh. + this._formSubscriptions.add( + this.form.valueChanges.subscribe(() => { + this.trackStepKeyRenames(); + this.refreshDerivedState(); + this.emitState(); + }) + ); + } + + private selectInitialStep(): void { + const startStep = this._definition?.startStep; + const startIndex = (this._definition?.steps ?? []).findIndex(step => step.key === startStep); + + if ((this._definition?.steps ?? []).length === 0) { + this.$selectedIndex.set(null); + return; + } + + this.$selectedIndex.set(startIndex >= 0 ? startIndex : 0); + } + + private applyReadOnly(): void { + if (!this.form) return; + + if (this.readOnly) { + this.form.disable({emitEvent: false}); + } else { + this.form.enable({emitEvent: false}); + } + } + + // Renaming a step key rewrites the start step and every transition that targeted the old key, so + // the definition never breaks while typing a new key. + private trackStepKeyRenames(): void { + const currentKeys = this.currentStepKeys(); + + if (currentKeys.length === this._previousStepKeys.length) { + const changedIndices = currentKeys + .map((key, index) => (key !== this._previousStepKeys[index] ? index : -1)) + .filter(index => index !== -1); + + if (changedIndices.length === 1) { + const index = changedIndices[0]; + this.formService.renameStepReferences( + this.form!, + this._previousStepKeys[index], + currentKeys[index] + ); + } + } + + this._previousStepKeys = currentKeys; + } + + private currentStepKeys(): string[] { + return this.stepsArray.controls.map(stepGroup => stepGroup.get('key')?.value ?? ''); + } + + private refreshDerivedState(): void { + if (!this.form) return; + + const startStep = this.form.get('startStep')?.value; + const keys = this.currentStepKeys(); + + this.$stepListItems.set( + this.stepsArray.controls.map(stepGroup => ({ + key: stepGroup.get('key')?.value ?? '', + title: stepGroup.get('title')?.value ?? '', + typeName: stepGroup.get('typeName')?.value ?? '', + isStart: stepGroup.get('key')?.value === startStep, + invalid: stepGroup.invalid, + })) + ); + + if (!this.arraysEqual(keys, this.$stepKeys())) { + this.$stepKeys.set(keys); + } + + this.$definitionErrors.set(this.collectDefinitionErrors()); + } + + // Loads the form definitions of the surrounding case definition or building block, so the + // `form` step type can offer them as choices instead of a free-text form key. + private loadFormOptions(): void { + this._subscriptions.add( + getContextObservable(this.route) + .pipe( + take(1), + switchMap(context => { + if (context === 'buildingBlock') { + return getBuildingBlockManagementRouteParams(this.route).pipe( + take(1), + switchMap(params => + this.formManagementService.queryFormDefinitionsBuildingBlock( + params?.buildingBlockDefinitionKey ?? '', + params?.buildingBlockDefinitionVersionTag ?? '', + {size: 1000} + ) + ), + map(page => page.content) + ); + } + + return getCaseManagementRouteParams(this.route).pipe( + take(1), + switchMap(params => + this.formManagementService.queryFormDefinitionsCase( + params?.caseDefinitionKey ?? '', + params?.caseDefinitionVersionTag ?? '', + {size: 1000} + ) + ), + map(response => response.content as unknown as FormDefinition[]) + ); + }), + map(formDefinitions => + [...new Set(formDefinitions.map(definition => definition.name).filter(Boolean))].sort() + ), + catchError(() => of([] as string[])) + ) + .subscribe(names => this.$formOptions.set(names)) + ); + } + + private arraysEqual(left: string[], right: string[]): boolean { + return left.length === right.length && left.every((value, index) => value === right[index]); + } + + private collectDefinitionErrors(): string[] { + const errors: string[] = []; + const formErrors = this.form?.errors ?? {}; + const stepErrors = this.form?.get('steps')?.errors ?? {}; + + if (stepErrors['duplicateStepKeys']) { + errors.push( + this.translateService.instant('formFlow.uiEditor.errors.duplicateKeys', { + keys: stepErrors['duplicateStepKeys'].keys.join(', '), + }) + ); + } + + if (formErrors['startStepMissing']) { + errors.push( + this.translateService.instant('formFlow.uiEditor.errors.startStepMissing', { + startStep: formErrors['startStepMissing'].startStep, + }) + ); + } + + if (formErrors['unknownTransitionTargets']) { + errors.push( + this.translateService.instant('formFlow.uiEditor.errors.unknownTargets', { + targets: formErrors['unknownTransitionTargets'].targets.join(', '), + }) + ); + } + + return errors; + } + + private emitState(): void { + if (!this.form) return; + + const definition = this.formService.serialize(this.form, this._definitionKey); + this.valueChangeEvent.emit(JSON.stringify(definition)); + this.validEvent.emit(this.form.valid); + } +} diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/constants/form-flow-editor.test-ids.ts b/frontend/projects/valtimo/form-flow-management/src/lib/constants/form-flow-editor.test-ids.ts new file mode 100644 index 0000000000..d71a79400a --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/constants/form-flow-editor.test-ids.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const FORM_FLOW_EDITOR_TEST_IDS = { + saveButton: 'formFlowEditorSaveButton', + backButton: 'formFlowEditorBackButton', + stepList: 'formFlowEditorStepList', + stepListItem: 'formFlowEditorStepListItem', + addStepButton: 'formFlowEditorAddStepButton', + deleteStepButton: 'formFlowEditorDeleteStepButton', + makeStartStepButton: 'formFlowEditorMakeStartStepButton', + stepKeyInput: 'formFlowEditorStepKeyInput', + stepTitleInput: 'formFlowEditorStepTitleInput', + stepTypeDropdown: 'formFlowEditorStepTypeDropdown', + stepPropertyInput: 'formFlowEditorStepPropertyInput', + stepPropertyDropdown: 'formFlowEditorStepPropertyDropdown', + addTransitionButton: 'formFlowEditorAddTransitionButton', + transitionRow: 'formFlowEditorTransitionRow', + addExpressionButton: 'formFlowEditorAddExpressionButton', + expressionInput: 'formFlowEditorExpressionInput', +} as const; diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/constants/index.ts b/frontend/projects/valtimo/form-flow-management/src/lib/constants/index.ts new file mode 100644 index 0000000000..c8497ba667 --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/constants/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './form-flow-editor.test-ids'; +export * from './injection-tokens'; diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/constants/injection-tokens.ts b/frontend/projects/valtimo/form-flow-management/src/lib/constants/injection-tokens.ts new file mode 100644 index 0000000000..41cf1ceb13 --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/constants/injection-tokens.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {InjectionToken} from '@angular/core'; +import {FormFlowCustomComponentDefinition} from '../models'; + +const FORM_FLOW_COMPONENT_TOKEN = new InjectionToken>( + 'Supported form-flow Angular components', + {factory: () => []} +); + +export {FORM_FLOW_COMPONENT_TOKEN}; diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/form-flow-management.module.ts b/frontend/projects/valtimo/form-flow-management/src/lib/form-flow-management.module.ts index 112033cfbd..74a646fb13 100644 --- a/frontend/projects/valtimo/form-flow-management/src/lib/form-flow-management.module.ts +++ b/frontend/projects/valtimo/form-flow-management/src/lib/form-flow-management.module.ts @@ -36,17 +36,16 @@ import { LoadingModule, ModalModule, NotificationModule, + TabsModule, } from 'carbon-components-angular'; import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {FormFlowEditorComponent} from './components/editor/form-flow-editor.component'; +import {FormFlowJsonEditorTabComponent} from './components/editor/tabs/form-flow-json-editor-tab/form-flow-json-editor-tab.component'; +import {FormFlowUiEditorTabComponent} from './components/editor/tabs/form-flow-ui-editor-tab/form-flow-ui-editor-tab.component'; import {DeleteFormFlowModalComponent} from './components/delete-form-flow-modal/delete-form-flow-modal.component'; @NgModule({ - declarations: [ - FormFlowOverviewComponent, - FormFlowEditorComponent, - DeleteFormFlowModalComponent, - ], + declarations: [FormFlowOverviewComponent, FormFlowEditorComponent, DeleteFormFlowModalComponent], imports: [ CommonModule, ButtonModule, @@ -69,6 +68,9 @@ import {DeleteFormFlowModalComponent} from './components/delete-form-flow-modal/ DropdownModule, ConfirmationModalModule, NewFormFlowModalComponent, + TabsModule, + FormFlowJsonEditorTabComponent, + FormFlowUiEditorTabComponent, ], }) export class FormFlowManagementModule {} diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/models/form-flow-custom-component.model.ts b/frontend/projects/valtimo/form-flow-management/src/lib/models/form-flow-custom-component.model.ts new file mode 100644 index 0000000000..d055140af1 --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/models/form-flow-custom-component.model.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {EventEmitter, Type} from '@angular/core'; +import {FormioSubmission} from '@valtimo/components'; + +interface ChangeEvent { + data: object; +} + +interface FormFlowCustomComponent { + formFlowInstanceId: string; + componentId?: string; + disabled: boolean; + changeEvent: EventEmitter; + submitEvent: EventEmitter; +} + +interface FormFlowCustomComponentDefinition { + id: string; + component: Type; +} + +export {ChangeEvent, FormFlowCustomComponent, FormFlowCustomComponentDefinition}; diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/models/form-flow-editor.model.ts b/frontend/projects/valtimo/form-flow-management/src/lib/models/form-flow-editor.model.ts new file mode 100644 index 0000000000..4b430edcad --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/models/form-flow-editor.model.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +enum FormFlowEditorTab { + EDITOR = 'editor', + JSON_EDITOR = 'jsonEditor', +} + +export {FormFlowEditorTab}; diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/models/form-flow.model.ts b/frontend/projects/valtimo/form-flow-management/src/lib/models/form-flow.model.ts index ab333d14e9..e3ee748ab1 100644 --- a/frontend/projects/valtimo/form-flow-management/src/lib/models/form-flow.model.ts +++ b/frontend/projects/valtimo/form-flow-management/src/lib/models/form-flow.model.ts @@ -34,21 +34,23 @@ interface FormFlowDefinitionId { interface FormFlowStep { key: string; - nextSteps: Array; - onBack: Array; - onOpen: Array; - onComplete: Array; + title?: string; + nextStep?: string; + nextSteps?: Array; + onBack?: Array; + onOpen?: Array; + onComplete?: Array; type: FormFlowStepType; } interface FormFlowNextStep { - condition?: string; + condition?: string | null; step: string; } interface FormFlowStepType { name: string; - properties: FormStepTypeProperties | CustomComponentStepTypeProperties; + properties: FormStepTypeProperties | CustomComponentStepTypeProperties | Record; } interface FormStepTypeProperties { diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/models/index.ts b/frontend/projects/valtimo/form-flow-management/src/lib/models/index.ts index df757d9ff2..65a49688c8 100644 --- a/frontend/projects/valtimo/form-flow-management/src/lib/models/index.ts +++ b/frontend/projects/valtimo/form-flow-management/src/lib/models/index.ts @@ -15,3 +15,5 @@ */ export * from './form-flow.model'; +export * from './form-flow-custom-component.model'; +export * from './form-flow-editor.model'; diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/services/form-flow-component.service.ts b/frontend/projects/valtimo/form-flow-management/src/lib/services/form-flow-component.service.ts new file mode 100644 index 0000000000..425e3b3a02 --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/services/form-flow-component.service.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Inject, Injectable} from '@angular/core'; +import {BehaviorSubject, filter, Observable} from 'rxjs'; +import {FORM_FLOW_COMPONENT_TOKEN} from '../constants'; +import {FormFlowCustomComponentDefinition} from '../models'; + +@Injectable({ + providedIn: 'root', +}) +export class FormFlowComponentService { + private readonly _supportedComponents$ = + new BehaviorSubject | null>(null); + + public get supportedComponents$(): Observable> { + return this._supportedComponents$.pipe(filter(components => !!components)); + } + + constructor( + @Inject(FORM_FLOW_COMPONENT_TOKEN) + private readonly supportedCustomComponents: Array + ) { + this.setSupportedComponents(supportedCustomComponents); + } + + private setSupportedComponents( + supportedComponents: Array + ): void { + this._supportedComponents$.next(supportedComponents); + } +} diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/services/form-flow-download.service.ts b/frontend/projects/valtimo/form-flow-management/src/lib/services/form-flow-download.service.ts index d30e4befe4..2fe1baa8aa 100644 --- a/frontend/projects/valtimo/form-flow-management/src/lib/services/form-flow-download.service.ts +++ b/frontend/projects/valtimo/form-flow-management/src/lib/services/form-flow-download.service.ts @@ -18,10 +18,7 @@ import {FormFlowEditorParams} from '../models'; @Injectable({providedIn: 'root'}) export class FormFlowDownloadService { - public downloadJson( - json: object, - params: FormFlowEditorParams - ): void { + public downloadJson(json: object, params: FormFlowEditorParams): void { const sJson = JSON.stringify(json, null, 2); const element = document.createElement('a'); element.setAttribute('href', 'data:text/json;charset=UTF-8,' + encodeURIComponent(sJson)); diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/services/form-flow-editor-form.service.ts b/frontend/projects/valtimo/form-flow-management/src/lib/services/form-flow-editor-form.service.ts new file mode 100644 index 0000000000..08b8e1fed2 --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/services/form-flow-editor-form.service.ts @@ -0,0 +1,278 @@ +/* + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import { + AbstractControl, + FormArray, + FormBuilder, + FormControl, + FormGroup, + ValidationErrors, + ValidatorFn, + Validators, +} from '@angular/forms'; +import {FormFlowRegistryDto} from '@valtimo/shared'; +import {FormFlowDefinition, FormFlowNextStep, FormFlowStep} from '../models'; + +/** + * Builds and reads the reactive form tree that backs the visual form flow editor. + * + * The form mirrors the definition JSON: a `startStep` control and a `steps` array where every step + * has its key, title, type (name plus a dynamic properties group driven by the registry), + * transitions and the three expression lists. Serialization produces the same JSON contract the + * JSON editor tab emits, so both tabs are interchangeable to the surrounding editor page. + */ +@Injectable() +export class FormFlowEditorFormService { + constructor(private readonly fb: FormBuilder) {} + + public buildDefinitionForm( + definition: FormFlowDefinition, + registry: FormFlowRegistryDto + ): FormGroup { + return this.fb.group( + { + startStep: this.fb.control(definition.startStep ?? '', Validators.required), + steps: this.fb.array( + (definition.steps ?? []).map(step => this.buildStepGroup(step, registry)), + [uniqueStepKeysValidator] + ), + }, + {validators: [startStepExistsValidator, transitionTargetsExistValidator]} + ); + } + + public buildStepGroup(step: FormFlowStep, registry: FormFlowRegistryDto): FormGroup { + const typeName = step.type?.name ?? ''; + // The legacy single `nextStep` field is normalized into a `nextSteps` transition, mirroring + // how the backend interprets it. + const transitions = + step.nextSteps ?? (step.nextStep ? [{step: step.nextStep} as FormFlowNextStep] : []); + + return this.fb.group({ + key: this.fb.control(step.key ?? '', Validators.required), + title: this.fb.control(step.title ?? ''), + typeName: this.fb.control(typeName, Validators.required), + properties: this.buildPropertiesGroup( + typeName, + (step.type?.properties ?? {}) as Record, + registry + ), + nextSteps: this.fb.array( + transitions.map(transition => this.buildTransitionGroup(transition)), + [singleDefaultTransitionValidator] + ), + onOpen: this.buildExpressionArray(step.onOpen), + onComplete: this.buildExpressionArray(step.onComplete), + onBack: this.buildExpressionArray(step.onBack), + }); + } + + public buildNewStepGroup(existingKeys: string[], registry: FormFlowRegistryDto): FormGroup { + const defaultType = + registry.stepTypes.find(stepType => stepType.name === 'form')?.name ?? + registry.stepTypes[0]?.name ?? + 'form'; + + return this.buildStepGroup( + { + key: this.generateStepKey(existingKeys), + type: {name: defaultType, properties: {}}, + nextSteps: [], + }, + registry + ); + } + + /** + * Builds the dynamic properties group for a step type. Known types get one required control per + * registry property; unknown (custom) types keep controls for whatever properties the loaded + * definition already contained. Values for properties that exist in both the old and the new + * type are preserved. + */ + public buildPropertiesGroup( + typeName: string, + currentProperties: Record, + registry: FormFlowRegistryDto + ): FormGroup { + const registryStepType = registry.stepTypes.find(stepType => stepType.name === typeName); + const propertyNames = registryStepType + ? registryStepType.properties.map(property => property.name) + : Object.keys(currentProperties); + + return this.fb.group( + Object.fromEntries( + propertyNames.map(name => [ + name, + this.fb.control(currentProperties[name] ?? '', Validators.required), + ]) + ) + ); + } + + public buildTransitionGroup(transition?: FormFlowNextStep): FormGroup { + return this.fb.group({ + step: this.fb.control(transition?.step ?? '', Validators.required), + condition: this.fb.control(transition?.condition ?? ''), + }); + } + + public buildExpressionControl(expression = ''): FormControl { + return this.fb.control(expression, Validators.required); + } + + public serialize(form: FormGroup, definitionKey: string): FormFlowDefinition { + const value = form.getRawValue(); + + return { + key: definitionKey, + startStep: value.startStep, + steps: (value.steps as StepFormValue[]).map(step => ({ + key: step.key, + ...(step.title?.trim() ? {title: step.title.trim()} : {}), + type: { + name: step.typeName, + properties: step.properties, + }, + nextSteps: step.nextSteps.map(transition => ({ + step: transition.step, + ...(transition.condition?.trim() ? {condition: transition.condition.trim()} : {}), + })), + onBack: step.onBack, + onOpen: step.onOpen, + onComplete: step.onComplete, + })), + }; + } + + /** + * Rewrites every reference to a renamed step key: the start step and all transition targets. + * Used while the user types a new key, so the definition stays internally consistent. + */ + public renameStepReferences(form: FormGroup, oldKey: string, newKey: string): void { + if (!oldKey || oldKey === newKey) return; + + const startStepControl = form.get('startStep'); + if (startStepControl?.value === oldKey) { + startStepControl.setValue(newKey, {emitEvent: false}); + } + + this.getStepsArray(form).controls.forEach(stepGroup => { + (stepGroup.get('nextSteps') as FormArray).controls.forEach(transitionGroup => { + const stepControl = transitionGroup.get('step'); + if (stepControl?.value === oldKey) { + stepControl.setValue(newKey, {emitEvent: false}); + } + }); + }); + } + + public getStepsArray(form: FormGroup): FormArray { + return form.get('steps') as FormArray; + } + + private buildExpressionArray(expressions: string[] | undefined): FormArray { + return this.fb.array( + (expressions ?? []).map(expression => this.buildExpressionControl(expression)) + ); + } + + private generateStepKey(existingKeys: string[]): string { + let index = existingKeys.length + 1; + while (existingKeys.includes(`step-${index}`)) { + index++; + } + + return `step-${index}`; + } +} + +interface StepFormValue { + key: string; + title: string; + typeName: string; + properties: Record; + nextSteps: Array<{step: string; condition: string}>; + onOpen: string[]; + onComplete: string[]; + onBack: string[]; +} + +/** Marks duplicated step keys invalid on the `steps` array. */ +const uniqueStepKeysValidator: ValidatorFn = ( + control: AbstractControl +): ValidationErrors | null => { + const keys = (control as FormArray).controls + .map(stepGroup => (stepGroup.get('key')?.value ?? '').trim()) + .filter(key => key !== ''); + const duplicates = keys.filter((key, index) => keys.indexOf(key) !== index); + + return duplicates.length ? {duplicateStepKeys: {keys: [...new Set(duplicates)]}} : null; +}; + +/** The configured start step must reference an existing step. */ +const startStepExistsValidator: ValidatorFn = ( + control: AbstractControl +): ValidationErrors | null => { + const startStep = control.get('startStep')?.value; + if (!startStep) return null; + + const keys = (control.get('steps') as FormArray).controls.map( + stepGroup => stepGroup.get('key')?.value + ); + + return keys.includes(startStep) ? null : {startStepMissing: {startStep}}; +}; + +/** Every transition must point to an existing step. */ +const transitionTargetsExistValidator: ValidatorFn = ( + control: AbstractControl +): ValidationErrors | null => { + const steps = (control.get('steps') as FormArray).controls; + const keys = steps.map(stepGroup => stepGroup.get('key')?.value); + + const unknownTargets = steps.flatMap(stepGroup => + (stepGroup.get('nextSteps') as FormArray).controls + .map(transitionGroup => transitionGroup.get('step')?.value) + .filter(target => !!target && !keys.includes(target)) + ); + + return unknownTargets.length + ? {unknownTransitionTargets: {targets: [...new Set(unknownTargets)]}} + : null; +}; + +/** + * Transitions are evaluated in order and the first condition-less entry acts as the default, so at + * most one transition per step may omit its condition. + */ +const singleDefaultTransitionValidator: ValidatorFn = ( + control: AbstractControl +): ValidationErrors | null => { + const defaultCount = (control as FormArray).controls.filter( + transitionGroup => !(transitionGroup.get('condition')?.value ?? '').trim() + ).length; + + return defaultCount > 1 ? {multipleDefaultTransitions: true} : null; +}; + +export { + uniqueStepKeysValidator, + startStepExistsValidator, + transitionTargetsExistValidator, + singleDefaultTransitionValidator, +}; diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/services/form-flow.service.ts b/frontend/projects/valtimo/form-flow-management/src/lib/services/form-flow.service.ts index 247b5df9ba..5042d8e5f8 100644 --- a/frontend/projects/valtimo/form-flow-management/src/lib/services/form-flow.service.ts +++ b/frontend/projects/valtimo/form-flow-management/src/lib/services/form-flow.service.ts @@ -16,8 +16,8 @@ import {HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; -import {ConfigService, Page, BaseApiService} from '@valtimo/shared'; -import {BehaviorSubject, catchError, Observable, of, switchMap, take, tap} from 'rxjs'; +import {ConfigService, FormFlowRegistryDto, Page, BaseApiService} from '@valtimo/shared'; +import {Observable} from 'rxjs'; import {FormFlowDefinition, FormFlowDefinitionId, ListFormFlowDefinition} from '../models'; @Injectable({ @@ -35,6 +35,15 @@ export class FormFlowService extends BaseApiService { return this.httpClient.get(this.getApiUrl('management/v1/form-flow-definition/schema')); } + // The registry describes what can be used in a form flow definition (step types, expression + // beans and additional properties). The backend builds it once at startup, so this is a cheap + // call that is made fresh every time — no client-side caching. + public getFormFlowRegistry(): Observable { + return this.httpClient.get( + this.getApiUrl('management/v1/form-flow/registry') + ); + } + public getFormFlowDefinitions( caseDefinitionKey: string, caseVersionTag: string diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/services/index.ts b/frontend/projects/valtimo/form-flow-management/src/lib/services/index.ts index 149495af87..010db44983 100644 --- a/frontend/projects/valtimo/form-flow-management/src/lib/services/index.ts +++ b/frontend/projects/valtimo/form-flow-management/src/lib/services/index.ts @@ -15,3 +15,4 @@ */ export * from './form-flow.service'; +export * from './form-flow-component.service'; diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/utils/index.ts b/frontend/projects/valtimo/form-flow-management/src/lib/utils/index.ts new file mode 100644 index 0000000000..6b827ca0b7 --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/utils/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './translate.utils'; diff --git a/frontend/projects/valtimo/form-flow-management/src/lib/utils/translate.utils.ts b/frontend/projects/valtimo/form-flow-management/src/lib/utils/translate.utils.ts new file mode 100644 index 0000000000..0d91e5ffa7 --- /dev/null +++ b/frontend/projects/valtimo/form-flow-management/src/lib/utils/translate.utils.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TranslateService} from '@ngx-translate/core'; + +/** + * Translates a key and falls back to the given value when no translation exists (ngx-translate + * returns the key itself in that case). Used for registry-driven labels where only the well-known + * entries have translations. + */ +export function translateWithFallback( + translateService: TranslateService, + key: string, + fallback: string +): string { + const translation = translateService.instant(key); + return translation === key ? fallback : translation; +} diff --git a/frontend/projects/valtimo/form-flow-management/src/public-api.ts b/frontend/projects/valtimo/form-flow-management/src/public-api.ts index eeb5a12561..c3f2bf13f2 100644 --- a/frontend/projects/valtimo/form-flow-management/src/public-api.ts +++ b/frontend/projects/valtimo/form-flow-management/src/public-api.ts @@ -18,6 +18,7 @@ * Public API Surface of form-flow */ +export * from './lib/constants'; export * from './lib/models'; export * from './lib/services'; export * from './lib/components/overview/form-flow-overview.component'; diff --git a/frontend/projects/valtimo/iko/src/lib/models/iko-management-list.model.ts b/frontend/projects/valtimo/iko/src/lib/models/iko-management-list.model.ts index 486683d0ab..aa130c77e1 100644 --- a/frontend/projects/valtimo/iko/src/lib/models/iko-management-list.model.ts +++ b/frontend/projects/valtimo/iko/src/lib/models/iko-management-list.model.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -type ColumnDefaultSort = 'ASC' | 'DESC'; +import {ColumnDefaultSort} from '@valtimo/shared'; interface DisplayType { type: string; diff --git a/frontend/projects/valtimo/process-link/src/lib/components/select-form-flow/select-form-flow.component.ts b/frontend/projects/valtimo/process-link/src/lib/components/select-form-flow/select-form-flow.component.ts index 3ef5e8aabf..ec421be666 100644 --- a/frontend/projects/valtimo/process-link/src/lib/components/select-form-flow/select-form-flow.component.ts +++ b/frontend/projects/valtimo/process-link/src/lib/components/select-form-flow/select-form-flow.component.ts @@ -192,7 +192,7 @@ export class SelectFormFlowComponent implements OnInit, OnDestroy { .subscribe(([selectedProcessLink, isUserTask, isStartEvent]) => { const updateProcessLinkRequest: FormFlowProcessLinkUpdateRequestDto = { id: selectedProcessLink.id, - formFlowDefinitionId: this.selectedFormFlowDefinitionId, + formFlowDefinitionKey: this.selectedFormFlowDefinitionId, activityId: selectedProcessLink.activityId, ...(isUserTask && { formDisplayType: this.formDisplayValue || this._DEFAULT_FORM_DISPLAY_TYPE, diff --git a/frontend/projects/valtimo/process-link/src/lib/constants/injection-tokens.ts b/frontend/projects/valtimo/process-link/src/lib/constants/injection-tokens.ts index 8b8968b7cf..dfc7fcbe1b 100644 --- a/frontend/projects/valtimo/process-link/src/lib/constants/injection-tokens.ts +++ b/frontend/projects/valtimo/process-link/src/lib/constants/injection-tokens.ts @@ -15,15 +15,14 @@ */ import {InjectionToken} from '@angular/core'; -import {FormCustomComponentConfig, FormFlowCustomComponentDefinition} from '../models'; +import {FormCustomComponentConfig} from '../models'; -const FORM_FLOW_COMPONENT_TOKEN = new InjectionToken>( - 'Supported form-flow Angular components', - {factory: () => []} -); +// Moved to @valtimo/form-flow-management (the owning library); re-exported here for backwards +// compatibility. +export {FORM_FLOW_COMPONENT_TOKEN} from '@valtimo/form-flow-management'; const FORM_CUSTOM_COMPONENT_TOKEN = new InjectionToken( 'Custom component that can be used instead of FormIO or form-flow' ); -export {FORM_FLOW_COMPONENT_TOKEN, FORM_CUSTOM_COMPONENT_TOKEN}; +export {FORM_CUSTOM_COMPONENT_TOKEN}; diff --git a/frontend/projects/valtimo/process-link/src/lib/models/form-flow.model.ts b/frontend/projects/valtimo/process-link/src/lib/models/form-flow.model.ts index 4d43e45819..09982a29e8 100644 --- a/frontend/projects/valtimo/process-link/src/lib/models/form-flow.model.ts +++ b/frontend/projects/valtimo/process-link/src/lib/models/form-flow.model.ts @@ -14,24 +14,10 @@ * limitations under the License. */ -import {EventEmitter, Type} from '@angular/core'; -import {FormioSubmission} from '@valtimo/components'; - -interface ChangeEvent { - data: object; -} - -interface FormFlowCustomComponent { - formFlowInstanceId: string; - componentId?: string; - disabled: boolean; - changeEvent: EventEmitter; - submitEvent: EventEmitter; -} - -interface FormFlowCustomComponentDefinition { - id: string; - component: Type; -} - -export {FormFlowCustomComponentDefinition, FormFlowCustomComponent, ChangeEvent}; +// Moved to @valtimo/form-flow-management (the owning library); re-exported here for backwards +// compatibility. +export { + ChangeEvent, + FormFlowCustomComponent, + FormFlowCustomComponentDefinition, +} from '@valtimo/form-flow-management'; diff --git a/frontend/projects/valtimo/process-link/src/lib/models/process-link.model.ts b/frontend/projects/valtimo/process-link/src/lib/models/process-link.model.ts index 42a4881af9..8a0f7ca4d2 100644 --- a/frontend/projects/valtimo/process-link/src/lib/models/process-link.model.ts +++ b/frontend/projects/valtimo/process-link/src/lib/models/process-link.model.ts @@ -15,6 +15,7 @@ */ import {PluginConfiguration} from '@valtimo/plugin'; import {ProcessInstanceTask} from '@valtimo/process'; +import {BuildingBlockSyncTiming, FormDisplayType} from '@valtimo/shared'; import {ListItem} from 'carbon-components-angular/dropdown'; interface ProcessLink { @@ -121,9 +122,10 @@ interface PluginProcessLinkUpdateDto { interface FormFlowProcessLinkUpdateRequestDto { id: string; activityId: string; - formFlowDefinitionId: string; + formFlowDefinitionKey: string; formDisplayType?: string; formSize?: string; + subtitles?: string[]; } interface FormProcessLinkUpdateRequestDto { @@ -136,8 +138,6 @@ interface FormProcessLinkUpdateRequestDto { subtitles?: string[]; } -type FormDisplayType = 'modal' | 'panel'; - type FormSize = 'extraSmall' | 'small' | 'medium' | 'large'; interface UIComponentProcessLinkCreateRequestDto { @@ -203,8 +203,6 @@ interface BuildingBlockProcessLinkUpdateDto { outputMappings: Array; } -type BuildingBlockSyncTiming = 'CONTINUOUS' | 'END'; - interface BuildingBlockInputMapping { source: string; target: string; diff --git a/frontend/projects/valtimo/process-link/src/lib/services/form-flow-component.service.ts b/frontend/projects/valtimo/process-link/src/lib/services/form-flow-component.service.ts index a745cdaf13..c8a74138c5 100644 --- a/frontend/projects/valtimo/process-link/src/lib/services/form-flow-component.service.ts +++ b/frontend/projects/valtimo/process-link/src/lib/services/form-flow-component.service.ts @@ -14,32 +14,6 @@ * limitations under the License. */ -import {Inject, Injectable} from '@angular/core'; -import {BehaviorSubject, filter, Observable} from 'rxjs'; -import {FormFlowCustomComponentDefinition} from '../models'; -import {FORM_FLOW_COMPONENT_TOKEN} from '../constants'; - -@Injectable({ - providedIn: 'root', -}) -export class FormFlowComponentService { - private readonly _supportedComponents$ = - new BehaviorSubject | null>(null); - - public get supportedComponents$(): Observable> { - return this._supportedComponents$.pipe(filter(components => !!components)); - } - - constructor( - @Inject(FORM_FLOW_COMPONENT_TOKEN) - private readonly supportedCustomComponents: Array - ) { - this.setSupportedComponents(supportedCustomComponents); - } - - private setSupportedComponents( - supportedComponents: Array - ): void { - this._supportedComponents$.next(supportedComponents); - } -} +// Moved to @valtimo/form-flow-management (the owning library); re-exported here for backwards +// compatibility. +export {FormFlowComponentService} from '@valtimo/form-flow-management'; diff --git a/frontend/projects/valtimo/process-management/src/lib/models/process-management.model.ts b/frontend/projects/valtimo/process-management/src/lib/models/process-management.model.ts index 50709e0e23..0a451e42de 100644 --- a/frontend/projects/valtimo/process-management/src/lib/models/process-management.model.ts +++ b/frontend/projects/valtimo/process-management/src/lib/models/process-management.model.ts @@ -15,7 +15,7 @@ */ import {ModalParams, ProcessLink} from '@valtimo/process-link'; -import {ManagementContext} from '@valtimo/shared'; +import {ManagementContext, ProcessDefinitionValidationError} from '@valtimo/shared'; interface OpenProcessLinkModalEvent { modalParams: ModalParams; @@ -40,16 +40,6 @@ interface UpdateProcessDefinitionCaseDefinitionRequest { startableByUser?: boolean; } -interface ProcessDefinitionValidationError { - elementId: string; - elementType: string; - elementName?: string; - reason: string; - errorCode?: string; - expression?: string; - severity?: 'ERROR' | 'WARNING'; -} - interface ProcessDefinitionValidationResult { isValid: boolean; hasWarnings: boolean; diff --git a/frontend/projects/valtimo/shared/assets/core/en.json b/frontend/projects/valtimo/shared/assets/core/en.json index 8b8668d785..faa993eecc 100644 --- a/frontend/projects/valtimo/shared/assets/core/en.json +++ b/frontend/projects/valtimo/shared/assets/core/en.json @@ -2976,6 +2976,102 @@ "noResults": { "description": "Click Add new form flow to start designing your form flow", "title": "No form flows added" + }, + "tabs": { + "editor": "Editor", + "jsonEditor": "JSON editor" + }, + "uiEditor": { + "stepsTitle": "Steps", + "startStep": "Start step", + "stepType": "Type", + "addStep": "Add step", + "deleteStep": "Delete step", + "makeStartStep": "Make start step", + "selectForm": "Select a form", + "selectComponent": "Select a component", + "fieldTooltips": { + "key": "Identifies this step. It is referenced by the start step and by transitions, so renaming it updates those references automatically.", + "title": "Optional. Shown in the breadcrumb trail while a user walks through the form flow.", + "type": "What this step shows: \"form\" renders a Form.io form, \"custom-component\" renders an Angular component registered by this implementation. Other values are handled by a matching step type handler in the backend.", + "typeNoComponents": "\"custom-component\" is unavailable because this implementation has not registered any custom components.", + "definition": "The form of this case definition or building block that is shown for this step.", + "componentId": "The registered Angular component that is shown for this step.", + "targetStep": "The step the user goes to when this transition is taken.", + "condition": "Optional SpEL expression between ${ and }. Transitions are evaluated from top to bottom; leave empty to make this the default route." + }, + "stepDetails": "Step details", + "title": "Title", + "titlePlaceholder": "Shown in the breadcrumb trail", + "keyPlaceholder": "For example personalDetailsStep", + "duplicateKey": "This key is already used by another step", + "required": "This field is required", + "navigation": "Navigation", + "navigationDescription": "Where the user can go after completing this step. Transitions are evaluated from top to bottom; the first one whose condition holds is taken and a transition without a condition is the default.", + "targetStep": "Next step", + "selectStep": "Select a step", + "condition": "Condition", + "conditionPlaceholder": "${step.submissionData.age >= 21} (leave empty for default)", + "moveUp": "Move up", + "moveDown": "Move down", + "addTransition": "Add transition", + "noTransitions": "No transitions — the form flow ends after this step.", + "multipleDefaultTransitions": "Only one transition may omit its condition; that transition is the default.", + "conditionExamplesTitle": "Conditions are Spring Expression Language (SpEL) expressions between ${ and }. They can use:", + "conditionExampleSubmission": "values the user submitted in this step, via step.submissionData", + "conditionExampleContext": "case and process context, via additionalProperties (the exact entries are listed below)", + "conditionExampleDefault": "Comparison operators like ==, !=, >, >= and boolean operators like && and || are supported. A transition without a condition is the default route.", + "additionalPropertiesTitle": "Available in additionalProperties:", + "additionalPropertiesUserTask": "When the form flow is linked to a user task:", + "additionalPropertiesStartEvent": "When the form flow is linked to a start event:", + "additionalPropertiesOptional": "(if available)", + "additionalProperties": { + "processInstanceId": "the id of the process instance the task belongs to", + "processInstanceBusinessKey": "the business key of that process instance", + "taskInstanceId": "the id of the user task that opened this form flow", + "documentId": "the id of the case, when the process belongs to one", + "processDefinitionKey": "the key of the process definition being started", + "documentDefinitionName": "the document definition of the new case, when starting a new case" + }, + "actions": "Actions", + "actionsDescription": "Actions run on the server at specific moments in the step's lifecycle, for example to prefill forms, store submissions, complete tasks or start processes.", + "help": { + "title": "How expressions work", + "button": "How do expressions work?", + "intro": "Conditions and actions are Spring Expression Language (SpEL) expressions written between ${ and }. They are evaluated on the server while the form flow runs.", + "conditionsHeading": "Conditions", + "actionsHeading": "Actions", + "actionsBody": "Actions run at specific moments in a step's lifecycle: when it opens, when it is completed and when the user navigates back. They can call the registered form flow functions listed under \"Add action\", and can read the submitted data via step.submissionData and the case and process context via additionalProperties. Use them to prefill forms, store submissions, complete tasks or start processes.", + "dataHeading": "Available data", + "conditionsInlineTitle": "Conditions are expressions.", + "conditionsInlineMessage": "For example ${step.submissionData.age >= 21}. Leave the condition empty for the default route.", + "actionsInlineTitle": "Actions are expressions.", + "actionsInlineMessage": "They can call registered functions and read submitted data and case context." + }, + "onOpen": "When the step opens", + "onOpenDescription": "Runs when the user opens this step, for example to prefill the form.", + "onComplete": "When the step is completed", + "onCompleteDescription": "Runs when the user submits this step, for example to store data or complete a task.", + "onBack": "When the user navigates back", + "onBackDescription": "Runs when the user goes back to the previous step, for example to clean up data.", + "addExpression": "Add action", + "blankExpression": "Blank expression", + "noExpressions": "No actions configured.", + "stepInvalid": "This step has configuration errors", + "noStepsTitle": "No steps yet", + "noStepsDescription": "Add a step to start designing this form flow.", + "parseFailedTitle": "Cannot open the visual editor", + "parseFailedMessage": "The definition contains invalid JSON. Fix it in the JSON editor first.", + "properties": { + "definition": "Form", + "componentId": "Component ID" + }, + "errors": { + "title": "The definition contains errors", + "duplicateKeys": "Duplicate step keys: {{keys}}.", + "startStepMissing": "The start step \"{{startStep}}\" does not exist.", + "unknownTargets": "Transitions point to unknown steps: {{targets}}." + } } }, "taskManagement": { diff --git a/frontend/projects/valtimo/shared/assets/core/nl.json b/frontend/projects/valtimo/shared/assets/core/nl.json index 923ee53ac9..7c124e925d 100644 --- a/frontend/projects/valtimo/shared/assets/core/nl.json +++ b/frontend/projects/valtimo/shared/assets/core/nl.json @@ -1215,7 +1215,9 @@ "title": "Er zijn geen statussen geconfigureerd", "description": "Klik hier om een status voor dit dossiertype te configureren" }, - "noResultsFinalVersion": {"description": "Er zijn geen status aangemaakt voor dit dossiertype."} + "noResultsFinalVersion": { + "description": "Er zijn geen status aangemaakt voor dit dossiertype." + } }, "caseTags": { "searchFieldTitle": "Tags", @@ -3009,6 +3011,102 @@ "noResults": { "description": "Klik op Nieuwe form flow toevoegen om te beginnen met het ontwerpen van je form flow", "title": "Geen form flows toegevoegd" + }, + "tabs": { + "editor": "Editor", + "jsonEditor": "JSON-editor" + }, + "uiEditor": { + "stepsTitle": "Stappen", + "startStep": "Startstap", + "stepType": "Type", + "addStep": "Stap toevoegen", + "deleteStep": "Stap verwijderen", + "makeStartStep": "Maak startstap", + "selectForm": "Kies een formulier", + "selectComponent": "Kies een component", + "fieldTooltips": { + "key": "Identificeert deze stap. De startstap en overgangen verwijzen ernaar; bij hernoemen worden die verwijzingen automatisch bijgewerkt.", + "title": "Optioneel. Wordt getoond in het kruimelpad terwijl een gebruiker de form flow doorloopt.", + "type": "Wat deze stap toont: \"form\" toont een Form.io-formulier, \"custom-component\" toont een Angular-component die door deze implementatie is geregistreerd. Andere waarden worden afgehandeld door een bijpassende staptype-handler in de backend.", + "typeNoComponents": "\"custom-component\" is niet beschikbaar omdat deze implementatie geen custom componenten heeft geregistreerd.", + "definition": "Het formulier van deze zaakdefinitie of building block dat voor deze stap wordt getoond.", + "componentId": "De geregistreerde Angular-component die voor deze stap wordt getoond.", + "targetStep": "De stap waar de gebruiker naartoe gaat wanneer deze overgang wordt gekozen.", + "condition": "Optionele SpEL-expressie tussen ${ en }. Overgangen worden van boven naar beneden geëvalueerd; laat leeg om dit de standaardroute te maken." + }, + "stepDetails": "Stapdetails", + "title": "Titel", + "titlePlaceholder": "Wordt getoond in het kruimelpad", + "keyPlaceholder": "Bijvoorbeeld persoonsgegevensStap", + "duplicateKey": "Deze key wordt al gebruikt door een andere stap", + "required": "Dit veld is verplicht", + "navigation": "Navigatie", + "navigationDescription": "Waar de gebruiker naartoe kan na het afronden van deze stap. Overgangen worden van boven naar beneden geëvalueerd; de eerste waarvan de conditie waar is wordt gekozen en een overgang zonder conditie is de standaard.", + "targetStep": "Volgende stap", + "selectStep": "Kies een stap", + "condition": "Conditie", + "conditionPlaceholder": "${step.submissionData.leeftijd >= 21} (leeg = standaard)", + "moveUp": "Omhoog", + "moveDown": "Omlaag", + "addTransition": "Overgang toevoegen", + "noTransitions": "Geen overgangen — de form flow eindigt na deze stap.", + "multipleDefaultTransitions": "Slechts één overgang mag zonder conditie zijn; die overgang is de standaard.", + "conditionExamplesTitle": "Condities zijn Spring Expression Language (SpEL)-expressies tussen ${ en }. Ze kunnen gebruikmaken van:", + "conditionExampleSubmission": "waarden die de gebruiker in deze stap heeft ingevuld, via step.submissionData", + "conditionExampleContext": "zaak- en procescontext, via additionalProperties (de exacte waarden staan hieronder)", + "conditionExampleDefault": "Vergelijkingsoperatoren zoals ==, !=, >, >= en booleaanse operatoren zoals && en || worden ondersteund. Een overgang zonder conditie is de standaardroute.", + "additionalPropertiesTitle": "Beschikbaar in additionalProperties:", + "additionalPropertiesUserTask": "Wanneer de form flow gekoppeld is aan een gebruikerstaak:", + "additionalPropertiesStartEvent": "Wanneer de form flow gekoppeld is aan een startgebeurtenis:", + "additionalPropertiesOptional": "(indien beschikbaar)", + "additionalProperties": { + "processInstanceId": "het id van de procesinstantie waar de taak bij hoort", + "processInstanceBusinessKey": "de business key van die procesinstantie", + "taskInstanceId": "het id van de gebruikerstaak die deze form flow heeft geopend", + "documentId": "het id van de zaak, wanneer het proces bij een zaak hoort", + "processDefinitionKey": "de key van de procesdefinitie die gestart wordt", + "documentDefinitionName": "de documentdefinitie van de nieuwe zaak, bij het starten van een nieuwe zaak" + }, + "actions": "Acties", + "actionsDescription": "Acties worden op de server uitgevoerd op vaste momenten in de levenscyclus van de stap, bijvoorbeeld om formulieren vooraf in te vullen, inzendingen op te slaan, taken af te ronden of processen te starten.", + "help": { + "title": "Hoe expressies werken", + "button": "Hoe werken expressies?", + "intro": "Condities en acties zijn Spring Expression Language (SpEL)-expressies tussen ${ en }. Ze worden op de server geëvalueerd terwijl de form flow draait.", + "conditionsHeading": "Condities", + "actionsHeading": "Acties", + "actionsBody": "Acties worden uitgevoerd op vaste momenten in de levenscyclus van een stap: bij het openen, bij het afronden en wanneer de gebruiker teruggaat. Ze kunnen de geregistreerde form flow-functies aanroepen uit het menu \"Actie toevoegen\", en kunnen de ingevulde gegevens lezen via step.submissionData en de zaak- en procescontext via additionalProperties. Gebruik ze om formulieren vooraf in te vullen, inzendingen op te slaan, taken af te ronden of processen te starten.", + "dataHeading": "Beschikbare gegevens", + "conditionsInlineTitle": "Condities zijn expressies.", + "conditionsInlineMessage": "Bijvoorbeeld ${step.submissionData.leeftijd >= 21}. Laat de conditie leeg voor de standaardroute.", + "actionsInlineTitle": "Acties zijn expressies.", + "actionsInlineMessage": "Ze kunnen geregistreerde functies aanroepen en ingevulde gegevens en zaakcontext lezen." + }, + "onOpen": "Bij openen van de stap", + "onOpenDescription": "Wordt uitgevoerd wanneer de gebruiker deze stap opent, bijvoorbeeld om het formulier vooraf in te vullen.", + "onComplete": "Bij afronden van de stap", + "onCompleteDescription": "Wordt uitgevoerd wanneer de gebruiker deze stap indient, bijvoorbeeld om gegevens op te slaan of een taak af te ronden.", + "onBack": "Bij teruggaan", + "onBackDescription": "Wordt uitgevoerd wanneer de gebruiker teruggaat naar de vorige stap, bijvoorbeeld om gegevens op te schonen.", + "addExpression": "Actie toevoegen", + "blankExpression": "Lege expressie", + "noExpressions": "Geen acties geconfigureerd.", + "stepInvalid": "Deze stap bevat configuratiefouten", + "noStepsTitle": "Nog geen stappen", + "noStepsDescription": "Voeg een stap toe om deze form flow te ontwerpen.", + "parseFailedTitle": "De visuele editor kan niet worden geopend", + "parseFailedMessage": "De definitie bevat ongeldige JSON. Herstel dit eerst in de JSON-editor.", + "properties": { + "definition": "Formulier", + "componentId": "Component-ID" + }, + "errors": { + "title": "De definitie bevat fouten", + "duplicateKeys": "Dubbele stap-keys: {{keys}}.", + "startStepMissing": "De startstap \"{{startStep}}\" bestaat niet.", + "unknownTargets": "Overgangen verwijzen naar onbekende stappen: {{targets}}." + } } }, "taskManagement": { diff --git a/frontend/projects/valtimo/shared/src/lib/generated/generated-backend-types.ts b/frontend/projects/valtimo/shared/src/lib/generated/generated-backend-types.ts index 8930721c69..5ab826a470 100644 --- a/frontend/projects/valtimo/shared/src/lib/generated/generated-backend-types.ts +++ b/frontend/projects/valtimo/shared/src/lib/generated/generated-backend-types.ts @@ -1,6 +1,33 @@ /* tslint:disable */ /* eslint-disable */ -// Generated using typescript-generator version 3.2.1263 on 2026-04-28 11:17:58. +// Generated using typescript-generator version 3.2.1263 on 2026-08-04 15:02:17. + +export interface AccentColorsDto { + colors: { [index: string]: string }; +} + +export interface AdminSettingsLogoDto { + logoType: string; + imageBase64: string; +} + +export interface AdminSettingsLogosDto { + logo: AdminSettingsLogoDto | null; + logoDarkMode: AdminSettingsLogoDto | null; +} + +export interface CreateAdminSettingsLogoDto { + imageBase64: string; +} + +export interface FeatureToggleOverridesDto { + overrides: { [index: string]: boolean }; +} + +export interface UpdateFeatureToggleDto { + key: string; + enabled: boolean; +} export interface PbacConditionFieldDto { name: string; @@ -43,20 +70,6 @@ export interface PbacResourceDto { containerTargets: string[]; } -export interface AdminSettingsLogoDto { - logoType: string; - imageBase64: string; -} - -export interface AdminSettingsLogosDto { - logo: AdminSettingsLogoDto | null; - logoDarkMode: AdminSettingsLogoDto | null; -} - -export interface CreateAdminSettingsLogoDto { - imageBase64: string; -} - export interface BuildingBlockDefinitionArtworkDto { key: string; versionTag: string; @@ -82,6 +95,19 @@ export interface BuildingBlockFormDefinitionDto { readOnly: boolean; } +export interface BuildingBlockInstanceDto { + id: string; + documentId: string; + caseDocumentId: string | null; + definitionKey: string; + definitionVersionTag: string; + activityId: string | null; + callerProcessDefinitionId: string | null; + processInstanceId: string | null; + parentBuildingBlockInstanceId: string | null; + rootBuildingBlockInstanceId: string | null; +} + export interface BuildingBlockProcessDefinitionDto { id: string; key: string; @@ -181,14 +207,15 @@ export interface CaseDefinitionDraftCreateRequest { name: string | null; description: string | null; basedOnCaseDefinitionVersion: string | null; - caseDefinitionId: CaseDefinitionId; basedOnCaseDefinitionId: CaseDefinitionId | null; + caseDefinitionId: CaseDefinitionId; } export interface CaseDefinitionImportPreviewResponse { key: string; name: string; versionTag: string; + pluginConfigurations: PluginConfigurationPreviewDto[]; final: boolean; } @@ -310,6 +337,10 @@ export interface HiddenCaseListColumnDto { columnKey: string; } +export interface HiddenTaskListColumnDto { + columnKey: string; +} + export interface ManagementStartableItemDto { type: StartableItemType; name: string | null; @@ -319,12 +350,22 @@ export interface ManagementStartableItemDto { sortOrder: number | null; } +export interface PluginConfigurationPreviewDto { + pluginConfigurationId: string; + pluginDefinitionKey: string | null; + pluginActionDefinitionKey: string; + processDefinitionKey: string; + activityId: string; + existsInTargetEnvironment: boolean; +} + export interface StartableItemDto { type: StartableItemType; name: string | null; key: string; versionTag: string | null; processDefinitionId: string | null; + draft: boolean; } export interface StartableItemOrderEntry { @@ -360,12 +401,13 @@ export interface AdminWidgetConfigurationResponseDto { displayType: string; dataSourceProperties: ObjectNode; displayTypeProperties: ObjectNode; - url: URI | null; + url: string | null; } export interface DashboardCreateRequestDto { title: string; description: string; + widgetLayout: DashboardWidgetLayout | null; } export interface DashboardResponseDto { @@ -374,12 +416,14 @@ export interface DashboardResponseDto { description: string; createdBy: string; createdOn: DateAsString; + widgetLayout: DashboardWidgetLayout | null; } export interface DashboardUpdateRequestDto { key: string; title: string; description: string; + widgetLayout: DashboardWidgetLayout | null; } export interface DashboardWidgetDataResultDto { @@ -391,6 +435,7 @@ export interface DashboardWithWidgetsResponseDto { key: string; title: string; widgets: WidgetConfigurationResponseDto[]; + widgetLayout: DashboardWidgetLayout | null; } export interface SingleWidgetConfigurationUpdateRequestDto { @@ -399,7 +444,7 @@ export interface SingleWidgetConfigurationUpdateRequestDto { displayType: string; dataSourceProperties: ObjectNode; displayTypeProperties: ObjectNode; - url: URI | null; + url: string | null; } export interface WidgetConfigurationCreateRequestDto { @@ -408,7 +453,7 @@ export interface WidgetConfigurationCreateRequestDto { displayType: string; dataSourceProperties: ObjectNode; displayTypeProperties: ObjectNode; - url: URI | null; + url: string | null; } export interface WidgetConfigurationResponseDto { @@ -416,7 +461,7 @@ export interface WidgetConfigurationResponseDto { title: string; displayType: string; displayTypeProperties: ObjectNode; - url: URI | null; + url: string | null; } export interface WidgetConfigurationUpdateRequestDto { @@ -426,7 +471,7 @@ export interface WidgetConfigurationUpdateRequestDto { displayType: string; dataSourceProperties: ObjectNode; displayTypeProperties: ObjectNode; - url: URI | null; + url: string | null; } export interface CaseTagCreateRequestDto { @@ -450,12 +495,32 @@ export interface CaseTagUpdateRequestDto { color: CaseTagColor; } +export interface DocumentInspectionDto { + id: string; + definitionId: DocumentDefinitionId; + createdOn: DateAsString; + modifiedOn: DateAsString; + createdBy: string; + sequence: number; + version: number; + assigneeId: string; + assigneeFullName: string; + assignedTeamKey: string; + assignedTeamTitle: string; + internalStatus: string; + caseTags: CaseTagResponseDto[]; + relations: DocumentRelation[]; + relatedFiles: RelatedFile[]; + content: any; +} + export interface InternalCaseStatusCreateRequestDto { key: string; title: string; visibleInCaseListByDefault: boolean; retentionPeriodInDays: number; color: InternalCaseStatusColor; + label: string | null; } export interface InternalCaseStatusResponseDto { @@ -466,6 +531,7 @@ export interface InternalCaseStatusResponseDto { retentionPeriodInDays: number; order: number; color: InternalCaseStatusColor; + label: string | null; } export interface InternalCaseStatusUpdateOrderRequestDto { @@ -474,6 +540,7 @@ export interface InternalCaseStatusUpdateOrderRequestDto { visibleInCaseListByDefault: boolean; retentionPeriodInDays: number; color: InternalCaseStatusColor; + label: string | null; } export interface InternalCaseStatusUpdateRequestDto { @@ -482,6 +549,7 @@ export interface InternalCaseStatusUpdateRequestDto { visibleInCaseListByDefault: boolean; retentionPeriodInDays: number; color: InternalCaseStatusColor; + label: string | null; } export interface ColumnKeyResponse { @@ -687,6 +755,12 @@ export interface IntermediateSubmission { export interface IntermediateSubmissionKt { } +export interface FormFlowAdditionalPropertyDto { + name: string; + context: string; + alwaysPresent: boolean; +} + export interface FormFlowBreadcrumbResponse { title: string | null; key: string; @@ -699,6 +773,22 @@ export interface FormFlowBreadcrumbsResponse { breadcrumbs: FormFlowBreadcrumbResponse[]; } +export interface FormFlowExpressionBeanDto { + name: string; + methods: FormFlowExpressionMethodDto[]; +} + +export interface FormFlowExpressionMethodDto { + name: string; + parameters: FormFlowExpressionParameterDto[]; + returnType: string; +} + +export interface FormFlowExpressionParameterDto { + name: string; + type: string; +} + export interface FormFlowProcessLinkCreateRequestDto extends ProcessLinkCreateRequestDto { formFlowDefinitionKey: string; formDisplayType: FormDisplayType | null; @@ -727,6 +817,22 @@ export interface FormFlowProcessLinkUpdateRequestDto extends ProcessLinkUpdateRe subtitles: string[] | null; } +export interface FormFlowRegistryDto { + stepTypes: FormFlowStepTypeDto[]; + expressionBeans: FormFlowExpressionBeanDto[]; + additionalProperties: FormFlowAdditionalPropertyDto[]; +} + +export interface FormFlowStepTypeDto { + name: string; + properties: FormFlowStepTypePropertyDto[]; +} + +export interface FormFlowStepTypePropertyDto { + name: string; + type: string; +} + export interface MultipleFormErrors { componentErrors: ComponentError[]; } @@ -788,6 +894,53 @@ export interface NoteUpdateRequestDto { content: string; } +export interface JobInspectionDto { + id: string; + jobDefinitionId: string | null; + executionId: string | null; + activityId: string | null; + jobType: JobType; + retries: number; + exceptionMessage: string | null; + dueDate: DateAsString | null; + suspended: boolean; +} + +export interface LogInspectionSearchRequest { + level: string | null; + likeFormattedMessage: string | null; + afterTimestamp: DateAsString | null; + beforeTimestamp: DateAsString | null; + additionalProperties: LoggingEventPropertyDto[]; +} + +export interface ProcessInstanceInspectionDto { + processInstanceId: string; + processDefinitionId: string | null; + processDefinitionKey: string | null; + processName: string | null; + version: number; + latestVersion: number; + active: boolean; + startedBy: string | null; + startedByUserId: string | null; + startedOn: DateAsString | null; + incidents: IncidentDto[]; + tasks: TaskInspectionDto[]; + variables: ProcessVariableDto[]; + jobs: JobInspectionDto[]; + buildingBlock: BuildingBlockProcessReference | null; +} + +export interface TaskInspectionDto { + id: string; + name: string | null; + assignee: string | null; + created: DateAsString | null; + dueDate: DateAsString | null; + taskDefinitionKey: string | null; +} + export interface URLProcessLinkCreateRequestDto extends ProcessLinkCreateRequestDto { url: string; } @@ -826,10 +979,28 @@ export interface CaseProcessDefinitionResponseDto { draft: boolean; } +export interface ProcessDefinitionConflictResponseDto { + processDefinitionKey: string; + processDefinitionId: string; + processDefinitionName: string | null; +} + export interface ProcessDefinitionResponseDto { processDefinition: ProcessDefinitionWithPropertiesDto; processLinks: ProcessLinkResponseDto[]; bpmn20Xml: string; + draft: boolean; +} + +export interface ProcessDefinitionValidateRequestDto { + bpmnXml: string; + processLinks: ProcessLinkCreateRequestDto[]; +} + +export interface ProcessDefinitionValidateResponseDto { + hasWarnings: boolean; + errors: ProcessDefinitionValidationError[]; + valid: boolean; } export interface ProcessLinkActivityResult { @@ -847,9 +1018,9 @@ export interface ProcessLinkActivityResultWithTask { export interface ProcessLinkCreateRequestDto { activityId: string; + processDefinitionId: string; activityType: ActivityTypeWithEventName; processLinkType: string; - processDefinitionId: string; } export interface ProcessLinkExportResponseDto { @@ -860,9 +1031,9 @@ export interface ProcessLinkExportResponseDto { export interface ProcessLinkResponseDto { activityId: string; + processDefinitionId: string; activityType: ActivityTypeWithEventName; processLinkType: string; - processDefinitionId: string; id: string; } @@ -891,6 +1062,7 @@ export interface TabDto { title: string | null; type: string; properties: { [index: string]: any | null } | null; + widgetLayout: TabWidgetLayout | null; } export interface TeamCreateRequestDto { @@ -982,6 +1154,21 @@ export interface CustomTaskDto { businessKey: string; } +export interface DecisionDefinitionResponseDto { + id: string; + key: string; + category: string | null; + name: string | null; + version: number; + resource: string | null; + deploymentId: string | null; + tenantId: string | null; + decisionRequirementsDefinitionId: string | null; + decisionRequirementsDefinitionKey: string | null; + versionTag: string | null; + historyTimeToLive: number | null; +} + export interface DefinitionDeploymentResponseDto { identifier: string; } @@ -1005,6 +1192,22 @@ export interface HeatmapTaskDTO { totalCount: number; } +export interface IncidentDto { + id: string; + processInstanceId: string; + processDefinitionId: string; + executionId: string; + activityId: string; + incidentType: string; + incidentMessage: string; + incidentTimestamp: DateAsString; + causeIncidentId: string; + rootCauseIncidentId: string; + configuration: string; + tenantId: string; + jobDefinitionId: string; +} + export interface KeyAndPasswordDTO { key: string; newPassword: string; @@ -1042,6 +1245,18 @@ export interface ProcessInstanceStatisticsDTO { processName: string; } +export interface ProcessVariableDto { + name: string; + type: string; + value: any; +} + +export interface ProcessVariableMutationRequest { + name: string; + type: ProcessVariableType; + value: any; +} + export interface StartFormDto { formLocation: string; formFields: FormField[]; @@ -1057,33 +1272,192 @@ export interface UserTeamDto { key: string; } +export interface TemplatePreviewRequest { + fileName: string; + content: string; +} + +export interface CreateTemplateRequest { + key: string; + caseDefinitionKey: string | null; + caseDefinitionVersionTag: string | null; + buildingBlockDefinitionKey: string | null; + buildingBlockDefinitionVersionTag: string | null; + type: string; + metadata: { [index: string]: any | null }; +} + +export interface DeleteTemplateRequest { + caseDefinitionKey: string | null; + caseDefinitionVersionTag: string | null; + buildingBlockDefinitionKey: string | null; + buildingBlockDefinitionVersionTag: string | null; + templates: TemplateKeyType[]; +} + +export interface TemplateKeyType { + key: string; + type: string; +} + +export interface TemplateListItemResponse { + key: string; + type: string; +} + +export interface TemplateResponse { + key: string; + caseDefinitionKey: string | null; + caseDefinitionVersionTag: string | null; + buildingBlockDefinitionKey: string | null; + buildingBlockDefinitionVersionTag: string | null; + type: string; + metadata: { [index: string]: any | null }; + content: string; +} + +export interface UpdateTemplateRequest { + key: string; + caseDefinitionKey: string | null; + caseDefinitionVersionTag: string | null; + buildingBlockDefinitionKey: string | null; + buildingBlockDefinitionVersionTag: string | null; + type: string; + metadata: { [index: string]: any | null }; + content: string; +} + export interface WidgetDto { type: string; - title: string; - compact: boolean | null; color: WidgetColor | null; - icon: string | null; width: number; - displayConditions: Condition[] | null; + icon: string | null; + compact: boolean | null; + title: string; highContrast: boolean; + displayConditions: Condition[] | null; key: string; actions: WidgetAction[]; } +export interface CaseZaakdetailsInspectionDto { + syncConfig: ZaakdetailsSyncConfigDto | null; + zaakdetailsObject: ZaakdetailsObjectDto | null; +} + +export interface ZaakdetailsObjectContentDto { + resolved: boolean; + record: any | null; + message: string | null; + objectUrl: string | null; +} + +export interface ZaakdetailsObjectDto { + documentId: string; + objectUrl: string; + linkedToZaak: boolean; +} + +export interface ZaakdetailsSyncConfigDto { + caseDefinitionKey: string; + caseDefinitionVersionTag: string; + objectManagementConfigurationId: string | null; + objectManagementTitle: string | null; + enabled: boolean; +} + +export interface CaseZgwInspectionDto { + zaakInstanceLink: ZaakInstanceLinkDto | null; + zaak: any | null; + eigenschappen: ZaakEigenschapDto[]; + rollen: ZaakRolDto[]; + statusHistory: ZaakStatusDto[]; + resultaat: ZaakResultaatDto | null; + zaakObjecten: ZaakObjectDto[]; + zaakInformatieObjecten: ZaakInformatieObjectDto[]; + besluiten: ZaakBesluitDto[]; + warnings: string[]; +} + +export interface ZaakBesluitDto { + url: string; + besluit: string; +} + +export interface ZaakEigenschapDto { + url: string; + eigenschap: string; + naam: string | null; + waarde: string; +} + +export interface ZaakInformatieObjectDto { + url: string; + informatieobject: string; + titel: string | null; + registratiedatum: DateAsString; +} + +export interface ZaakInstanceLinkDto { + zaakInstanceUrl: string; + zaakInstanceId: string; + zaakTypeUrl: string; +} + +export interface ZaakObjectDto { + url: string; + objectUrl: string; + objectType: string; + objectTypeOverige: string | null; + relatieomschrijving: string | null; +} + +export interface ZaakResultaatDto { + url: string; + resultaattype: string; + toelichting: string | null; +} + +export interface ZaakRolDto { + url: string | null; + betrokkeneType: string; + roltype: string; + omschrijving: string | null; + omschrijvingGeneriek: string | null; + indicatieMachtiging: string | null; + betrokkeneIdentificatie: any | null; +} + +export interface ZaakStatusDto { + url: string; + statustype: string; + datumStatusGezet: DateAsString; + statustoelichting: string | null; +} + +export interface ZaakobjectResolveResultDto { + resolved: boolean; + record: any | null; + message: string | null; + objectUrl: string; +} + export interface BuildingBlockInputMapping { source: string; target: string; + prefixedTarget: string; } export interface BuildingBlockOutputMapping { source: string; target: string; syncTiming: BuildingBlockSyncTiming; + prefixedSource: string; } export interface CaseDefinitionId extends AbstractId, BlueprintId { key: string; - versionTag: Semver; + versionTag: string; } export interface CaseListItemDto { @@ -1094,14 +1468,22 @@ export interface CaseListItemDto { export interface ObjectNode extends ContainerNode, Serializable { } -export interface URI extends Comparable, Serializable { +export interface DocumentDefinitionId { + buildingBlockDefinitionId: BuildingBlockDefinitionId; + caseDefinitionId: CaseDefinitionId; + name: string; +} + +export interface DocumentRelation { + relationType: DocumentRelationType; + id: string; } export interface RelatedFile { + createdBy: string; createdOn: DateAsString; - fileId: string; sizeInBytes: number; - createdBy: string; + fileId: string; fileName: string; } @@ -1116,11 +1498,18 @@ export interface ComponentError { message: string; } +export interface BuildingBlockProcessReference { + instanceId: string; + definitionKey: string; + definitionVersionTag: string; + documentId: string; +} + export interface ProcessLinkDeployDto { processLinkType: "url"; activityId: string; - activityType: ActivityTypeWithEventName; processDefinitionId: string; + activityType: ActivityTypeWithEventName; } export interface ProcessDefinitionCaseDefinition { @@ -1129,6 +1518,17 @@ export interface ProcessDefinitionCaseDefinition { startableByUser: boolean; processDefinitionName: string | null; processDefinitionKey: string | null; + draft: boolean; +} + +export interface ProcessDefinitionValidationError { + elementId: string; + elementType: string; + elementName: string | null; + reason: string; + errorCode: string | null; + expression: string | null; + severity: ValidationSeverity; } export interface TaskInstanceWithIdentityLink { @@ -1198,9 +1598,9 @@ export interface OperatonTaskDto { } export interface FormField { - businessKey: boolean; - label: string; validationConstraints: FormFieldValidationConstraint[]; + label: string; + businessKey: boolean; value: TypedValue; typeName: string; properties: { [index: string]: string }; @@ -1230,23 +1630,23 @@ export interface ProcessDefinitionDto { } export interface HistoricActivityInstance { - executionId: string; - canceled: boolean; - removalTime: DateAsString; - activityId: string; - assignee: string; - tenantId: string; - startTime: DateAsString; - endTime: DateAsString; - taskId: string; - activityType: string; - processDefinitionId: string; - processDefinitionKey: string; rootProcessInstanceId: string; parentActivityInstanceId: string; calledProcessInstanceId: string; calledCaseInstanceId: string; + processDefinitionKey: string; + taskId: string; + startTime: DateAsString; + endTime: DateAsString; + activityId: string; + executionId: string; processInstanceId: string; + processDefinitionId: string; + activityType: string; + assignee: string; + tenantId: string; + canceled: boolean; + removalTime: DateAsString; activityName: string; durationInMillis: number; completeScope: boolean; @@ -1267,16 +1667,6 @@ export interface Condition { export interface WidgetAction { } -export interface Semver extends Comparable { - major: number; - minor: number; - patch: number; - preRelease: string[]; - build: string[]; - version: string; - stable: boolean; -} - export interface BlueprintId { tagPrefix: string; idKey: string; @@ -1285,6 +1675,11 @@ export interface BlueprintId { export interface Serializable { } +export interface BuildingBlockDefinitionId extends AbstractId, BlueprintId { + key: string; + versionTag: string; +} + export interface ProcessDefinitionCaseDefinitionId extends AbstractId { processDefinitionId: ProcessDefinitionId; caseDefinitionId: CaseDefinitionId; @@ -1366,9 +1761,6 @@ export interface AbstractId extends Identity, Serializable { export interface ContainerNode extends BaseJsonNode, JsonNodeCreator { } -export interface Comparable { -} - export interface ProcessDefinitionId { id: string; } @@ -1403,10 +1795,16 @@ export type DateAsString = string; export type StartableItemType = "PROCESS" | "BUILDING_BLOCK"; +export type JobType = "TIMER" | "ASYNC_CONTINUATION" | "MESSAGE" | "BATCH" | "OTHER"; + +export type ProcessVariableType = "STRING" | "INTEGER" | "LONG" | "DOUBLE" | "BOOLEAN" | "JSON"; + export type ColumnDefaultSort = "ASC" | "DESC"; export type CaseTabType = "standard" | "formio" | "custom" | "widgets"; +export type DashboardWidgetLayout = "MUURI_GAP_FREE" | "MUURI" | "BEAUTIFUL"; + export type CaseTagColor = "WARMGRAY" | "RED" | "MAGENTA" | "PURPLE" | "BLUE" | "CYAN" | "TEAL" | "GREEN" | "GRAY" | "COOLGRAY" | "HIGHCONTRAST" | "OUTLINE"; export type InternalCaseStatusColor = "WARMGRAY" | "RED" | "MAGENTA" | "PURPLE" | "BLUE" | "CYAN" | "TEAL" | "GREEN" | "GRAY" | "COOLGRAY" | "HIGHCONTRAST" | "OUTLINE"; @@ -1425,10 +1823,16 @@ export type FieldType = "text_contains" | "single" | "range" | "single-select-dr export type SearchFieldMatchType = "like" | "exact"; +export type TabWidgetLayout = "MUURI_GAP_FREE" | "MUURI" | "BEAUTIFUL"; + export type WidgetColor = "YELLOW" | "ORANGE" | "RED" | "BROWN" | "GREEN" | "TURQOISE" | "PURPLE" | "PERIWINKLE" | "BLUE" | "HIGHCONTRAST" | "WHITE"; export type BuildingBlockSyncTiming = "CONTINUOUS" | "END"; +export type DocumentRelationType = "PREVIOUS" | "NEXT" | "SUPPORTING"; + +export type ValidationSeverity = "ERROR" | "WARNING"; + export type ExpressionOperator = "!=" | "==" | ">" | ">=" | "<" | "<=" | "list_contains" | "in"; export type ProcessVariableDTOV2Union = StringProcessVariableDTOV2 | DateProcessVariableDTOV2 | BooleanProcessVariableDTOV2 | EnumProcessVariableDTOV2 | LongProcessVariableDTOV2 | FileUploadProcessVariableDTOV2; diff --git a/frontend/projects/valtimo/task/src/lib/models/task-intermediate-save.model.ts b/frontend/projects/valtimo/task/src/lib/models/task-intermediate-save.model.ts index 2c5f6a90ee..b00a9026ca 100644 --- a/frontend/projects/valtimo/task/src/lib/models/task-intermediate-save.model.ts +++ b/frontend/projects/valtimo/task/src/lib/models/task-intermediate-save.model.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +export {IntermediateSaveRequest} from '@valtimo/shared'; + export interface IntermediateSubmission { submission: object; taskInstanceId: string; @@ -22,8 +24,3 @@ export interface IntermediateSubmission { editedBy?: string; editedOn?: string; } - -export interface IntermediateSaveRequest { - submission: object; - taskInstanceId: string; -} diff --git a/frontend/projects/valtimo/zgw/src/lib/case-inspection/case-inspection.models.ts b/frontend/projects/valtimo/zgw/src/lib/case-inspection/case-inspection.models.ts index 208eaaad22..93a3edf4ba 100644 --- a/frontend/projects/valtimo/zgw/src/lib/case-inspection/case-inspection.models.ts +++ b/frontend/projects/valtimo/zgw/src/lib/case-inspection/case-inspection.models.ts @@ -14,108 +14,6 @@ * limitations under the License. */ -interface ZaakInstanceLinkDto { - zaakInstanceUrl: string; - zaakInstanceId: string; - zaakTypeUrl: string; -} - -interface ZaakEigenschapDto { - url: string; - eigenschap: string; - naam: string | null; - waarde: string; -} - -interface ZaakRolDto { - url: string | null; - betrokkeneType: string; - roltype: string; - omschrijving: string | null; - omschrijvingGeneriek: string | null; - indicatieMachtiging: string | null; - betrokkeneIdentificatie: Record | null; -} - -interface ZaakStatusDto { - url: string; - statustype: string; - datumStatusGezet: string; - statustoelichting: string | null; -} - -interface ZaakResultaatDto { - url: string; - resultaattype: string; - toelichting: string | null; -} - -interface ZaakObjectDto { - url: string; - objectUrl: string; - objectType: string; - objectTypeOverige: string | null; - relatieomschrijving: string | null; -} - -interface ZaakInformatieObjectDto { - url: string; - informatieobject: string; - titel: string | null; - registratiedatum: string; -} - -interface ZaakBesluitDto { - url: string; - besluit: string; -} - -interface CaseZgwInspectionDto { - zaakInstanceLink: ZaakInstanceLinkDto | null; - zaak: Record | null; - eigenschappen: ZaakEigenschapDto[]; - rollen: ZaakRolDto[]; - statusHistory: ZaakStatusDto[]; - resultaat: ZaakResultaatDto | null; - zaakObjecten: ZaakObjectDto[]; - zaakInformatieObjecten: ZaakInformatieObjectDto[]; - besluiten: ZaakBesluitDto[]; - warnings: string[]; -} - -interface ZaakobjectResolveResultDto { - resolved: boolean; - record: Record | null; - message: string | null; - objectUrl: string; -} - -interface ZaakdetailsSyncConfigDto { - caseDefinitionKey: string; - caseDefinitionVersionTag: string; - objectManagementConfigurationId: string | null; - objectManagementTitle: string | null; - enabled: boolean; -} - -interface ZaakdetailsObjectDto { - documentId: string; - objectUrl: string; - linkedToZaak: boolean; -} - -interface CaseZaakdetailsInspectionDto { - syncConfig: ZaakdetailsSyncConfigDto | null; - zaakdetailsObject: ZaakdetailsObjectDto | null; -} - -interface ZaakdetailsObjectContentDto { - resolved: boolean; - record: Record | null; - message: string | null; - objectUrl: string | null; -} - export { ZaakInstanceLinkDto, ZaakEigenschapDto, @@ -131,4 +29,4 @@ export { ZaakdetailsObjectDto, CaseZaakdetailsInspectionDto, ZaakdetailsObjectContentDto, -}; +} from '@valtimo/shared';