feat(fullstack): Add logical AND and OR operators to seed files, add Sensitiviäts-Check

This commit is contained in:
2026-02-04 06:22:08 +01:00
parent 40957bd496
commit 1bc2e9b111
23 changed files with 3488 additions and 2001 deletions

View File

@@ -1548,10 +1548,9 @@ components:
format: uuid format: uuid
nullable: true nullable: true
visibilityConditions: visibilityConditions:
type: array $ref: "#/components/schemas/VisibilityConditionGroup"
items: nullable: true
$ref: "#/components/schemas/FormElementVisibilityCondition" description: Recursive visibility condition tree (AND/OR groups with leaf conditions)
description: List of visibility conditions (all must be met for element to be visible - AND logic)
sectionSpawnTriggers: sectionSpawnTriggers:
type: array type: array
items: items:
@@ -1612,9 +1611,8 @@ components:
items: items:
$ref: "#/components/schemas/FormOptionDto" $ref: "#/components/schemas/FormOptionDto"
visibilityConditions: visibilityConditions:
type: array $ref: "#/components/schemas/VisibilityConditionGroup"
items: nullable: true
$ref: "#/components/schemas/FormElementVisibilityCondition"
sectionSpawnTriggers: sectionSpawnTriggers:
type: array type: array
items: items:
@@ -1643,6 +1641,8 @@ components:
$ref: "#/components/schemas/EmployeeDataCategory" $ref: "#/components/schemas/EmployeeDataCategory"
columnConfig: columnConfig:
$ref: "#/components/schemas/TableColumnConfigDto" $ref: "#/components/schemas/TableColumnConfigDto"
visibilityConditions:
$ref: "#/components/schemas/VisibilityConditionGroup"
TableColumnConfigDto: TableColumnConfigDto:
type: object type: object
@@ -1716,24 +1716,56 @@ components:
- TABLE - TABLE
- FILE_UPLOAD - FILE_UPLOAD
FormElementVisibilityCondition: VisibilityConditionNode:
type: object type: object
required: description: A visibility condition node - either a leaf (single condition) or a group (AND/OR of conditions). Use nodeType to determine which properties are relevant.
- formElementConditionType
- sourceFormElementReference
properties: properties:
nodeType:
type: string
enum: [LEAF, GROUP]
default: LEAF
description: Type discriminator - LEAF for single condition, GROUP for AND/OR of conditions
# Leaf properties (used when nodeType=LEAF)
formElementConditionType: formElementConditionType:
$ref: "#/components/schemas/VisibilityConditionType" $ref: "#/components/schemas/VisibilityConditionType"
sourceFormElementReference: sourceFormElementReference:
type: string type: string
description: Reference key of the source form element to check nullable: true
description: "[LEAF] Reference key of the source form element to check"
formElementExpectedValue: formElementExpectedValue:
type: string type: string
nullable: true nullable: true
description: Expected value to compare against the source element's value property. Not required for IS_EMPTY and IS_NOT_EMPTY operators. description: "[LEAF] Expected value to compare against the source element's value"
formElementOperator: formElementOperator:
$ref: "#/components/schemas/VisibilityConditionOperator" $ref: "#/components/schemas/VisibilityConditionOperator"
default: EQUALS # Group properties (used when nodeType=GROUP)
groupOperator:
type: string
enum: [AND, OR]
nullable: true
description: "[GROUP] Logical operator to combine conditions"
conditions:
type: array
nullable: true
items:
$ref: "#/components/schemas/VisibilityConditionNode"
description: "[GROUP] List of child conditions"
VisibilityConditionGroup:
type: object
description: Root-level visibility condition group containing child conditions
properties:
operator:
type: string
enum: [AND, OR]
nullable: true
description: Logical operator to combine conditions
conditions:
type: array
nullable: true
items:
$ref: "#/components/schemas/VisibilityConditionNode"
description: List of child conditions (can be leaves or nested groups)
VisibilityConditionType: VisibilityConditionType:
type: string type: string
@@ -1748,6 +1780,8 @@ components:
- NOT_EQUALS - NOT_EQUALS
- IS_EMPTY - IS_EMPTY
- IS_NOT_EMPTY - IS_NOT_EMPTY
- CONTAINS
- NOT_CONTAINS
SectionSpawnTriggerDto: SectionSpawnTriggerDto:
type: object type: object

View File

@@ -11,7 +11,6 @@ import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormSnapshotD
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSectionSnapshotDto import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSectionSnapshotDto
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSnapshotDto import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSnapshotDto
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSubSectionSnapshotDto import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSubSectionSnapshotDto
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementVisibilityCondition
import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@@ -21,6 +20,8 @@ import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionGroup as VisibilityConditionGroupDto
import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionNode as VisibilityConditionNodeDto
import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionOperator as VisibilityConditionOperatorDto import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionOperator as VisibilityConditionOperatorDto
import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionType as VisibilityConditionTypeDto import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionType as VisibilityConditionTypeDto
@@ -168,12 +169,12 @@ class ApplicationFormFormatService(
if (element.options.isEmpty()) return "Keine Daten" to false if (element.options.isEmpty()) return "Keine Daten" to false
val objectMapper = jacksonObjectMapper() val objectMapper = jacksonObjectMapper()
val headers = element.options.map { LatexEscaper.escape(it.label ?: "") } val headers = element.options.map { LatexEscaper.escape(it.label) }
val columnData = val columnData =
element.options.map { option -> element.options.map { option ->
try { try {
val typeRef = object : TypeReference<List<String>>() {} val typeRef = object : TypeReference<List<String>>() {}
objectMapper.readValue(option.value ?: "[]", typeRef) objectMapper.readValue(option.value, typeRef)
} catch (e: Exception) { } catch (e: Exception) {
emptyList<String>() emptyList<String>()
} }
@@ -268,26 +269,61 @@ class ApplicationFormFormatService(
element: FormElementSnapshotDto, element: FormElementSnapshotDto,
formElementsByRef: Map<String, FormElementSnapshotDto>, formElementsByRef: Map<String, FormElementSnapshotDto>,
): Boolean { ): Boolean {
val conditions = element.visibilityConditions val group = element.visibilityConditions ?: return true
if (conditions.isNullOrEmpty()) return true if (group.conditions?.isEmpty() != false) return true
// All conditions must be met (AND logic) return evaluateGroup(group, formElementsByRef)
return conditions.all { condition -> evaluateSingleCondition(condition, formElementsByRef) }
} }
private fun evaluateSingleCondition( private fun evaluateGroup(
condition: FormElementVisibilityCondition, group: VisibilityConditionGroupDto,
formElementsByRef: Map<String, FormElementSnapshotDto>, formElementsByRef: Map<String, FormElementSnapshotDto>,
): Boolean { ): Boolean {
val sourceElement = formElementsByRef[condition.sourceFormElementReference] ?: return false val conditions = group.conditions ?: return true
val results = conditions.map { evaluateNode(it, formElementsByRef) }
return when (group.operator) {
VisibilityConditionGroupDto.Operator.AND -> results.all { it }
VisibilityConditionGroupDto.Operator.OR -> results.any { it }
null -> true
}
}
private fun evaluateNode(
node: VisibilityConditionNodeDto,
formElementsByRef: Map<String, FormElementSnapshotDto>,
): Boolean =
when (node.nodeType) {
VisibilityConditionNodeDto.NodeType.LEAF, null -> evaluateLeafCondition(node, formElementsByRef)
VisibilityConditionNodeDto.NodeType.GROUP -> {
val nestedGroup =
VisibilityConditionGroupDto(
operator =
when (node.groupOperator) {
VisibilityConditionNodeDto.GroupOperator.AND -> VisibilityConditionGroupDto.Operator.AND
VisibilityConditionNodeDto.GroupOperator.OR -> VisibilityConditionGroupDto.Operator.OR
null -> VisibilityConditionGroupDto.Operator.AND
},
conditions = node.conditions,
)
evaluateGroup(nestedGroup, formElementsByRef)
}
}
private fun evaluateLeafCondition(
node: VisibilityConditionNodeDto,
formElementsByRef: Map<String, FormElementSnapshotDto>,
): Boolean {
val sourceRef = node.sourceFormElementReference ?: return false
val sourceElement = formElementsByRef[sourceRef] ?: return false
val sourceValue = getFormElementValue(sourceElement) val sourceValue = getFormElementValue(sourceElement)
val operator = condition.formElementOperator ?: VisibilityConditionOperatorDto.EQUALS val operator = node.formElementOperator ?: VisibilityConditionOperatorDto.EQUALS
val conditionMet = evaluateCondition(sourceValue, condition.formElementExpectedValue, operator) val conditionMet = evaluateCondition(sourceValue, node.formElementExpectedValue ?: "", operator)
return when (condition.formElementConditionType) { return when (node.formElementConditionType) {
VisibilityConditionTypeDto.SHOW -> conditionMet VisibilityConditionTypeDto.SHOW -> conditionMet
VisibilityConditionTypeDto.HIDE -> !conditionMet VisibilityConditionTypeDto.HIDE -> !conditionMet
null -> conditionMet
} }
} }
@@ -314,5 +350,9 @@ class ApplicationFormFormatService(
expectedValue?.let { !actualValue.equals(it, ignoreCase = true) } ?: false expectedValue?.let { !actualValue.equals(it, ignoreCase = true) } ?: false
VisibilityConditionOperatorDto.IS_EMPTY -> actualValue.isEmpty() VisibilityConditionOperatorDto.IS_EMPTY -> actualValue.isEmpty()
VisibilityConditionOperatorDto.IS_NOT_EMPTY -> actualValue.isNotEmpty() VisibilityConditionOperatorDto.IS_NOT_EMPTY -> actualValue.isNotEmpty()
VisibilityConditionOperatorDto.CONTAINS ->
expectedValue?.let { actualValue.contains(it, ignoreCase = true) } ?: false
VisibilityConditionOperatorDto.NOT_CONTAINS ->
expectedValue?.let { !actualValue.contains(it, ignoreCase = true) } ?: true
} }
} }

View File

@@ -127,11 +127,9 @@ class ApplicationFormVersionService(
type = element.type, type = element.type,
options = element.options.map { formOptionMapper.toFormOptionDto(it) }, options = element.options.map { formOptionMapper.toFormOptionDto(it) },
visibilityConditions = visibilityConditions =
element.visibilityConditions element.visibilityConditions?.let {
.map { visibilityConditionMapper.toGroupConditionDto(it)
visibilityConditionMapper },
.toFormElementVisibilityConditionDto(it)
},
sectionSpawnTriggers = sectionSpawnTriggers =
element.sectionSpawnTriggers.map { element.sectionSpawnTriggers.map {
spawnTriggerMapper.toSectionSpawnTriggerDto(it) spawnTriggerMapper.toSectionSpawnTriggerDto(it)
@@ -186,10 +184,9 @@ class ApplicationFormVersionService(
.map { formOptionMapper.toFormOption(it) } .map { formOptionMapper.toFormOption(it) }
.toMutableList(), .toMutableList(),
visibilityConditions = visibilityConditions =
elementSnapshot.visibilityConditions elementSnapshot.visibilityConditions?.let {
?.map { visibilityConditionMapper.toFormElementVisibilityCondition(it) } visibilityConditionMapper.toGroupCondition(it)
?.toMutableList() },
?: mutableListOf(),
sectionSpawnTriggers = sectionSpawnTriggers =
elementSnapshot.sectionSpawnTriggers elementSnapshot.sectionSpawnTriggers
?.map { spawnTriggerMapper.toSectionSpawnTrigger(it) } ?.map { spawnTriggerMapper.toSectionSpawnTrigger(it) }

View File

@@ -0,0 +1,11 @@
package com.betriebsratkanzlei.legalconsenthub.config
import com.fasterxml.jackson.databind.Module
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class JacksonConfig {
@Bean
fun visibilityConditionJacksonModule(): Module = visibilityConditionModule()
}

View File

@@ -0,0 +1,99 @@
package com.betriebsratkanzlei.legalconsenthub.config
import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionNode
import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionOperator
import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionType
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.module.SimpleModule
class VisibilityConditionNodeDeserializer : JsonDeserializer<VisibilityConditionNode>() {
override fun deserialize(
p: JsonParser,
ctxt: DeserializationContext,
): VisibilityConditionNode {
val node = p.codec.readTree<JsonNode>(p)
val nodeType = node.get("nodeType")?.asText()
return if (nodeType == "GROUP") {
val groupOperator =
node.get("groupOperator")?.asText()?.let {
VisibilityConditionNode.GroupOperator.forValue(it)
}
val conditions =
node.get("conditions")?.map { childNode ->
val childParser = childNode.traverse(p.codec)
childParser.nextToken()
deserialize(childParser, ctxt)
} ?: emptyList()
VisibilityConditionNode(
nodeType = VisibilityConditionNode.NodeType.GROUP,
groupOperator = groupOperator,
conditions = conditions,
)
} else {
// Default to LEAF for backward compatibility
val sourceRef = node.get("sourceFormElementReference")?.asText()
val conditionType =
node.get("formElementConditionType")?.asText()?.let {
VisibilityConditionType.valueOf(it)
}
val expectedValue = node.get("formElementExpectedValue")?.asText()
val formElementOperator =
node.get("formElementOperator")?.asText()?.let {
VisibilityConditionOperator.valueOf(it)
}
VisibilityConditionNode(
nodeType = VisibilityConditionNode.NodeType.LEAF,
sourceFormElementReference = sourceRef,
formElementConditionType = conditionType,
formElementExpectedValue = expectedValue,
formElementOperator = formElementOperator,
)
}
}
}
class VisibilityConditionNodeSerializer : JsonSerializer<VisibilityConditionNode>() {
override fun serialize(
value: VisibilityConditionNode,
gen: JsonGenerator,
serializers: SerializerProvider,
) {
gen.writeStartObject()
gen.writeStringField("nodeType", (value.nodeType ?: VisibilityConditionNode.NodeType.LEAF).value)
if (value.nodeType == VisibilityConditionNode.NodeType.GROUP) {
value.groupOperator?.let { gen.writeStringField("groupOperator", it.value) }
gen.writeArrayFieldStart("conditions")
value.conditions?.forEach { serialize(it, gen, serializers) }
gen.writeEndArray()
} else {
value.formElementConditionType?.let {
gen.writeStringField("formElementConditionType", it.value)
}
value.sourceFormElementReference?.let {
gen.writeStringField("sourceFormElementReference", it)
}
value.formElementExpectedValue?.let {
gen.writeStringField("formElementExpectedValue", it)
}
value.formElementOperator?.let {
gen.writeStringField("formElementOperator", it.value)
}
}
gen.writeEndObject()
}
}
fun visibilityConditionModule(): SimpleModule =
SimpleModule("VisibilityConditionModule").apply {
addDeserializer(VisibilityConditionNode::class.java, VisibilityConditionNodeDeserializer())
addSerializer(VisibilityConditionNode::class.java, VisibilityConditionNodeSerializer())
}

View File

@@ -12,6 +12,8 @@ import jakarta.persistence.GeneratedValue
import jakarta.persistence.Id import jakarta.persistence.Id
import jakarta.persistence.JoinColumn import jakarta.persistence.JoinColumn
import jakarta.persistence.ManyToOne import jakarta.persistence.ManyToOne
import org.hibernate.annotations.JdbcTypeCode
import org.hibernate.type.SqlTypes
import java.util.UUID import java.util.UUID
@Entity @Entity
@@ -30,9 +32,9 @@ class FormElement(
@ManyToOne @ManyToOne
@JoinColumn(name = "form_element_sub_section_id", nullable = false) @JoinColumn(name = "form_element_sub_section_id", nullable = false)
var formElementSubSection: FormElementSubSection? = null, var formElementSubSection: FormElementSubSection? = null,
@ElementCollection @JdbcTypeCode(SqlTypes.JSON)
@CollectionTable(name = "visibility_conditions", joinColumns = [JoinColumn(name = "form_element_id")]) @Column(columnDefinition = "jsonb")
var visibilityConditions: MutableList<FormElementVisibilityCondition> = mutableListOf(), var visibilityConditions: GroupCondition? = null,
@ElementCollection @ElementCollection
@CollectionTable(name = "section_spawn_triggers", joinColumns = [JoinColumn(name = "form_element_id")]) @CollectionTable(name = "section_spawn_triggers", joinColumns = [JoinColumn(name = "form_element_id")])
var sectionSpawnTriggers: MutableList<SectionSpawnTrigger> = mutableListOf(), var sectionSpawnTriggers: MutableList<SectionSpawnTrigger> = mutableListOf(),

View File

@@ -22,8 +22,8 @@ class FormElementMapper(
formElement.formElementSubSection?.id formElement.formElementSubSection?.id
?: throw IllegalStateException("FormElementSubSection ID must not be null!"), ?: throw IllegalStateException("FormElementSubSection ID must not be null!"),
visibilityConditions = visibilityConditions =
formElement.visibilityConditions.map { formElement.visibilityConditions?.let {
visibilityConditionMapper.toFormElementVisibilityConditionDto(it) visibilityConditionMapper.toGroupConditionDto(it)
}, },
sectionSpawnTriggers = sectionSpawnTriggers =
formElement.sectionSpawnTriggers.map { formElement.sectionSpawnTriggers.map {
@@ -49,10 +49,9 @@ class FormElementMapper(
type = formElement.type, type = formElement.type,
formElementSubSection = formElementSubSection, formElementSubSection = formElementSubSection,
visibilityConditions = visibilityConditions =
formElement.visibilityConditions formElement.visibilityConditions?.let {
?.map { visibilityConditionMapper.toFormElementVisibilityCondition(it) } visibilityConditionMapper.toGroupCondition(it)
?.toMutableList() },
?: mutableListOf(),
sectionSpawnTriggers = sectionSpawnTriggers =
formElement.sectionSpawnTriggers formElement.sectionSpawnTriggers
?.map { spawnTriggerMapper.toSectionSpawnTrigger(it) } ?.map { spawnTriggerMapper.toSectionSpawnTrigger(it) }
@@ -78,10 +77,9 @@ class FormElementMapper(
type = formElement.type, type = formElement.type,
formElementSubSection = formElementSubSection, formElementSubSection = formElementSubSection,
visibilityConditions = visibilityConditions =
formElement.visibilityConditions formElement.visibilityConditions?.let {
?.map { visibilityConditionMapper.toFormElementVisibilityCondition(it) } visibilityConditionMapper.toGroupCondition(it)
?.toMutableList() },
?: mutableListOf(),
sectionSpawnTriggers = sectionSpawnTriggers =
formElement.sectionSpawnTriggers formElement.sectionSpawnTriggers
?.map { spawnTriggerMapper.toSectionSpawnTrigger(it) } ?.map { spawnTriggerMapper.toSectionSpawnTrigger(it) }

View File

@@ -1,15 +0,0 @@
package com.betriebsratkanzlei.legalconsenthub.form_element
import jakarta.persistence.Embeddable
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
@Embeddable
data class FormElementVisibilityCondition(
@Enumerated(EnumType.STRING)
val formElementConditionType: VisibilityConditionType,
val sourceFormElementReference: String,
val formElementExpectedValue: String?,
@Enumerated(EnumType.STRING)
val formElementOperator: VisibilityConditionOperator = VisibilityConditionOperator.EQUALS,
)

View File

@@ -1,34 +1,87 @@
package com.betriebsratkanzlei.legalconsenthub.form_element package com.betriebsratkanzlei.legalconsenthub.form_element
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementVisibilityCondition as FormElementVisibilityConditionDto import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionGroup as VisibilityConditionGroupDto
import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionNode as VisibilityConditionNodeDto
import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionOperator as VisibilityConditionOperatorDto import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionOperator as VisibilityConditionOperatorDto
import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionType as VisibilityConditionTypeDto import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionType as VisibilityConditionTypeDto
@Component @Component
class FormElementVisibilityConditionMapper { class FormElementVisibilityConditionMapper {
fun toFormElementVisibilityConditionDto( fun toGroupConditionDto(group: GroupCondition): VisibilityConditionGroupDto =
condition: FormElementVisibilityCondition, VisibilityConditionGroupDto(
): FormElementVisibilityConditionDto = operator = toGroupOperatorDto(group.operator),
FormElementVisibilityConditionDto( conditions = group.conditions.map { toNodeDto(it) },
formElementConditionType = toVisibilityConditionTypeDto(condition.formElementConditionType),
sourceFormElementReference = condition.sourceFormElementReference,
formElementExpectedValue = condition.formElementExpectedValue,
formElementOperator = toVisibilityConditionOperatorDto(condition.formElementOperator),
) )
fun toFormElementVisibilityCondition( fun toGroupCondition(groupDto: VisibilityConditionGroupDto): GroupCondition =
conditionDto: FormElementVisibilityConditionDto, GroupCondition(
): FormElementVisibilityCondition = operator = toGroupOperator(groupDto.operator ?: VisibilityConditionGroupDto.Operator.AND),
FormElementVisibilityCondition( conditions = groupDto.conditions?.map { toNode(it) } ?: emptyList(),
formElementConditionType = toVisibilityConditionType(conditionDto.formElementConditionType),
sourceFormElementReference = conditionDto.sourceFormElementReference,
formElementExpectedValue = conditionDto.formElementExpectedValue,
formElementOperator =
conditionDto.formElementOperator?.let { toVisibilityConditionOperator(it) }
?: VisibilityConditionOperator.EQUALS,
) )
private fun toNodeDto(node: VisibilityConditionNode): VisibilityConditionNodeDto =
when (node) {
is LeafCondition ->
VisibilityConditionNodeDto(
nodeType = VisibilityConditionNodeDto.NodeType.LEAF,
sourceFormElementReference = node.sourceFormElementReference,
formElementConditionType = node.formElementConditionType?.let { toVisibilityConditionTypeDto(it) },
formElementExpectedValue = node.formElementExpectedValue,
formElementOperator = toVisibilityConditionOperatorDto(node.formElementOperator),
)
is GroupCondition ->
VisibilityConditionNodeDto(
nodeType = VisibilityConditionNodeDto.NodeType.GROUP,
groupOperator = toNodeGroupOperatorDto(node.operator),
conditions = node.conditions.map { toNodeDto(it) },
)
}
private fun toNode(nodeDto: VisibilityConditionNodeDto): VisibilityConditionNode =
when (nodeDto.nodeType) {
VisibilityConditionNodeDto.NodeType.GROUP ->
GroupCondition(
operator = toGroupOperatorFromNode(nodeDto.groupOperator),
conditions = nodeDto.conditions?.map { toNode(it) } ?: emptyList(),
)
VisibilityConditionNodeDto.NodeType.LEAF, null ->
LeafCondition(
formElementConditionType = nodeDto.formElementConditionType?.let { toVisibilityConditionType(it) },
sourceFormElementReference = nodeDto.sourceFormElementReference ?: "",
formElementExpectedValue = nodeDto.formElementExpectedValue,
formElementOperator =
nodeDto.formElementOperator?.let { toVisibilityConditionOperator(it) }
?: VisibilityConditionOperator.EQUALS,
)
}
private fun toGroupOperatorDto(op: GroupOperator): VisibilityConditionGroupDto.Operator =
when (op) {
GroupOperator.AND -> VisibilityConditionGroupDto.Operator.AND
GroupOperator.OR -> VisibilityConditionGroupDto.Operator.OR
}
private fun toGroupOperator(op: VisibilityConditionGroupDto.Operator): GroupOperator =
when (op) {
VisibilityConditionGroupDto.Operator.AND -> GroupOperator.AND
VisibilityConditionGroupDto.Operator.OR -> GroupOperator.OR
}
private fun toNodeGroupOperatorDto(op: GroupOperator): VisibilityConditionNodeDto.GroupOperator =
when (op) {
GroupOperator.AND -> VisibilityConditionNodeDto.GroupOperator.AND
GroupOperator.OR -> VisibilityConditionNodeDto.GroupOperator.OR
}
private fun toGroupOperatorFromNode(op: VisibilityConditionNodeDto.GroupOperator?): GroupOperator =
when (op) {
VisibilityConditionNodeDto.GroupOperator.AND -> GroupOperator.AND
VisibilityConditionNodeDto.GroupOperator.OR -> GroupOperator.OR
null -> GroupOperator.AND
}
private fun toVisibilityConditionTypeDto(type: VisibilityConditionType): VisibilityConditionTypeDto = private fun toVisibilityConditionTypeDto(type: VisibilityConditionType): VisibilityConditionTypeDto =
when (type) { when (type) {
VisibilityConditionType.SHOW -> VisibilityConditionTypeDto.SHOW VisibilityConditionType.SHOW -> VisibilityConditionTypeDto.SHOW
@@ -45,27 +98,23 @@ class FormElementVisibilityConditionMapper {
operator: VisibilityConditionOperator, operator: VisibilityConditionOperator,
): VisibilityConditionOperatorDto = ): VisibilityConditionOperatorDto =
when (operator) { when (operator) {
VisibilityConditionOperator.EQUALS -> VisibilityConditionOperator.EQUALS -> VisibilityConditionOperatorDto.EQUALS
VisibilityConditionOperatorDto.EQUALS VisibilityConditionOperator.NOT_EQUALS -> VisibilityConditionOperatorDto.NOT_EQUALS
VisibilityConditionOperator.NOT_EQUALS -> VisibilityConditionOperator.IS_EMPTY -> VisibilityConditionOperatorDto.IS_EMPTY
VisibilityConditionOperatorDto.NOT_EQUALS VisibilityConditionOperator.IS_NOT_EMPTY -> VisibilityConditionOperatorDto.IS_NOT_EMPTY
VisibilityConditionOperator.IS_EMPTY -> VisibilityConditionOperator.CONTAINS -> VisibilityConditionOperatorDto.CONTAINS
VisibilityConditionOperatorDto.IS_EMPTY VisibilityConditionOperator.NOT_CONTAINS -> VisibilityConditionOperatorDto.NOT_CONTAINS
VisibilityConditionOperator.IS_NOT_EMPTY ->
VisibilityConditionOperatorDto.IS_NOT_EMPTY
} }
private fun toVisibilityConditionOperator( private fun toVisibilityConditionOperator(
operatorDto: VisibilityConditionOperatorDto, operatorDto: VisibilityConditionOperatorDto,
): VisibilityConditionOperator = ): VisibilityConditionOperator =
when (operatorDto) { when (operatorDto) {
VisibilityConditionOperatorDto.EQUALS -> VisibilityConditionOperatorDto.EQUALS -> VisibilityConditionOperator.EQUALS
VisibilityConditionOperator.EQUALS VisibilityConditionOperatorDto.NOT_EQUALS -> VisibilityConditionOperator.NOT_EQUALS
VisibilityConditionOperatorDto.NOT_EQUALS -> VisibilityConditionOperatorDto.IS_EMPTY -> VisibilityConditionOperator.IS_EMPTY
VisibilityConditionOperator.NOT_EQUALS VisibilityConditionOperatorDto.IS_NOT_EMPTY -> VisibilityConditionOperator.IS_NOT_EMPTY
VisibilityConditionOperatorDto.IS_EMPTY -> VisibilityConditionOperatorDto.CONTAINS -> VisibilityConditionOperator.CONTAINS
VisibilityConditionOperator.IS_EMPTY VisibilityConditionOperatorDto.NOT_CONTAINS -> VisibilityConditionOperator.NOT_CONTAINS
VisibilityConditionOperatorDto.IS_NOT_EMPTY ->
VisibilityConditionOperator.IS_NOT_EMPTY
} }
} }

View File

@@ -7,6 +7,8 @@ import jakarta.persistence.AttributeOverrides
import jakarta.persistence.Column import jakarta.persistence.Column
import jakarta.persistence.Embeddable import jakarta.persistence.Embeddable
import jakarta.persistence.Embedded import jakarta.persistence.Embedded
import org.hibernate.annotations.JdbcTypeCode
import org.hibernate.type.SqlTypes
@Embeddable @Embeddable
class FormOption( class FormOption(
@@ -35,4 +37,7 @@ class FormOption(
AttributeOverride(name = "isCheckbox", column = Column(name = "col_config_is_checkbox")), AttributeOverride(name = "isCheckbox", column = Column(name = "col_config_is_checkbox")),
) )
var columnConfig: TableColumnConfig? = null, var columnConfig: TableColumnConfig? = null,
@JdbcTypeCode(SqlTypes.JSON)
@Column(columnDefinition = "jsonb", name = "visibility_conditions")
var visibilityConditions: GroupCondition? = null,
) )

View File

@@ -6,6 +6,7 @@ import org.springframework.stereotype.Component
@Component @Component
class FormOptionMapper( class FormOptionMapper(
private val columnConfigMapper: TableColumnConfigMapper, private val columnConfigMapper: TableColumnConfigMapper,
private val visibilityConditionMapper: FormElementVisibilityConditionMapper,
) { ) {
fun toFormOptionDto(formOption: FormOption): FormOptionDto = fun toFormOptionDto(formOption: FormOption): FormOptionDto =
FormOptionDto( FormOptionDto(
@@ -14,6 +15,10 @@ class FormOptionMapper(
processingPurpose = formOption.processingPurpose, processingPurpose = formOption.processingPurpose,
employeeDataCategory = formOption.employeeDataCategory, employeeDataCategory = formOption.employeeDataCategory,
columnConfig = formOption.columnConfig?.let { columnConfigMapper.toTableColumnConfigDto(it) }, columnConfig = formOption.columnConfig?.let { columnConfigMapper.toTableColumnConfigDto(it) },
visibilityConditions =
formOption.visibilityConditions?.let {
visibilityConditionMapper.toGroupConditionDto(it)
},
) )
fun toFormOption(formOptionDto: FormOptionDto): FormOption = fun toFormOption(formOptionDto: FormOptionDto): FormOption =
@@ -23,5 +28,9 @@ class FormOptionMapper(
processingPurpose = formOptionDto.processingPurpose, processingPurpose = formOptionDto.processingPurpose,
employeeDataCategory = formOptionDto.employeeDataCategory, employeeDataCategory = formOptionDto.employeeDataCategory,
columnConfig = formOptionDto.columnConfig?.let { columnConfigMapper.toTableColumnConfig(it) }, columnConfig = formOptionDto.columnConfig?.let { columnConfigMapper.toTableColumnConfig(it) },
visibilityConditions =
formOptionDto.visibilityConditions?.let {
visibilityConditionMapper.toGroupCondition(it)
},
) )
} }

View File

@@ -43,6 +43,8 @@ class SectionSpawnTriggerMapper {
VisibilityConditionOperator.NOT_EQUALS -> VisibilityConditionOperatorDto.NOT_EQUALS VisibilityConditionOperator.NOT_EQUALS -> VisibilityConditionOperatorDto.NOT_EQUALS
VisibilityConditionOperator.IS_EMPTY -> VisibilityConditionOperatorDto.IS_EMPTY VisibilityConditionOperator.IS_EMPTY -> VisibilityConditionOperatorDto.IS_EMPTY
VisibilityConditionOperator.IS_NOT_EMPTY -> VisibilityConditionOperatorDto.IS_NOT_EMPTY VisibilityConditionOperator.IS_NOT_EMPTY -> VisibilityConditionOperatorDto.IS_NOT_EMPTY
VisibilityConditionOperator.CONTAINS -> VisibilityConditionOperatorDto.CONTAINS
VisibilityConditionOperator.NOT_CONTAINS -> VisibilityConditionOperatorDto.NOT_CONTAINS
} }
private fun toVisibilityConditionOperator( private fun toVisibilityConditionOperator(
@@ -53,5 +55,7 @@ class SectionSpawnTriggerMapper {
VisibilityConditionOperatorDto.NOT_EQUALS -> VisibilityConditionOperator.NOT_EQUALS VisibilityConditionOperatorDto.NOT_EQUALS -> VisibilityConditionOperator.NOT_EQUALS
VisibilityConditionOperatorDto.IS_EMPTY -> VisibilityConditionOperator.IS_EMPTY VisibilityConditionOperatorDto.IS_EMPTY -> VisibilityConditionOperator.IS_EMPTY
VisibilityConditionOperatorDto.IS_NOT_EMPTY -> VisibilityConditionOperator.IS_NOT_EMPTY VisibilityConditionOperatorDto.IS_NOT_EMPTY -> VisibilityConditionOperator.IS_NOT_EMPTY
VisibilityConditionOperatorDto.CONTAINS -> VisibilityConditionOperator.CONTAINS
VisibilityConditionOperatorDto.NOT_CONTAINS -> VisibilityConditionOperator.NOT_CONTAINS
} }
} }

View File

@@ -26,6 +26,8 @@ class TableColumnFilterMapper {
VisibilityConditionOperator.NOT_EQUALS -> VisibilityConditionOperatorDto.NOT_EQUALS VisibilityConditionOperator.NOT_EQUALS -> VisibilityConditionOperatorDto.NOT_EQUALS
VisibilityConditionOperator.IS_EMPTY -> VisibilityConditionOperatorDto.IS_EMPTY VisibilityConditionOperator.IS_EMPTY -> VisibilityConditionOperatorDto.IS_EMPTY
VisibilityConditionOperator.IS_NOT_EMPTY -> VisibilityConditionOperatorDto.IS_NOT_EMPTY VisibilityConditionOperator.IS_NOT_EMPTY -> VisibilityConditionOperatorDto.IS_NOT_EMPTY
VisibilityConditionOperator.CONTAINS -> VisibilityConditionOperatorDto.CONTAINS
VisibilityConditionOperator.NOT_CONTAINS -> VisibilityConditionOperatorDto.NOT_CONTAINS
} }
private fun VisibilityConditionOperatorDto.toEntity(): VisibilityConditionOperator = private fun VisibilityConditionOperatorDto.toEntity(): VisibilityConditionOperator =
@@ -34,5 +36,7 @@ class TableColumnFilterMapper {
VisibilityConditionOperatorDto.NOT_EQUALS -> VisibilityConditionOperator.NOT_EQUALS VisibilityConditionOperatorDto.NOT_EQUALS -> VisibilityConditionOperator.NOT_EQUALS
VisibilityConditionOperatorDto.IS_EMPTY -> VisibilityConditionOperator.IS_EMPTY VisibilityConditionOperatorDto.IS_EMPTY -> VisibilityConditionOperator.IS_EMPTY
VisibilityConditionOperatorDto.IS_NOT_EMPTY -> VisibilityConditionOperator.IS_NOT_EMPTY VisibilityConditionOperatorDto.IS_NOT_EMPTY -> VisibilityConditionOperator.IS_NOT_EMPTY
VisibilityConditionOperatorDto.CONTAINS -> VisibilityConditionOperator.CONTAINS
VisibilityConditionOperatorDto.NOT_CONTAINS -> VisibilityConditionOperator.NOT_CONTAINS
} }
} }

View File

@@ -0,0 +1,31 @@
package com.betriebsratkanzlei.legalconsenthub.form_element
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
@JsonTypeInfo(
use = JsonTypeInfo.Id.DEDUCTION,
defaultImpl = LeafCondition::class,
)
@JsonSubTypes(
JsonSubTypes.Type(GroupCondition::class),
JsonSubTypes.Type(LeafCondition::class),
)
sealed interface VisibilityConditionNode
data class GroupCondition(
val operator: GroupOperator,
val conditions: List<VisibilityConditionNode>,
) : VisibilityConditionNode
data class LeafCondition(
val formElementConditionType: VisibilityConditionType? = null,
val sourceFormElementReference: String,
val formElementExpectedValue: String? = null,
val formElementOperator: VisibilityConditionOperator = VisibilityConditionOperator.EQUALS,
) : VisibilityConditionNode
enum class GroupOperator {
AND,
OR,
}

View File

@@ -5,4 +5,6 @@ enum class VisibilityConditionOperator {
NOT_EQUALS, NOT_EQUALS,
IS_EMPTY, IS_EMPTY,
IS_NOT_EMPTY, IS_NOT_EMPTY,
CONTAINS,
NOT_CONTAINS,
} }

View File

@@ -3,6 +3,7 @@ package com.betriebsratkanzlei.legalconsenthub.seed
import com.betriebsratkanzlei.legalconsenthub.application_form.ApplicationFormMapper import com.betriebsratkanzlei.legalconsenthub.application_form.ApplicationFormMapper
import com.betriebsratkanzlei.legalconsenthub.application_form.ApplicationFormRepository import com.betriebsratkanzlei.legalconsenthub.application_form.ApplicationFormRepository
import com.betriebsratkanzlei.legalconsenthub.application_form_version.ApplicationFormVersionService import com.betriebsratkanzlei.legalconsenthub.application_form_version.ApplicationFormVersionService
import com.betriebsratkanzlei.legalconsenthub.config.visibilityConditionModule
import com.betriebsratkanzlei.legalconsenthub.user.User import com.betriebsratkanzlei.legalconsenthub.user.User
import com.betriebsratkanzlei.legalconsenthub.user.UserRepository import com.betriebsratkanzlei.legalconsenthub.user.UserRepository
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto
@@ -25,7 +26,10 @@ class InitialApplicationFormSeeder(
private val userRepository: UserRepository, private val userRepository: UserRepository,
private val versionService: ApplicationFormVersionService, private val versionService: ApplicationFormVersionService,
) : ApplicationRunner { ) : ApplicationRunner {
private val yamlMapper = ObjectMapper(YAMLFactory()).findAndRegisterModules() private val yamlMapper =
ObjectMapper(
YAMLFactory(),
).findAndRegisterModules().registerModule(visibilityConditionModule())
override fun run(args: ApplicationArguments) { override fun run(args: ApplicationArguments) {
seedInitialFormIfMissing() seedInitialFormIfMissing()

View File

@@ -2,6 +2,7 @@ package com.betriebsratkanzlei.legalconsenthub.seed
import com.betriebsratkanzlei.legalconsenthub.application_form.ApplicationFormMapper import com.betriebsratkanzlei.legalconsenthub.application_form.ApplicationFormMapper
import com.betriebsratkanzlei.legalconsenthub.application_form.ApplicationFormRepository import com.betriebsratkanzlei.legalconsenthub.application_form.ApplicationFormRepository
import com.betriebsratkanzlei.legalconsenthub.config.visibilityConditionModule
import com.betriebsratkanzlei.legalconsenthub.user.User import com.betriebsratkanzlei.legalconsenthub.user.User
import com.betriebsratkanzlei.legalconsenthub.user.UserRepository import com.betriebsratkanzlei.legalconsenthub.user.UserRepository
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto
@@ -22,7 +23,10 @@ class InitialApplicationFormTemplateSeeder(
private val applicationFormMapper: ApplicationFormMapper, private val applicationFormMapper: ApplicationFormMapper,
private val userRepository: UserRepository, private val userRepository: UserRepository,
) : ApplicationRunner { ) : ApplicationRunner {
private val yamlMapper = ObjectMapper(YAMLFactory()).findAndRegisterModules() private val yamlMapper =
ObjectMapper(
YAMLFactory(),
).findAndRegisterModules().registerModule(visibilityConditionModule())
override fun run(args: ApplicationArguments) { override fun run(args: ApplicationArguments) {
seedInitialTemplateIfMissing() seedInitialTemplateIfMissing()

View File

@@ -13,7 +13,7 @@ spring:
database-platform: org.hibernate.dialect.PostgreSQLDialect database-platform: org.hibernate.dialect.PostgreSQLDialect
hibernate: hibernate:
ddl-auto: create ddl-auto: create
show-sql: true show-sql: false
properties: properties:
hibernate: hibernate:
format_sql: true format_sql: true

View File

@@ -68,12 +68,13 @@ create table form_element_options
form_element_id uuid not null, form_element_id uuid not null,
col_config_filter_expected_val varchar(255), col_config_filter_expected_val varchar(255),
col_config_filter_operator varchar(255) check (col_config_filter_operator in col_config_filter_operator varchar(255) check (col_config_filter_operator in
('EQUALS', 'NOT_EQUALS', 'IS_EMPTY', ('EQUALS', 'NOT_EQUALS', 'IS_EMPTY', 'IS_NOT_EMPTY',
'IS_NOT_EMPTY')), 'CONTAINS', 'NOT_CONTAINS')),
col_config_source_table_ref varchar(255), col_config_source_table_ref varchar(255),
label varchar(255) not null, label varchar(255) not null,
option_value TEXT not null, option_value TEXT not null,
row_constraint_table_reference varchar(255) row_constraint_table_reference varchar(255),
visibility_conditions jsonb
); );
create table form_element create table form_element
@@ -89,9 +90,11 @@ create table form_element
reference varchar(255), reference varchar(255),
row_preset_filter_expected_val varchar(255), row_preset_filter_expected_val varchar(255),
row_preset_filter_operator varchar(255) check (row_preset_filter_operator in row_preset_filter_operator varchar(255) check (row_preset_filter_operator in
('EQUALS', 'NOT_EQUALS', 'IS_EMPTY', 'IS_NOT_EMPTY')), ('EQUALS', 'NOT_EQUALS', 'IS_EMPTY', 'IS_NOT_EMPTY', 'CONTAINS',
'NOT_CONTAINS')),
row_preset_source_table_ref varchar(255), row_preset_source_table_ref varchar(255),
title varchar(255), title varchar(255),
visibility_conditions jsonb,
primary key (id) primary key (id)
); );
@@ -141,7 +144,8 @@ create table section_spawn_triggers
section_spawn_condition_type varchar(255) check (section_spawn_condition_type in ('SHOW', 'HIDE')), section_spawn_condition_type varchar(255) check (section_spawn_condition_type in ('SHOW', 'HIDE')),
section_spawn_expected_value varchar(255), section_spawn_expected_value varchar(255),
section_spawn_operator varchar(255) check (section_spawn_operator in section_spawn_operator varchar(255) check (section_spawn_operator in
('EQUALS', 'NOT_EQUALS', 'IS_EMPTY', 'IS_NOT_EMPTY')), ('EQUALS', 'NOT_EQUALS', 'IS_EMPTY', 'IS_NOT_EMPTY', 'CONTAINS',
'NOT_CONTAINS')),
template_reference varchar(255) template_reference varchar(255)
); );
@@ -169,16 +173,6 @@ create table uploaded_file
primary key (id) primary key (id)
); );
create table visibility_conditions
(
form_element_id uuid not null,
form_element_condition_type varchar(255) check (form_element_condition_type in ('SHOW', 'HIDE')),
form_element_expected_value varchar(255),
form_element_operator varchar(255) check (form_element_operator in
('EQUALS', 'NOT_EQUALS', 'IS_EMPTY', 'IS_NOT_EMPTY')),
source_form_element_reference varchar(255)
);
alter table if exists application_form alter table if exists application_form
add constraint FKhtad5onoy2jknhtyfmx6cvvey add constraint FKhtad5onoy2jknhtyfmx6cvvey
foreign key (created_by_id) foreign key (created_by_id)
@@ -259,8 +253,3 @@ alter table if exists uploaded_file
add constraint FKtg323a9339lx0do79gu4eftao add constraint FKtg323a9339lx0do79gu4eftao
foreign key (uploaded_by_id) foreign key (uploaded_by_id)
references app_user; references app_user;
alter table if exists visibility_conditions
add constraint FK5xuf7bd179ogpq5a1m3g8q7jb
foreign key (form_element_id)
references form_element;

View File

@@ -78,6 +78,7 @@
import type { FormElementDto, FormOptionDto, TableRowPresetDto } from '~~/.api-client' import type { FormElementDto, FormOptionDto, TableRowPresetDto } from '~~/.api-client'
import type { TableColumn } from '@nuxt/ui' import type { TableColumn } from '@nuxt/ui'
import { useTableCrossReferences } from '~/composables/useTableCrossReferences' import { useTableCrossReferences } from '~/composables/useTableCrossReferences'
import { useFormElementVisibility } from '~/composables/useFormElementVisibility'
const props = defineProps<{ const props = defineProps<{
formOptions: FormOptionDto[] formOptions: FormOptionDto[]
@@ -86,6 +87,8 @@ const props = defineProps<{
tableRowPreset?: TableRowPresetDto tableRowPreset?: TableRowPresetDto
}>() }>()
const { isFormOptionVisible } = useFormElementVisibility()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:formOptions', value: FormOptionDto[]): void (e: 'update:formOptions', value: FormOptionDto[]): void
}>() }>()
@@ -131,16 +134,33 @@ interface DataColumn {
colIndex: number colIndex: number
} }
// Filter columns based on visibility conditions
interface VisibleColumn {
option: FormOptionDto
originalIndex: number
}
const visibleColumns = computed<VisibleColumn[]>(() => {
return props.formOptions
.map((option, index) => ({ option, originalIndex: index }))
.filter(({ option }) => {
if (!option.visibilityConditions || !props.allFormElements) {
return true
}
return isFormOptionVisible(option.visibilityConditions, props.allFormElements)
})
})
const dataColumns = computed<DataColumn[]>(() => const dataColumns = computed<DataColumn[]>(() =>
props.formOptions.map((_, index) => ({ visibleColumns.value.map(({ originalIndex }) => ({
key: `col_${index}`, key: `col_${originalIndex}`,
colIndex: index colIndex: originalIndex
})) }))
) )
const tableColumns = computed<TableColumn<TableRowData>[]>(() => { const tableColumns = computed<TableColumn<TableRowData>[]>(() => {
const columns: TableColumn<TableRowData>[] = props.formOptions.map((option, index) => ({ const columns: TableColumn<TableRowData>[] = visibleColumns.value.map(({ option, originalIndex }) => ({
accessorKey: `col_${index}`, accessorKey: `col_${originalIndex}`,
header: option.label || '' header: option.label || ''
})) }))

View File

@@ -1,15 +1,14 @@
import type { FormElementDto, FormElementVisibilityCondition, VisibilityConditionOperator } from '~~/.api-client' import type { FormElementDto, VisibilityConditionGroup, VisibilityConditionNode } from '~~/.api-client'
import { import {
VisibilityConditionOperator as VCOperator,
VisibilityConditionType as VCType, VisibilityConditionType as VCType,
VisibilityConditionNodeNodeTypeEnum as VCNodeTypeEnum,
VisibilityConditionGroupOperatorEnum as VCGroupOperatorEnum,
VisibilityConditionNodeGroupOperatorEnum as VCNodeGroupOperatorEnum,
VisibilityConditionOperator as VCOperator,
FormElementType FormElementType
} from '~~/.api-client' } from '~~/.api-client'
export function useFormElementVisibility() { export function useFormElementVisibility() {
/**
* Evaluates visibility for all form elements based on their visibility conditions.
* Returns a map of element key (id or reference) to visibility status.
*/
function evaluateFormElementVisibility(allFormElements: FormElementDto[]): Map<string, boolean> { function evaluateFormElementVisibility(allFormElements: FormElementDto[]): Map<string, boolean> {
const formElementsByRef = buildFormElementsMap(allFormElements) const formElementsByRef = buildFormElementsMap(allFormElements)
const visibilityMap = new Map<string, boolean>() const visibilityMap = new Map<string, boolean>()
@@ -25,6 +24,22 @@ export function useFormElementVisibility() {
return visibilityMap return visibilityMap
} }
/**
* Evaluates visibility conditions for a FormOption (e.g., table column).
* Unlike evaluateFormElementVisibility which works on FormElements,
* this evaluates standalone condition groups for options/columns.
*/
function isFormOptionVisible(
conditions: VisibilityConditionGroup | undefined,
allFormElements: FormElementDto[]
): boolean {
if (!conditions || !conditions.conditions || conditions.conditions.length === 0) {
return true
}
const formElementsByRef = buildFormElementsMap(allFormElements)
return evaluateGroup(conditions, formElementsByRef)
}
function buildFormElementsMap(formElements: FormElementDto[]): Map<string, FormElementDto> { function buildFormElementsMap(formElements: FormElementDto[]): Map<string, FormElementDto> {
const map = new Map<string, FormElementDto>() const map = new Map<string, FormElementDto>()
formElements.forEach((element) => { formElements.forEach((element) => {
@@ -35,96 +50,104 @@ export function useFormElementVisibility() {
return map return map
} }
/**
* Evaluates if an element is visible based on its visibility conditions.
* Multiple conditions use AND logic - all conditions must be met for the element to be visible.
*/
function isElementVisible(element: FormElementDto, formElementsByRef: Map<string, FormElementDto>): boolean { function isElementVisible(element: FormElementDto, formElementsByRef: Map<string, FormElementDto>): boolean {
const conditions = element.visibilityConditions const group = element.visibilityConditions
if (!conditions || conditions.length === 0) { if (!group || !group.conditions || group.conditions.length === 0) {
return true return true
} }
// All conditions must be met (AND logic) return evaluateGroup(group, formElementsByRef)
return conditions.every((condition) => evaluateSingleCondition(condition, formElementsByRef))
} }
/** function evaluateGroup(group: VisibilityConditionGroup, formElementsByRef: Map<string, FormElementDto>): boolean {
* Evaluates a single visibility condition against the form state. if (!group.conditions || group.conditions.length === 0) {
*/ return true
function evaluateSingleCondition( }
condition: FormElementVisibilityCondition, const results = group.conditions.map((c) => evaluateNode(c, formElementsByRef))
return group.operator === VCGroupOperatorEnum.And ? results.every(Boolean) : results.some(Boolean)
}
function evaluateNode(node: VisibilityConditionNode, formElementsByRef: Map<string, FormElementDto>): boolean {
if (node.nodeType === VCNodeTypeEnum.Group) {
return evaluateNodeGroup(node, formElementsByRef)
}
return evaluateLeafCondition(node, formElementsByRef)
}
function evaluateNodeGroup(node: VisibilityConditionNode, formElementsByRef: Map<string, FormElementDto>): boolean {
if (!node.conditions || node.conditions.length === 0) {
return true
}
const results = node.conditions.map((c) => evaluateNode(c, formElementsByRef))
return node.groupOperator === VCNodeGroupOperatorEnum.And ? results.every(Boolean) : results.some(Boolean)
}
function evaluateLeafCondition(
leaf: VisibilityConditionNode,
formElementsByRef: Map<string, FormElementDto> formElementsByRef: Map<string, FormElementDto>
): boolean { ): boolean {
const sourceElement = formElementsByRef.get(condition.sourceFormElementReference) if (!leaf.sourceFormElementReference) {
return false
}
const sourceElement = formElementsByRef.get(leaf.sourceFormElementReference)
if (!sourceElement) { if (!sourceElement) {
return false return false
} }
// Special handling for CHECKBOX with multiple options // Special handling for CHECKBOX with multiple options
if (sourceElement.type === FormElementType.Checkbox && sourceElement.options.length > 1) { if (sourceElement.type === FormElementType.Checkbox && sourceElement.options.length > 1) {
const operator = condition.formElementOperator || VCOperator.Equals const operator = leaf.formElementOperator || VCOperator.Equals
const conditionMet = evaluateCheckboxCondition(sourceElement, condition.formElementExpectedValue || '', operator) const conditionMet = evaluateCheckboxCondition(sourceElement, leaf.formElementExpectedValue || '', operator)
return condition.formElementConditionType === VCType.Show ? conditionMet : !conditionMet return leaf.formElementConditionType === VCType.Hide ? !conditionMet : conditionMet
} }
const sourceValue = getFormElementValue(sourceElement) const sourceValue = getFormElementValue(sourceElement)
const operator = condition.formElementOperator || VCOperator.Equals const operator = leaf.formElementOperator || VCOperator.Equals
const conditionMet = evaluateCondition(sourceValue, condition.formElementExpectedValue || '', operator) const conditionMet = evaluateCondition(sourceValue, leaf.formElementExpectedValue || '', operator)
return condition.formElementConditionType === VCType.Show ? conditionMet : !conditionMet return leaf.formElementConditionType === VCType.Hide ? !conditionMet : conditionMet
} }
function getFormElementValue(element: FormElementDto): string { function getFormElementValue(element: FormElementDto): string {
// For CHECKBOX with a single option, return the value directly
if (element.type === FormElementType.Checkbox && element.options.length === 1) { if (element.type === FormElementType.Checkbox && element.options.length === 1) {
return element.options[0]?.value || '' return element.options[0]?.value || ''
} }
// For other element types (RADIOBUTTON, SELECT, etc.), find the selected option and return its label
const selectedOption = element.options.find((option) => option.value === 'true') const selectedOption = element.options.find((option) => option.value === 'true')
return selectedOption?.label || '' return selectedOption?.label || ''
} }
/** function evaluateCheckboxCondition(element: FormElementDto, expectedValue: string, operator: string): boolean {
* Evaluates visibility condition for CHECKBOX with multiple options.
* Checks if ANY of the selected checkboxes matches the expected value.
*/
function evaluateCheckboxCondition(
element: FormElementDto,
expectedValue: string,
operator: VisibilityConditionOperator
): boolean {
const selectedLabels = element.options.filter((option) => option.value === 'true').map((option) => option.label) const selectedLabels = element.options.filter((option) => option.value === 'true').map((option) => option.label)
switch (operator) { switch (operator) {
case VCOperator.Equals: case VCOperator.Equals:
// Check if any selected checkbox label matches the expected value
return selectedLabels.some((label) => label.toLowerCase() === expectedValue.toLowerCase()) return selectedLabels.some((label) => label.toLowerCase() === expectedValue.toLowerCase())
case VCOperator.NotEquals: case VCOperator.NotEquals:
// Check if no selected checkbox label matches the expected value return !selectedLabels.some((label) => label.toLowerCase() === expectedValue.toLowerCase())
case VCOperator.Contains:
return selectedLabels.some((label) => label.toLowerCase() === expectedValue.toLowerCase())
case VCOperator.NotContains:
return !selectedLabels.some((label) => label.toLowerCase() === expectedValue.toLowerCase()) return !selectedLabels.some((label) => label.toLowerCase() === expectedValue.toLowerCase())
case VCOperator.IsEmpty: case VCOperator.IsEmpty:
// Check if no checkboxes are selected
return selectedLabels.length === 0 return selectedLabels.length === 0
case VCOperator.IsNotEmpty: case VCOperator.IsNotEmpty:
// Check if at least one checkbox is selected
return selectedLabels.length > 0 return selectedLabels.length > 0
default: default:
return false return false
} }
} }
function evaluateCondition( function evaluateCondition(actualValue: string, expectedValue: string, operator: string): boolean {
actualValue: string,
expectedValue: string,
operator: VisibilityConditionOperator
): boolean {
switch (operator) { switch (operator) {
case VCOperator.Equals: case VCOperator.Equals:
return actualValue.toLowerCase() === expectedValue.toLowerCase() return actualValue.toLowerCase() === expectedValue.toLowerCase()
case VCOperator.NotEquals: case VCOperator.NotEquals:
return actualValue.toLowerCase() !== expectedValue.toLowerCase() return actualValue.toLowerCase() !== expectedValue.toLowerCase()
case VCOperator.Contains:
return actualValue.toLowerCase().includes(expectedValue.toLowerCase())
case VCOperator.NotContains:
return !actualValue.toLowerCase().includes(expectedValue.toLowerCase())
case VCOperator.IsEmpty: case VCOperator.IsEmpty:
return actualValue === '' return actualValue === ''
case VCOperator.IsNotEmpty: case VCOperator.IsNotEmpty:
@@ -135,6 +158,7 @@ export function useFormElementVisibility() {
} }
return { return {
evaluateFormElementVisibility evaluateFormElementVisibility,
isFormOptionVisible
} }
} }