Compare commits

...

10 Commits

33 changed files with 2403 additions and 677 deletions

View File

@@ -1681,6 +1681,30 @@ components:
type: boolean type: boolean
default: false default: false
description: If true, renders a checkbox instead of text input 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: TableRowConstraintDto:
type: object type: object

View File

@@ -35,6 +35,18 @@ class FormOption(
AttributeOverride(name = "filterCondition.operator", column = Column(name = "col_config_filter_operator")), AttributeOverride(name = "filterCondition.operator", column = Column(name = "col_config_filter_operator")),
AttributeOverride(name = "isReadOnly", column = Column(name = "col_config_is_read_only")), AttributeOverride(name = "isReadOnly", column = Column(name = "col_config_is_read_only")),
AttributeOverride(name = "isCheckbox", column = Column(name = "col_config_is_checkbox")), 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, var columnConfig: TableColumnConfig? = null,
@JdbcTypeCode(SqlTypes.JSON) @JdbcTypeCode(SqlTypes.JSON)

View File

@@ -5,6 +5,8 @@ import jakarta.persistence.AttributeOverrides
import jakarta.persistence.Column import jakarta.persistence.Column
import jakarta.persistence.Embeddable import jakarta.persistence.Embeddable
import jakarta.persistence.Embedded import jakarta.persistence.Embedded
import org.hibernate.annotations.JdbcTypeCode
import org.hibernate.type.SqlTypes
@Embeddable @Embeddable
data class TableColumnConfig( data class TableColumnConfig(
@@ -29,4 +31,11 @@ data class TableColumnConfig(
val isReadOnly: Boolean = false, val isReadOnly: Boolean = false,
val isMultipleAllowed: Boolean = false, val isMultipleAllowed: Boolean = false,
val isCheckbox: 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 package com.betriebsratkanzlei.legalconsenthub.form_element
import com.betriebsratkanzlei.legalconsenthub_api.model.TableColumnConfigDto import com.betriebsratkanzlei.legalconsenthub_api.model.TableColumnConfigDto
import com.betriebsratkanzlei.legalconsenthub_api.model.TableRowVisibilityConditionDto
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionOperator as VisibilityConditionOperatorDto
@Component @Component
class TableColumnConfigMapper( class TableColumnConfigMapper(
private val filterMapper: TableColumnFilterMapper, private val filterMapper: TableColumnFilterMapper,
private val rowConstraintMapper: TableRowConstraintMapper, private val rowConstraintMapper: TableRowConstraintMapper,
private val visibilityConditionMapper: FormElementVisibilityConditionMapper,
) { ) {
fun toTableColumnConfigDto(config: TableColumnConfig): TableColumnConfigDto = fun toTableColumnConfigDto(config: TableColumnConfig): TableColumnConfigDto =
TableColumnConfigDto( TableColumnConfigDto(
@@ -17,6 +20,9 @@ class TableColumnConfigMapper(
isReadOnly = config.isReadOnly, isReadOnly = config.isReadOnly,
isMultipleAllowed = config.isMultipleAllowed, isMultipleAllowed = config.isMultipleAllowed,
isCheckbox = config.isCheckbox, isCheckbox = config.isCheckbox,
readOnlyConditions = config.readOnlyConditions?.let { visibilityConditionMapper.toGroupConditionDto(it) },
readOnlyDefaultValue = config.readOnlyDefaultValue,
rowVisibilityCondition = config.rowVisibilityCondition?.let { toRowVisibilityConditionDto(it) },
) )
fun toTableColumnConfig(dto: TableColumnConfigDto): TableColumnConfig = fun toTableColumnConfig(dto: TableColumnConfigDto): TableColumnConfig =
@@ -28,5 +34,42 @@ class TableColumnConfigMapper(
isReadOnly = dto.isReadOnly ?: false, isReadOnly = dto.isReadOnly ?: false,
isMultipleAllowed = dto.isMultipleAllowed ?: false, isMultipleAllowed = dto.isMultipleAllowed ?: false,
isCheckbox = dto.isCheckbox ?: 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, val formElementOperator: VisibilityConditionOperator = VisibilityConditionOperator.EQUALS,
) : VisibilityConditionNode ) : VisibilityConditionNode
data class RowVisibilityCondition(
val sourceColumnIndex: Int,
val expectedValues: List<String>,
val operator: VisibilityConditionOperator = VisibilityConditionOperator.CONTAINS,
)
enum class GroupOperator { enum class GroupOperator {
AND, AND,
OR, OR,

View File

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

View File

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

View File

@@ -14,39 +14,26 @@ formElementSubSections:
description: '' description: ''
type: TABLE type: TABLE
visibilityConditions: visibilityConditions:
operator: AND operator: OR
conditions: conditions:
- nodeType: LEAF - nodeType: LEAF
formElementConditionType: SHOW formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit sourceFormElementReference: sens_luv
formElementExpectedValue: Für Administratoren formElementExpectedValue: Aggregiert (Team)
formElementOperator: NOT_EQUALS formElementOperator: CONTAINS
- nodeType: LEAF - nodeType: LEAF
formElementConditionType: SHOW formElementConditionType: SHOW
sourceFormElementReference: sens_auswertung sourceFormElementReference: sens_luv
formElementExpectedValue: Funktionen vorhanden formElementExpectedValue: Aggregiert (Abteilung)
formElementOperator: EQUALS formElementOperator: CONTAINS
- nodeType: GROUP - nodeType: LEAF
groupOperator: OR formElementConditionType: SHOW
conditions: sourceFormElementReference: sens_luv
- nodeType: LEAF formElementExpectedValue: Individuell/vergleichend
formElementConditionType: SHOW formElementOperator: CONTAINS
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: options:
- value: '["V001", "V002", "V003", "V004", "V005"]' - value: '["V001", "V002", "V003", "V004", "V005"]'
label: Verarbeitungsvorgang-ID label: Verarbeitungs-ID
processingPurpose: SYSTEM_OPERATION processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL employeeDataCategory: NON_CRITICAL
- value: '["Personalstammdatenpflege", "Zeiterfassung", "Gehaltsabrechnung", "Leistungsbeurteilung", "Produktionsauswertung"]' - value: '["Personalstammdatenpflege", "Zeiterfassung", "Gehaltsabrechnung", "Leistungsbeurteilung", "Produktionsauswertung"]'
@@ -54,13 +41,13 @@ formElementSubSections:
processingPurpose: SYSTEM_OPERATION processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL employeeDataCategory: NON_CRITICAL
- value: '["HCM Master Data", "CATS Zeiterfassung", "Payroll Processing", "Performance Management", "Shop Floor Control"]' - value: '["HCM Master Data", "CATS Zeiterfassung", "Payroll Processing", "Performance Management", "Shop Floor Control"]'
label: Systemfunktion/Verarbeitungsform label: 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
processingPurpose: SYSTEM_OPERATION processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL 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"]' - value: '["Stammdaten", "Arbeitszeitdaten", "Gehaltsdaten", "Leistungsdaten", "Produktionsdaten"]'
label: Datenkategorien label: Datenkategorien
processingPurpose: DATA_ANALYSIS processingPurpose: DATA_ANALYSIS
@@ -73,14 +60,6 @@ formElementSubSections:
label: Betroffene Mitarbeiter label: Betroffene Mitarbeiter
processingPurpose: DATA_ANALYSIS processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE 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"]' - value: '["R002", "R002,R004", "R001,R002", "R002,R004", "R003,R004"]'
label: Rollen-Sichtbarkeit (grob) label: Rollen-Sichtbarkeit (grob)
processingPurpose: DATA_ANALYSIS processingPurpose: DATA_ANALYSIS
@@ -103,9 +82,17 @@ formElementSubSections:
formElementExpectedValue: Für Administratoren formElementExpectedValue: Für Administratoren
formElementOperator: NOT_EQUALS formElementOperator: NOT_EQUALS
- value: '["Nein", "Ja - an Vorgesetzte", "Nein", "Ja - an Management", "Ja - an Produktionsleitung"]' - value: '["Nein", "Ja - an Vorgesetzte", "Nein", "Ja - an Management", "Ja - an Produktionsleitung"]'
label: Export/Weitergabe (Ja/Nein + Ziel) label: Export/Weitergabe
processingPurpose: DATA_ANALYSIS processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED 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]' - value: '[false, true, false, true, true]'
label: Leistungs-/Verhaltenskontrolle beabsichtigt? label: Leistungs-/Verhaltenskontrolle beabsichtigt?
processingPurpose: DATA_ANALYSIS processingPurpose: DATA_ANALYSIS
@@ -113,6 +100,79 @@ formElementSubSections:
columnConfig: columnConfig:
isCheckbox: true isCheckbox: true
# Rollen-Sichtbarkeit (Umfassende Darstellung - shown when LuV contains Team, Abteilung, or Individuell)
- title: Rollen-Sichtbarkeit (Umfassende Darstellung)
formElements:
- reference: rollen_sichtbarkeit_umfassend_tabelle
title: Welche Rollen können welche Verarbeitungsvorgänge sehen? (Umfassende Darstellung)
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:
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 # Angaben zur Leistungs-/Verhaltenskontrolle
- title: Angaben zur Leistungs-/Verhaltenskontrolle - title: Angaben zur Leistungs-/Verhaltenskontrolle
formElements: formElements:
@@ -123,7 +183,7 @@ formElementSubSections:
tableRowPreset: tableRowPreset:
sourceTableReference: umfassende_datenverarbeitung_tabelle sourceTableReference: umfassende_datenverarbeitung_tabelle
filterCondition: filterCondition:
sourceColumnIndex: 11 sourceColumnIndex: 9
expectedValue: 'true' expectedValue: 'true'
operator: EQUALS operator: EQUALS
columnMappings: columnMappings:
@@ -131,39 +191,26 @@ formElementSubSections:
targetColumnIndex: 0 targetColumnIndex: 0
canAddRows: false canAddRows: false
visibilityConditions: visibilityConditions:
operator: AND operator: OR
conditions: conditions:
- nodeType: LEAF - nodeType: LEAF
formElementConditionType: SHOW formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit sourceFormElementReference: sens_luv
formElementExpectedValue: Für Administratoren formElementExpectedValue: Aggregiert (Team)
formElementOperator: NOT_EQUALS formElementOperator: CONTAINS
- nodeType: LEAF - nodeType: LEAF
formElementConditionType: SHOW formElementConditionType: SHOW
sourceFormElementReference: sens_auswertung sourceFormElementReference: sens_luv
formElementExpectedValue: Funktionen vorhanden formElementExpectedValue: Aggregiert (Abteilung)
formElementOperator: EQUALS formElementOperator: CONTAINS
- nodeType: GROUP - nodeType: LEAF
groupOperator: OR formElementConditionType: SHOW
conditions: sourceFormElementReference: sens_luv
- nodeType: LEAF formElementExpectedValue: Individuell/vergleichend
formElementConditionType: SHOW formElementOperator: CONTAINS
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: options:
- value: '["V002", "V004", "V005"]' - value: '["V002", "V004", "V005"]'
label: Verarbeitungsvorgang-ID label: Verarbeitungs-ID
processingPurpose: SYSTEM_OPERATION processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL employeeDataCategory: NON_CRITICAL
columnConfig: columnConfig:
@@ -171,7 +218,11 @@ formElementSubSections:
sourceColumnIndex: 0 sourceColumnIndex: 0
isReadOnly: true isReadOnly: true
- value: '["Überwachung der Einhaltung von Arbeitszeiten", "Bewertung der individuellen Zielerreichung", "Auswertung der Produktivität pro Mitarbeiter/Schicht"]' - value: '["Überwachung der Einhaltung von Arbeitszeiten", "Bewertung der individuellen Zielerreichung", "Auswertung der Produktivität pro Mitarbeiter/Schicht"]'
label: Konkreter Kontrollzweck 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 processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE employeeDataCategory: SENSITIVE
- value: '["Berichte", "Dashboards", "Berichte,Dashboards"]' - value: '["Berichte", "Dashboards", "Berichte,Dashboards"]'
@@ -182,10 +233,10 @@ formElementSubSections:
sourceTableReference: sens_art_analytische_funktionen sourceTableReference: sens_art_analytische_funktionen
sourceColumnIndex: 0 sourceColumnIndex: 0
isMultipleAllowed: true isMultipleAllowed: true
- value: '["Hinweis bei Abweichungen, keine automatischen Konsequenzen", "Einfluss auf Bonuszahlungen und Beförderungen", "Grundlage für Schichtplanung und Personalentscheidungen"]' - value: '["Täglich", "Jährlich/Halbjährlich", "Täglich/Schichtweise"]'
label: Entscheidungswirkung label: Häufigkeit/Anlass
processingPurpose: DATA_ANALYSIS processingPurpose: SYSTEM_OPERATION
employeeDataCategory: SENSITIVE employeeDataCategory: NON_CRITICAL
- value: '["Aggregiert (Team)", "Individuell/vergleichend", "Aggregiert (Team),Individuell/vergleichend"]' - value: '["Aggregiert (Team)", "Individuell/vergleichend", "Aggregiert (Team),Individuell/vergleichend"]'
label: Granularität/Bezugsebene label: Granularität/Bezugsebene
processingPurpose: DATA_ANALYSIS processingPurpose: DATA_ANALYSIS
@@ -194,70 +245,24 @@ formElementSubSections:
sourceTableReference: sens_luv sourceTableReference: sens_luv
sourceColumnIndex: 0 sourceColumnIndex: 0
isMultipleAllowed: true isMultipleAllowed: true
- value: '["Ja", "Ja", "Ja, bei begründetem Verdacht"]' - value: '[true, true, true]'
label: Drilldown bis Person möglich? label: Drilldown auf Individualebene
processingPurpose: DATA_ANALYSIS processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE employeeDataCategory: SENSITIVE
- value: '["Nein", "Ja - Ranking im Team", "Ja - Vergleich mit Durchschnitt"]' columnConfig:
label: Ranking/Scoring isCheckbox: true
processingPurpose: DATA_ANALYSIS rowVisibilityCondition:
employeeDataCategory: SENSITIVE sourceColumnIndex: 5
visibilityConditions: expectedValues:
operator: OR - "Aggregiert (Team)"
conditions: - "Aggregiert (Abteilung)"
- nodeType: LEAF operator: CONTAINS
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"]' - value: '["N/A", "Min. 5 Personen im Vergleich", "Min. 10 Personen pro Auswertung"]'
label: Mindestgruppe/Schwelle label: Mindestgruppe/Schwelle
processingPurpose: DATA_ANALYSIS processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE 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: visibilityConditions:
operator: AND operator: OR
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: conditions:
- nodeType: LEAF - nodeType: LEAF
formElementConditionType: SHOW formElementConditionType: SHOW
@@ -269,14 +274,71 @@ formElementSubSections:
sourceFormElementReference: sens_luv sourceFormElementReference: sens_luv
formElementExpectedValue: Aggregiert (Abteilung) formElementExpectedValue: Aggregiert (Abteilung)
formElementOperator: CONTAINS 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 - nodeType: LEAF
formElementConditionType: SHOW formElementConditionType: SHOW
sourceFormElementReference: sens_luv sourceFormElementReference: sens_alarme
formElementExpectedValue: Individuell/vergleichend formElementExpectedValue: Nein
formElementOperator: CONTAINS 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: options:
- value: '["V001", "V002", "V003", "V004", "V005"]' - value: '["V001", "V002", "V003", "V004", "V005"]'
label: Verarbeitungsvorgang-ID label: Verarbeitungs-ID
processingPurpose: SYSTEM_OPERATION processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL employeeDataCategory: NON_CRITICAL
columnConfig: columnConfig:
@@ -316,39 +378,26 @@ formElementSubSections:
description: '' description: ''
type: TABLE type: TABLE
visibilityConditions: visibilityConditions:
operator: AND operator: OR
conditions: conditions:
- nodeType: LEAF - nodeType: LEAF
formElementConditionType: SHOW formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit sourceFormElementReference: sens_luv
formElementExpectedValue: Für Administratoren formElementExpectedValue: Aggregiert (Team)
formElementOperator: NOT_EQUALS formElementOperator: CONTAINS
- nodeType: LEAF - nodeType: LEAF
formElementConditionType: SHOW formElementConditionType: SHOW
sourceFormElementReference: sens_auswertung sourceFormElementReference: sens_luv
formElementExpectedValue: Funktionen vorhanden formElementExpectedValue: Aggregiert (Abteilung)
formElementOperator: EQUALS formElementOperator: CONTAINS
- nodeType: GROUP - nodeType: LEAF
groupOperator: OR formElementConditionType: SHOW
conditions: sourceFormElementReference: sens_luv
- nodeType: LEAF formElementExpectedValue: Individuell/vergleichend
formElementConditionType: SHOW formElementOperator: CONTAINS
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: options:
- value: '["V001", "V002", "V003", "V004", "V005"]' - value: '["V001", "V002", "V003", "V004", "V005"]'
label: Verarbeitungsvorgang-ID label: Verarbeitungs-ID
processingPurpose: SYSTEM_OPERATION processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL employeeDataCategory: NON_CRITICAL
columnConfig: columnConfig:

View File

@@ -116,16 +116,6 @@ formElementSubSections:
sourceFormElementReference: loeschkonzept_hinterlegen sourceFormElementReference: loeschkonzept_hinterlegen
formElementExpectedValue: 'true' formElementExpectedValue: 'true'
formElementOperator: EQUALS 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 - nodeType: GROUP
groupOperator: OR groupOperator: OR
conditions: conditions:

View File

@@ -17,36 +17,23 @@ formElementSubSections:
description: '' description: ''
type: TABLE type: TABLE
visibilityConditions: visibilityConditions:
operator: AND operator: OR
conditions: conditions:
- nodeType: LEAF - nodeType: LEAF
formElementConditionType: SHOW formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit sourceFormElementReference: sens_luv
formElementExpectedValue: Für Administratoren formElementExpectedValue: Aggregiert (Team)
formElementOperator: NOT_EQUALS formElementOperator: CONTAINS
- nodeType: LEAF - nodeType: LEAF
formElementConditionType: SHOW formElementConditionType: SHOW
sourceFormElementReference: sens_auswertung sourceFormElementReference: sens_luv
formElementExpectedValue: Funktionen vorhanden formElementExpectedValue: Aggregiert (Abteilung)
formElementOperator: EQUALS formElementOperator: CONTAINS
- nodeType: GROUP - nodeType: LEAF
groupOperator: OR formElementConditionType: SHOW
conditions: sourceFormElementReference: sens_luv
- nodeType: LEAF formElementExpectedValue: Individuell/vergleichend
formElementConditionType: SHOW formElementOperator: CONTAINS
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: options:
# Column 0: Schnittstellen-ID # Column 0: Schnittstellen-ID
- value: '["IF001", "IF002", "IF003", "IF004", "IF005"]' - value: '["IF001", "IF002", "IF003", "IF004", "IF005"]'

View File

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

View File

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

View File

@@ -57,33 +57,48 @@ formElementSubSections:
processingPurpose: SYSTEM_OPERATION processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL employeeDataCategory: NON_CRITICAL
- value: '[]' - value: '[]'
label: Systemfunktion/Verarbeitungsform label: Verarbeitungsform
processingPurpose: SYSTEM_OPERATION processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL employeeDataCategory: NON_CRITICAL
- value: '[]' - value: '[]'
label: Kurzbeschreibung label: Verarbeitungszweck
processingPurpose: SYSTEM_OPERATION processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL employeeDataCategory: NON_CRITICAL
- value: '[]' - value: '[]'
label: Datenkategorien label: Datenkategorien
processingPurpose: DATA_ANALYSIS processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED employeeDataCategory: REVIEW_REQUIRED
- value: '[]'
label: Allgemeiner Zweck
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
- value: '[]' - value: '[]'
label: Betroffene Mitarbeiter label: Betroffene Mitarbeiter
processingPurpose: DATA_ANALYSIS processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE employeeDataCategory: SENSITIVE
- value: '[]'
label: Häufigkeit/Anlass
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
- value: '[]' - value: '[]'
label: Leistungs-/Verhaltenskontrolle label: Leistungs-/Verhaltenskontrolle
processingPurpose: DATA_ANALYSIS processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE 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) - title: Rollen-Sichtbarkeit (Einfache Darstellung)
formElements: formElements:
- reference: rollen_sichtbarkeit_einfach_tabelle - reference: rollen_sichtbarkeit_einfach_tabelle
@@ -128,7 +143,7 @@ formElementSubSections:
formElementOperator: NOT_CONTAINS formElementOperator: NOT_CONTAINS
options: options:
- value: '[]' - value: '[]'
label: Verarbeitungsvorgang-ID label: Verarbeitungs-ID
processingPurpose: SYSTEM_OPERATION processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL employeeDataCategory: NON_CRITICAL
columnConfig: columnConfig:
@@ -145,10 +160,26 @@ formElementSubSections:
label: Export/Weitergabe label: Export/Weitergabe
processingPurpose: DATA_ANALYSIS processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED employeeDataCategory: REVIEW_REQUIRED
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_schnittstellen_export
formElementExpectedValue: Exporte möglich
formElementOperator: CONTAINS
- value: '[]' - value: '[]'
label: Empfänger label: Empfänger
processingPurpose: DATA_ANALYSIS processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED employeeDataCategory: REVIEW_REQUIRED
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_schnittstellen_export
formElementExpectedValue: Exporte möglich
formElementOperator: CONTAINS
- value: '[]' - value: '[]'
label: Personenbezug möglich label: Personenbezug möglich
processingPurpose: DATA_ANALYSIS processingPurpose: DATA_ANALYSIS
@@ -164,39 +195,26 @@ formElementSubSections:
description: '' description: ''
type: TABLE type: TABLE
visibilityConditions: visibilityConditions:
operator: AND operator: OR
conditions: conditions:
- nodeType: LEAF - nodeType: LEAF
formElementConditionType: SHOW formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit sourceFormElementReference: sens_luv
formElementExpectedValue: Für Administratoren formElementExpectedValue: Aggregiert (Team)
formElementOperator: NOT_EQUALS formElementOperator: CONTAINS
- nodeType: LEAF - nodeType: LEAF
formElementConditionType: SHOW formElementConditionType: SHOW
sourceFormElementReference: sens_auswertung sourceFormElementReference: sens_luv
formElementExpectedValue: Funktionen vorhanden formElementExpectedValue: Aggregiert (Abteilung)
formElementOperator: EQUALS formElementOperator: CONTAINS
- nodeType: GROUP - nodeType: LEAF
groupOperator: OR formElementConditionType: SHOW
conditions: sourceFormElementReference: sens_luv
- nodeType: LEAF formElementExpectedValue: Individuell/vergleichend
formElementConditionType: SHOW formElementOperator: CONTAINS
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: options:
- value: '[]' - value: '[]'
label: Verarbeitungsvorgang-ID label: Verarbeitungs-ID
processingPurpose: SYSTEM_OPERATION processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL employeeDataCategory: NON_CRITICAL
- value: '[]' - value: '[]'
@@ -204,13 +222,13 @@ formElementSubSections:
processingPurpose: SYSTEM_OPERATION processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL employeeDataCategory: NON_CRITICAL
- value: '[]' - value: '[]'
label: Systemfunktion/Verarbeitungsform label: Verarbeitungsform
processingPurpose: SYSTEM_OPERATION processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL employeeDataCategory: NON_CRITICAL
- value: '[]' - value: '[]'
label: Kurzbeschreibung label: Verarbeitungszweck
processingPurpose: SYSTEM_OPERATION processingPurpose: DATA_ANALYSIS
employeeDataCategory: NON_CRITICAL employeeDataCategory: REVIEW_REQUIRED
- value: '[]' - value: '[]'
label: Datenkategorien label: Datenkategorien
processingPurpose: DATA_ANALYSIS processingPurpose: DATA_ANALYSIS
@@ -223,14 +241,6 @@ formElementSubSections:
label: Betroffene Mitarbeiter label: Betroffene Mitarbeiter
processingPurpose: DATA_ANALYSIS processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE employeeDataCategory: SENSITIVE
- value: '[]'
label: Allgemeiner Zweck
processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED
- value: '[]'
label: Häufigkeit/Anlass
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL
- value: '[]' - value: '[]'
label: Rollen-Sichtbarkeit (grob) label: Rollen-Sichtbarkeit (grob)
processingPurpose: DATA_ANALYSIS processingPurpose: DATA_ANALYSIS
@@ -253,7 +263,7 @@ formElementSubSections:
formElementExpectedValue: Für Administratoren formElementExpectedValue: Für Administratoren
formElementOperator: NOT_EQUALS formElementOperator: NOT_EQUALS
- value: '[]' - value: '[]'
label: Export/Weitergabe (Ja/Nein + Ziel) label: Export/Weitergabe
processingPurpose: DATA_ANALYSIS processingPurpose: DATA_ANALYSIS
employeeDataCategory: REVIEW_REQUIRED employeeDataCategory: REVIEW_REQUIRED
visibilityConditions: visibilityConditions:
@@ -262,14 +272,85 @@ formElementSubSections:
- nodeType: LEAF - nodeType: LEAF
formElementConditionType: SHOW formElementConditionType: SHOW
sourceFormElementReference: sens_schnittstellen_export sourceFormElementReference: sens_schnittstellen_export
formElementExpectedValue: Nein formElementExpectedValue: Exporte möglich
formElementOperator: NOT_CONTAINS formElementOperator: CONTAINS
- value: '[]' - value: '[]'
label: Leistungs-/Verhaltenskontrolle beabsichtigt? label: Leistungs-/Verhaltenskontrolle beabsichtigt?
processingPurpose: DATA_ANALYSIS processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE employeeDataCategory: SENSITIVE
columnConfig: columnConfig:
isCheckbox: true isCheckbox: true
- title: Rollen-Sichtbarkeit (Umfassende Darstellung)
formElements:
- reference: rollen_sichtbarkeit_umfassend_tabelle
title: Welche Rollen können welche Verarbeitungsvorgänge sehen? (Umfassende Darstellung)
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: '[]'
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 - title: Angaben zur Leistungs-/Verhaltenskontrolle
formElements: formElements:
- reference: luv_details_tabelle - reference: luv_details_tabelle
@@ -279,7 +360,7 @@ formElementSubSections:
tableRowPreset: tableRowPreset:
sourceTableReference: umfassende_datenverarbeitung_tabelle sourceTableReference: umfassende_datenverarbeitung_tabelle
filterCondition: filterCondition:
sourceColumnIndex: 11 sourceColumnIndex: 9
expectedValue: 'true' expectedValue: 'true'
operator: EQUALS operator: EQUALS
columnMappings: columnMappings:
@@ -287,39 +368,26 @@ formElementSubSections:
targetColumnIndex: 0 targetColumnIndex: 0
canAddRows: false canAddRows: false
visibilityConditions: visibilityConditions:
operator: AND operator: OR
conditions: conditions:
- nodeType: LEAF - nodeType: LEAF
formElementConditionType: SHOW formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit sourceFormElementReference: sens_luv
formElementExpectedValue: Für Administratoren formElementExpectedValue: Aggregiert (Team)
formElementOperator: NOT_EQUALS formElementOperator: CONTAINS
- nodeType: LEAF - nodeType: LEAF
formElementConditionType: SHOW formElementConditionType: SHOW
sourceFormElementReference: sens_auswertung sourceFormElementReference: sens_luv
formElementExpectedValue: Funktionen vorhanden formElementExpectedValue: Aggregiert (Abteilung)
formElementOperator: EQUALS formElementOperator: CONTAINS
- nodeType: GROUP - nodeType: LEAF
groupOperator: OR formElementConditionType: SHOW
conditions: sourceFormElementReference: sens_luv
- nodeType: LEAF formElementExpectedValue: Individuell/vergleichend
formElementConditionType: SHOW formElementOperator: CONTAINS
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: options:
- value: '[]' - value: '[]'
label: Verarbeitungsvorgang-ID label: Verarbeitungs-ID
processingPurpose: SYSTEM_OPERATION processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL employeeDataCategory: NON_CRITICAL
columnConfig: columnConfig:
@@ -327,7 +395,11 @@ formElementSubSections:
sourceColumnIndex: 0 sourceColumnIndex: 0
isReadOnly: true isReadOnly: true
- value: '[]' - value: '[]'
label: Konkreter Kontrollzweck label: Kontrollzweck
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
- value: '[]'
label: Berechtigtes Kontrollinteresse
processingPurpose: DATA_ANALYSIS processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE employeeDataCategory: SENSITIVE
- value: '[]' - value: '[]'
@@ -339,9 +411,9 @@ formElementSubSections:
sourceColumnIndex: 0 sourceColumnIndex: 0
isMultipleAllowed: true isMultipleAllowed: true
- value: '[]' - value: '[]'
label: Entscheidungswirkung label: Häufigkeit/Anlass
processingPurpose: DATA_ANALYSIS processingPurpose: SYSTEM_OPERATION
employeeDataCategory: SENSITIVE employeeDataCategory: NON_CRITICAL
- value: '[]' - value: '[]'
label: Granularität/Bezugsebene label: Granularität/Bezugsebene
processingPurpose: DATA_ANALYSIS processingPurpose: DATA_ANALYSIS
@@ -351,67 +423,23 @@ formElementSubSections:
sourceColumnIndex: 0 sourceColumnIndex: 0
isMultipleAllowed: true isMultipleAllowed: true
- value: '[]' - value: '[]'
label: Drilldown bis Person möglich? label: Drilldown auf Individualebene
processingPurpose: DATA_ANALYSIS processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE employeeDataCategory: SENSITIVE
- value: '[]' columnConfig:
label: Ranking/Scoring isCheckbox: true
processingPurpose: DATA_ANALYSIS rowVisibilityCondition:
employeeDataCategory: SENSITIVE sourceColumnIndex: 5
visibilityConditions: expectedValues:
operator: OR - "Aggregiert (Team)"
conditions: - "Aggregiert (Abteilung)"
- nodeType: LEAF operator: CONTAINS
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: '[]' - value: '[]'
label: Mindestgruppe/Schwelle label: Mindestgruppe/Schwelle
processingPurpose: DATA_ANALYSIS processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE employeeDataCategory: SENSITIVE
- value: '[]'
label: Automatisierte Alerts/Entscheidungen
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
visibilityConditions: visibilityConditions:
operator: AND operator: OR
conditions:
- nodeType: LEAF
formElementConditionType: SHOW
sourceFormElementReference: sens_alarme
formElementExpectedValue: Nein
formElementOperator: NOT_CONTAINS
- value: '[]'
label: Schutzmaßnahmen/Governance
processingPurpose: SYSTEM_OPERATION
employeeDataCategory: REVIEW_REQUIRED
- 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: conditions:
- nodeType: LEAF - nodeType: LEAF
formElementConditionType: SHOW formElementConditionType: SHOW
@@ -423,14 +451,69 @@ formElementSubSections:
sourceFormElementReference: sens_luv sourceFormElementReference: sens_luv
formElementExpectedValue: Aggregiert (Abteilung) formElementExpectedValue: Aggregiert (Abteilung)
formElementOperator: CONTAINS formElementOperator: CONTAINS
- value: '[]'
label: Automatisierte Alerts
processingPurpose: DATA_ANALYSIS
employeeDataCategory: SENSITIVE
visibilityConditions:
operator: AND
conditions:
- nodeType: LEAF - nodeType: LEAF
formElementConditionType: SHOW formElementConditionType: SHOW
sourceFormElementReference: sens_luv sourceFormElementReference: sens_alarme
formElementExpectedValue: Individuell/vergleichend formElementExpectedValue: Nein
formElementOperator: CONTAINS formElementOperator: NOT_CONTAINS
- value: '[]'
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
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: options:
- value: '[]' - value: '[]'
label: Verarbeitungsvorgang-ID label: Verarbeitungs-ID
processingPurpose: SYSTEM_OPERATION processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL employeeDataCategory: NON_CRITICAL
columnConfig: columnConfig:
@@ -468,39 +551,26 @@ formElementSubSections:
description: '' description: ''
type: TABLE type: TABLE
visibilityConditions: visibilityConditions:
operator: AND operator: OR
conditions: conditions:
- nodeType: LEAF - nodeType: LEAF
formElementConditionType: SHOW formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit sourceFormElementReference: sens_luv
formElementExpectedValue: Für Administratoren formElementExpectedValue: Aggregiert (Team)
formElementOperator: NOT_EQUALS formElementOperator: CONTAINS
- nodeType: LEAF - nodeType: LEAF
formElementConditionType: SHOW formElementConditionType: SHOW
sourceFormElementReference: sens_auswertung sourceFormElementReference: sens_luv
formElementExpectedValue: Funktionen vorhanden formElementExpectedValue: Aggregiert (Abteilung)
formElementOperator: EQUALS formElementOperator: CONTAINS
- nodeType: GROUP - nodeType: LEAF
groupOperator: OR formElementConditionType: SHOW
conditions: sourceFormElementReference: sens_luv
- nodeType: LEAF formElementExpectedValue: Individuell/vergleichend
formElementConditionType: SHOW formElementOperator: CONTAINS
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: options:
- value: '[]' - value: '[]'
label: Verarbeitungsvorgang-ID label: Verarbeitungs-ID
processingPurpose: SYSTEM_OPERATION processingPurpose: SYSTEM_OPERATION
employeeDataCategory: NON_CRITICAL employeeDataCategory: NON_CRITICAL
columnConfig: columnConfig:
@@ -532,4 +602,4 @@ formElementSubSections:
- value: '[]' - value: '[]'
label: Bedingungen label: Bedingungen
processingPurpose: SYSTEM_OPERATION processingPurpose: SYSTEM_OPERATION
employeeDataCategory: REVIEW_REQUIRED employeeDataCategory: REVIEW_REQUIRED

View File

@@ -212,16 +212,6 @@ formElementSubSections:
sourceFormElementReference: loeschkonzept_hinterlegen sourceFormElementReference: loeschkonzept_hinterlegen
formElementExpectedValue: 'true' formElementExpectedValue: 'true'
formElementOperator: EQUALS 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 - nodeType: GROUP
groupOperator: OR groupOperator: OR
conditions: conditions:

View File

@@ -98,36 +98,23 @@ formElementSubSections:
description: '' description: ''
type: TABLE type: TABLE
visibilityConditions: visibilityConditions:
operator: AND operator: OR
conditions: conditions:
- nodeType: LEAF - nodeType: LEAF
formElementConditionType: SHOW formElementConditionType: SHOW
sourceFormElementReference: sens_sichtbarkeit sourceFormElementReference: sens_luv
formElementExpectedValue: Für Administratoren formElementExpectedValue: Aggregiert (Team)
formElementOperator: NOT_EQUALS formElementOperator: CONTAINS
- nodeType: LEAF - nodeType: LEAF
formElementConditionType: SHOW formElementConditionType: SHOW
sourceFormElementReference: sens_auswertung sourceFormElementReference: sens_luv
formElementExpectedValue: Funktionen vorhanden formElementExpectedValue: Aggregiert (Abteilung)
formElementOperator: EQUALS formElementOperator: CONTAINS
- nodeType: GROUP - nodeType: LEAF
groupOperator: OR formElementConditionType: SHOW
conditions: sourceFormElementReference: sens_luv
- nodeType: LEAF formElementExpectedValue: Individuell/vergleichend
formElementConditionType: SHOW formElementOperator: CONTAINS
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: options:
- value: '[]' - value: '[]'
label: Schnittstellen-ID label: Schnittstellen-ID

View File

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

View File

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

View File

@@ -3,20 +3,40 @@
</template> </template>
<script setup lang="ts"> <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<{ const props = defineProps<{
formOptions: FormOptionDto[] formOptions: FormOptionDto[]
allFormElements?: FormElementDto[]
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:formOptions', value: FormOptionDto[]): void (e: 'update:formOptions', value: FormOptionDto[]): void
}>() }>()
// Map options to items format expected by UCheckboxGroup const { isFormOptionVisible } = useFormElementVisibility()
const items = computed(() => props.formOptions.map((option) => ({ label: option.label, value: option.label })))
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({ const modelValue = computed({
get: () => props.formOptions.filter((option) => option.value === 'true').map((option) => option.label), get: () => props.formOptions.filter((option) => option.value === 'true').map((option) => option.label),
set: (selectedLabels: string[]) => { set: (selectedLabels: string[]) => {

View File

@@ -3,19 +3,41 @@
</template> </template>
<script setup lang="ts"> <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<{ const props = defineProps<{
label?: string label?: string
formOptions: FormOptionDto[] formOptions: FormOptionDto[]
allFormElements?: FormElementDto[]
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:formOptions', value: FormOptionDto[]): void (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 // 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({ const modelValue = computed({
get: () => props.formOptions.find((option) => option.value === 'true')?.label, get: () => props.formOptions.find((option) => option.value === 'true')?.label,

View File

@@ -22,6 +22,8 @@
:disabled="disabled" :disabled="disabled"
:can-modify-rows="canModifyRows" :can-modify-rows="canModifyRows"
:get-column-options="getColumnOptions" :get-column-options="getColumnOptions"
:read-only-column-indices="readOnlyColumnIndices"
:is-cell-visible="isCellVisible"
@update:cell="updateCell" @update:cell="updateCell"
@update:cell-value="updateCellValue" @update:cell-value="updateCellValue"
@update:checkbox-cell="updateCheckboxCell" @update:checkbox-cell="updateCheckboxCell"
@@ -60,6 +62,8 @@
:disabled="disabled" :disabled="disabled"
:can-modify-rows="canModifyRows" :can-modify-rows="canModifyRows"
:get-column-options="getColumnOptions" :get-column-options="getColumnOptions"
:read-only-column-indices="readOnlyColumnIndices"
:is-cell-visible="isCellVisible"
add-row-button-class="mt-4" add-row-button-class="mt-4"
@update:cell="updateCell" @update:cell="updateCell"
@update:cell-value="updateCellValue" @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[]>(() => const dataColumns = computed<DataColumn[]>(() =>
visibleColumns.value.map(({ originalIndex }) => ({ visibleColumns.value.map(({ originalIndex }) => ({
key: `col_${originalIndex}`, key: `col_${originalIndex}`,
@@ -179,23 +221,16 @@ const tableData = computed<TableRowData[]>(() => {
if (props.formOptions.length === 0) return [] if (props.formOptions.length === 0) return []
const columnData: CellValue[][] = props.formOptions.map((option, colIndex) => { const columnData: CellValue[][] = props.formOptions.map((option, colIndex) => {
try { const parsed = parseColumnValues(option.value)
const parsed = JSON.parse(option.value || '[]')
if (!Array.isArray(parsed)) return []
// For multi-select columns, each cell value is already an array // Normalize cell values based on column type
// For checkbox columns, each cell value is a boolean if (isColumnMultipleAllowed(colIndex)) {
// For single-select columns, each cell value is a string return parsed.map((val) => (Array.isArray(val) ? val : []))
if (isColumnMultipleAllowed(colIndex)) {
return parsed.map((val: CellValue) => (Array.isArray(val) ? val : []))
}
if (isColumnCheckbox(colIndex)) {
return parsed.map((val: CellValue) => val === true)
}
return parsed
} catch {
return []
} }
if (isColumnCheckbox(colIndex)) {
return parsed.map((val) => val === true)
}
return parsed
}) })
const rowCount = Math.max(...columnData.map((col) => col.length), 0) 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) => { const updatedOptions = props.formOptions.map((option, index) => {
if (index !== colIndex) return option if (index !== colIndex) return option
let columnValues: CellValue[] const columnValues = parseColumnValues(option.value)
try {
columnValues = JSON.parse(option.value || '[]')
if (!Array.isArray(columnValues)) columnValues = []
} catch {
columnValues = []
}
while (columnValues.length <= rowIndex) { while (columnValues.length <= rowIndex) {
columnValues.push('') columnValues.push('')
} }
@@ -289,14 +317,7 @@ function updateCellValue(rowIndex: number, _columnKey: string, colIndex: number,
const updatedOptions = props.formOptions.map((option, index) => { const updatedOptions = props.formOptions.map((option, index) => {
if (index !== colIndex) return option if (index !== colIndex) return option
let columnValues: CellValue[] const columnValues = parseColumnValues(option.value)
try {
columnValues = JSON.parse(option.value || '[]')
if (!Array.isArray(columnValues)) columnValues = []
} catch {
columnValues = []
}
const isMultiple = isColumnMultipleAllowed(colIndex) const isMultiple = isColumnMultipleAllowed(colIndex)
while (columnValues.length <= rowIndex) { while (columnValues.length <= rowIndex) {
columnValues.push(isMultiple ? [] : '') columnValues.push(isMultiple ? [] : '')
@@ -313,14 +334,7 @@ function updateCheckboxCell(rowIndex: number, colIndex: number, value: boolean)
const updatedOptions = props.formOptions.map((option, index) => { const updatedOptions = props.formOptions.map((option, index) => {
if (index !== colIndex) return option if (index !== colIndex) return option
let columnValues: CellValue[] const columnValues = parseColumnValues(option.value)
try {
columnValues = JSON.parse(option.value || '[]')
if (!Array.isArray(columnValues)) columnValues = []
} catch {
columnValues = []
}
while (columnValues.length <= rowIndex) { while (columnValues.length <= rowIndex) {
columnValues.push(false) columnValues.push(false)
} }
@@ -334,24 +348,18 @@ function updateCheckboxCell(rowIndex: number, colIndex: number, value: boolean)
function addRow() { function addRow() {
const updatedOptions = props.formOptions.map((option, colIndex) => { const updatedOptions = props.formOptions.map((option, colIndex) => {
let columnValues: CellValue[] const columnValues = parseColumnValues(option.value)
try {
columnValues = JSON.parse(option.value || '[]')
if (!Array.isArray(columnValues)) columnValues = []
} catch {
columnValues = []
}
// For multi-select columns, initialize with empty array // Determine initial value based on column type
// For checkbox columns, initialize with false let initialValue: CellValue = ''
// Otherwise empty string if (readOnlyColumnIndices.value.has(colIndex)) {
let emptyValue: CellValue = '' initialValue = isColumnCheckbox(colIndex) ? false : (option.columnConfig?.readOnlyDefaultValue ?? '')
if (isColumnMultipleAllowed(colIndex)) { } else if (isColumnMultipleAllowed(colIndex)) {
emptyValue = [] initialValue = []
} else if (isColumnCheckbox(colIndex)) { } else if (isColumnCheckbox(colIndex)) {
emptyValue = false initialValue = false
} }
columnValues.push(emptyValue) columnValues.push(initialValue)
return { ...option, value: JSON.stringify(columnValues) } return { ...option, value: JSON.stringify(columnValues) }
}) })
@@ -361,19 +369,44 @@ function addRow() {
function removeRow(rowIndex: number) { function removeRow(rowIndex: number) {
const updatedOptions = props.formOptions.map((option) => { const updatedOptions = props.formOptions.map((option) => {
let columnValues: CellValue[] const columnValues = parseColumnValues(option.value)
try {
columnValues = JSON.parse(option.value || '[]')
if (!Array.isArray(columnValues)) columnValues = []
} catch {
columnValues = []
}
columnValues.splice(rowIndex, 1) columnValues.splice(rowIndex, 1)
return { ...option, value: JSON.stringify(columnValues) } return { ...option, value: JSON.stringify(columnValues) }
}) })
emit('update:formOptions', updatedOptions) 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> </script>

View File

@@ -2,54 +2,64 @@
<div> <div>
<UTable :data="tableData" :columns="tableColumns" class="w-full" :ui="{ td: 'p-2' }"> <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"> <template v-for="col in dataColumns" :key="col.key" #[`${col.key}-cell`]="slotProps">
<!-- Column with cross-reference --> <span
<USelectMenu v-if="
v-if="hasColumnReference(col.colIndex)" props.isCellVisible &&
:model-value="getCellValueForSelect(slotProps.row as TableRow<TableRowData>, col.key, col.colIndex)" !props.isCellVisible(col.colIndex, (slotProps.row as TableRow<TableRowData>).original)
:items="getColumnOptions(col.colIndex, (slotProps.row as TableRow<TableRowData>).original)"
:disabled="disabled"
:placeholder="$t('applicationForms.formElements.table.selectValue')"
:multiple="isColumnMultipleAllowed(col.colIndex)"
class="w-full min-w-32"
@update:model-value="
(val: string | string[]) =>
$emit('update:cellValue', (slotProps.row as TableRow<TableRowData>).index, col.key, col.colIndex, val)
" "
/> >
<!-- Read-only column --> -
<span v-else-if="isColumnReadOnly(col.colIndex)" class="text-muted px-2 py-1">
{{ formatCellDisplay(slotProps.row as any, col.key, col.colIndex) }}
</span> </span>
<!-- Checkbox column --> <template v-else>
<div v-else-if="isColumnCheckbox(col.colIndex)" class="flex justify-center"> <!-- Column with cross-reference -->
<UCheckbox <USelectMenu
:model-value="getCellValueForCheckbox(slotProps.row as TableRow<TableRowData>, col.key)" v-if="hasColumnReference(col.colIndex)"
:model-value="getCellValueForSelect(slotProps.row as TableRow<TableRowData>, col.key, col.colIndex)"
:items="getColumnOptions(col.colIndex, (slotProps.row as TableRow<TableRowData>).original)"
:disabled="disabled" :disabled="disabled"
:placeholder="$t('applicationForms.formElements.table.selectValue')"
:multiple="isColumnMultipleAllowed(col.colIndex)"
class="w-full min-w-32"
@update:model-value=" @update:model-value="
(val: boolean | 'indeterminate') => (val: string | string[]) =>
$emit( $emit('update:cellValue', (slotProps.row as TableRow<TableRowData>).index, col.key, col.colIndex, val)
'update:checkboxCell',
(slotProps.row as TableRow<TableRowData>).index,
col.colIndex,
val === true
)
" "
/> />
</div> <!-- Read-only column -->
<!-- Regular text input with auto-resizing textarea --> <span v-else-if="isColumnReadOnly(col.colIndex)" class="text-muted px-2 py-1">
<UTextarea {{ formatCellDisplay(slotProps.row as any, col.key, col.colIndex) }}
v-else </span>
:model-value="getCellValue(slotProps.row as TableRow<TableRowData>, col.key)" <!-- Checkbox column -->
:disabled="disabled" <div v-else-if="isColumnCheckbox(col.colIndex)" class="flex justify-center">
:rows="1" <UCheckbox
autoresize :model-value="getCellValueForCheckbox(slotProps.row as TableRow<TableRowData>, col.key)"
:maxrows="0" :disabled="disabled"
class="w-full min-w-32" @update:model-value="
@update:model-value=" (val: boolean | 'indeterminate') =>
(val: string | number) => $emit(
$emit('update:cell', (slotProps.row as TableRow<TableRowData>).index, col.key, String(val)) 'update:checkboxCell',
" (slotProps.row as TableRow<TableRowData>).index,
/> col.colIndex,
val === true
)
"
/>
</div>
<!-- Regular text input with auto-resizing textarea -->
<UTextarea
v-else
:model-value="getCellValue(slotProps.row as TableRow<TableRowData>, col.key)"
:disabled="disabled"
:rows="1"
autoresize
:maxrows="0"
class="w-full min-w-32"
@update:model-value="
(val: string | number) =>
$emit('update:cell', (slotProps.row as TableRow<TableRowData>).index, col.key, String(val))
"
/>
</template>
</template> </template>
<template v-if="canModifyRows" #actions-cell="{ row }"> <template v-if="canModifyRows" #actions-cell="{ row }">
<UButton <UButton
@@ -102,6 +112,8 @@ const props = defineProps<{
canModifyRows: boolean canModifyRows: boolean
addRowButtonClass?: string addRowButtonClass?: string
getColumnOptions: (colIndex: number, currentRowData?: TableRowData) => string[] getColumnOptions: (colIndex: number, currentRowData?: TableRowData) => string[]
readOnlyColumnIndices?: Set<number>
isCellVisible?: (colIndex: number, rowData: TableRowData) => boolean
}>() }>()
defineEmits<{ defineEmits<{
@@ -119,7 +131,7 @@ function hasColumnReference(colIndex: number): boolean {
function isColumnReadOnly(colIndex: number): boolean { function isColumnReadOnly(colIndex: number): boolean {
const option = props.formOptions[colIndex] 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 { function isColumnMultipleAllowed(colIndex: number): boolean {

View File

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

View File

@@ -6,7 +6,7 @@ import type {
FormOptionDto, FormOptionDto,
FormElementType FormElementType
} from '~~/.api-client' } 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 // Element types that use true/false selection model
const SELECTION_TYPES: FormElementType[] = ['SELECT', 'RADIOBUTTON', 'CHECKBOX', 'SWITCH'] const SELECTION_TYPES: FormElementType[] = ['SELECT', 'RADIOBUTTON', 'CHECKBOX', 'SWITCH']

View File

@@ -13,6 +13,8 @@
"lint": "eslint .", "lint": "eslint .",
"lint:fix": "eslint . --fix", "lint:fix": "eslint . --fix",
"test": "vitest run", "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", "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" "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/eslint": "1.1.0",
"@nuxt/test-utils": "^3.21.0", "@nuxt/test-utils": "^3.21.0",
"@openapitools/openapi-generator-cli": "2.16.3", "@openapitools/openapi-generator-cli": "2.16.3",
"@pinia/testing": "^0.1.7",
"@vitest/coverage-v8": "4.0.16",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"eslint": "9.20.1", "eslint": "9.20.1",
"happy-dom": "^20.0.11", "happy-dom": "^20.0.11",

View File

@@ -60,6 +60,12 @@ importers:
'@openapitools/openapi-generator-cli': '@openapitools/openapi-generator-cli':
specifier: 2.16.3 specifier: 2.16.3
version: 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': '@vue/test-utils':
specifier: ^2.4.6 specifier: ^2.4.6
version: 2.4.6 version: 2.4.6
@@ -228,6 +234,10 @@ packages:
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
engines: {node: '>=6.9.0'} 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': '@bomb.sh/tab@0.0.10':
resolution: {integrity: sha512-6ALS2rh/4LKn0Yxwm35V6LcgQuSiECHbqQo7+9g4rkgGyXZ0siOc8K+IuWIq/4u0Zkv2mevP9QSqgKhGIvLJMw==} resolution: {integrity: sha512-6ALS2rh/4LKn0Yxwm35V6LcgQuSiECHbqQo7+9g4rkgGyXZ0siOc8K+IuWIq/4u0Zkv2mevP9QSqgKhGIvLJMw==}
hasBin: true hasBin: true
@@ -1541,6 +1551,11 @@ packages:
peerDependencies: peerDependencies:
pinia: ^3.0.3 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': '@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -2362,6 +2377,15 @@ packages:
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0
vue: ^3.2.25 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': '@vitest/expect@4.0.16':
resolution: {integrity: sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==} resolution: {integrity: sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==}
@@ -2686,6 +2710,9 @@ packages:
resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==}
engines: {node: '>=4'} engines: {node: '>=4'}
ast-v8-to-istanbul@0.3.12:
resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==}
ast-walker-scope@0.6.2: ast-walker-scope@0.6.2:
resolution: {integrity: sha512-1UWOyC50xI3QZkRuDj6PqDtpm1oHWtYs+NQGwqL/2R11eN3Q81PHAHPM0SWW3BNQm53UDwS//Jv8L4CCVLM1bQ==} resolution: {integrity: sha512-1UWOyC50xI3QZkRuDj6PqDtpm1oHWtYs+NQGwqL/2R11eN3Q81PHAHPM0SWW3BNQm53UDwS//Jv8L4CCVLM1bQ==}
engines: {node: '>=16.14.0'} engines: {node: '>=16.14.0'}
@@ -3796,6 +3823,9 @@ packages:
resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==}
engines: {node: ^16.14.0 || >=18.0.0} engines: {node: ^16.14.0 || >=18.0.0}
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
html-to-text@9.0.5: html-to-text@9.0.5:
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -3989,6 +4019,22 @@ packages:
isomorphic.js@0.2.5: isomorphic.js@0.2.5:
resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} 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: iterare@1.2.1:
resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -4012,6 +4058,9 @@ packages:
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
engines: {node: '>=14'} engines: {node: '>=14'}
js-tokens@10.0.0:
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
js-tokens@4.0.0: js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -4257,6 +4306,10 @@ packages:
magicast@0.5.1: magicast@0.5.1:
resolution: {integrity: sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==} 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: markdown-it@14.1.0:
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
hasBin: true hasBin: true
@@ -6340,6 +6393,8 @@ snapshots:
'@babel/helper-string-parser': 7.27.1 '@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5 '@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)': '@bomb.sh/tab@0.0.10(cac@6.7.14)(citty@0.1.6)':
optionalDependencies: optionalDependencies:
cac: 6.7.14 cac: 6.7.14
@@ -7928,6 +7983,14 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- magicast - 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': '@pkgjs/parseargs@0.11.0':
optional: true 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) 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) 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': '@vitest/expect@4.0.16':
dependencies: dependencies:
'@standard-schema/spec': 1.1.0 '@standard-schema/spec': 1.1.0
@@ -9125,6 +9205,12 @@ snapshots:
dependencies: dependencies:
tslib: 2.8.1 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: ast-walker-scope@0.6.2:
dependencies: dependencies:
'@babel/parser': 7.28.5 '@babel/parser': 7.28.5
@@ -10347,6 +10433,8 @@ snapshots:
dependencies: dependencies:
lru-cache: 10.4.3 lru-cache: 10.4.3
html-escaper@2.0.2: {}
html-to-text@9.0.5: html-to-text@9.0.5:
dependencies: dependencies:
'@selderee/plugin-htmlparser2': 0.11.0 '@selderee/plugin-htmlparser2': 0.11.0
@@ -10536,6 +10624,27 @@ snapshots:
isomorphic.js@0.2.5: {} 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: {} iterare@1.2.1: {}
jackspeak@3.4.3: jackspeak@3.4.3:
@@ -10558,6 +10667,8 @@ snapshots:
js-cookie@3.0.5: {} js-cookie@3.0.5: {}
js-tokens@10.0.0: {}
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
js-tokens@9.0.1: {} js-tokens@9.0.1: {}
@@ -10787,6 +10898,10 @@ snapshots:
'@babel/types': 7.28.5 '@babel/types': 7.28.5
source-map-js: 1.2.1 source-map-js: 1.2.1
make-dir@4.0.0:
dependencies:
semver: 7.7.3
markdown-it@14.1.0: markdown-it@14.1.0:
dependencies: dependencies:
argparse: 2.0.1 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 { describe, it, expect } from 'vitest'
import { compareApplicationFormValues, groupChangesBySection } from '../../app/utils/formDiff' import { compareApplicationFormValues, groupChangesBySection } from '../../../app/utils/formSnapshotComparison'
import type { import type {
ApplicationFormDto, ApplicationFormDto,
ApplicationFormSnapshotDto, ApplicationFormSnapshotDto,
@@ -11,7 +11,7 @@ import type {
FormElementSectionSnapshotDto, FormElementSectionSnapshotDto,
FormElementSubSectionSnapshotDto, FormElementSubSectionSnapshotDto,
FormElementType FormElementType
} from '../../.api-client' } from '../../../.api-client'
// Helper to create a minimal FormOptionDto // Helper to create a minimal FormOptionDto
function createOption(value: string, label: string): 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('compareApplicationFormValues', () => {
describe('TEXTFIELD element', () => { describe('TEXTFIELD element', () => {
it('should detect new answer (empty → filled)', () => { it('should detect new answer (empty → filled)', () => {
@@ -110,9 +110,9 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1) expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].elementTitle).toBe('Name') expect(diff.newAnswers[0]?.elementTitle).toBe('Name')
expect(diff.newAnswers[0].currentLabel).toBe('John Doe') expect(diff.newAnswers[0]?.currentLabel).toBe('John Doe')
expect(diff.newAnswers[0].previousLabel).toBeNull() expect(diff.newAnswers[0]?.previousLabel).toBeNull()
expect(diff.changedAnswers).toHaveLength(0) expect(diff.changedAnswers).toHaveLength(0)
expect(diff.clearedAnswers).toHaveLength(0) expect(diff.clearedAnswers).toHaveLength(0)
}) })
@@ -128,8 +128,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1) expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].previousLabel).toBe('John Doe') expect(diff.changedAnswers[0]?.previousLabel).toBe('John Doe')
expect(diff.changedAnswers[0].currentLabel).toBe('Jane Doe') expect(diff.changedAnswers[0]?.currentLabel).toBe('Jane Doe')
expect(diff.newAnswers).toHaveLength(0) expect(diff.newAnswers).toHaveLength(0)
expect(diff.clearedAnswers).toHaveLength(0) expect(diff.clearedAnswers).toHaveLength(0)
}) })
@@ -143,8 +143,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1) expect(diff.clearedAnswers).toHaveLength(1)
expect(diff.clearedAnswers[0].previousLabel).toBe('John Doe') expect(diff.clearedAnswers[0]?.previousLabel).toBe('John Doe')
expect(diff.clearedAnswers[0].currentLabel).toBeNull() expect(diff.clearedAnswers[0]?.currentLabel).toBeNull()
expect(diff.newAnswers).toHaveLength(0) expect(diff.newAnswers).toHaveLength(0)
expect(diff.changedAnswers).toHaveLength(0) expect(diff.changedAnswers).toHaveLength(0)
}) })
@@ -179,7 +179,7 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1) 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', () => { it('should detect changed answer', () => {
@@ -193,8 +193,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1) expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].previousLabel).toBe('Original text') expect(diff.changedAnswers[0]?.previousLabel).toBe('Original text')
expect(diff.changedAnswers[0].currentLabel).toBe('Updated text') expect(diff.changedAnswers[0]?.currentLabel).toBe('Updated text')
}) })
it('should detect cleared answer', () => { it('should detect cleared answer', () => {
@@ -206,7 +206,7 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1) 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) const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1) 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', () => { it('should detect changed answer', () => {
@@ -238,8 +238,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1) expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].previousLabel).toBe('<p>Original</p>') expect(diff.changedAnswers[0]?.previousLabel).toBe('<p>Original</p>')
expect(diff.changedAnswers[0].currentLabel).toBe('<p>Updated</p>') expect(diff.changedAnswers[0]?.currentLabel).toBe('<p>Updated</p>')
}) })
it('should detect cleared answer', () => { it('should detect cleared answer', () => {
@@ -264,7 +264,7 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1) 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', () => { it('should detect changed answer', () => {
@@ -278,8 +278,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1) expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].previousLabel).toBe('2024-01-15') expect(diff.changedAnswers[0]?.previousLabel).toBe('2024-01-15')
expect(diff.changedAnswers[0].currentLabel).toBe('2024-02-20') expect(diff.changedAnswers[0]?.currentLabel).toBe('2024-02-20')
}) })
it('should detect cleared answer', () => { it('should detect cleared answer', () => {
@@ -314,8 +314,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1) expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].currentLabel).toBe('Medium') expect(diff.newAnswers[0]?.currentLabel).toBe('Medium')
expect(diff.newAnswers[0].previousLabel).toBeNull() expect(diff.newAnswers[0]?.previousLabel).toBeNull()
}) })
it('should detect changed answer (different option selected)', () => { it('should detect changed answer (different option selected)', () => {
@@ -337,8 +337,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1) expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].previousLabel).toBe('Low') expect(diff.changedAnswers[0]?.previousLabel).toBe('Low')
expect(diff.changedAnswers[0].currentLabel).toBe('High') expect(diff.changedAnswers[0]?.currentLabel).toBe('High')
}) })
it('should detect cleared answer (option selected → nothing selected)', () => { it('should detect cleared answer (option selected → nothing selected)', () => {
@@ -360,8 +360,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1) expect(diff.clearedAnswers).toHaveLength(1)
expect(diff.clearedAnswers[0].previousLabel).toBe('Medium') expect(diff.clearedAnswers[0]?.previousLabel).toBe('Medium')
expect(diff.clearedAnswers[0].currentLabel).toBeNull() expect(diff.clearedAnswers[0]?.currentLabel).toBeNull()
}) })
}) })
@@ -385,7 +385,7 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1) expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].currentLabel).toBe('Male') expect(diff.newAnswers[0]?.currentLabel).toBe('Male')
}) })
it('should detect changed answer', () => { it('should detect changed answer', () => {
@@ -407,8 +407,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1) expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].previousLabel).toBe('Male') expect(diff.changedAnswers[0]?.previousLabel).toBe('Male')
expect(diff.changedAnswers[0].currentLabel).toBe('Female') expect(diff.changedAnswers[0]?.currentLabel).toBe('Female')
}) })
it('should detect cleared answer', () => { it('should detect cleared answer', () => {
@@ -430,7 +430,7 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1) 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) const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1) 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)', () => { it('should detect new answer (multiple checkboxes selected)', () => {
@@ -476,7 +476,7 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1) 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)', () => { it('should detect changed answer (different checkboxes selected)', () => {
@@ -498,8 +498,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1) expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].previousLabel).toBe('Feature A') expect(diff.changedAnswers[0]?.previousLabel).toBe('Feature A')
expect(diff.changedAnswers[0].currentLabel).toBe('Feature B, Feature C') expect(diff.changedAnswers[0]?.currentLabel).toBe('Feature B, Feature C')
}) })
it('should detect cleared answer (all checkboxes deselected)', () => { it('should detect cleared answer (all checkboxes deselected)', () => {
@@ -521,7 +521,7 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1) 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) const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1) 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)', () => { it('should detect cleared answer (switch turned off)', () => {
@@ -551,7 +551,7 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1) 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) const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1) expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].currentLabel).toBe('2 Zeilen') expect(diff.newAnswers[0]?.currentLabel).toBe('2 Zeilen')
expect(diff.newAnswers[0].tableDiff).toBeDefined() expect(diff.newAnswers[0]?.tableDiff).toBeDefined()
expect(diff.newAnswers[0].tableDiff!.addedCount).toBe(2) expect(diff.newAnswers[0]?.tableDiff!.addedCount).toBe(2)
expect(diff.newAnswers[0].tableDiff!.removedCount).toBe(0) expect(diff.newAnswers[0]?.tableDiff!.removedCount).toBe(0)
expect(diff.newAnswers[0].tableDiff!.modifiedCount).toBe(0) expect(diff.newAnswers[0]?.tableDiff!.modifiedCount).toBe(0)
}) })
it('should detect rows removed', () => { it('should detect rows removed', () => {
@@ -594,10 +594,10 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1) expect(diff.clearedAnswers).toHaveLength(1)
expect(diff.clearedAnswers[0].previousLabel).toBe('2 Zeilen') expect(diff.clearedAnswers[0]?.previousLabel).toBe('2 Zeilen')
expect(diff.clearedAnswers[0].tableDiff).toBeDefined() expect(diff.clearedAnswers[0]?.tableDiff).toBeDefined()
expect(diff.clearedAnswers[0].tableDiff!.removedCount).toBe(2) expect(diff.clearedAnswers[0]?.tableDiff!.removedCount).toBe(2)
expect(diff.clearedAnswers[0].tableDiff!.addedCount).toBe(0) expect(diff.clearedAnswers[0]?.tableDiff!.addedCount).toBe(0)
}) })
it('should detect modified rows when row count changes', () => { it('should detect modified rows when row count changes', () => {
@@ -619,12 +619,12 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1) expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].tableDiff).toBeDefined() expect(diff.changedAnswers[0]?.tableDiff).toBeDefined()
expect(diff.changedAnswers[0].tableDiff!.addedCount).toBe(1) expect(diff.changedAnswers[0]?.tableDiff!.addedCount).toBe(1)
expect(diff.changedAnswers[0].tableDiff!.modifiedCount).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]?.changeType).toBe('modified')
expect(diff.changedAnswers[0].tableDiff!.rows[1].previousValues['Name']).toBe('Jane') 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!.rows[1]?.currentValues['Name']).toBe('Janet')
}) })
it('should detect modified rows when row count is the same', () => { it('should detect modified rows when row count is the same', () => {
@@ -644,12 +644,12 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1) expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].tableDiff).toBeDefined() expect(diff.changedAnswers[0]?.tableDiff).toBeDefined()
expect(diff.changedAnswers[0].tableDiff!.addedCount).toBe(0) expect(diff.changedAnswers[0]?.tableDiff!.addedCount).toBe(0)
expect(diff.changedAnswers[0].tableDiff!.modifiedCount).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]?.changeType).toBe('modified')
expect(diff.changedAnswers[0].tableDiff!.rows[1].previousValues['Name']).toBe('Jane') 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!.rows[1]?.currentValues['Name']).toBe('Janet')
}) })
it('should detect mixed changes (added, removed, modified)', () => { it('should detect mixed changes (added, removed, modified)', () => {
@@ -669,12 +669,12 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1) 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 0: modified (John → John Updated, Developer → Senior Dev)
// Row 1: modified (Jane → New Person, Designer → Manager) // Row 1: modified (Jane → New Person, Designer → Manager)
// Row 2: removed (Bob, Tester) // Row 2: removed (Bob, Tester)
expect(diff.changedAnswers[0].tableDiff!.modifiedCount).toBe(2) expect(diff.changedAnswers[0]?.tableDiff!.modifiedCount).toBe(2)
expect(diff.changedAnswers[0].tableDiff!.removedCount).toBe(1) expect(diff.changedAnswers[0]?.tableDiff!.removedCount).toBe(1)
}) })
it('should handle single row correctly', () => { it('should handle single row correctly', () => {
@@ -694,7 +694,7 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1) 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', () => { it('should handle boolean values in table cells', () => {
@@ -715,8 +715,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1) expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].tableDiff!.rows[0].previousValues['Enabled']).toBe('Nein') 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]?.currentValues['Enabled']).toBe('Ja')
}) })
it('should handle array values in table cells', () => { it('should handle array values in table cells', () => {
@@ -736,8 +736,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1) expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].tableDiff!.rows[0].previousValues['Tags']).toBe('Tag1') 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]?.currentValues['Tags']).toBe('Tag1, Tag2')
}) })
}) })
@@ -751,8 +751,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1) expect(diff.clearedAnswers).toHaveLength(1)
expect(diff.clearedAnswers[0].elementTitle).toBe('Name') expect(diff.clearedAnswers[0]?.elementTitle).toBe('Name')
expect(diff.clearedAnswers[0].previousLabel).toBe('John Doe') expect(diff.clearedAnswers[0]?.previousLabel).toBe('John Doe')
}) })
it('should not report removed element if it had no value', () => { it('should not report removed element if it had no value', () => {
@@ -777,8 +777,8 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1) expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].elementTitle).toBe('Name') expect(diff.newAnswers[0]?.elementTitle).toBe('Name')
expect(diff.newAnswers[0].currentLabel).toBe('John Doe') expect(diff.newAnswers[0]?.currentLabel).toBe('John Doe')
}) })
it('should not report new element if it has no value', () => { it('should not report new element if it has no value', () => {
@@ -813,13 +813,13 @@ describe('formDiff', () => {
const diff = compareApplicationFormValues(current, version) const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1) 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).toHaveLength(1)
expect(diff.changedAnswers[0].elementTitle).toBe('Email') expect(diff.changedAnswers[0]?.elementTitle).toBe('Email')
expect(diff.clearedAnswers).toHaveLength(1) 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) const grouped = groupChangesBySection(diff)
expect(grouped).toHaveLength(1) expect(grouped).toHaveLength(1)
expect(grouped[0].sectionTitle).toBe('Section A') expect(grouped[0]?.sectionTitle).toBe('Section A')
expect(grouped[0].changes).toHaveLength(3) expect(grouped[0]?.changes).toHaveLength(3)
}) })
}) })
}) })

View File

@@ -4,18 +4,32 @@ import { defineVitestProject } from '@nuxt/test-utils/config'
export default defineConfig({ export default defineConfig({
test: { test: {
projects: [ projects: [
{
test: {
name: 'unit',
include: ['test/{e2e,unit}/*.{test,spec}.ts'],
environment: 'node'
}
},
await defineVitestProject({ await defineVitestProject({
test: { test: {
name: 'nuxt', name: 'unit',
include: ['test/nuxt/*.{test,spec}.ts'], include: ['test/unit/**/*.{test,spec}.ts'],
environment: 'nuxt' 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
}
}
}
} }
}) })
] ]