Compare commits

..

10 Commits

33 changed files with 2403 additions and 677 deletions

View File

@@ -1681,6 +1681,30 @@ components:
type: boolean
default: false
description: If true, renders a checkbox instead of text input
readOnlyConditions:
$ref: "#/components/schemas/VisibilityConditionGroup"
description: If set, the column is read-only when conditions evaluate to true.
readOnlyDefaultValue:
type: string
description: Value to write into each cell when the column transitions to read-only via readOnlyConditions.
rowVisibilityCondition:
$ref: "#/components/schemas/TableRowVisibilityConditionDto"
description: If set, individual cells are hidden/shown based on the row's value in the specified column
TableRowVisibilityConditionDto:
type: object
description: Per-row cell visibility condition referencing another column in the same table
properties:
sourceColumnIndex:
type: integer
description: Index of the column in the same table to evaluate (0-based)
expectedValues:
type: array
items:
type: string
description: Cell is visible if the source column's value matches any of these with the given operator
operator:
$ref: "#/components/schemas/VisibilityConditionOperator"
TableRowConstraintDto:
type: object

View File

@@ -35,6 +35,18 @@ class FormOption(
AttributeOverride(name = "filterCondition.operator", column = Column(name = "col_config_filter_operator")),
AttributeOverride(name = "isReadOnly", column = Column(name = "col_config_is_read_only")),
AttributeOverride(name = "isCheckbox", column = Column(name = "col_config_is_checkbox")),
AttributeOverride(
name = "readOnlyDefaultValue",
column = Column(name = "col_config_read_only_default_value"),
),
AttributeOverride(
name = "readOnlyConditions",
column = Column(name = "col_config_read_only_conditions", columnDefinition = "jsonb"),
),
AttributeOverride(
name = "rowVisibilityCondition",
column = Column(name = "col_config_row_visibility_condition", columnDefinition = "jsonb"),
),
)
var columnConfig: TableColumnConfig? = null,
@JdbcTypeCode(SqlTypes.JSON)

View File

@@ -5,6 +5,8 @@ import jakarta.persistence.AttributeOverrides
import jakarta.persistence.Column
import jakarta.persistence.Embeddable
import jakarta.persistence.Embedded
import org.hibernate.annotations.JdbcTypeCode
import org.hibernate.type.SqlTypes
@Embeddable
data class TableColumnConfig(
@@ -29,4 +31,11 @@ data class TableColumnConfig(
val isReadOnly: Boolean = false,
val isMultipleAllowed: Boolean = false,
val isCheckbox: Boolean = false,
@JdbcTypeCode(SqlTypes.JSON)
@Column(columnDefinition = "jsonb")
val readOnlyConditions: GroupCondition? = null,
val readOnlyDefaultValue: String? = null,
@JdbcTypeCode(SqlTypes.JSON)
@Column(columnDefinition = "jsonb")
val rowVisibilityCondition: RowVisibilityCondition? = null,
)

View File

@@ -1,12 +1,15 @@
package com.betriebsratkanzlei.legalconsenthub.form_element
import com.betriebsratkanzlei.legalconsenthub_api.model.TableColumnConfigDto
import com.betriebsratkanzlei.legalconsenthub_api.model.TableRowVisibilityConditionDto
import org.springframework.stereotype.Component
import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionOperator as VisibilityConditionOperatorDto
@Component
class TableColumnConfigMapper(
private val filterMapper: TableColumnFilterMapper,
private val rowConstraintMapper: TableRowConstraintMapper,
private val visibilityConditionMapper: FormElementVisibilityConditionMapper,
) {
fun toTableColumnConfigDto(config: TableColumnConfig): TableColumnConfigDto =
TableColumnConfigDto(
@@ -17,6 +20,9 @@ class TableColumnConfigMapper(
isReadOnly = config.isReadOnly,
isMultipleAllowed = config.isMultipleAllowed,
isCheckbox = config.isCheckbox,
readOnlyConditions = config.readOnlyConditions?.let { visibilityConditionMapper.toGroupConditionDto(it) },
readOnlyDefaultValue = config.readOnlyDefaultValue,
rowVisibilityCondition = config.rowVisibilityCondition?.let { toRowVisibilityConditionDto(it) },
)
fun toTableColumnConfig(dto: TableColumnConfigDto): TableColumnConfig =
@@ -28,5 +34,42 @@ class TableColumnConfigMapper(
isReadOnly = dto.isReadOnly ?: false,
isMultipleAllowed = dto.isMultipleAllowed ?: false,
isCheckbox = dto.isCheckbox ?: false,
readOnlyConditions = dto.readOnlyConditions?.let { visibilityConditionMapper.toGroupCondition(it) },
readOnlyDefaultValue = dto.readOnlyDefaultValue,
rowVisibilityCondition = dto.rowVisibilityCondition?.let { toRowVisibilityCondition(it) },
)
private fun toRowVisibilityConditionDto(entity: RowVisibilityCondition): TableRowVisibilityConditionDto =
TableRowVisibilityConditionDto(
sourceColumnIndex = entity.sourceColumnIndex,
expectedValues = entity.expectedValues,
operator = entity.operator.toDto(),
)
private fun toRowVisibilityCondition(dto: TableRowVisibilityConditionDto): RowVisibilityCondition =
RowVisibilityCondition(
sourceColumnIndex = dto.sourceColumnIndex ?: 0,
expectedValues = dto.expectedValues ?: emptyList(),
operator = dto.operator?.toEntity() ?: VisibilityConditionOperator.CONTAINS,
)
private fun VisibilityConditionOperator.toDto(): VisibilityConditionOperatorDto =
when (this) {
VisibilityConditionOperator.EQUALS -> VisibilityConditionOperatorDto.EQUALS
VisibilityConditionOperator.NOT_EQUALS -> VisibilityConditionOperatorDto.NOT_EQUALS
VisibilityConditionOperator.IS_EMPTY -> VisibilityConditionOperatorDto.IS_EMPTY
VisibilityConditionOperator.IS_NOT_EMPTY -> VisibilityConditionOperatorDto.IS_NOT_EMPTY
VisibilityConditionOperator.CONTAINS -> VisibilityConditionOperatorDto.CONTAINS
VisibilityConditionOperator.NOT_CONTAINS -> VisibilityConditionOperatorDto.NOT_CONTAINS
}
private fun VisibilityConditionOperatorDto.toEntity(): VisibilityConditionOperator =
when (this) {
VisibilityConditionOperatorDto.EQUALS -> VisibilityConditionOperator.EQUALS
VisibilityConditionOperatorDto.NOT_EQUALS -> VisibilityConditionOperator.NOT_EQUALS
VisibilityConditionOperatorDto.IS_EMPTY -> VisibilityConditionOperator.IS_EMPTY
VisibilityConditionOperatorDto.IS_NOT_EMPTY -> VisibilityConditionOperator.IS_NOT_EMPTY
VisibilityConditionOperatorDto.CONTAINS -> VisibilityConditionOperator.CONTAINS
VisibilityConditionOperatorDto.NOT_CONTAINS -> VisibilityConditionOperator.NOT_CONTAINS
}
}

View File

@@ -25,6 +25,12 @@ data class LeafCondition(
val formElementOperator: VisibilityConditionOperator = VisibilityConditionOperator.EQUALS,
) : VisibilityConditionNode
data class RowVisibilityCondition(
val sourceColumnIndex: Int,
val expectedValues: List<String>,
val operator: VisibilityConditionOperator = VisibilityConditionOperator.CONTAINS,
)
enum class GroupOperator {
AND,
OR,

View File

@@ -70,10 +70,13 @@ create table form_element_options
col_config_filter_operator varchar(255) check (col_config_filter_operator in
('EQUALS', 'NOT_EQUALS', 'IS_EMPTY', 'IS_NOT_EMPTY',
'CONTAINS', 'NOT_CONTAINS')),
col_config_read_only_default_value varchar(255),
col_config_source_table_ref varchar(255),
label varchar(255) not null,
option_value TEXT not null,
row_constraint_table_reference varchar(255),
col_config_read_only_conditions jsonb,
col_config_row_visibility_condition jsonb,
visibility_conditions jsonb
);

View File

@@ -31,14 +31,6 @@ formElementSubSections:
sectionSpawnConditionType: SHOW
sectionSpawnExpectedValue: Einführung
sectionSpawnOperator: EQUALS
- templateReference: loeschkonzept_template
sectionSpawnConditionType: SHOW
sectionSpawnExpectedValue: Einführung
sectionSpawnOperator: EQUALS
- templateReference: datenschutz_template
sectionSpawnConditionType: SHOW
sectionSpawnExpectedValue: Einführung
sectionSpawnOperator: EQUALS
- templateReference: auswirkungen_arbeitnehmer_template
sectionSpawnConditionType: SHOW
sectionSpawnExpectedValue: Einführung
@@ -520,6 +512,14 @@ formElementSubSections:
sectionSpawnConditionType: SHOW
sectionSpawnExpectedValue: Personenbeziehbar
sectionSpawnOperator: EQUALS
- templateReference: loeschkonzept_template
sectionSpawnConditionType: SHOW
sectionSpawnExpectedValue: Personenbeziehbar
sectionSpawnOperator: EQUALS
- templateReference: datenschutz_template
sectionSpawnConditionType: SHOW
sectionSpawnExpectedValue: Personenbeziehbar
sectionSpawnOperator: EQUALS
visibilityConditions:
operator: OR
conditions:
@@ -669,6 +669,23 @@ formElementSubSections:
sourceFormElementReference: sens_auswertung
formElementExpectedValue: Funktionen vorhanden
formElementOperator: EQUALS
- reference: sens_art_analytische_funktionen_sonstiges
title: Beschreibung der sonstigen analytischen Funktionen
description: ''
options:
- value: ''
label: Beschreibung
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
type: TEXTAREA
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_art_analytische_funktionen
formElementExpectedValue: Sonstiges
formElementOperator: CONTAINS
- reference: sens_luv
title: Werden analytischen Funktionen für Leistungs-/Verhaltenskontrolle genutzt?
description: ''
@@ -724,7 +741,7 @@ formElementSubSections:
title: Werden Ereignisse, Nutzungen und Logs erfasst?
description: ''
options:
- value: ''
- value: 'false'
label: Nein
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
@@ -732,15 +749,23 @@ formElementSubSections:
label: Technisch (Betrieb, Sicherheit)
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
- value: ''
- value: 'false'
label: Ja (Nutzer-/Aktivitätsbezug)
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
- value: ''
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit
formElementOperator: NOT_EQUALS
formElementExpectedValue: "Für Administratoren"
- value: 'false'
label: Audit-Logs
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
type: RADIOBUTTON
type: CHECKBOX
visibilityConditions:
operator: AND
conditions:
@@ -791,6 +816,14 @@ formElementSubSections:
label: Fachlich
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit
formElementOperator: NOT_EQUALS
formElementExpectedValue: "Für Administratoren"
type: CHECKBOX
visibilityConditions:
operator: AND
@@ -830,31 +863,41 @@ formElementSubSections:
title: Bewertet / empfiehlt das System Maßnahmen über Beschäftigte oder bereitet Entscheidungen maßgeblich vor?
description: ''
options:
- value: 'true'
label: Ja
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
- value: ''
label: Nein
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
- value: 'true'
label: Unterstützend (Empfehlung)
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
- value: ''
label: Auto-Entscheidungen
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
type: CHECKBOX
type: RADIOBUTTON
visibilityConditions:
operator: OR
operator: AND
conditions:
- sourceFormElementReference: art_der_massnahme
- nodeType: GROUP
groupOperator: OR
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: art_der_massnahme
formElementExpectedValue: Einführung
formElementOperator: EQUALS
- sourceFormElementReference: art_der_massnahme
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: art_der_massnahme
formElementExpectedValue: Einführung mit einhergehender Ablösung
formElementOperator: EQUALS
- sourceFormElementReference: art_der_massnahme
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: art_der_massnahme
formElementExpectedValue: Änderung IT-System
formElementOperator: EQUALS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit
formElementExpectedValue: Für Administratoren
formElementOperator: NOT_EQUALS
- reference: sens_ki
title: Kommt im System Künstliche Intelligenz zum Einsatz?
description: ''
@@ -894,7 +937,7 @@ formElementSubSections:
label: Schnittstellen vorhanden
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
- value: ''
- value: 'true'
label: Exporte möglich
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED

View File

@@ -14,20 +14,7 @@ formElementSubSections:
description: ''
type: TABLE
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit
formElementExpectedValue: Für Administratoren
formElementOperator: NOT_EQUALS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_auswertung
formElementExpectedValue: Funktionen vorhanden
formElementOperator: EQUALS
- nodeType: GROUP
groupOperator: OR
operator: OR
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
@@ -46,7 +33,7 @@ formElementSubSections:
formElementOperator: CONTAINS
options:
- value: '["V001", "V002", "V003", "V004", "V005"]'
label: Verarbeitungsvorgang-ID
label: Verarbeitungs-ID
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
- value: '["Personalstammdatenpflege", "Zeiterfassung", "Gehaltsabrechnung", "Leistungsbeurteilung", "Produktionsauswertung"]'
@@ -54,13 +41,13 @@ formElementSubSections:
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
- value: '["HCM Master Data", "CATS Zeiterfassung", "Payroll Processing", "Performance Management", "Shop Floor Control"]'
label: Systemfunktion/Verarbeitungsform
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
- value: '["Anlage und Pflege von Mitarbeiterstammdaten", "Erfassung von Arbeitszeiten und Abwesenheiten", "Berechnung und Auszahlung von Gehältern", "Erfassung und Auswertung von Leistungsdaten", "Analyse von Produktionskennzahlen pro Schicht"]'
label: Kurzbeschreibung
label: Verarbeitungsform
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
- value: '["Anlage und Pflege von Mitarbeiterstammdaten zur Personalverwaltung", "Erfassung von Arbeitszeiten und Abwesenheiten zur Arbeitszeitdokumentation", "Berechnung und Auszahlung von Gehältern zur Entgeltabrechnung", "Erfassung und Auswertung von Leistungsdaten zur Personalentwicklung", "Analyse von Produktionskennzahlen pro Schicht zur Produktionssteuerung"]'
label: Verarbeitungszweck
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
- value: '["Stammdaten", "Arbeitszeitdaten", "Gehaltsdaten", "Leistungsdaten", "Produktionsdaten"]'
label: Datenkategorien
processingPurpose: DATA_ANALYSIS
@@ -73,14 +60,6 @@ formElementSubSections:
label: Betroffene Mitarbeiter
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
- value: '["Personalverwaltung", "Arbeitszeitdokumentation", "Entgeltabrechnung", "Personalentwicklung", "Produktionssteuerung"]'
label: Allgemeiner Zweck
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
- value: '["Fortlaufend", "Täglich", "Monatlich", "Jährlich/Halbjährlich", "Täglich/Schichtweise"]'
label: Häufigkeit/Anlass
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
- value: '["R002", "R002,R004", "R001,R002", "R002,R004", "R003,R004"]'
label: Rollen-Sichtbarkeit (grob)
processingPurpose: DATA_ANALYSIS
@@ -103,9 +82,17 @@ formElementSubSections:
formElementExpectedValue: Für Administratoren
formElementOperator: NOT_EQUALS
- value: '["Nein", "Ja - an Vorgesetzte", "Nein", "Ja - an Management", "Ja - an Produktionsleitung"]'
label: Export/Weitergabe (Ja/Nein + Ziel)
label: Export/Weitergabe
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_schnittstellen_export
formElementExpectedValue: Exporte möglich
formElementOperator: CONTAINS
- value: '[false, true, false, true, true]'
label: Leistungs-/Verhaltenskontrolle beabsichtigt?
processingPurpose: DATA_ANALYSIS
@@ -113,152 +100,16 @@ formElementSubSections:
columnConfig:
isCheckbox: true
# Angaben zur Leistungs-/Verhaltenskontrolle
- title: Angaben zur Leistungs-/Verhaltenskontrolle
# Rollen-Sichtbarkeit (Umfassende Darstellung - shown when LuV contains Team, Abteilung, or Individuell)
- title: Rollen-Sichtbarkeit (Umfassende Darstellung)
formElements:
- reference: luv_details_tabelle
title: Angaben zur Leistungs-/Verhaltenskontrolle
- reference: rollen_sichtbarkeit_umfassend_tabelle
title: Welche Rollen können welche Verarbeitungsvorgänge sehen? (Umfassende Darstellung)
description: ''
type: TABLE
tableRowPreset:
sourceTableReference: umfassende_datenverarbeitung_tabelle
filterCondition:
sourceColumnIndex: 11
expectedValue: 'true'
operator: EQUALS
columnMappings:
- sourceColumnIndex: 0
targetColumnIndex: 0
canAddRows: false
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit
formElementExpectedValue: Für Administratoren
formElementOperator: NOT_EQUALS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_auswertung
formElementExpectedValue: Funktionen vorhanden
formElementOperator: EQUALS
- nodeType: GROUP
groupOperator: OR
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_luv
formElementExpectedValue: Aggregiert (Team)
formElementOperator: CONTAINS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_luv
formElementExpectedValue: Aggregiert (Abteilung)
formElementOperator: CONTAINS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_luv
formElementExpectedValue: Individuell/vergleichend
formElementOperator: CONTAINS
options:
- value: '["V002", "V004", "V005"]'
label: Verarbeitungsvorgang-ID
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
columnConfig:
sourceTableReference: umfassende_datenverarbeitung_tabelle
sourceColumnIndex: 0
isReadOnly: true
- value: '["Überwachung der Einhaltung von Arbeitszeiten", "Bewertung der individuellen Zielerreichung", "Auswertung der Produktivität pro Mitarbeiter/Schicht"]'
label: Konkreter Kontrollzweck
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
- value: '["Berichte", "Dashboards", "Berichte,Dashboards"]'
label: Kontrollart
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
columnConfig:
sourceTableReference: sens_art_analytische_funktionen
sourceColumnIndex: 0
isMultipleAllowed: true
- value: '["Hinweis bei Abweichungen, keine automatischen Konsequenzen", "Einfluss auf Bonuszahlungen und Beförderungen", "Grundlage für Schichtplanung und Personalentscheidungen"]'
label: Entscheidungswirkung
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
- value: '["Aggregiert (Team)", "Individuell/vergleichend", "Aggregiert (Team),Individuell/vergleichend"]'
label: Granularität/Bezugsebene
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
columnConfig:
sourceTableReference: sens_luv
sourceColumnIndex: 0
isMultipleAllowed: true
- value: '["Ja", "Ja", "Ja, bei begründetem Verdacht"]'
label: Drilldown bis Person möglich?
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
- value: '["Nein", "Ja - Ranking im Team", "Ja - Vergleich mit Durchschnitt"]'
label: Ranking/Scoring
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
visibilityConditions:
operator: OR
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_art_analytische_funktionen
formElementExpectedValue: Rankings
formElementOperator: CONTAINS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_art_analytische_funktionen
formElementExpectedValue: Scores
formElementOperator: CONTAINS
- value: '["N/A", "Min. 5 Personen im Vergleich", "Min. 10 Personen pro Auswertung"]'
label: Mindestgruppe/Schwelle
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
- value: '["Automatischer Hinweis bei > 10h Arbeitszeit", "Nein, manuelle Bewertung", "Alert bei Produktivität < 80% des Durchschnitts"]'
label: Automatisierte Alerts/Entscheidungen
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_alarme
formElementExpectedValue: Nein
formElementOperator: NOT_CONTAINS
- value: '["Benachrichtigung an Mitarbeiter und Vorgesetzten", "4-Augen-Prinzip bei Bewertungen", "Prüfung durch BR vor Einzelauswertungen"]'
label: Schutzmaßnahmen/Governance
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: REVIEW_REQUIRED
# Access rules table
- title: Zugriffsregeln hinsichtlich der Verarbeitungsvorgänge
formElements:
- reference: zugriffsregeln_tabelle
title: Zugriffsregeln hinsichtlich der Verarbeitungsvorgänge
description: ''
type: TABLE
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit
formElementExpectedValue: Für Administratoren
formElementOperator: NOT_EQUALS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_auswertung
formElementExpectedValue: Funktionen vorhanden
formElementOperator: EQUALS
- nodeType: GROUP
groupOperator: OR
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_luv
@@ -276,7 +127,218 @@ formElementSubSections:
formElementOperator: CONTAINS
options:
- value: '["V001", "V002", "V003", "V004", "V005"]'
label: Verarbeitungsvorgang-ID
label: Verarbeitungs-ID
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
columnConfig:
sourceTableReference: umfassende_datenverarbeitung_tabelle
sourceColumnIndex: 0
- value: '["R002", "R002", "R001", "R004", "R003"]'
label: Rollen-ID
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
columnConfig:
sourceTableReference: rollenstamm_tabelle
sourceColumnIndex: 0
- value: '["Nein", "Ja - an Vorgesetzte", "Nein", "Ja - an Management", "Ja - an Produktionsleitung"]'
label: Export/Weitergabe
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_schnittstellen_export
formElementExpectedValue: Exporte möglich
formElementOperator: CONTAINS
- value: '["", "Direkter Vorgesetzter (Team Lead)", "", "HR-Management und Geschäftsführung", "Produktionsleitung und Schichtführer"]'
label: Empfänger
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_schnittstellen_export
formElementExpectedValue: Exporte möglich
formElementOperator: CONTAINS
- value: '[true, true, false, true, true]'
label: Personenbezug möglich
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
- value: '["Aggregierte Stammdaten sichtbar", "Nur eigene Teamdaten", "Nur Finanzkennzahlen ohne Personenbezug", "Leistungsberichte mit Namensnennung", "Schichtauswertungen mit Mitarbeiterliste"]'
label: Hinweise
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
# Angaben zur Leistungs-/Verhaltenskontrolle
- title: Angaben zur Leistungs-/Verhaltenskontrolle
formElements:
- reference: luv_details_tabelle
title: Angaben zur Leistungs-/Verhaltenskontrolle
description: ''
type: TABLE
tableRowPreset:
sourceTableReference: umfassende_datenverarbeitung_tabelle
filterCondition:
sourceColumnIndex: 9
expectedValue: 'true'
operator: EQUALS
columnMappings:
- sourceColumnIndex: 0
targetColumnIndex: 0
canAddRows: false
visibilityConditions:
operator: OR
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_luv
formElementExpectedValue: Aggregiert (Team)
formElementOperator: CONTAINS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_luv
formElementExpectedValue: Aggregiert (Abteilung)
formElementOperator: CONTAINS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_luv
formElementExpectedValue: Individuell/vergleichend
formElementOperator: CONTAINS
options:
- value: '["V002", "V004", "V005"]'
label: Verarbeitungs-ID
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
columnConfig:
sourceTableReference: umfassende_datenverarbeitung_tabelle
sourceColumnIndex: 0
isReadOnly: true
- value: '["Überwachung der Einhaltung von Arbeitszeiten", "Bewertung der individuellen Zielerreichung", "Auswertung der Produktivität pro Mitarbeiter/Schicht"]'
label: Kontrollzweck
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
- value: '["Sicherstellung korrekter Arbeitszeiterfassung und -vergütung", "Leistungsorientierte Vergütung und Personalentwicklung", "Optimierung der Produktionseffizienz und Ressourcenplanung"]'
label: Berechtigtes Kontrollinteresse
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
- value: '["Berichte", "Dashboards", "Berichte,Dashboards"]'
label: Kontrollart
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
columnConfig:
sourceTableReference: sens_art_analytische_funktionen
sourceColumnIndex: 0
isMultipleAllowed: true
- value: '["Täglich", "Jährlich/Halbjährlich", "Täglich/Schichtweise"]'
label: Häufigkeit/Anlass
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
- value: '["Aggregiert (Team)", "Individuell/vergleichend", "Aggregiert (Team),Individuell/vergleichend"]'
label: Granularität/Bezugsebene
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
columnConfig:
sourceTableReference: sens_luv
sourceColumnIndex: 0
isMultipleAllowed: true
- value: '[true, true, true]'
label: Drilldown auf Individualebene
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
columnConfig:
isCheckbox: true
rowVisibilityCondition:
sourceColumnIndex: 5
expectedValues:
- "Aggregiert (Team)"
- "Aggregiert (Abteilung)"
operator: CONTAINS
- value: '["N/A", "Min. 5 Personen im Vergleich", "Min. 10 Personen pro Auswertung"]'
label: Mindestgruppe/Schwelle
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
visibilityConditions:
operator: OR
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_luv
formElementExpectedValue: Aggregiert (Team)
formElementOperator: CONTAINS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_luv
formElementExpectedValue: Aggregiert (Abteilung)
formElementOperator: CONTAINS
- value: '["Automatischer Hinweis bei > 10h Arbeitszeit", "Nein", "Alert bei Produktivität < 80% des Durchschnitts"]'
label: Automatisierte Alerts
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_alarme
formElementExpectedValue: Nein
formElementOperator: NOT_CONTAINS
- value: '["Nein", "Empfehlung für Bonushöhe", "Nein"]'
label: Automatisierte Entscheidungen
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_automatisierte_entscheidungen
formElementExpectedValue: Ja
formElementOperator: EQUALS
- value: '["Unterstützend", "Empfehlung", "Unterstützend"]'
label: Art der Entscheidungsunterstützung
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_automatisierte_entscheidungen
formElementExpectedValue: Ja
formElementOperator: EQUALS
# Access rules table
- title: Zugriffsregeln hinsichtlich der Verarbeitungsvorgänge
formElements:
- reference: zugriffsregeln_tabelle
title: Zugriffsregeln hinsichtlich der Verarbeitungsvorgänge
description: ''
type: TABLE
visibilityConditions:
operator: OR
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_luv
formElementExpectedValue: Aggregiert (Team)
formElementOperator: CONTAINS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_luv
formElementExpectedValue: Aggregiert (Abteilung)
formElementOperator: CONTAINS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_luv
formElementExpectedValue: Individuell/vergleichend
formElementOperator: CONTAINS
options:
- value: '["V001", "V002", "V003", "V004", "V005"]'
label: Verarbeitungs-ID
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
columnConfig:
@@ -316,20 +378,7 @@ formElementSubSections:
description: ''
type: TABLE
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit
formElementExpectedValue: Für Administratoren
formElementOperator: NOT_EQUALS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_auswertung
formElementExpectedValue: Funktionen vorhanden
formElementOperator: EQUALS
- nodeType: GROUP
groupOperator: OR
operator: OR
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
@@ -348,7 +397,7 @@ formElementSubSections:
formElementOperator: CONTAINS
options:
- value: '["V001", "V002", "V003", "V004", "V005"]'
label: Verarbeitungsvorgang-ID
label: Verarbeitungs-ID
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
columnConfig:

View File

@@ -116,16 +116,6 @@ formElementSubSections:
sourceFormElementReference: loeschkonzept_hinterlegen
formElementExpectedValue: 'true'
formElementOperator: EQUALS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit
formElementExpectedValue: Für Administratoren
formElementOperator: NOT_EQUALS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_auswertung
formElementExpectedValue: Funktionen vorhanden
formElementOperator: EQUALS
- nodeType: GROUP
groupOperator: OR
conditions:

View File

@@ -17,20 +17,7 @@ formElementSubSections:
description: ''
type: TABLE
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit
formElementExpectedValue: Für Administratoren
formElementOperator: NOT_EQUALS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_auswertung
formElementExpectedValue: Funktionen vorhanden
formElementOperator: EQUALS
- nodeType: GROUP
groupOperator: OR
operator: OR
conditions:
- nodeType: LEAF
formElementConditionType: SHOW

View File

@@ -118,11 +118,6 @@ formElementSubSections:
sourceFormElementReference: datenschutz_verantwortlichkeit_art
formElementExpectedValue: Auftragsdatenverarbeitung
formElementOperator: EQUALS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_luv
formElementExpectedValue: Individuell/vergleichend
formElementOperator: CONTAINS
options:
- value: '["SAP SE (Cloud Services)", "Amazon Web Services EMEA SARL"]'
label: Auftragsdatenverarbeiter
@@ -186,11 +181,6 @@ formElementSubSections:
sourceFormElementReference: datenschutz_verantwortlichkeit_art
formElementExpectedValue: Gemeinsame Verantwortlichkeit
formElementOperator: EQUALS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_luv
formElementExpectedValue: Individuell/vergleichend
formElementOperator: CONTAINS
options:
- value: '["Betriebsrat", "Konzern-IT"]'
label: Gemeinsame Verantwortlichkeit mit

View File

@@ -29,14 +29,6 @@ formElementSubSections:
sectionSpawnConditionType: SHOW
sectionSpawnExpectedValue: Einführung
sectionSpawnOperator: EQUALS
- templateReference: loeschkonzept_template
sectionSpawnConditionType: SHOW
sectionSpawnExpectedValue: Einführung
sectionSpawnOperator: EQUALS
- templateReference: datenschutz_template
sectionSpawnConditionType: SHOW
sectionSpawnExpectedValue: Einführung
sectionSpawnOperator: EQUALS
- templateReference: auswirkungen_arbeitnehmer_template
sectionSpawnConditionType: SHOW
sectionSpawnExpectedValue: Einführung
@@ -488,6 +480,14 @@ formElementSubSections:
sectionSpawnConditionType: SHOW
sectionSpawnExpectedValue: Personenbeziehbar
sectionSpawnOperator: EQUALS
- templateReference: loeschkonzept_template
sectionSpawnConditionType: SHOW
sectionSpawnExpectedValue: Personenbeziehbar
sectionSpawnOperator: EQUALS
- templateReference: datenschutz_template
sectionSpawnConditionType: SHOW
sectionSpawnExpectedValue: Personenbeziehbar
sectionSpawnOperator: EQUALS
visibilityConditions:
operator: OR
conditions:
@@ -637,6 +637,23 @@ formElementSubSections:
sourceFormElementReference: sens_auswertung
formElementExpectedValue: Funktionen vorhanden
formElementOperator: EQUALS
- reference: sens_art_analytische_funktionen_sonstiges
title: Beschreibung der sonstigen analytischen Funktionen
description: ''
options:
- value: ''
label: Beschreibung
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
type: TEXTAREA
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_art_analytische_funktionen
formElementExpectedValue: Sonstiges
formElementOperator: CONTAINS
- reference: sens_luv
title: Werden analytischen Funktionen für Leistungs-/Verhaltenskontrolle genutzt?
description: ''
@@ -692,23 +709,31 @@ formElementSubSections:
title: Werden Ereignisse, Nutzungen und Logs erfasst?
description: ''
options:
- value: ''
- value: 'false'
label: Nein
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
- value: ''
- value: 'false'
label: Technisch (Betrieb, Sicherheit)
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
- value: ''
- value: 'false'
label: Ja (Nutzer-/Aktivitätsbezug)
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
- value: ''
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit
formElementOperator: NOT_EQUALS
formElementExpectedValue: "Für Administratoren"
- value: 'false'
label: Audit-Logs
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
type: RADIOBUTTON
type: CHECKBOX
visibilityConditions:
operator: AND
conditions:
@@ -759,6 +784,14 @@ formElementSubSections:
label: Fachlich
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit
formElementOperator: NOT_EQUALS
formElementExpectedValue: "Für Administratoren"
type: CHECKBOX
visibilityConditions:
operator: AND
@@ -799,30 +832,40 @@ formElementSubSections:
description: ''
options:
- value: ''
label: Nein
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
- value: ''
label: Unterstützend (Empfehlung)
label: Ja
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
- value: ''
label: Auto-Entscheidungen
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
type: CHECKBOX
label: Nein
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
type: RADIOBUTTON
visibilityConditions:
operator: OR
operator: AND
conditions:
- sourceFormElementReference: art_der_massnahme
- nodeType: GROUP
groupOperator: OR
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: art_der_massnahme
formElementExpectedValue: Einführung
formElementOperator: EQUALS
- sourceFormElementReference: art_der_massnahme
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: art_der_massnahme
formElementExpectedValue: Einführung mit einhergehender Ablösung
formElementOperator: EQUALS
- sourceFormElementReference: art_der_massnahme
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: art_der_massnahme
formElementExpectedValue: Änderung IT-System
formElementOperator: EQUALS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit
formElementExpectedValue: Für Administratoren
formElementOperator: NOT_EQUALS
- reference: sens_ki
title: Kommt im System Künstliche Intelligenz zum Einsatz?
description: ''

View File

@@ -57,33 +57,48 @@ formElementSubSections:
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
- value: '[]'
label: Systemfunktion/Verarbeitungsform
label: Verarbeitungsform
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
- value: '[]'
label: Kurzbeschreibung
label: Verarbeitungszweck
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
- value: '[]'
label: Datenkategorien
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
- value: '[]'
label: Allgemeiner Zweck
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
- value: '[]'
label: Betroffene Mitarbeiter
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
- value: '[]'
label: Häufigkeit/Anlass
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
- value: '[]'
label: Leistungs-/Verhaltenskontrolle
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
columnConfig:
readOnlyDefaultValue: "Nein"
readOnlyConditions:
operator: OR
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit
formElementExpectedValue: Für Administratoren
formElementOperator: EQUALS
- nodeType: GROUP
groupOperator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit
formElementExpectedValue: Für mehrere Rollen
formElementOperator: EQUALS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_auswertung
formElementExpectedValue: Keine
formElementOperator: EQUALS
- title: Rollen-Sichtbarkeit (Einfache Darstellung)
formElements:
- reference: rollen_sichtbarkeit_einfach_tabelle
@@ -128,7 +143,7 @@ formElementSubSections:
formElementOperator: NOT_CONTAINS
options:
- value: '[]'
label: Verarbeitungsvorgang-ID
label: Verarbeitungs-ID
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
columnConfig:
@@ -145,10 +160,26 @@ formElementSubSections:
label: Export/Weitergabe
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_schnittstellen_export
formElementExpectedValue: Exporte möglich
formElementOperator: CONTAINS
- value: '[]'
label: Empfänger
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_schnittstellen_export
formElementExpectedValue: Exporte möglich
formElementOperator: CONTAINS
- value: '[]'
label: Personenbezug möglich
processingPurpose: DATA_ANALYSIS
@@ -164,20 +195,7 @@ formElementSubSections:
description: ''
type: TABLE
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit
formElementExpectedValue: Für Administratoren
formElementOperator: NOT_EQUALS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_auswertung
formElementExpectedValue: Funktionen vorhanden
formElementOperator: EQUALS
- nodeType: GROUP
groupOperator: OR
operator: OR
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
@@ -196,7 +214,7 @@ formElementSubSections:
formElementOperator: CONTAINS
options:
- value: '[]'
label: Verarbeitungsvorgang-ID
label: Verarbeitungs-ID
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
- value: '[]'
@@ -204,13 +222,13 @@ formElementSubSections:
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
- value: '[]'
label: Systemfunktion/Verarbeitungsform
label: Verarbeitungsform
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
- value: '[]'
label: Kurzbeschreibung
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
label: Verarbeitungszweck
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
- value: '[]'
label: Datenkategorien
processingPurpose: DATA_ANALYSIS
@@ -223,14 +241,6 @@ formElementSubSections:
label: Betroffene Mitarbeiter
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
- value: '[]'
label: Allgemeiner Zweck
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
- value: '[]'
label: Häufigkeit/Anlass
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
- value: '[]'
label: Rollen-Sichtbarkeit (grob)
processingPurpose: DATA_ANALYSIS
@@ -253,7 +263,7 @@ formElementSubSections:
formElementExpectedValue: Für Administratoren
formElementOperator: NOT_EQUALS
- value: '[]'
label: Export/Weitergabe (Ja/Nein + Ziel)
label: Export/Weitergabe
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
visibilityConditions:
@@ -262,45 +272,22 @@ formElementSubSections:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_schnittstellen_export
formElementExpectedValue: Nein
formElementOperator: NOT_CONTAINS
formElementExpectedValue: Exporte möglich
formElementOperator: CONTAINS
- value: '[]'
label: Leistungs-/Verhaltenskontrolle beabsichtigt?
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
columnConfig:
isCheckbox: true
- title: Angaben zur Leistungs-/Verhaltenskontrolle
- title: Rollen-Sichtbarkeit (Umfassende Darstellung)
formElements:
- reference: luv_details_tabelle
title: Angaben zur Leistungs-/Verhaltenskontrolle
- reference: rollen_sichtbarkeit_umfassend_tabelle
title: Welche Rollen können welche Verarbeitungsvorgänge sehen? (Umfassende Darstellung)
description: ''
type: TABLE
tableRowPreset:
sourceTableReference: umfassende_datenverarbeitung_tabelle
filterCondition:
sourceColumnIndex: 11
expectedValue: 'true'
operator: EQUALS
columnMappings:
- sourceColumnIndex: 0
targetColumnIndex: 0
canAddRows: false
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit
formElementExpectedValue: Für Administratoren
formElementOperator: NOT_EQUALS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_auswertung
formElementExpectedValue: Funktionen vorhanden
formElementOperator: EQUALS
- nodeType: GROUP
groupOperator: OR
operator: OR
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
@@ -319,7 +306,88 @@ formElementSubSections:
formElementOperator: CONTAINS
options:
- value: '[]'
label: Verarbeitungsvorgang-ID
label: Verarbeitungs-ID
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
columnConfig:
sourceTableReference: umfassende_datenverarbeitung_tabelle
sourceColumnIndex: 0
- value: '[]'
label: Rollen-ID
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
columnConfig:
sourceTableReference: rollenstamm_tabelle
sourceColumnIndex: 0
- value: '[]'
label: Export/Weitergabe
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_schnittstellen_export
formElementExpectedValue: Exporte möglich
formElementOperator: CONTAINS
- value: '[]'
label: Empfänger
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_schnittstellen_export
formElementExpectedValue: Exporte möglich
formElementOperator: CONTAINS
- value: '[]'
label: Personenbezug möglich
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
- value: '[]'
label: Hinweise
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
- title: Angaben zur Leistungs-/Verhaltenskontrolle
formElements:
- reference: luv_details_tabelle
title: Angaben zur Leistungs-/Verhaltenskontrolle
description: ''
type: TABLE
tableRowPreset:
sourceTableReference: umfassende_datenverarbeitung_tabelle
filterCondition:
sourceColumnIndex: 9
expectedValue: 'true'
operator: EQUALS
columnMappings:
- sourceColumnIndex: 0
targetColumnIndex: 0
canAddRows: false
visibilityConditions:
operator: OR
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_luv
formElementExpectedValue: Aggregiert (Team)
formElementOperator: CONTAINS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_luv
formElementExpectedValue: Aggregiert (Abteilung)
formElementOperator: CONTAINS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_luv
formElementExpectedValue: Individuell/vergleichend
formElementOperator: CONTAINS
options:
- value: '[]'
label: Verarbeitungs-ID
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
columnConfig:
@@ -327,7 +395,11 @@ formElementSubSections:
sourceColumnIndex: 0
isReadOnly: true
- value: '[]'
label: Konkreter Kontrollzweck
label: Kontrollzweck
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
- value: '[]'
label: Berechtigtes Kontrollinteresse
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
- value: '[]'
@@ -339,9 +411,9 @@ formElementSubSections:
sourceColumnIndex: 0
isMultipleAllowed: true
- value: '[]'
label: Entscheidungswirkung
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
label: Häufigkeit/Anlass
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
- value: '[]'
label: Granularität/Bezugsebene
processingPurpose: DATA_ANALYSIS
@@ -351,11 +423,19 @@ formElementSubSections:
sourceColumnIndex: 0
isMultipleAllowed: true
- value: '[]'
label: Drilldown bis Person möglich?
label: Drilldown auf Individualebene
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
columnConfig:
isCheckbox: true
rowVisibilityCondition:
sourceColumnIndex: 5
expectedValues:
- "Aggregiert (Team)"
- "Aggregiert (Abteilung)"
operator: CONTAINS
- value: '[]'
label: Ranking/Scoring
label: Mindestgruppe/Schwelle
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
visibilityConditions:
@@ -363,20 +443,16 @@ formElementSubSections:
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_art_analytische_funktionen
formElementExpectedValue: Rankings
sourceFormElementReference: sens_luv
formElementExpectedValue: Aggregiert (Team)
formElementOperator: CONTAINS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_art_analytische_funktionen
formElementExpectedValue: Scores
sourceFormElementReference: sens_luv
formElementExpectedValue: Aggregiert (Abteilung)
formElementOperator: CONTAINS
- value: '[]'
label: Mindestgruppe/Schwelle
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
- value: '[]'
label: Automatisierte Alerts/Entscheidungen
label: Automatisierte Alerts
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
visibilityConditions:
@@ -388,9 +464,29 @@ formElementSubSections:
formElementExpectedValue: Nein
formElementOperator: NOT_CONTAINS
- value: '[]'
label: Schutzmaßnahmen/Governance
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: REVIEW_REQUIRED
label: Automatisierte Entscheidungen
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_automatisierte_entscheidungen
formElementExpectedValue: Ja
formElementOperator: EQUALS
- value: '[]'
label: Art der Entscheidungsunterstützung
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_automatisierte_entscheidungen
formElementExpectedValue: Ja
formElementOperator: EQUALS
- title: Zugriffsregeln hinsichtlich der Verarbeitungsvorgänge
formElements:
- reference: zugriffsregeln_tabelle
@@ -398,20 +494,7 @@ formElementSubSections:
description: ''
type: TABLE
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit
formElementExpectedValue: Für Administratoren
formElementOperator: NOT_EQUALS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_auswertung
formElementExpectedValue: Funktionen vorhanden
formElementOperator: EQUALS
- nodeType: GROUP
groupOperator: OR
operator: OR
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
@@ -430,7 +513,7 @@ formElementSubSections:
formElementOperator: CONTAINS
options:
- value: '[]'
label: Verarbeitungsvorgang-ID
label: Verarbeitungs-ID
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
columnConfig:
@@ -468,20 +551,7 @@ formElementSubSections:
description: ''
type: TABLE
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit
formElementExpectedValue: Für Administratoren
formElementOperator: NOT_EQUALS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_auswertung
formElementExpectedValue: Funktionen vorhanden
formElementOperator: EQUALS
- nodeType: GROUP
groupOperator: OR
operator: OR
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
@@ -500,7 +570,7 @@ formElementSubSections:
formElementOperator: CONTAINS
options:
- value: '[]'
label: Verarbeitungsvorgang-ID
label: Verarbeitungs-ID
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
columnConfig:

View File

@@ -212,16 +212,6 @@ formElementSubSections:
sourceFormElementReference: loeschkonzept_hinterlegen
formElementExpectedValue: 'true'
formElementOperator: EQUALS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit
formElementExpectedValue: Für Administratoren
formElementOperator: NOT_EQUALS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_auswertung
formElementExpectedValue: Funktionen vorhanden
formElementOperator: EQUALS
- nodeType: GROUP
groupOperator: OR
conditions:

View File

@@ -98,20 +98,7 @@ formElementSubSections:
description: ''
type: TABLE
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit
formElementExpectedValue: Für Administratoren
formElementOperator: NOT_EQUALS
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_auswertung
formElementExpectedValue: Funktionen vorhanden
formElementOperator: EQUALS
- nodeType: GROUP
groupOperator: OR
operator: OR
conditions:
- nodeType: LEAF
formElementConditionType: SHOW

View File

@@ -198,7 +198,7 @@ const hasOverflow = ref(false)
const { evaluateFormElementVisibility } = useFormElementVisibility()
const { clearHiddenFormElementValues } = useFormElementValueClearing()
const { processSpawnTriggers } = useSectionSpawning()
const { cloneElement } = useClonableElements()
const { cloneElement } = useFormElementDuplication()
const { isSwiping } = usePointerSwipe(stepperScrollEl, {
threshold: 0,

View File

@@ -183,8 +183,8 @@
<script setup lang="ts">
import type { AccordionItem } from '@nuxt/ui'
import type { ApplicationFormDto, ApplicationFormVersionDto } from '~~/.api-client'
import type { FormValueDiff, ValueChange, SectionChanges, TableRowDiff } from '~~/types/formDiff'
import { compareApplicationFormValues, groupChangesBySection } from '~/utils/formDiff'
import type { FormValueDiff, ValueChange, SectionChanges, TableRowDiff } from '~~/types/formSnapshotComparison'
import { compareApplicationFormValues, groupChangesBySection } from '~/utils/formSnapshotComparison'
const props = defineProps<{
open: boolean

View File

@@ -3,20 +3,40 @@
</template>
<script setup lang="ts">
import type { FormOptionDto } from '~~/.api-client'
import type { FormElementDto, FormOptionDto } from '~~/.api-client'
import { useFormElementVisibility } from '~/composables/useFormElementVisibility'
const props = defineProps<{
formOptions: FormOptionDto[]
allFormElements?: FormElementDto[]
}>()
const emit = defineEmits<{
(e: 'update:formOptions', value: FormOptionDto[]): void
}>()
// Map options to items format expected by UCheckboxGroup
const items = computed(() => props.formOptions.map((option) => ({ label: option.label, value: option.label })))
const { isFormOptionVisible } = useFormElementVisibility()
const visibleOptions = computed(() => {
if (!props.allFormElements) return props.formOptions
return props.formOptions.filter((opt) => isFormOptionVisible(opt.visibilityConditions, props.allFormElements!))
})
// Auto-clear hidden options that are still selected
watchEffect(() => {
if (!props.allFormElements) return
const hiddenSelected = props.formOptions.filter(
(opt) => opt.value === 'true' && !isFormOptionVisible(opt.visibilityConditions, props.allFormElements!)
)
if (hiddenSelected.length === 0) return
emit(
'update:formOptions',
props.formOptions.map((opt) => (hiddenSelected.includes(opt) ? { ...opt, value: 'false' } : opt))
)
})
const items = computed(() => visibleOptions.value.map((option) => ({ label: option.label, value: option.label })))
// Model value is an array of labels for checkboxes where value === 'true'
const modelValue = computed({
get: () => props.formOptions.filter((option) => option.value === 'true').map((option) => option.label),
set: (selectedLabels: string[]) => {

View File

@@ -3,19 +3,41 @@
</template>
<script setup lang="ts">
import type { FormOptionDto } from '~~/.api-client'
import type { FormElementDto, FormOptionDto } from '~~/.api-client'
import { useFormElementVisibility } from '~/composables/useFormElementVisibility'
const props = defineProps<{
label?: string
formOptions: FormOptionDto[]
allFormElements?: FormElementDto[]
}>()
const emit = defineEmits<{
(e: 'update:formOptions', value: FormOptionDto[]): void
}>()
const { isFormOptionVisible } = useFormElementVisibility()
const visibleOptions = computed(() => {
if (!props.allFormElements) return props.formOptions
return props.formOptions.filter((opt) => isFormOptionVisible(opt.visibilityConditions, props.allFormElements!))
})
// Auto-clear selected option if it becomes hidden
watchEffect(() => {
if (!props.allFormElements) return
const selectedOption = props.formOptions.find((opt) => opt.value === 'true')
if (!selectedOption) return
if (!isFormOptionVisible(selectedOption.visibilityConditions, props.allFormElements!)) {
emit(
'update:formOptions',
props.formOptions.map((opt) => ({ ...opt, value: 'false' }))
)
}
})
// Our "label" is the "value" of the radio button
const items = computed(() => props.formOptions.map((option) => ({ label: option.label, value: option.label })))
const items = computed(() => visibleOptions.value.map((option) => ({ label: option.label, value: option.label })))
const modelValue = computed({
get: () => props.formOptions.find((option) => option.value === 'true')?.label,

View File

@@ -22,6 +22,8 @@
:disabled="disabled"
:can-modify-rows="canModifyRows"
:get-column-options="getColumnOptions"
:read-only-column-indices="readOnlyColumnIndices"
:is-cell-visible="isCellVisible"
@update:cell="updateCell"
@update:cell-value="updateCellValue"
@update:checkbox-cell="updateCheckboxCell"
@@ -60,6 +62,8 @@
:disabled="disabled"
:can-modify-rows="canModifyRows"
:get-column-options="getColumnOptions"
:read-only-column-indices="readOnlyColumnIndices"
:is-cell-visible="isCellVisible"
add-row-button-class="mt-4"
@update:cell="updateCell"
@update:cell-value="updateCellValue"
@@ -151,6 +155,44 @@ const visibleColumns = computed<VisibleColumn[]>(() => {
})
})
const readOnlyColumnIndices = computed<Set<number>>(() => {
if (!props.allFormElements) return new Set()
return new Set(
props.formOptions
.map((option, index) => ({ option, index }))
.filter(({ option }) => {
const conditions = option.columnConfig?.readOnlyConditions
return conditions && isFormOptionVisible(conditions, props.allFormElements!)
})
.map(({ index }) => index)
)
})
// When columns become read-only, reset their values to the configured default
watch(
readOnlyColumnIndices,
(currentSet, previousSet) => {
const newlyReadOnlyIndices = [...currentSet].filter((i) => !previousSet?.has(i))
if (newlyReadOnlyIndices.length === 0) return
const updatedOptions = props.formOptions.map((option, colIndex) => {
if (!newlyReadOnlyIndices.includes(colIndex)) return option
const columnValues = parseColumnValues(option.value)
const defaultValue = isColumnCheckbox(colIndex) ? false : (option.columnConfig?.readOnlyDefaultValue ?? '')
const newValue = JSON.stringify(columnValues.map(() => defaultValue))
return newValue !== option.value ? { ...option, value: newValue } : option
})
if (updatedOptions.some((opt, i) => opt !== props.formOptions[i])) {
emit('update:formOptions', updatedOptions)
}
},
{ immediate: true }
)
const dataColumns = computed<DataColumn[]>(() =>
visibleColumns.value.map(({ originalIndex }) => ({
key: `col_${originalIndex}`,
@@ -179,23 +221,16 @@ const tableData = computed<TableRowData[]>(() => {
if (props.formOptions.length === 0) return []
const columnData: CellValue[][] = props.formOptions.map((option, colIndex) => {
try {
const parsed = JSON.parse(option.value || '[]')
if (!Array.isArray(parsed)) return []
const parsed = parseColumnValues(option.value)
// For multi-select columns, each cell value is already an array
// For checkbox columns, each cell value is a boolean
// For single-select columns, each cell value is a string
// Normalize cell values based on column type
if (isColumnMultipleAllowed(colIndex)) {
return parsed.map((val: CellValue) => (Array.isArray(val) ? val : []))
return parsed.map((val) => (Array.isArray(val) ? val : []))
}
if (isColumnCheckbox(colIndex)) {
return parsed.map((val: CellValue) => val === true)
return parsed.map((val) => val === true)
}
return parsed
} catch {
return []
}
})
const rowCount = Math.max(...columnData.map((col) => col.length), 0)
@@ -266,14 +301,7 @@ function updateCell(rowIndex: number, columnKey: string, value: string) {
const updatedOptions = props.formOptions.map((option, index) => {
if (index !== colIndex) return option
let columnValues: CellValue[]
try {
columnValues = JSON.parse(option.value || '[]')
if (!Array.isArray(columnValues)) columnValues = []
} catch {
columnValues = []
}
const columnValues = parseColumnValues(option.value)
while (columnValues.length <= rowIndex) {
columnValues.push('')
}
@@ -289,14 +317,7 @@ function updateCellValue(rowIndex: number, _columnKey: string, colIndex: number,
const updatedOptions = props.formOptions.map((option, index) => {
if (index !== colIndex) return option
let columnValues: CellValue[]
try {
columnValues = JSON.parse(option.value || '[]')
if (!Array.isArray(columnValues)) columnValues = []
} catch {
columnValues = []
}
const columnValues = parseColumnValues(option.value)
const isMultiple = isColumnMultipleAllowed(colIndex)
while (columnValues.length <= rowIndex) {
columnValues.push(isMultiple ? [] : '')
@@ -313,14 +334,7 @@ function updateCheckboxCell(rowIndex: number, colIndex: number, value: boolean)
const updatedOptions = props.formOptions.map((option, index) => {
if (index !== colIndex) return option
let columnValues: CellValue[]
try {
columnValues = JSON.parse(option.value || '[]')
if (!Array.isArray(columnValues)) columnValues = []
} catch {
columnValues = []
}
const columnValues = parseColumnValues(option.value)
while (columnValues.length <= rowIndex) {
columnValues.push(false)
}
@@ -334,24 +348,18 @@ function updateCheckboxCell(rowIndex: number, colIndex: number, value: boolean)
function addRow() {
const updatedOptions = props.formOptions.map((option, colIndex) => {
let columnValues: CellValue[]
try {
columnValues = JSON.parse(option.value || '[]')
if (!Array.isArray(columnValues)) columnValues = []
} catch {
columnValues = []
}
const columnValues = parseColumnValues(option.value)
// For multi-select columns, initialize with empty array
// For checkbox columns, initialize with false
// Otherwise empty string
let emptyValue: CellValue = ''
if (isColumnMultipleAllowed(colIndex)) {
emptyValue = []
// Determine initial value based on column type
let initialValue: CellValue = ''
if (readOnlyColumnIndices.value.has(colIndex)) {
initialValue = isColumnCheckbox(colIndex) ? false : (option.columnConfig?.readOnlyDefaultValue ?? '')
} else if (isColumnMultipleAllowed(colIndex)) {
initialValue = []
} else if (isColumnCheckbox(colIndex)) {
emptyValue = false
initialValue = false
}
columnValues.push(emptyValue)
columnValues.push(initialValue)
return { ...option, value: JSON.stringify(columnValues) }
})
@@ -361,19 +369,44 @@ function addRow() {
function removeRow(rowIndex: number) {
const updatedOptions = props.formOptions.map((option) => {
let columnValues: CellValue[]
try {
columnValues = JSON.parse(option.value || '[]')
if (!Array.isArray(columnValues)) columnValues = []
} catch {
columnValues = []
}
const columnValues = parseColumnValues(option.value)
columnValues.splice(rowIndex, 1)
return { ...option, value: JSON.stringify(columnValues) }
})
emit('update:formOptions', updatedOptions)
}
function isCellVisible(colIndex: number, rowData: TableRowData): boolean {
const option = props.formOptions[colIndex]
const rowVisibility = option?.columnConfig?.rowVisibilityCondition
if (!rowVisibility) return true
const { sourceColumnIndex, expectedValues, operator } = rowVisibility
const sourceKey = `col_${sourceColumnIndex}`
const cellValue = rowData[sourceKey]
let sourceValues: string[] = []
if (Array.isArray(cellValue)) {
sourceValues = cellValue
} else if (typeof cellValue === 'string' && cellValue) {
sourceValues = cellValue.split(',').map((v) => v.trim())
}
if (operator === 'CONTAINS' || operator === 'EQUALS') {
return (expectedValues ?? []).some((expected) =>
sourceValues.some((v) => v.toLowerCase() === expected.toLowerCase())
)
}
return true
}
function parseColumnValues(value: string | undefined): CellValue[] {
try {
const parsed = JSON.parse(value || '[]')
return Array.isArray(parsed) ? parsed : []
} catch {
return []
}
}
</script>

View File

@@ -2,6 +2,15 @@
<div>
<UTable :data="tableData" :columns="tableColumns" class="w-full" :ui="{ td: 'p-2' }">
<template v-for="col in dataColumns" :key="col.key" #[`${col.key}-cell`]="slotProps">
<span
v-if="
props.isCellVisible &&
!props.isCellVisible(col.colIndex, (slotProps.row as TableRow<TableRowData>).original)
"
>
-
</span>
<template v-else>
<!-- Column with cross-reference -->
<USelectMenu
v-if="hasColumnReference(col.colIndex)"
@@ -51,6 +60,7 @@
"
/>
</template>
</template>
<template v-if="canModifyRows" #actions-cell="{ row }">
<UButton
v-if="!disabled"
@@ -102,6 +112,8 @@ const props = defineProps<{
canModifyRows: boolean
addRowButtonClass?: string
getColumnOptions: (colIndex: number, currentRowData?: TableRowData) => string[]
readOnlyColumnIndices?: Set<number>
isCellVisible?: (colIndex: number, rowData: TableRowData) => boolean
}>()
defineEmits<{
@@ -119,7 +131,7 @@ function hasColumnReference(colIndex: number): boolean {
function isColumnReadOnly(colIndex: number): boolean {
const option = props.formOptions[colIndex]
return option?.columnConfig?.isReadOnly === true
return option?.columnConfig?.isReadOnly === true || (props.readOnlyColumnIndices?.has(colIndex) ?? false)
}
function isColumnMultipleAllowed(colIndex: number): boolean {

View File

@@ -1,11 +1,13 @@
import type { FormElementDto } from '~~/.api-client'
export function useClonableElements() {
function cloneElement(element: FormElementDto, existingElements: FormElementDto[]): FormElementDto {
const newReference = element.reference ? generateNextReference(existingElements, element.reference) : undefined
const isTextField = element.type === 'TEXTAREA' || element.type === 'TEXTFIELD'
export function useFormElementDuplication() {
function cloneElement(elementToClone: FormElementDto, existingElements: FormElementDto[]): FormElementDto {
const newReference = elementToClone.reference
? generateNextReference(existingElements, elementToClone.reference)
: undefined
const isTextField = elementToClone.type === 'TEXTAREA' || elementToClone.type === 'TEXTFIELD'
const clonedElement = JSON.parse(JSON.stringify(element)) as FormElementDto
const clonedElement = JSON.parse(JSON.stringify(elementToClone)) as FormElementDto
const resetOptions = clonedElement.options.map((option) => ({
...option,
value: isTextField ? '' : option.value

View File

@@ -6,7 +6,7 @@ import type {
FormOptionDto,
FormElementType
} from '~~/.api-client'
import type { FormValueDiff, ValueChange, SectionChanges, TableDiff, TableRowDiff } from '~~/types/formDiff'
import type { FormValueDiff, ValueChange, SectionChanges, TableDiff, TableRowDiff } from '~~/types/formSnapshotComparison'
// Element types that use true/false selection model
const SELECTION_TYPES: FormElementType[] = ['SELECT', 'RADIOBUTTON', 'CHECKBOX', 'SWITCH']

View File

@@ -13,6 +13,8 @@
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "vitest run",
"test:unit": "vitest run --project unit",
"test:integration": "vitest run --project integration",
"check": "pnpm run lint && pnpm run type-check && pnpm run format && pnpm run test",
"api:generate": "openapi-generator-cli generate -i ../api/legalconsenthub.yml -g typescript-fetch -o .api-client"
},
@@ -36,6 +38,8 @@
"@nuxt/eslint": "1.1.0",
"@nuxt/test-utils": "^3.21.0",
"@openapitools/openapi-generator-cli": "2.16.3",
"@pinia/testing": "^0.1.7",
"@vitest/coverage-v8": "4.0.16",
"@vue/test-utils": "^2.4.6",
"eslint": "9.20.1",
"happy-dom": "^20.0.11",

View File

@@ -60,6 +60,12 @@ importers:
'@openapitools/openapi-generator-cli':
specifier: 2.16.3
version: 2.16.3
'@pinia/testing':
specifier: ^0.1.7
version: 0.1.7(pinia@3.0.3(typescript@5.7.3)(vue@3.5.26(typescript@5.7.3)))(vue@3.5.26(typescript@5.7.3))
'@vitest/coverage-v8':
specifier: 4.0.16
version: 4.0.16(vitest@4.0.16(@types/node@20.19.27)(happy-dom@20.0.11)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
'@vue/test-utils':
specifier: ^2.4.6
version: 2.4.6
@@ -228,6 +234,10 @@ packages:
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
engines: {node: '>=6.9.0'}
'@bcoe/v8-coverage@1.0.2':
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
'@bomb.sh/tab@0.0.10':
resolution: {integrity: sha512-6ALS2rh/4LKn0Yxwm35V6LcgQuSiECHbqQo7+9g4rkgGyXZ0siOc8K+IuWIq/4u0Zkv2mevP9QSqgKhGIvLJMw==}
hasBin: true
@@ -1541,6 +1551,11 @@ packages:
peerDependencies:
pinia: ^3.0.3
'@pinia/testing@0.1.7':
resolution: {integrity: sha512-xcDq6Ry/kNhZ5bsUMl7DeoFXwdume1NYzDggCiDUDKoPQ6Mo0eH9VU7bJvBtlurqe6byAntWoX5IhVFqWzRz/Q==}
peerDependencies:
pinia: '>=2.2.6'
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
@@ -2362,6 +2377,15 @@ packages:
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0
vue: ^3.2.25
'@vitest/coverage-v8@4.0.16':
resolution: {integrity: sha512-2rNdjEIsPRzsdu6/9Eq0AYAzYdpP6Bx9cje9tL3FE5XzXRQF1fNU9pe/1yE8fCrS0HD+fBtt6gLPh6LI57tX7A==}
peerDependencies:
'@vitest/browser': 4.0.16
vitest: 4.0.16
peerDependenciesMeta:
'@vitest/browser':
optional: true
'@vitest/expect@4.0.16':
resolution: {integrity: sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==}
@@ -2686,6 +2710,9 @@ packages:
resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==}
engines: {node: '>=4'}
ast-v8-to-istanbul@0.3.12:
resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==}
ast-walker-scope@0.6.2:
resolution: {integrity: sha512-1UWOyC50xI3QZkRuDj6PqDtpm1oHWtYs+NQGwqL/2R11eN3Q81PHAHPM0SWW3BNQm53UDwS//Jv8L4CCVLM1bQ==}
engines: {node: '>=16.14.0'}
@@ -3796,6 +3823,9 @@ packages:
resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==}
engines: {node: ^16.14.0 || >=18.0.0}
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
html-to-text@9.0.5:
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
engines: {node: '>=14'}
@@ -3989,6 +4019,22 @@ packages:
isomorphic.js@0.2.5:
resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==}
istanbul-lib-coverage@3.2.2:
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
engines: {node: '>=8'}
istanbul-lib-report@3.0.1:
resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
engines: {node: '>=10'}
istanbul-lib-source-maps@5.0.6:
resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==}
engines: {node: '>=10'}
istanbul-reports@3.2.0:
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
engines: {node: '>=8'}
iterare@1.2.1:
resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==}
engines: {node: '>=6'}
@@ -4012,6 +4058,9 @@ packages:
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
engines: {node: '>=14'}
js-tokens@10.0.0:
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -4257,6 +4306,10 @@ packages:
magicast@0.5.1:
resolution: {integrity: sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==}
make-dir@4.0.0:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'}
markdown-it@14.1.0:
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
hasBin: true
@@ -6340,6 +6393,8 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
'@bcoe/v8-coverage@1.0.2': {}
'@bomb.sh/tab@0.0.10(cac@6.7.14)(citty@0.1.6)':
optionalDependencies:
cac: 6.7.14
@@ -7928,6 +7983,14 @@ snapshots:
transitivePeerDependencies:
- magicast
'@pinia/testing@0.1.7(pinia@3.0.3(typescript@5.7.3)(vue@3.5.26(typescript@5.7.3)))(vue@3.5.26(typescript@5.7.3))':
dependencies:
pinia: 3.0.3(typescript@5.7.3)(vue@3.5.26(typescript@5.7.3))
vue-demi: 0.14.10(vue@3.5.26(typescript@5.7.3))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@pkgjs/parseargs@0.11.0':
optional: true
@@ -8734,6 +8797,23 @@ snapshots:
vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)
vue: 3.5.26(typescript@5.7.3)
'@vitest/coverage-v8@4.0.16(vitest@4.0.16(@types/node@20.19.27)(happy-dom@20.0.11)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))':
dependencies:
'@bcoe/v8-coverage': 1.0.2
'@vitest/utils': 4.0.16
ast-v8-to-istanbul: 0.3.12
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
istanbul-lib-source-maps: 5.0.6
istanbul-reports: 3.2.0
magicast: 0.5.1
obug: 2.1.1
std-env: 3.10.0
tinyrainbow: 3.0.3
vitest: 4.0.16(@types/node@20.19.27)(happy-dom@20.0.11)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)
transitivePeerDependencies:
- supports-color
'@vitest/expect@4.0.16':
dependencies:
'@standard-schema/spec': 1.1.0
@@ -9125,6 +9205,12 @@ snapshots:
dependencies:
tslib: 2.8.1
ast-v8-to-istanbul@0.3.12:
dependencies:
'@jridgewell/trace-mapping': 0.3.31
estree-walker: 3.0.3
js-tokens: 10.0.0
ast-walker-scope@0.6.2:
dependencies:
'@babel/parser': 7.28.5
@@ -10347,6 +10433,8 @@ snapshots:
dependencies:
lru-cache: 10.4.3
html-escaper@2.0.2: {}
html-to-text@9.0.5:
dependencies:
'@selderee/plugin-htmlparser2': 0.11.0
@@ -10536,6 +10624,27 @@ snapshots:
isomorphic.js@0.2.5: {}
istanbul-lib-coverage@3.2.2: {}
istanbul-lib-report@3.0.1:
dependencies:
istanbul-lib-coverage: 3.2.2
make-dir: 4.0.0
supports-color: 7.2.0
istanbul-lib-source-maps@5.0.6:
dependencies:
'@jridgewell/trace-mapping': 0.3.31
debug: 4.4.3
istanbul-lib-coverage: 3.2.2
transitivePeerDependencies:
- supports-color
istanbul-reports@3.2.0:
dependencies:
html-escaper: 2.0.2
istanbul-lib-report: 3.0.1
iterare@1.2.1: {}
jackspeak@3.4.3:
@@ -10558,6 +10667,8 @@ snapshots:
js-cookie@3.0.5: {}
js-tokens@10.0.0: {}
js-tokens@4.0.0: {}
js-tokens@9.0.1: {}
@@ -10787,6 +10898,10 @@ snapshots:
'@babel/types': 7.28.5
source-map-js: 1.2.1
make-dir@4.0.0:
dependencies:
semver: 7.7.3
markdown-it@14.1.0:
dependencies:
argparse: 2.0.1

View File

@@ -0,0 +1,4 @@
{
"extends": "../.nuxt/tsconfig.json",
"include": ["**/*"]
}

View File

@@ -0,0 +1,285 @@
import { describe, it, expect } from 'vitest'
import { useFormElementDuplication } from '../../../app/composables/useFormElementDuplication'
import type { FormElementDto, FormOptionDto, FormElementType } from '../../../.api-client'
// Helper to create a FormOptionDto
function createOption(value: string, label: string): FormOptionDto {
return {
value,
label,
processingPurpose: 'NONE',
employeeDataCategory: 'NONE'
}
}
// Helper to create a FormElementDto
function createFormElement(
reference: string,
title: string,
type: FormElementType,
options: FormOptionDto[] = [],
description?: string
): FormElementDto {
return {
id: `id-${reference}`,
reference,
title,
type,
options,
description
}
}
describe('useFormElementDuplication', () => {
describe('cloneElement()', () => {
it('should generate incremented reference from existing reference with suffix', () => {
const { cloneElement } = useFormElementDuplication()
const elementToClone = createFormElement('modul_1', 'Module', 'TEXTFIELD', [createOption('value', '')])
const existingElements: FormElementDto[] = [elementToClone]
const cloned = cloneElement(elementToClone, existingElements)
expect(cloned.reference).toBe('modul_2')
})
it('should handle reference without numeric suffix (defaults to _2)', () => {
const { cloneElement } = useFormElementDuplication()
const elementToClone = createFormElement('modul', 'Module', 'TEXTFIELD', [createOption('value', '')])
const existingElements: FormElementDto[] = [elementToClone]
const cloned = cloneElement(elementToClone, existingElements)
expect(cloned.reference).toBe('modul_2')
})
it('should clear id and formElementSubSectionId on cloned element', () => {
const { cloneElement } = useFormElementDuplication()
const elementToClone = createFormElement('elem_1', 'Element', 'TEXTFIELD', [createOption('value', '')])
elementToClone.id = 'some-uuid-id'
elementToClone.formElementSubSectionId = 'subsection-uuid'
const existingElements: FormElementDto[] = [elementToClone]
const cloned = cloneElement(elementToClone, existingElements)
expect(cloned.id).toBeUndefined()
expect(cloned.formElementSubSectionId).toBeUndefined()
})
it('should deep clone element (modifying clone does not affect original)', () => {
const { cloneElement } = useFormElementDuplication()
const elementToClone = createFormElement('elem_1', 'Element', 'SELECT', [
createOption('true', 'Yes'),
createOption('false', 'No')
])
const existingElements: FormElementDto[] = [elementToClone]
const cloned = cloneElement(elementToClone, existingElements)
cloned.options[0]!.value = 'modified'
cloned.title = 'Modified Title'
expect(elementToClone.options[0]!.value).toBe('true')
expect(elementToClone.title).toBe('Element')
})
it('should handle multiple existing clones and generate correct next reference', () => {
const { cloneElement } = useFormElementDuplication()
const element1 = createFormElement('modul_1', 'Module', 'TEXTFIELD', [createOption('value1', '')])
const element2 = createFormElement('modul_2', 'Module', 'TEXTFIELD', [createOption('value2', '')])
const element3 = createFormElement('modul_3', 'Module', 'TEXTFIELD', [createOption('value3', '')])
const existingElements: FormElementDto[] = [element1, element2, element3]
const cloned = cloneElement(element1, existingElements)
expect(cloned.reference).toBe('modul_4')
})
it('should handle elements with no options', () => {
const { cloneElement } = useFormElementDuplication()
const elementToClone = createFormElement('elem_1', 'Element', 'TEXTFIELD', [])
const existingElements: FormElementDto[] = [elementToClone]
const cloned = cloneElement(elementToClone, existingElements)
expect(cloned.options).toEqual([])
expect(cloned.reference).toBe('elem_2')
})
it('should handle sparse numeric suffix correctly', () => {
const { cloneElement } = useFormElementDuplication()
const element1 = createFormElement('item_1', 'Item', 'TEXTFIELD', [createOption('v1', '')])
const element3 = createFormElement('item_3', 'Item', 'TEXTFIELD', [createOption('v3', '')])
const existingElements: FormElementDto[] = [element1, element3]
const cloned = cloneElement(element1, existingElements)
expect(cloned.reference).toBe('item_4')
})
it('should handle element with formElementSubSectionId undefined already', () => {
const { cloneElement } = useFormElementDuplication()
const elementToClone = createFormElement('elem_1', 'Element', 'TEXTFIELD', [createOption('value', '')])
const existingElements: FormElementDto[] = [elementToClone]
const cloned = cloneElement(elementToClone, existingElements)
expect(cloned.formElementSubSectionId).toBeUndefined()
})
describe('option value handling by form element type', () => {
it('should reset TEXTAREA option values to empty string', () => {
const { cloneElement } = useFormElementDuplication()
const elementToClone = createFormElement('textarea_1', 'My Title', 'TEXTAREA', [
createOption('Original text content', '')
])
const existingElements: FormElementDto[] = [elementToClone]
const cloned = cloneElement(elementToClone, existingElements)
expect(cloned.options[0]!.value).toBe('')
expect(cloned.reference).toBe('textarea_2')
})
it('should reset TEXTFIELD option values to empty string', () => {
const { cloneElement } = useFormElementDuplication()
const elementToClone = createFormElement('textfield_1', 'My Title', 'TEXTFIELD', [
createOption('Original text content', '')
])
const existingElements: FormElementDto[] = [elementToClone]
const cloned = cloneElement(elementToClone, existingElements)
expect(cloned.options[0]!.value).toBe('')
expect(cloned.reference).toBe('textfield_2')
})
it('should preserve SELECT option values', () => {
const { cloneElement } = useFormElementDuplication()
const elementToClone = createFormElement('select_1', 'Choice', 'SELECT', [
createOption('false', 'Option A'),
createOption('true', 'Option B')
])
const existingElements: FormElementDto[] = [elementToClone]
const cloned = cloneElement(elementToClone, existingElements)
expect(cloned.options[0]!.value).toBe('false')
expect(cloned.options[1]!.value).toBe('true')
expect(cloned.reference).toBe('select_2')
})
it('should preserve CHECKBOX option values', () => {
const { cloneElement } = useFormElementDuplication()
const elementToClone = createFormElement('checkbox_1', 'Features', 'CHECKBOX', [
createOption('true', 'Feature A'),
createOption('false', 'Feature B')
])
const existingElements: FormElementDto[] = [elementToClone]
const cloned = cloneElement(elementToClone, existingElements)
expect(cloned.options[0]!.value).toBe('true')
expect(cloned.options[1]!.value).toBe('false')
expect(cloned.reference).toBe('checkbox_2')
})
it('should preserve RADIOBUTTON option values', () => {
const { cloneElement } = useFormElementDuplication()
const elementToClone = createFormElement('radio_1', 'Gender', 'RADIOBUTTON', [
createOption('true', 'Male'),
createOption('false', 'Female')
])
const existingElements: FormElementDto[] = [elementToClone]
const cloned = cloneElement(elementToClone, existingElements)
expect(cloned.options[0]!.value).toBe('true')
expect(cloned.options[1]!.value).toBe('false')
expect(cloned.reference).toBe('radio_2')
})
it('should handle SWITCH element option values', () => {
const { cloneElement } = useFormElementDuplication()
const elementToClone = createFormElement('switch_1', 'Enable', 'SWITCH', [createOption('true', 'Enabled')])
const existingElements: FormElementDto[] = [elementToClone]
const cloned = cloneElement(elementToClone, existingElements)
expect(cloned.options[0]!.value).toBe('true')
expect(cloned.reference).toBe('switch_2')
})
it('should handle DATE element option values', () => {
const { cloneElement } = useFormElementDuplication()
const elementToClone = createFormElement('date_1', 'Start Date', 'DATE', [createOption('2024-01-15', 'label')])
const existingElements: FormElementDto[] = [elementToClone]
const cloned = cloneElement(elementToClone, existingElements)
expect(cloned.options[0]!.value).toBe('2024-01-15')
expect(cloned.reference).toBe('date_2')
})
it('should handle RICH_TEXT element option values', () => {
const { cloneElement } = useFormElementDuplication()
const elementToClone = createFormElement('richtext_1', 'Notes', 'RICH_TEXT', [
createOption('<p>Content</p>', '')
])
const existingElements: FormElementDto[] = [elementToClone]
const cloned = cloneElement(elementToClone, existingElements)
expect(cloned.options[0]!.value).toBe('<p>Content</p>')
expect(cloned.reference).toBe('richtext_2')
})
it('should handle TABLE element option values', () => {
const { cloneElement } = useFormElementDuplication()
const elementToClone = createFormElement('table_1', 'Employees', 'TABLE', [
createOption('["row1", "row2"]', 'Name'),
createOption('["dev", "designer"]', 'Role')
])
const existingElements: FormElementDto[] = [elementToClone]
const cloned = cloneElement(elementToClone, existingElements)
expect(cloned.options[0]!.value).toBe('["row1", "row2"]')
expect(cloned.options[1]!.value).toBe('["dev", "designer"]')
expect(cloned.reference).toBe('table_2')
})
it('should preserve all FormOptionDto properties when cloning', () => {
const { cloneElement } = useFormElementDuplication()
const option = createOption('true', 'Yes')
option.processingPurpose = 'BUSINESS_PROCESS'
option.employeeDataCategory = 'SENSITIVE'
const elementToClone = createFormElement('elem_1', 'Element', 'SELECT', [option])
const existingElements: FormElementDto[] = [elementToClone]
const cloned = cloneElement(elementToClone, existingElements)
expect(cloned.options[0]!.processingPurpose).toBe('BUSINESS_PROCESS')
expect(cloned.options[0]!.employeeDataCategory).toBe('SENSITIVE')
})
})
})
})

View File

@@ -0,0 +1,268 @@
import { describe, it, expect } from 'vitest'
import type {
FormElementDto,
FormElementSectionDto,
FormElementSubSectionDto,
FormOptionDto
} from '../../../.api-client'
import { useFormElementValueClearing } from '../../../app/composables/useFormElementValueClearing'
describe('useFormElementValueClearing', () => {
const { clearHiddenFormElementValues } = useFormElementValueClearing()
// Visibility state constants
const VISIBLE = true
const HIDDEN = false
// Helper functions for creating test data
function createFormOption(overrides: Partial<FormOptionDto> = {}): FormOptionDto {
return {
value: '',
label: 'Option',
...overrides
} as FormOptionDto
}
function createFormElement(type: string, overrides: Partial<FormElementDto> = {}): FormElementDto {
return {
id: 'element1',
reference: 'ref_element1',
title: 'Element',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: type as any,
options: [createFormOption()],
...overrides
}
}
function createSubSection(formElements: FormElementDto[] = []): FormElementSubSectionDto {
return {
id: 'subsection1',
title: 'SubSection',
formElements: formElements.length > 0 ? formElements : [createFormElement('TEXTFIELD')]
}
}
function createSection(subsections: FormElementSubSectionDto[] = []): FormElementSectionDto {
return {
id: 'section1',
title: 'Section',
formElementSubSections: subsections.length > 0 ? subsections : [createSubSection()]
}
}
describe('clearHiddenFormElementValues', () => {
it('should return sections unchanged when no elements become newly hidden', () => {
const sections = [createSection()]
const previousMap = new Map([['element1', VISIBLE]])
const currentMap = new Map([['element1', VISIBLE]])
const result = clearHiddenFormElementValues(sections, previousMap, currentMap)
expect(result).toEqual(sections)
})
it('should clear value of newly hidden TEXTFIELD element', () => {
const element = createFormElement('TEXTFIELD', {
id: 'element1',
options: [createFormOption({ value: 'some text' })]
})
const subsection = createSubSection([element])
const section = createSection([subsection])
const previousMap = new Map([['element1', VISIBLE]])
const currentMap = new Map([['element1', HIDDEN]])
const result = clearHiddenFormElementValues([section], previousMap, currentMap)
expect(result[0]!.formElementSubSections[0]!.formElements[0]!.options[0]!.value).toBe('')
})
it('should clear value of newly hidden SELECT element', () => {
const element = createFormElement('SELECT', {
id: 'element1',
options: [createFormOption({ value: 'option1' }), createFormOption({ value: 'option2' })]
})
const subsection = createSubSection([element])
const section = createSection([subsection])
const previousMap = new Map([['element1', VISIBLE]])
const currentMap = new Map([['element1', HIDDEN]])
const result = clearHiddenFormElementValues([section], previousMap, currentMap)
expect(result[0]!.formElementSubSections[0]!.formElements[0]!.options[0]!.value).toBe('false')
expect(result[0]!.formElementSubSections[0]!.formElements[0]!.options[1]!.value).toBe('false')
})
it('should clear value of newly hidden CHECKBOX element', () => {
const element = createFormElement('CHECKBOX', {
id: 'element1',
options: [createFormOption({ value: 'true' }), createFormOption({ value: 'true' })]
})
const subsection = createSubSection([element])
const section = createSection([subsection])
const previousMap = new Map([['element1', VISIBLE]])
const currentMap = new Map([['element1', HIDDEN]])
const result = clearHiddenFormElementValues([section], previousMap, currentMap)
expect(result[0]!.formElementSubSections[0]!.formElements[0]!.options[0]!.value).toBe('false')
expect(result[0]!.formElementSubSections[0]!.formElements[0]!.options[1]!.value).toBe('false')
})
it('should clear value of newly hidden RADIOBUTTON element', () => {
const element = createFormElement('RADIOBUTTON', {
id: 'element1',
options: [createFormOption({ value: 'option1' }), createFormOption({ value: 'option2' })]
})
const subsection = createSubSection([element])
const section = createSection([subsection])
const previousMap = new Map([['element1', VISIBLE]])
const currentMap = new Map([['element1', HIDDEN]])
const result = clearHiddenFormElementValues([section], previousMap, currentMap)
expect(result[0]!.formElementSubSections[0]!.formElements[0]!.options[0]!.value).toBe('false')
expect(result[0]!.formElementSubSections[0]!.formElements[0]!.options[1]!.value).toBe('false')
})
it('should not clear visible elements', () => {
const originalValue = 'original value'
const element = createFormElement('TEXTFIELD', {
id: 'element1',
options: [createFormOption({ value: originalValue })]
})
const subsection = createSubSection([element])
const section = createSection([subsection])
const previousMap = new Map([['element1', VISIBLE]])
const currentMap = new Map([['element1', VISIBLE]])
const result = clearHiddenFormElementValues([section], previousMap, currentMap)
expect(result[0]!.formElementSubSections[0]!.formElements[0]!.options[0]!.value).toBe(originalValue)
})
it('should not clear elements that were already hidden (previousMap false, currentMap false)', () => {
const originalValue = 'original value'
const element = createFormElement('TEXTFIELD', {
id: 'element1',
options: [createFormOption({ value: originalValue })]
})
const subsection = createSubSection([element])
const section = createSection([subsection])
const previousMap = new Map([['element1', HIDDEN]])
const currentMap = new Map([['element1', HIDDEN]])
const result = clearHiddenFormElementValues([section], previousMap, currentMap)
expect(result[0]!.formElementSubSections[0]!.formElements[0]!.options[0]!.value).toBe(originalValue)
})
it('should handle elements identified by id', () => {
const element = createFormElement('TEXTFIELD', {
id: 'element1',
reference: undefined,
options: [createFormOption({ value: 'text' })]
})
const subsection = createSubSection([element])
const section = createSection([subsection])
const previousMap = new Map([['element1', VISIBLE]])
const currentMap = new Map([['element1', HIDDEN]])
const result = clearHiddenFormElementValues([section], previousMap, currentMap)
expect(result[0]!.formElementSubSections[0]!.formElements[0]!.options[0]!.value).toBe('')
})
it('should handle elements identified by reference (when id is undefined)', () => {
const element = createFormElement('TEXTFIELD', {
id: undefined,
reference: 'ref_element1',
options: [createFormOption({ value: 'text' })]
})
const subsection = createSubSection([element])
const section = createSection([subsection])
const previousMap = new Map([['ref_element1', VISIBLE]])
const currentMap = new Map([['ref_element1', HIDDEN]])
const result = clearHiddenFormElementValues([section], previousMap, currentMap)
expect(result[0]!.formElementSubSections[0]!.formElements[0]!.options[0]!.value).toBe('')
})
it('should handle multiple sections with multiple subsections', () => {
const element1 = createFormElement('TEXTFIELD', {
id: 'element1',
options: [createFormOption({ value: 'text1' })]
})
const element2 = createFormElement('SELECT', {
id: 'element2',
options: [createFormOption({ value: 'option' })]
})
const subsection1 = createSubSection([element1])
const subsection2 = createSubSection([element2])
const section1 = createSection([subsection1, subsection2])
const element3 = createFormElement('CHECKBOX', {
id: 'element3',
options: [createFormOption({ value: 'true' })]
})
const subsection3 = createSubSection([element3])
const section2 = createSection([subsection3])
const previousMap = new Map([
['element1', VISIBLE],
['element2', VISIBLE],
['element3', VISIBLE]
])
const currentMap = new Map([
['element1', HIDDEN],
['element2', VISIBLE],
['element3', HIDDEN]
])
const result = clearHiddenFormElementValues([section1, section2], previousMap, currentMap)
expect(result[0]!.formElementSubSections[0]!.formElements[0]!.options[0]!.value).toBe('')
expect(result[0]!.formElementSubSections[1]!.formElements[0]!.options[0]!.value).toBe('option')
expect(result[1]!.formElementSubSections[0]!.formElements[0]!.options[0]!.value).toBe('false')
})
it('should handle mixed TEXTFIELD and SELECT in same section', () => {
const textElement = createFormElement('TEXTFIELD', {
id: 'text1',
options: [createFormOption({ value: 'text content' })]
})
const selectElement = createFormElement('SELECT', {
id: 'select1',
options: [createFormOption({ value: 'val1' }), createFormOption({ value: 'val2' })]
})
const subsection = createSubSection([textElement, selectElement])
const section = createSection([subsection])
const previousMap = new Map([
['text1', VISIBLE],
['select1', VISIBLE]
])
const currentMap = new Map([
['text1', HIDDEN],
['select1', HIDDEN]
])
const result = clearHiddenFormElementValues([section], previousMap, currentMap)
expect(result[0]!.formElementSubSections[0]!.formElements[0]!.options[0]!.value).toBe('')
expect(result[0]!.formElementSubSections[0]!.formElements[1]!.options[0]!.value).toBe('false')
expect(result[0]!.formElementSubSections[0]!.formElements[1]!.options[1]!.value).toBe('false')
})
})
})

View File

@@ -0,0 +1,237 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import type { FormElementSectionDto } from '../../../.api-client'
import { useFormStepper } from '../../../app/composables/useFormStepper'
import { ref } from 'vue'
describe('useFormStepper', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('visibleSections', () => {
it('should skip only template sections and keep non-template sections', () => {
const sections: FormElementSectionDto[] = [
{
id: '1',
title: 'Normal Section',
shortTitle: 'Normal',
description: 'A normal section',
isTemplate: false,
formElementSubSections: []
},
{
id: '2',
title: 'Template Section',
shortTitle: 'Tmpl',
description: 'A template section',
isTemplate: true,
formElementSubSections: []
}
]
const composable = useFormStepper(sections)
const stepperItems = composable.stepperItems.value
expect(stepperItems).toHaveLength(1)
expect(stepperItems[0]?.title).toBe('Normal')
})
})
describe('stepperItems', () => {
it('should be empty when no sections provided', () => {
const composable = useFormStepper([])
const stepperItems = composable.stepperItems.value
expect(stepperItems).toEqual([])
expect(stepperItems).toHaveLength(0)
})
it('should return empty array when all sections are templates', () => {
const sections: FormElementSectionDto[] = [
{
id: '1',
title: 'Template 1',
shortTitle: 'T1',
description: 'Template description',
isTemplate: true,
formElementSubSections: []
},
{
id: '2',
title: 'Template 2',
shortTitle: 'T2',
description: 'Another template',
isTemplate: true,
formElementSubSections: []
}
]
const composable = useFormStepper(sections)
const stepperItems = composable.stepperItems.value
expect(stepperItems).toHaveLength(0)
})
it('should update stepperItems when formElementSections changes', async () => {
const sections = ref<FormElementSectionDto[]>([
{
id: '1',
title: 'Section 1',
shortTitle: 'S1',
description: 'First',
isTemplate: false,
formElementSubSections: []
}
])
const composable = useFormStepper(sections)
expect(composable.stepperItems.value).toHaveLength(1)
// Update sections by mutating the ref
sections.value = [
...sections.value,
{
id: '2',
title: 'Section 2',
shortTitle: 'S2',
description: 'Second',
isTemplate: false,
formElementSubSections: []
}
]
const stepperItems = composable.stepperItems.value
expect(stepperItems).toHaveLength(2)
})
})
describe('currentFormElementSection', () => {
it('should return the section at activeStepperItemIndex', () => {
const sections: FormElementSectionDto[] = [
{
id: '1',
title: 'Section 1',
shortTitle: 'S1',
description: 'First',
isTemplate: false,
formElementSubSections: []
},
{
id: '2',
title: 'Section 2',
shortTitle: 'S2',
description: 'Second',
isTemplate: false,
formElementSubSections: []
},
{
id: '3',
title: 'Section 3',
shortTitle: 'S3',
description: 'Third',
isTemplate: false,
formElementSubSections: []
}
]
const composable = useFormStepper(sections)
// Initially at index 0
let current = composable.currentFormElementSection.value
expect(current?.id).toBe('1')
// Change index to 1
composable.activeStepperItemIndex.value = 1
current = composable.currentFormElementSection.value
expect(current?.id).toBe('2')
// Change index to 2
composable.activeStepperItemIndex.value = 2
current = composable.currentFormElementSection.value
expect(current?.id).toBe('3')
})
it('should filter templates when determining current section', () => {
const sections: FormElementSectionDto[] = [
{
id: '1',
title: 'Section 1',
shortTitle: 'S1',
description: 'First',
isTemplate: false,
formElementSubSections: []
},
{
id: '2',
title: 'Template Section',
shortTitle: 'T1',
description: 'Template',
isTemplate: true,
formElementSubSections: []
},
{
id: '3',
title: 'Section 3',
shortTitle: 'S3',
description: 'Third',
isTemplate: false,
formElementSubSections: []
}
]
const composable = useFormStepper(sections)
// activeStepperItemIndex refers to visible sections only
composable.activeStepperItemIndex.value = 1
const current = composable.currentFormElementSection.value
// Should get the second visible section (id 3), skipping template
expect(current?.id).toBe('3')
})
})
describe('undefined formElementSections', () => {
it('should handle undefined formElementSections', () => {
const composable = useFormStepper(undefined)
const stepperItems = composable.stepperItems.value
expect(stepperItems).toEqual([])
expect(stepperItems).toHaveLength(0)
})
it('should default to empty array and return undefined for currentSection', () => {
const composable = useFormStepper(undefined)
expect(composable.stepperItems.value).toHaveLength(0)
expect(composable.currentFormElementSection.value).toBeUndefined()
})
})
describe('stepper and navigateStepper', () => {
it.each`
direction
${'forward'}
${'backward'}
`(
'should call onNavigate callback if provided during navigation ($direction)',
async ({ direction }: { direction: 'forward' | 'backward' }) => {
const sections: FormElementSectionDto[] = [
{
id: '1',
title: 'Section 1',
shortTitle: 'S1',
description: 'First',
isTemplate: false,
formElementSubSections: []
}
]
const onNavigate = vi.fn()
const composable = useFormStepper(sections, { onNavigate })
await composable.navigateStepper(direction)
expect(onNavigate).toHaveBeenCalled()
}
)
})
})

View File

@@ -0,0 +1,464 @@
import { describe, it, expect } from 'vitest'
import { useSectionSpawning } from '../../../app/composables/useSectionSpawning'
import type {
FormElementSectionDto,
FormElementSubSectionDto,
FormElementDto,
FormOptionDto,
SectionSpawnTriggerDto,
FormElementType
} from '../../../.api-client'
// Helper to create a FormOptionDto
function createOption(value: string, label: string): FormOptionDto {
return {
value,
label,
processingPurpose: 'NONE',
employeeDataCategory: 'NONE'
}
}
// Helper to create a FormElementDto
function createFormElement(
reference: string,
title: string,
type: FormElementType,
options: FormOptionDto[] = [],
sectionSpawnTriggers: SectionSpawnTriggerDto[] = []
): FormElementDto {
return {
id: `id-${reference}`,
reference,
title,
type,
options,
sectionSpawnTriggers
}
}
// Helper to create a SectionSpawnTriggerDto
function createSpawnTrigger(
templateReference: string,
conditionType: 'SHOW' | 'HIDE' = 'SHOW',
expectedValue: string = 'true',
operator: 'EQUALS' | 'NOT_EQUALS' | 'IS_EMPTY' | 'IS_NOT_EMPTY' | 'CONTAINS' | 'NOT_CONTAINS' = 'EQUALS'
): SectionSpawnTriggerDto {
return {
templateReference,
sectionSpawnConditionType: conditionType,
sectionSpawnExpectedValue: expectedValue,
sectionSpawnOperator: operator
}
}
// Helper to create a template section with title interpolation
function createTemplateSection(
templateReference: string,
title: string,
titleTemplate?: string,
shortTitle?: string,
description?: string,
formElementSubSections: FormElementSubSectionDto[] = []
): FormElementSectionDto {
return {
id: `template-${templateReference}`,
title,
shortTitle,
description,
titleTemplate,
isTemplate: true,
templateReference,
formElementSubSections
}
}
describe('useSectionSpawning', () => {
it('should remove spawned section when condition no longer met', () => {
const { processSpawnTriggers } = useSectionSpawning()
const templateSection = createTemplateSection('template-1', 'Template Section')
const spawnedSection: FormElementSectionDto = {
id: 'spawned-1',
title: 'Spawned Section',
isTemplate: false,
spawnedFromElementReference: 'trigger_1',
templateReference: 'template-1',
formElementSubSections: []
}
const trigger = createSpawnTrigger('template-1', 'SHOW', 'true', 'EQUALS')
const triggerElement = createFormElement(
'trigger_1',
'Trigger',
'SELECT',
[createOption('true', 'No'), createOption('false', 'Yes')],
[trigger]
)
const sections = [templateSection, spawnedSection]
const result = processSpawnTriggers(sections, [triggerElement])
expect(result).toHaveLength(1)
expect(result[0]!.isTemplate).toBe(true)
})
it('should NOT remove spawned section if another trigger for same template still satisfies condition', () => {
const { processSpawnTriggers } = useSectionSpawning()
const templateSection = createTemplateSection('template-1', 'Template Section')
const spawnedSection: FormElementSectionDto = {
id: 'spawned-1',
title: 'Spawned Section',
isTemplate: false,
spawnedFromElementReference: 'trigger_1',
templateReference: 'template-1',
formElementSubSections: []
}
const trigger1 = createSpawnTrigger('template-1', 'SHOW', 'true', 'EQUALS')
const trigger2 = createSpawnTrigger('template-1', 'SHOW', 'maybe', 'EQUALS')
const triggerElement = createFormElement(
'trigger_1',
'Trigger',
'SELECT',
[createOption('false', 'No'), createOption('false', 'Yes'), createOption('true', 'Maybe')],
[trigger1, trigger2]
)
const sections = [templateSection, spawnedSection]
const result = processSpawnTriggers(sections, [triggerElement])
expect(result).toHaveLength(2)
expect(result[1]!.spawnedFromElementReference).toBe('trigger_1')
})
it('should update titles with {{triggerValue}} interpolation when value changes', () => {
const { processSpawnTriggers } = useSectionSpawning()
const templateSection = createTemplateSection(
'template-1',
'Section title',
'Section template title {{triggerValue}}',
'Short title {{triggerValue}}',
'Description {{triggerValue}}'
)
const spawnedSection: FormElementSectionDto = {
id: 'spawned-1',
title: 'Section OldValue',
shortTitle: 'Short OldValue',
description: 'Description OldValue',
isTemplate: false,
spawnedFromElementReference: 'trigger_1',
templateReference: 'template-1',
formElementSubSections: []
}
const trigger = createSpawnTrigger('template-1', 'SHOW', 'yes', 'EQUALS')
const triggerElement = createFormElement(
'trigger_1',
'Trigger',
'SELECT',
[createOption('false', 'No'), createOption('true', 'Yes')],
[trigger]
)
const sections = [templateSection, spawnedSection]
const result = processSpawnTriggers(sections, [triggerElement])
expect(result[1]!.title).toBe('Section template title Yes')
expect(result[1]!.shortTitle).toBe('Short title Yes')
expect(result[1]!.description).toBe('Description Yes')
})
it('should insert spawned section after template section', () => {
const { processSpawnTriggers } = useSectionSpawning()
const templateSection = createTemplateSection('template-1', 'Template Section')
const otherSection: FormElementSectionDto = {
id: 'other-1',
title: 'Other Section',
isTemplate: false,
formElementSubSections: []
}
const trigger = createSpawnTrigger('template-1', 'SHOW', 'Yes', 'EQUALS')
const triggerElement = createFormElement(
'trigger_1',
'Trigger',
'SELECT',
[createOption('false', 'No'), createOption('true', 'Yes')],
[trigger]
)
const sections = [templateSection, otherSection]
const result = processSpawnTriggers(sections, [triggerElement])
expect(result).toHaveLength(3)
expect(result[0]!.isTemplate).toBe(true)
expect(result[1]!.spawnedFromElementReference).toBe('trigger_1')
expect(result[2]!.id).toBe('other-1')
})
it('should handle TEXTFIELD element value extraction', () => {
const { processSpawnTriggers } = useSectionSpawning()
const templateSection = createTemplateSection('template-1', 'Section {{triggerValue}}', 'Section {{triggerValue}}')
const trigger = createSpawnTrigger('template-1', 'SHOW', 'john', 'EQUALS')
const triggerElement = createFormElement('trigger_1', 'Name', 'TEXTFIELD', [createOption('john', '')], [trigger])
const sections = [templateSection]
const result = processSpawnTriggers(sections, [triggerElement])
expect(result).toHaveLength(2)
expect(result[1]!.title).toBe('Section john')
})
it('should interpolate shortTitle with {{triggerValue}}', () => {
const { processSpawnTriggers } = useSectionSpawning()
const templateSection = createTemplateSection('template-1', 'Template', undefined, 'Short {{triggerValue}}')
const spawnedSection: FormElementSectionDto = {
id: 'spawned-1',
title: 'Spawned',
shortTitle: 'Short OldValue',
isTemplate: false,
spawnedFromElementReference: 'trigger_1',
templateReference: 'template-1',
formElementSubSections: []
}
const trigger = createSpawnTrigger('template-1', 'SHOW', 'yes', 'EQUALS')
const triggerElement = createFormElement(
'trigger_1',
'Trigger',
'SELECT',
[createOption('false', 'No'), createOption('true', 'Yes')],
[trigger]
)
const sections = [templateSection, spawnedSection]
const result = processSpawnTriggers(sections, [triggerElement])
expect(result[1]!.shortTitle).toBe('Short Yes')
})
it('should interpolate description with {{triggerValue}}', () => {
const { processSpawnTriggers } = useSectionSpawning()
const templateSection = createTemplateSection(
'template-1',
'Template',
undefined,
undefined,
'Description {{triggerValue}}'
)
const spawnedSection: FormElementSectionDto = {
id: 'spawned-1',
title: 'Spawned',
description: 'Description OldValue',
isTemplate: false,
spawnedFromElementReference: 'trigger_1',
templateReference: 'template-1',
formElementSubSections: []
}
const trigger = createSpawnTrigger('template-1', 'SHOW', 'yes', 'EQUALS')
const triggerElement = createFormElement(
'trigger_1',
'Trigger',
'SELECT',
[createOption('false', 'No'), createOption('true', 'Yes')],
[trigger]
)
const sections = [templateSection, spawnedSection]
const result = processSpawnTriggers(sections, [triggerElement])
expect(result[1]!.description).toBe('Description Yes')
})
it('should preserve non-triggering elements during processing', () => {
const { processSpawnTriggers } = useSectionSpawning()
const templateSection = createTemplateSection('template-1', 'Template')
const nonTriggerElement = createFormElement('other_1', 'Other', 'TEXTFIELD', [createOption('value', '')])
const trigger = createSpawnTrigger('template-1', 'SHOW', 'Yes', 'EQUALS')
const triggerElement = createFormElement(
'trigger_1',
'Trigger',
'SELECT',
[createOption('false', 'No'), createOption('true', 'Yes')],
[trigger]
)
const sections = [templateSection]
const result = processSpawnTriggers(sections, [triggerElement, nonTriggerElement])
expect(result).toHaveLength(2)
expect(result[1]!.spawnedFromElementReference).toBe('trigger_1')
})
it('should handle empty trigger list on element', () => {
const { processSpawnTriggers } = useSectionSpawning()
const templateSection = createTemplateSection('template-1', 'Template')
const triggerElement = createFormElement(
'trigger_1',
'Trigger',
'SELECT',
[createOption('false', 'No'), createOption('true', 'Yes')],
[]
)
const sections = [templateSection]
const result = processSpawnTriggers(sections, [triggerElement])
expect(result).toHaveLength(1)
})
describe('operators', () => {
describe('EQUALS', () => {
it('should spawn section when SHOW/EQUALS condition is met with SELECT element', () => {
const { processSpawnTriggers } = useSectionSpawning()
const templateSection = createTemplateSection('template-1', 'Template Section', 'Section Title Template')
const trigger = createSpawnTrigger('template-1', 'SHOW', 'Yes', 'EQUALS')
const triggerElement = createFormElement(
'trigger_1',
'Trigger',
'SELECT',
[createOption('false', 'No'), createOption('true', 'Yes')],
[trigger]
)
const result = processSpawnTriggers([templateSection], [triggerElement])
expect(result).toHaveLength(2)
expect(result[1]!.spawnedFromElementReference).toBe('trigger_1')
expect(result[1]!.templateReference).toBe('template-1')
expect(result[1]!.title).toBe('Section Title Template')
})
it('should NOT spawn section when SHOW/EQUALS condition is NOT met', () => {
const { processSpawnTriggers } = useSectionSpawning()
const templateSection = createTemplateSection('template-1', 'Template Section')
const trigger = createSpawnTrigger('template-1', 'SHOW', 'Yes', 'EQUALS')
const triggerElement = createFormElement(
'trigger_1',
'Trigger',
'SELECT',
[createOption('true', 'No'), createOption('false', 'Yes')],
[trigger]
)
const sections = [templateSection]
const result = processSpawnTriggers(sections, [triggerElement])
expect(result).toHaveLength(1)
})
it('should handle case-insensitive comparison for EQUALS operator', () => {
const { processSpawnTriggers } = useSectionSpawning()
const templateSection = createTemplateSection('template-1', 'Template')
const trigger = createSpawnTrigger('template-1', 'SHOW', 'YES', 'EQUALS')
const triggerElement = createFormElement(
'trigger_1',
'Trigger',
'SELECT',
[createOption('false', 'No'), createOption('true', 'yes')],
[trigger]
)
const sections = [templateSection]
const result = processSpawnTriggers(sections, [triggerElement])
expect(result).toHaveLength(2)
})
})
describe('NOT_EQUALS', () => {
it('should handle NOT_EQUALS operator', () => {
const { processSpawnTriggers } = useSectionSpawning()
const templateSection = createTemplateSection('template-1', 'Template')
const trigger = createSpawnTrigger('template-1', 'SHOW', 'Yes', 'NOT_EQUALS')
const triggerElement = createFormElement(
'trigger_1',
'Trigger',
'SELECT',
[createOption('true', 'No'), createOption('false', 'Yes')],
[trigger]
)
const sections = [templateSection]
const result = processSpawnTriggers(sections, [triggerElement])
expect(result).toHaveLength(2)
expect(result[1]!.spawnedFromElementReference).toBe('trigger_1')
})
})
describe('IS_EMPTY', () => {
it('should handle IS_EMPTY operator', () => {
const { processSpawnTriggers } = useSectionSpawning()
const templateSection = createTemplateSection('template-1', 'Template Section')
const trigger = createSpawnTrigger('template-1', 'SHOW', 'PLACEHOLDER', 'IS_EMPTY')
const triggerElement = createFormElement('trigger_1', 'Trigger', 'TEXTFIELD', [createOption('', '')], [trigger])
const sections = [templateSection]
const result = processSpawnTriggers(sections, [triggerElement])
expect(result).toHaveLength(2)
expect(result[1]!.spawnedFromElementReference).toBe('trigger_1')
})
})
describe('IS_NOT_EMPTY', () => {
it('should handle IS_NOT_EMPTY operator', () => {
const { processSpawnTriggers } = useSectionSpawning()
const templateSection = createTemplateSection('template-1', 'Template Section')
const trigger = createSpawnTrigger('template-1', 'SHOW', 'PLACEHOLDER', 'IS_NOT_EMPTY')
const triggerElement = createFormElement(
'trigger_1',
'Trigger',
'TEXTFIELD',
[createOption('some value', '')],
[trigger]
)
const sections = [templateSection]
const result = processSpawnTriggers(sections, [triggerElement])
expect(result).toHaveLength(2)
expect(result[1]!.spawnedFromElementReference).toBe('trigger_1')
})
})
describe('HIDE', () => {
it('should handle HIDE condition type', () => {
const { processSpawnTriggers } = useSectionSpawning()
const templateSection = createTemplateSection('template-1', 'Template Section')
const trigger = createSpawnTrigger('template-1', 'HIDE', 'No', 'EQUALS')
const triggerElement = createFormElement(
'trigger_1',
'Trigger',
'SELECT',
[createOption('true', 'No'), createOption('false', 'Yes')],
[trigger]
)
const sections = [templateSection]
const result = processSpawnTriggers(sections, [triggerElement])
expect(result).toHaveLength(1)
})
})
})
})

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'
import { compareApplicationFormValues, groupChangesBySection } from '../../app/utils/formDiff'
import { compareApplicationFormValues, groupChangesBySection } from '../../../app/utils/formSnapshotComparison'
import type {
ApplicationFormDto,
ApplicationFormSnapshotDto,
@@ -11,7 +11,7 @@ import type {
FormElementSectionSnapshotDto,
FormElementSubSectionSnapshotDto,
FormElementType
} from '../../.api-client'
} from '../../../.api-client'
// Helper to create a minimal FormOptionDto
function createOption(value: string, label: string): FormOptionDto {
@@ -96,7 +96,7 @@ function createSnapshot(elements: FormElementSnapshotDto[], sectionTitle = 'Test
}
}
describe('formDiff', () => {
describe('formSnapshotComparison', () => {
describe('compareApplicationFormValues', () => {
describe('TEXTFIELD element', () => {
it('should detect new answer (empty → filled)', () => {
@@ -110,9 +110,9 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].elementTitle).toBe('Name')
expect(diff.newAnswers[0].currentLabel).toBe('John Doe')
expect(diff.newAnswers[0].previousLabel).toBeNull()
expect(diff.newAnswers[0]?.elementTitle).toBe('Name')
expect(diff.newAnswers[0]?.currentLabel).toBe('John Doe')
expect(diff.newAnswers[0]?.previousLabel).toBeNull()
expect(diff.changedAnswers).toHaveLength(0)
expect(diff.clearedAnswers).toHaveLength(0)
})
@@ -128,8 +128,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].previousLabel).toBe('John Doe')
expect(diff.changedAnswers[0].currentLabel).toBe('Jane Doe')
expect(diff.changedAnswers[0]?.previousLabel).toBe('John Doe')
expect(diff.changedAnswers[0]?.currentLabel).toBe('Jane Doe')
expect(diff.newAnswers).toHaveLength(0)
expect(diff.clearedAnswers).toHaveLength(0)
})
@@ -143,8 +143,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1)
expect(diff.clearedAnswers[0].previousLabel).toBe('John Doe')
expect(diff.clearedAnswers[0].currentLabel).toBeNull()
expect(diff.clearedAnswers[0]?.previousLabel).toBe('John Doe')
expect(diff.clearedAnswers[0]?.currentLabel).toBeNull()
expect(diff.newAnswers).toHaveLength(0)
expect(diff.changedAnswers).toHaveLength(0)
})
@@ -179,7 +179,7 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].currentLabel).toBe('This is a long description text.')
expect(diff.newAnswers[0]?.currentLabel).toBe('This is a long description text.')
})
it('should detect changed answer', () => {
@@ -193,8 +193,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].previousLabel).toBe('Original text')
expect(diff.changedAnswers[0].currentLabel).toBe('Updated text')
expect(diff.changedAnswers[0]?.previousLabel).toBe('Original text')
expect(diff.changedAnswers[0]?.currentLabel).toBe('Updated text')
})
it('should detect cleared answer', () => {
@@ -206,7 +206,7 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1)
expect(diff.clearedAnswers[0].previousLabel).toBe('Some text')
expect(diff.clearedAnswers[0]?.previousLabel).toBe('Some text')
})
})
@@ -224,7 +224,7 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].currentLabel).toBe('<p>Rich <strong>text</strong> content</p>')
expect(diff.newAnswers[0]?.currentLabel).toBe('<p>Rich <strong>text</strong> content</p>')
})
it('should detect changed answer', () => {
@@ -238,8 +238,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].previousLabel).toBe('<p>Original</p>')
expect(diff.changedAnswers[0].currentLabel).toBe('<p>Updated</p>')
expect(diff.changedAnswers[0]?.previousLabel).toBe('<p>Original</p>')
expect(diff.changedAnswers[0]?.currentLabel).toBe('<p>Updated</p>')
})
it('should detect cleared answer', () => {
@@ -264,7 +264,7 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].currentLabel).toBe('2024-01-15')
expect(diff.newAnswers[0]?.currentLabel).toBe('2024-01-15')
})
it('should detect changed answer', () => {
@@ -278,8 +278,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].previousLabel).toBe('2024-01-15')
expect(diff.changedAnswers[0].currentLabel).toBe('2024-02-20')
expect(diff.changedAnswers[0]?.previousLabel).toBe('2024-01-15')
expect(diff.changedAnswers[0]?.currentLabel).toBe('2024-02-20')
})
it('should detect cleared answer', () => {
@@ -314,8 +314,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].currentLabel).toBe('Medium')
expect(diff.newAnswers[0].previousLabel).toBeNull()
expect(diff.newAnswers[0]?.currentLabel).toBe('Medium')
expect(diff.newAnswers[0]?.previousLabel).toBeNull()
})
it('should detect changed answer (different option selected)', () => {
@@ -337,8 +337,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].previousLabel).toBe('Low')
expect(diff.changedAnswers[0].currentLabel).toBe('High')
expect(diff.changedAnswers[0]?.previousLabel).toBe('Low')
expect(diff.changedAnswers[0]?.currentLabel).toBe('High')
})
it('should detect cleared answer (option selected → nothing selected)', () => {
@@ -360,8 +360,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1)
expect(diff.clearedAnswers[0].previousLabel).toBe('Medium')
expect(diff.clearedAnswers[0].currentLabel).toBeNull()
expect(diff.clearedAnswers[0]?.previousLabel).toBe('Medium')
expect(diff.clearedAnswers[0]?.currentLabel).toBeNull()
})
})
@@ -385,7 +385,7 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].currentLabel).toBe('Male')
expect(diff.newAnswers[0]?.currentLabel).toBe('Male')
})
it('should detect changed answer', () => {
@@ -407,8 +407,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].previousLabel).toBe('Male')
expect(diff.changedAnswers[0].currentLabel).toBe('Female')
expect(diff.changedAnswers[0]?.previousLabel).toBe('Male')
expect(diff.changedAnswers[0]?.currentLabel).toBe('Female')
})
it('should detect cleared answer', () => {
@@ -430,7 +430,7 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1)
expect(diff.clearedAnswers[0].previousLabel).toBe('Other')
expect(diff.clearedAnswers[0]?.previousLabel).toBe('Other')
})
})
@@ -454,7 +454,7 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].currentLabel).toBe('Feature A')
expect(diff.newAnswers[0]?.currentLabel).toBe('Feature A')
})
it('should detect new answer (multiple checkboxes selected)', () => {
@@ -476,7 +476,7 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].currentLabel).toBe('Feature A, Feature B')
expect(diff.newAnswers[0]?.currentLabel).toBe('Feature A, Feature B')
})
it('should detect changed answer (different checkboxes selected)', () => {
@@ -498,8 +498,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].previousLabel).toBe('Feature A')
expect(diff.changedAnswers[0].currentLabel).toBe('Feature B, Feature C')
expect(diff.changedAnswers[0]?.previousLabel).toBe('Feature A')
expect(diff.changedAnswers[0]?.currentLabel).toBe('Feature B, Feature C')
})
it('should detect cleared answer (all checkboxes deselected)', () => {
@@ -521,7 +521,7 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1)
expect(diff.clearedAnswers[0].previousLabel).toBe('Feature A, Feature B')
expect(diff.clearedAnswers[0]?.previousLabel).toBe('Feature A, Feature B')
})
})
@@ -537,7 +537,7 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].currentLabel).toBe('Enabled')
expect(diff.newAnswers[0]?.currentLabel).toBe('Enabled')
})
it('should detect cleared answer (switch turned off)', () => {
@@ -551,7 +551,7 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1)
expect(diff.clearedAnswers[0].previousLabel).toBe('Enabled')
expect(diff.clearedAnswers[0]?.previousLabel).toBe('Enabled')
})
})
@@ -573,11 +573,11 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].currentLabel).toBe('2 Zeilen')
expect(diff.newAnswers[0].tableDiff).toBeDefined()
expect(diff.newAnswers[0].tableDiff!.addedCount).toBe(2)
expect(diff.newAnswers[0].tableDiff!.removedCount).toBe(0)
expect(diff.newAnswers[0].tableDiff!.modifiedCount).toBe(0)
expect(diff.newAnswers[0]?.currentLabel).toBe('2 Zeilen')
expect(diff.newAnswers[0]?.tableDiff).toBeDefined()
expect(diff.newAnswers[0]?.tableDiff!.addedCount).toBe(2)
expect(diff.newAnswers[0]?.tableDiff!.removedCount).toBe(0)
expect(diff.newAnswers[0]?.tableDiff!.modifiedCount).toBe(0)
})
it('should detect rows removed', () => {
@@ -594,10 +594,10 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1)
expect(diff.clearedAnswers[0].previousLabel).toBe('2 Zeilen')
expect(diff.clearedAnswers[0].tableDiff).toBeDefined()
expect(diff.clearedAnswers[0].tableDiff!.removedCount).toBe(2)
expect(diff.clearedAnswers[0].tableDiff!.addedCount).toBe(0)
expect(diff.clearedAnswers[0]?.previousLabel).toBe('2 Zeilen')
expect(diff.clearedAnswers[0]?.tableDiff).toBeDefined()
expect(diff.clearedAnswers[0]?.tableDiff!.removedCount).toBe(2)
expect(diff.clearedAnswers[0]?.tableDiff!.addedCount).toBe(0)
})
it('should detect modified rows when row count changes', () => {
@@ -619,12 +619,12 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].tableDiff).toBeDefined()
expect(diff.changedAnswers[0].tableDiff!.addedCount).toBe(1)
expect(diff.changedAnswers[0].tableDiff!.modifiedCount).toBe(1)
expect(diff.changedAnswers[0].tableDiff!.rows[1].changeType).toBe('modified')
expect(diff.changedAnswers[0].tableDiff!.rows[1].previousValues['Name']).toBe('Jane')
expect(diff.changedAnswers[0].tableDiff!.rows[1].currentValues['Name']).toBe('Janet')
expect(diff.changedAnswers[0]?.tableDiff).toBeDefined()
expect(diff.changedAnswers[0]?.tableDiff!.addedCount).toBe(1)
expect(diff.changedAnswers[0]?.tableDiff!.modifiedCount).toBe(1)
expect(diff.changedAnswers[0]?.tableDiff!.rows[1]?.changeType).toBe('modified')
expect(diff.changedAnswers[0]?.tableDiff!.rows[1]?.previousValues['Name']).toBe('Jane')
expect(diff.changedAnswers[0]?.tableDiff!.rows[1]?.currentValues['Name']).toBe('Janet')
})
it('should detect modified rows when row count is the same', () => {
@@ -644,12 +644,12 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].tableDiff).toBeDefined()
expect(diff.changedAnswers[0].tableDiff!.addedCount).toBe(0)
expect(diff.changedAnswers[0].tableDiff!.modifiedCount).toBe(1)
expect(diff.changedAnswers[0].tableDiff!.rows[1].changeType).toBe('modified')
expect(diff.changedAnswers[0].tableDiff!.rows[1].previousValues['Name']).toBe('Jane')
expect(diff.changedAnswers[0].tableDiff!.rows[1].currentValues['Name']).toBe('Janet')
expect(diff.changedAnswers[0]?.tableDiff).toBeDefined()
expect(diff.changedAnswers[0]?.tableDiff!.addedCount).toBe(0)
expect(diff.changedAnswers[0]?.tableDiff!.modifiedCount).toBe(1)
expect(diff.changedAnswers[0]?.tableDiff!.rows[1]?.changeType).toBe('modified')
expect(diff.changedAnswers[0]?.tableDiff!.rows[1]?.previousValues['Name']).toBe('Jane')
expect(diff.changedAnswers[0]?.tableDiff!.rows[1]?.currentValues['Name']).toBe('Janet')
})
it('should detect mixed changes (added, removed, modified)', () => {
@@ -669,12 +669,12 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].tableDiff).toBeDefined()
expect(diff.changedAnswers[0]?.tableDiff).toBeDefined()
// Row 0: modified (John → John Updated, Developer → Senior Dev)
// Row 1: modified (Jane → New Person, Designer → Manager)
// Row 2: removed (Bob, Tester)
expect(diff.changedAnswers[0].tableDiff!.modifiedCount).toBe(2)
expect(diff.changedAnswers[0].tableDiff!.removedCount).toBe(1)
expect(diff.changedAnswers[0]?.tableDiff!.modifiedCount).toBe(2)
expect(diff.changedAnswers[0]?.tableDiff!.removedCount).toBe(1)
})
it('should handle single row correctly', () => {
@@ -694,7 +694,7 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].currentLabel).toBe('1 Zeile')
expect(diff.newAnswers[0]?.currentLabel).toBe('1 Zeile')
})
it('should handle boolean values in table cells', () => {
@@ -715,8 +715,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].tableDiff!.rows[0].previousValues['Enabled']).toBe('Nein')
expect(diff.changedAnswers[0].tableDiff!.rows[0].currentValues['Enabled']).toBe('Ja')
expect(diff.changedAnswers[0]?.tableDiff!.rows[0]?.previousValues['Enabled']).toBe('Nein')
expect(diff.changedAnswers[0]?.tableDiff!.rows[0]?.currentValues['Enabled']).toBe('Ja')
})
it('should handle array values in table cells', () => {
@@ -736,8 +736,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].tableDiff!.rows[0].previousValues['Tags']).toBe('Tag1')
expect(diff.changedAnswers[0].tableDiff!.rows[0].currentValues['Tags']).toBe('Tag1, Tag2')
expect(diff.changedAnswers[0]?.tableDiff!.rows[0]?.previousValues['Tags']).toBe('Tag1')
expect(diff.changedAnswers[0]?.tableDiff!.rows[0]?.currentValues['Tags']).toBe('Tag1, Tag2')
})
})
@@ -751,8 +751,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1)
expect(diff.clearedAnswers[0].elementTitle).toBe('Name')
expect(diff.clearedAnswers[0].previousLabel).toBe('John Doe')
expect(diff.clearedAnswers[0]?.elementTitle).toBe('Name')
expect(diff.clearedAnswers[0]?.previousLabel).toBe('John Doe')
})
it('should not report removed element if it had no value', () => {
@@ -777,8 +777,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].elementTitle).toBe('Name')
expect(diff.newAnswers[0].currentLabel).toBe('John Doe')
expect(diff.newAnswers[0]?.elementTitle).toBe('Name')
expect(diff.newAnswers[0]?.currentLabel).toBe('John Doe')
})
it('should not report new element if it has no value', () => {
@@ -813,13 +813,13 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].elementTitle).toBe('Name')
expect(diff.newAnswers[0]?.elementTitle).toBe('Name')
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].elementTitle).toBe('Email')
expect(diff.changedAnswers[0]?.elementTitle).toBe('Email')
expect(diff.clearedAnswers).toHaveLength(1)
expect(diff.clearedAnswers[0].elementTitle).toBe('Status')
expect(diff.clearedAnswers[0]?.elementTitle).toBe('Status')
})
})
@@ -1088,8 +1088,8 @@ describe('formDiff', () => {
const grouped = groupChangesBySection(diff)
expect(grouped).toHaveLength(1)
expect(grouped[0].sectionTitle).toBe('Section A')
expect(grouped[0].changes).toHaveLength(3)
expect(grouped[0]?.sectionTitle).toBe('Section A')
expect(grouped[0]?.changes).toHaveLength(3)
})
})
})

View File

@@ -4,18 +4,32 @@ import { defineVitestProject } from '@nuxt/test-utils/config'
export default defineConfig({
test: {
projects: [
{
test: {
name: 'unit',
include: ['test/{e2e,unit}/*.{test,spec}.ts'],
environment: 'node'
}
},
await defineVitestProject({
test: {
name: 'nuxt',
include: ['test/nuxt/*.{test,spec}.ts'],
environment: 'nuxt'
name: 'unit',
include: ['test/unit/**/*.{test,spec}.ts'],
environment: 'nuxt',
environmentOptions: {
nuxt: {
domEnvironment: 'happy-dom'
}
}
}
}),
await defineVitestProject({
test: {
name: 'integration',
include: ['test/integration/**/*.{test,spec}.ts'],
environment: 'nuxt',
environmentOptions: {
nuxt: {
domEnvironment: 'happy-dom',
mock: {
intersectionObserver: true,
indexedDb: true
}
}
}
}
})
]