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

@@ -78,6 +78,7 @@
import type { FormElementDto, FormOptionDto, TableRowPresetDto } from '~~/.api-client'
import type { TableColumn } from '@nuxt/ui'
import { useTableCrossReferences } from '~/composables/useTableCrossReferences'
import { useFormElementVisibility } from '~/composables/useFormElementVisibility'
const props = defineProps<{
formOptions: FormOptionDto[]
@@ -86,6 +87,8 @@ const props = defineProps<{
tableRowPreset?: TableRowPresetDto
}>()
const { isFormOptionVisible } = useFormElementVisibility()
const emit = defineEmits<{
(e: 'update:formOptions', value: FormOptionDto[]): void
}>()
@@ -131,16 +134,33 @@ interface DataColumn {
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[]>(() =>
props.formOptions.map((_, index) => ({
key: `col_${index}`,
colIndex: index
visibleColumns.value.map(({ originalIndex }) => ({
key: `col_${originalIndex}`,
colIndex: originalIndex
}))
)
const tableColumns = computed<TableColumn<TableRowData>[]>(() => {
const columns: TableColumn<TableRowData>[] = props.formOptions.map((option, index) => ({
accessorKey: `col_${index}`,
const columns: TableColumn<TableRowData>[] = visibleColumns.value.map(({ option, originalIndex }) => ({
accessorKey: `col_${originalIndex}`,
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 {
VisibilityConditionOperator as VCOperator,
VisibilityConditionType as VCType,
VisibilityConditionNodeNodeTypeEnum as VCNodeTypeEnum,
VisibilityConditionGroupOperatorEnum as VCGroupOperatorEnum,
VisibilityConditionNodeGroupOperatorEnum as VCNodeGroupOperatorEnum,
VisibilityConditionOperator as VCOperator,
FormElementType
} from '~~/.api-client'
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> {
const formElementsByRef = buildFormElementsMap(allFormElements)
const visibilityMap = new Map<string, boolean>()
@@ -25,6 +24,22 @@ export function useFormElementVisibility() {
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> {
const map = new Map<string, FormElementDto>()
formElements.forEach((element) => {
@@ -35,96 +50,104 @@ export function useFormElementVisibility() {
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 {
const conditions = element.visibilityConditions
if (!conditions || conditions.length === 0) {
const group = element.visibilityConditions
if (!group || !group.conditions || group.conditions.length === 0) {
return true
}
// All conditions must be met (AND logic)
return conditions.every((condition) => evaluateSingleCondition(condition, formElementsByRef))
return evaluateGroup(group, formElementsByRef)
}
/**
* Evaluates a single visibility condition against the form state.
*/
function evaluateSingleCondition(
condition: FormElementVisibilityCondition,
function evaluateGroup(group: VisibilityConditionGroup, formElementsByRef: Map<string, FormElementDto>): boolean {
if (!group.conditions || group.conditions.length === 0) {
return true
}
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>
): boolean {
const sourceElement = formElementsByRef.get(condition.sourceFormElementReference)
if (!leaf.sourceFormElementReference) {
return false
}
const sourceElement = formElementsByRef.get(leaf.sourceFormElementReference)
if (!sourceElement) {
return false
}
// Special handling for CHECKBOX with multiple options
if (sourceElement.type === FormElementType.Checkbox && sourceElement.options.length > 1) {
const operator = condition.formElementOperator || VCOperator.Equals
const conditionMet = evaluateCheckboxCondition(sourceElement, condition.formElementExpectedValue || '', operator)
return condition.formElementConditionType === VCType.Show ? conditionMet : !conditionMet
const operator = leaf.formElementOperator || VCOperator.Equals
const conditionMet = evaluateCheckboxCondition(sourceElement, leaf.formElementExpectedValue || '', operator)
return leaf.formElementConditionType === VCType.Hide ? !conditionMet : conditionMet
}
const sourceValue = getFormElementValue(sourceElement)
const operator = condition.formElementOperator || VCOperator.Equals
const conditionMet = evaluateCondition(sourceValue, condition.formElementExpectedValue || '', operator)
const operator = leaf.formElementOperator || VCOperator.Equals
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 {
// For CHECKBOX with a single option, return the value directly
if (element.type === FormElementType.Checkbox && element.options.length === 1) {
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')
return selectedOption?.label || ''
}
/**
* 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 {
function evaluateCheckboxCondition(element: FormElementDto, expectedValue: string, operator: string): boolean {
const selectedLabels = element.options.filter((option) => option.value === 'true').map((option) => option.label)
switch (operator) {
case VCOperator.Equals:
// Check if any selected checkbox label matches the expected value
return selectedLabels.some((label) => label.toLowerCase() === expectedValue.toLowerCase())
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())
case VCOperator.IsEmpty:
// Check if no checkboxes are selected
return selectedLabels.length === 0
case VCOperator.IsNotEmpty:
// Check if at least one checkbox is selected
return selectedLabels.length > 0
default:
return false
}
}
function evaluateCondition(
actualValue: string,
expectedValue: string,
operator: VisibilityConditionOperator
): boolean {
function evaluateCondition(actualValue: string, expectedValue: string, operator: string): boolean {
switch (operator) {
case VCOperator.Equals:
return actualValue.toLowerCase() === expectedValue.toLowerCase()
case VCOperator.NotEquals:
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:
return actualValue === ''
case VCOperator.IsNotEmpty:
@@ -135,6 +158,7 @@ export function useFormElementVisibility() {
}
return {
evaluateFormElementVisibility
evaluateFormElementVisibility,
isFormOptionVisible
}
}