feat: Add table logic for role and permission sections

This commit is contained in:
2025-12-29 08:58:00 +01:00
parent be9d2ec9d7
commit 0490f2357e
31 changed files with 1957 additions and 390 deletions

View File

@@ -1393,14 +1393,51 @@ components:
type: string type: string
format: uuid format: uuid
nullable: true nullable: true
visibilityCondition: visibilityConditions:
$ref: "#/components/schemas/FormElementVisibilityCondition" type: array
sectionSpawnTrigger: items:
$ref: "#/components/schemas/SectionSpawnTriggerDto" $ref: "#/components/schemas/FormElementVisibilityCondition"
description: List of visibility conditions (all must be met for element to be visible - AND logic)
sectionSpawnTriggers:
type: array
items:
$ref: "#/components/schemas/SectionSpawnTriggerDto"
description: List of triggers that can spawn template sections when conditions are met
isClonable: isClonable:
type: boolean type: boolean
default: false default: false
description: If true, user can add more instances of this element description: If true, user can add more instances of this element
tableRowPreset:
$ref: "#/components/schemas/TableRowPresetDto"
TableRowPresetDto:
type: object
description: Configuration for automatically creating table rows based on source table data
properties:
sourceTableReference:
type: string
description: Reference to source table element to pull rows from
filterCondition:
$ref: "#/components/schemas/TableColumnFilterDto"
columnMappings:
type: array
items:
$ref: "#/components/schemas/TableColumnMappingDto"
canAddRows:
type: boolean
default: true
description: If true, users can manually add or remove rows. If false, rows are fully controlled by the source table.
TableColumnMappingDto:
type: object
description: Mapping between source and target columns for row presets
properties:
sourceColumnIndex:
type: integer
description: Index of source column (0-based)
targetColumnIndex:
type: integer
description: Index of target column (0-based)
FormElementSnapshotDto: FormElementSnapshotDto:
type: object type: object
@@ -1420,13 +1457,19 @@ components:
type: array type: array
items: items:
$ref: "#/components/schemas/FormOptionDto" $ref: "#/components/schemas/FormOptionDto"
visibilityCondition: visibilityConditions:
$ref: "#/components/schemas/FormElementVisibilityCondition" type: array
sectionSpawnTrigger: items:
$ref: "#/components/schemas/SectionSpawnTriggerDto" $ref: "#/components/schemas/FormElementVisibilityCondition"
sectionSpawnTriggers:
type: array
items:
$ref: "#/components/schemas/SectionSpawnTriggerDto"
isClonable: isClonable:
type: boolean type: boolean
default: false default: false
tableRowPreset:
$ref: "#/components/schemas/TableRowPresetDto"
FormOptionDto: FormOptionDto:
type: object type: object
@@ -1444,6 +1487,66 @@ components:
$ref: "#/components/schemas/ProcessingPurpose" $ref: "#/components/schemas/ProcessingPurpose"
employeeDataCategory: employeeDataCategory:
$ref: "#/components/schemas/EmployeeDataCategory" $ref: "#/components/schemas/EmployeeDataCategory"
columnConfig:
$ref: "#/components/schemas/TableColumnConfigDto"
TableColumnConfigDto:
type: object
description: Configuration for table column cross-references
properties:
sourceTableReference:
type: string
description: Reference to source table element to get values from
sourceColumnIndex:
type: integer
description: Index of source column to reference (0-based)
filterCondition:
$ref: "#/components/schemas/TableColumnFilterDto"
rowConstraint:
$ref: "#/components/schemas/TableRowConstraintDto"
isReadOnly:
type: boolean
default: false
description: If true, column values cannot be edited by user
isMultipleAllowed:
type: boolean
default: false
description: If true, allows selecting multiple values in this column
isCheckbox:
type: boolean
default: false
description: If true, renders a checkbox instead of text input
TableRowConstraintDto:
type: object
description: Configuration for row-based value constraints from another table
properties:
constraintTableReference:
type: string
description: Reference to the constraint table that defines allowed value mappings
constraintKeyColumnIndex:
type: integer
description: Column index in constraint table that matches the key (e.g., Rollen-ID)
constraintValueColumnIndex:
type: integer
description: Column index in constraint table with allowed values (e.g., Permission-ID)
currentRowKeyColumnIndex:
type: integer
description: Column index in current table row to use as the lookup key
TableColumnFilterDto:
type: object
description: Filter condition for constraining available values
properties:
sourceColumnIndex:
type: integer
description: Index of source column to check for filter condition
expectedValue:
type: string
description: Expected value to match in the source column
operator:
$ref: "#/components/schemas/VisibilityConditionOperator"
default: EQUALS
FormElementType: FormElementType:
type: string type: string

View File

@@ -11,6 +11,7 @@ import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormSnapshotD
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSectionSnapshotDto import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSectionSnapshotDto
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSnapshotDto import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSnapshotDto
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSubSectionSnapshotDto import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSubSectionSnapshotDto
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementVisibilityCondition
import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@@ -80,6 +81,7 @@ class ApplicationFormFormatService(
title = LatexEscaper.escape(element.title ?: ""), title = LatexEscaper.escape(element.title ?: ""),
description = LatexEscaper.escape(element.description ?: ""), description = LatexEscaper.escape(element.description ?: ""),
value = renderElementValue(element), value = renderElementValue(element),
isTable = element.type.name == "TABLE",
) )
}, },
) )
@@ -152,8 +154,10 @@ class ApplicationFormFormatService(
val rowCount = columnData.maxOfOrNull { col -> col.size } ?: 0 val rowCount = columnData.maxOfOrNull { col -> col.size } ?: 0
if (rowCount == 0) return "Keine Daten" if (rowCount == 0) return "Keine Daten"
val columnSpec = headers.joinToString(" | ") { "l" } // Use tabularx with Y columns (auto-wrapping) for flexible width distribution
val headerRow = headers.joinToString(" & ") // Y is defined as >{\raggedright\arraybackslash}X in the template
val columnSpec = headers.joinToString("") { "Y" }
val headerRow = headers.joinToString(" & ") { "\\textbf{$it}" }
val dataRows = val dataRows =
(0 until rowCount).map { rowIndex -> (0 until rowCount).map { rowIndex ->
columnData.joinToString(" & ") { col: List<String> -> columnData.joinToString(" & ") { col: List<String> ->
@@ -163,15 +167,15 @@ class ApplicationFormFormatService(
} }
return buildString { return buildString {
appendLine("\\begin{tabular}{$columnSpec}") appendLine("\\begin{tabularx}{\\textwidth}{$columnSpec}")
appendLine("\\hline") appendLine("\\toprule")
appendLine("$headerRow \\\\") appendLine("$headerRow \\\\")
appendLine("\\hline") appendLine("\\midrule")
dataRows.forEach { row: String -> dataRows.forEach { row: String ->
appendLine("$row \\\\") appendLine("$row \\\\")
} }
appendLine("\\hline") appendLine("\\bottomrule")
appendLine("\\end{tabular}") appendLine("\\end{tabularx}")
} }
} }
@@ -213,8 +217,17 @@ class ApplicationFormFormatService(
element: FormElementSnapshotDto, element: FormElementSnapshotDto,
formElementsByRef: Map<String, FormElementSnapshotDto>, formElementsByRef: Map<String, FormElementSnapshotDto>,
): Boolean { ): Boolean {
val condition = element.visibilityCondition ?: return true val conditions = element.visibilityConditions
if (conditions.isNullOrEmpty()) return true
// All conditions must be met (AND logic)
return conditions.all { condition -> evaluateSingleCondition(condition, formElementsByRef) }
}
private fun evaluateSingleCondition(
condition: FormElementVisibilityCondition,
formElementsByRef: Map<String, FormElementSnapshotDto>,
): Boolean {
val sourceElement = formElementsByRef[condition.sourceFormElementReference] ?: return false val sourceElement = formElementsByRef[condition.sourceFormElementReference] ?: return false
val sourceValue = getFormElementValue(sourceElement) val sourceValue = getFormElementValue(sourceElement)

View File

@@ -3,7 +3,10 @@ package com.betriebsratkanzlei.legalconsenthub.application_form.export.latex
object LatexEscaper { object LatexEscaper {
fun escape(text: String?): String { fun escape(text: String?): String {
if (text == null) return "" if (text == null) return ""
return text // First decode common HTML entities that may be present in user input
val decoded = decodeHtmlEntities(text)
// Then escape for LaTeX
return decoded
.replace("\\", "\\textbackslash{}") .replace("\\", "\\textbackslash{}")
.replace("{", "\\{") .replace("{", "\\{")
.replace("}", "\\}") .replace("}", "\\}")
@@ -16,4 +19,14 @@ object LatexEscaper {
.replace("~", "\\textasciitilde{}") .replace("~", "\\textasciitilde{}")
.replace("\n", "\\\\") .replace("\n", "\\\\")
} }
private fun decodeHtmlEntities(text: String): String =
text
.replace("&amp;", "&")
.replace("&lt;", "<")
.replace("&gt;", ">")
.replace("&quot;", "\"")
.replace("&#39;", "'")
.replace("&apos;", "'")
.replace("&nbsp;", " ")
} }

View File

@@ -28,4 +28,5 @@ data class LatexFormElement(
val title: String, val title: String,
val description: String?, val description: String?,
val value: String, val value: String,
val isTable: Boolean = false,
) )

View File

@@ -8,14 +8,14 @@ import com.betriebsratkanzlei.legalconsenthub.form_element.FormElement
import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementSection import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementSection
import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementSubSection import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementSubSection
import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementVisibilityConditionMapper import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementVisibilityConditionMapper
import com.betriebsratkanzlei.legalconsenthub.form_element.FormOption import com.betriebsratkanzlei.legalconsenthub.form_element.FormOptionMapper
import com.betriebsratkanzlei.legalconsenthub.form_element.SectionSpawnTriggerMapper import com.betriebsratkanzlei.legalconsenthub.form_element.SectionSpawnTriggerMapper
import com.betriebsratkanzlei.legalconsenthub.form_element.TableRowPresetMapper
import com.betriebsratkanzlei.legalconsenthub.user.User import com.betriebsratkanzlei.legalconsenthub.user.User
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormSnapshotDto import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormSnapshotDto
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSectionSnapshotDto import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSectionSnapshotDto
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSnapshotDto import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSnapshotDto
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSubSectionSnapshotDto import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSubSectionSnapshotDto
import com.betriebsratkanzlei.legalconsenthub_api.model.FormOptionDto
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
@@ -28,6 +28,8 @@ class ApplicationFormVersionService(
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
private val spawnTriggerMapper: SectionSpawnTriggerMapper, private val spawnTriggerMapper: SectionSpawnTriggerMapper,
private val visibilityConditionMapper: FormElementVisibilityConditionMapper, private val visibilityConditionMapper: FormElementVisibilityConditionMapper,
private val tableRowPresetMapper: TableRowPresetMapper,
private val formOptionMapper: FormOptionMapper,
) { ) {
@Transactional @Transactional
fun createVersion( fun createVersion(
@@ -123,26 +125,22 @@ class ApplicationFormVersionService(
title = element.title, title = element.title,
description = element.description, description = element.description,
type = element.type, type = element.type,
options = options = element.options.map { formOptionMapper.toFormOptionDto(it) },
element.options.map { option -> visibilityConditions =
FormOptionDto( element.visibilityConditions
value = option.value, .map {
label = option.label, visibilityConditionMapper
processingPurpose = option.processingPurpose, .toFormElementVisibilityConditionDto(it)
employeeDataCategory = option.employeeDataCategory, },
) sectionSpawnTriggers =
}, element.sectionSpawnTriggers.map {
visibilityCondition =
element.visibilityCondition?.let {
visibilityConditionMapper.toFormElementVisibilityConditionDto(
it,
)
},
sectionSpawnTrigger =
element.sectionSpawnTrigger?.let {
spawnTriggerMapper.toSectionSpawnTriggerDto(it) spawnTriggerMapper.toSectionSpawnTriggerDto(it)
}, },
isClonable = element.isClonable, isClonable = element.isClonable,
tableRowPreset =
element.tableRowPreset?.let {
tableRowPresetMapper.toTableRowPresetDto(it)
},
) )
}, },
) )
@@ -185,23 +183,23 @@ class ApplicationFormVersionService(
formElementSubSection = subsection, formElementSubSection = subsection,
options = options =
elementSnapshot.options elementSnapshot.options
.map { optionDto -> .map { formOptionMapper.toFormOption(it) }
FormOption( .toMutableList(),
value = optionDto.value, visibilityConditions =
label = optionDto.label, elementSnapshot.visibilityConditions
processingPurpose = optionDto.processingPurpose, ?.map { visibilityConditionMapper.toFormElementVisibilityCondition(it) }
employeeDataCategory = optionDto.employeeDataCategory, ?.toMutableList()
) ?: mutableListOf(),
}.toMutableList(), sectionSpawnTriggers =
visibilityCondition = elementSnapshot.sectionSpawnTriggers
elementSnapshot.visibilityCondition?.let { ?.map { spawnTriggerMapper.toSectionSpawnTrigger(it) }
visibilityConditionMapper.toFormElementVisibilityCondition(it) ?.toMutableList()
}, ?: mutableListOf(),
sectionSpawnTrigger =
elementSnapshot.sectionSpawnTrigger?.let {
spawnTriggerMapper.toSectionSpawnTrigger(it)
},
isClonable = elementSnapshot.isClonable ?: false, isClonable = elementSnapshot.isClonable ?: false,
tableRowPreset =
elementSnapshot.tableRowPreset?.let {
tableRowPresetMapper.toTableRowPreset(it)
},
) )
subsection.formElements.add(element) subsection.formElements.add(element)
} }

View File

@@ -1,6 +1,8 @@
package com.betriebsratkanzlei.legalconsenthub.form_element package com.betriebsratkanzlei.legalconsenthub.form_element
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementType import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementType
import jakarta.persistence.AttributeOverride
import jakarta.persistence.AttributeOverrides
import jakarta.persistence.CollectionTable import jakarta.persistence.CollectionTable
import jakarta.persistence.Column import jakarta.persistence.Column
import jakarta.persistence.ElementCollection import jakarta.persistence.ElementCollection
@@ -28,9 +30,25 @@ class FormElement(
@ManyToOne @ManyToOne
@JoinColumn(name = "form_element_sub_section_id", nullable = false) @JoinColumn(name = "form_element_sub_section_id", nullable = false)
var formElementSubSection: FormElementSubSection? = null, var formElementSubSection: FormElementSubSection? = null,
@Embedded @ElementCollection
var visibilityCondition: FormElementVisibilityCondition? = null, @CollectionTable(name = "visibility_conditions", joinColumns = [JoinColumn(name = "form_element_id")])
@Embedded var visibilityConditions: MutableList<FormElementVisibilityCondition> = mutableListOf(),
var sectionSpawnTrigger: SectionSpawnTrigger? = null, @ElementCollection
@CollectionTable(name = "section_spawn_triggers", joinColumns = [JoinColumn(name = "form_element_id")])
var sectionSpawnTriggers: MutableList<SectionSpawnTrigger> = mutableListOf(),
var isClonable: Boolean = false, var isClonable: Boolean = false,
@Embedded
@AttributeOverrides(
AttributeOverride(name = "sourceTableReference", column = Column(name = "row_preset_source_table_ref")),
AttributeOverride(
name = "filterCondition.sourceColumnIndex",
column = Column(name = "row_preset_filter_src_col_idx"),
),
AttributeOverride(
name = "filterCondition.expectedValue",
column = Column(name = "row_preset_filter_expected_val"),
),
AttributeOverride(name = "filterCondition.operator", column = Column(name = "row_preset_filter_operator")),
)
var tableRowPreset: TableRowPreset? = null,
) )

View File

@@ -8,6 +8,7 @@ class FormElementMapper(
private val formOptionMapper: FormOptionMapper, private val formOptionMapper: FormOptionMapper,
private val visibilityConditionMapper: FormElementVisibilityConditionMapper, private val visibilityConditionMapper: FormElementVisibilityConditionMapper,
private val spawnTriggerMapper: SectionSpawnTriggerMapper, private val spawnTriggerMapper: SectionSpawnTriggerMapper,
private val tableRowPresetMapper: TableRowPresetMapper,
) { ) {
fun toFormElementDto(formElement: FormElement): FormElementDto = fun toFormElementDto(formElement: FormElement): FormElementDto =
FormElementDto( FormElementDto(
@@ -20,15 +21,19 @@ class FormElementMapper(
formElementSubSectionId = formElementSubSectionId =
formElement.formElementSubSection?.id formElement.formElementSubSection?.id
?: throw IllegalStateException("FormElementSubSection ID must not be null!"), ?: throw IllegalStateException("FormElementSubSection ID must not be null!"),
visibilityCondition = visibilityConditions =
formElement.visibilityCondition?.let { formElement.visibilityConditions.map {
visibilityConditionMapper.toFormElementVisibilityConditionDto(it) visibilityConditionMapper.toFormElementVisibilityConditionDto(it)
}, },
sectionSpawnTrigger = sectionSpawnTriggers =
formElement.sectionSpawnTrigger?.let { formElement.sectionSpawnTriggers.map {
spawnTriggerMapper.toSectionSpawnTriggerDto(it) spawnTriggerMapper.toSectionSpawnTriggerDto(it)
}, },
isClonable = formElement.isClonable, isClonable = formElement.isClonable,
tableRowPreset =
formElement.tableRowPreset?.let {
tableRowPresetMapper.toTableRowPresetDto(it)
},
) )
fun toFormElement( fun toFormElement(
@@ -43,15 +48,21 @@ class FormElementMapper(
options = formElement.options.map { formOptionMapper.toFormOption(it) }.toMutableList(), options = formElement.options.map { formOptionMapper.toFormOption(it) }.toMutableList(),
type = formElement.type, type = formElement.type,
formElementSubSection = formElementSubSection, formElementSubSection = formElementSubSection,
visibilityCondition = visibilityConditions =
formElement.visibilityCondition?.let { formElement.visibilityConditions
visibilityConditionMapper.toFormElementVisibilityCondition(it) ?.map { visibilityConditionMapper.toFormElementVisibilityCondition(it) }
}, ?.toMutableList()
sectionSpawnTrigger = ?: mutableListOf(),
formElement.sectionSpawnTrigger?.let { sectionSpawnTriggers =
spawnTriggerMapper.toSectionSpawnTrigger(it) formElement.sectionSpawnTriggers
}, ?.map { spawnTriggerMapper.toSectionSpawnTrigger(it) }
?.toMutableList()
?: mutableListOf(),
isClonable = formElement.isClonable ?: false, isClonable = formElement.isClonable ?: false,
tableRowPreset =
formElement.tableRowPreset?.let {
tableRowPresetMapper.toTableRowPreset(it)
},
) )
fun toNewFormElement( fun toNewFormElement(
@@ -66,14 +77,20 @@ class FormElementMapper(
options = formElement.options.map { formOptionMapper.toFormOption(it) }.toMutableList(), options = formElement.options.map { formOptionMapper.toFormOption(it) }.toMutableList(),
type = formElement.type, type = formElement.type,
formElementSubSection = formElementSubSection, formElementSubSection = formElementSubSection,
visibilityCondition = visibilityConditions =
formElement.visibilityCondition?.let { formElement.visibilityConditions
visibilityConditionMapper.toFormElementVisibilityCondition(it) ?.map { visibilityConditionMapper.toFormElementVisibilityCondition(it) }
}, ?.toMutableList()
sectionSpawnTrigger = ?: mutableListOf(),
formElement.sectionSpawnTrigger?.let { sectionSpawnTriggers =
spawnTriggerMapper.toSectionSpawnTrigger(it) formElement.sectionSpawnTriggers
}, ?.map { spawnTriggerMapper.toSectionSpawnTrigger(it) }
?.toMutableList()
?: mutableListOf(),
isClonable = formElement.isClonable ?: false, isClonable = formElement.isClonable ?: false,
tableRowPreset =
formElement.tableRowPreset?.let {
tableRowPresetMapper.toTableRowPreset(it)
},
) )
} }

View File

@@ -2,8 +2,11 @@ package com.betriebsratkanzlei.legalconsenthub.form_element
import com.betriebsratkanzlei.legalconsenthub_api.model.EmployeeDataCategory import com.betriebsratkanzlei.legalconsenthub_api.model.EmployeeDataCategory
import com.betriebsratkanzlei.legalconsenthub_api.model.ProcessingPurpose import com.betriebsratkanzlei.legalconsenthub_api.model.ProcessingPurpose
import jakarta.persistence.AttributeOverride
import jakarta.persistence.AttributeOverrides
import jakarta.persistence.Column import jakarta.persistence.Column
import jakarta.persistence.Embeddable import jakarta.persistence.Embeddable
import jakarta.persistence.Embedded
@Embeddable @Embeddable
class FormOption( class FormOption(
@@ -15,4 +18,21 @@ class FormOption(
var processingPurpose: ProcessingPurpose, var processingPurpose: ProcessingPurpose,
@Column(nullable = false) @Column(nullable = false)
var employeeDataCategory: EmployeeDataCategory, var employeeDataCategory: EmployeeDataCategory,
@Embedded
@AttributeOverrides(
AttributeOverride(name = "sourceTableReference", column = Column(name = "col_config_source_table_ref")),
AttributeOverride(name = "sourceColumnIndex", column = Column(name = "col_config_source_col_idx")),
AttributeOverride(
name = "filterCondition.sourceColumnIndex",
column = Column(name = "col_config_filter_src_col_idx"),
),
AttributeOverride(
name = "filterCondition.expectedValue",
column = Column(name = "col_config_filter_expected_val"),
),
AttributeOverride(name = "filterCondition.operator", column = Column(name = "col_config_filter_operator")),
AttributeOverride(name = "isReadOnly", column = Column(name = "col_config_is_read_only")),
AttributeOverride(name = "isCheckbox", column = Column(name = "col_config_is_checkbox")),
)
var columnConfig: TableColumnConfig? = null,
) )

View File

@@ -4,13 +4,16 @@ import com.betriebsratkanzlei.legalconsenthub_api.model.FormOptionDto
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
@Component @Component
class FormOptionMapper { class FormOptionMapper(
private val columnConfigMapper: TableColumnConfigMapper,
) {
fun toFormOptionDto(formOption: FormOption): FormOptionDto = fun toFormOptionDto(formOption: FormOption): FormOptionDto =
FormOptionDto( FormOptionDto(
value = formOption.value, value = formOption.value,
label = formOption.label, label = formOption.label,
processingPurpose = formOption.processingPurpose, processingPurpose = formOption.processingPurpose,
employeeDataCategory = formOption.employeeDataCategory, employeeDataCategory = formOption.employeeDataCategory,
columnConfig = formOption.columnConfig?.let { columnConfigMapper.toTableColumnConfigDto(it) },
) )
fun toFormOption(formOptionDto: FormOptionDto): FormOption = fun toFormOption(formOptionDto: FormOptionDto): FormOption =
@@ -19,5 +22,6 @@ class FormOptionMapper {
label = formOptionDto.label, label = formOptionDto.label,
processingPurpose = formOptionDto.processingPurpose, processingPurpose = formOptionDto.processingPurpose,
employeeDataCategory = formOptionDto.employeeDataCategory, employeeDataCategory = formOptionDto.employeeDataCategory,
columnConfig = formOptionDto.columnConfig?.let { columnConfigMapper.toTableColumnConfig(it) },
) )
} }

View File

@@ -0,0 +1,32 @@
package com.betriebsratkanzlei.legalconsenthub.form_element
import jakarta.persistence.AttributeOverride
import jakarta.persistence.AttributeOverrides
import jakarta.persistence.Column
import jakarta.persistence.Embeddable
import jakarta.persistence.Embedded
@Embeddable
data class TableColumnConfig(
val sourceTableReference: String? = null,
val sourceColumnIndex: Int? = null,
@Embedded
val filterCondition: TableColumnFilter? = null,
@Embedded
@AttributeOverrides(
AttributeOverride(name = "constraintTableReference", column = Column(name = "row_constraint_table_reference")),
AttributeOverride(name = "constraintKeyColumnIndex", column = Column(name = "row_constraint_key_column_index")),
AttributeOverride(
name = "constraintValueColumnIndex",
column = Column(name = "row_constraint_value_column_index"),
),
AttributeOverride(
name = "currentRowKeyColumnIndex",
column = Column(name = "row_constraint_current_row_key_column_index"),
),
)
val rowConstraint: TableRowConstraint? = null,
val isReadOnly: Boolean = false,
val isMultipleAllowed: Boolean = false,
val isCheckbox: Boolean = false,
)

View File

@@ -0,0 +1,32 @@
package com.betriebsratkanzlei.legalconsenthub.form_element
import com.betriebsratkanzlei.legalconsenthub_api.model.TableColumnConfigDto
import org.springframework.stereotype.Component
@Component
class TableColumnConfigMapper(
private val filterMapper: TableColumnFilterMapper,
private val rowConstraintMapper: TableRowConstraintMapper,
) {
fun toTableColumnConfigDto(config: TableColumnConfig): TableColumnConfigDto =
TableColumnConfigDto(
sourceTableReference = config.sourceTableReference,
sourceColumnIndex = config.sourceColumnIndex,
filterCondition = config.filterCondition?.let { filterMapper.toTableColumnFilterDto(it) },
rowConstraint = config.rowConstraint?.let { rowConstraintMapper.toTableRowConstraintDto(it) },
isReadOnly = config.isReadOnly,
isMultipleAllowed = config.isMultipleAllowed,
isCheckbox = config.isCheckbox,
)
fun toTableColumnConfig(dto: TableColumnConfigDto): TableColumnConfig =
TableColumnConfig(
sourceTableReference = dto.sourceTableReference,
sourceColumnIndex = dto.sourceColumnIndex,
filterCondition = dto.filterCondition?.let { filterMapper.toTableColumnFilter(it) },
rowConstraint = dto.rowConstraint?.let { rowConstraintMapper.toTableRowConstraint(it) },
isReadOnly = dto.isReadOnly ?: false,
isMultipleAllowed = dto.isMultipleAllowed ?: false,
isCheckbox = dto.isCheckbox ?: false,
)
}

View File

@@ -0,0 +1,13 @@
package com.betriebsratkanzlei.legalconsenthub.form_element
import jakarta.persistence.Embeddable
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
@Embeddable
data class TableColumnFilter(
val sourceColumnIndex: Int? = null,
val expectedValue: String? = null,
@Enumerated(EnumType.STRING)
val operator: VisibilityConditionOperator = VisibilityConditionOperator.EQUALS,
)

View File

@@ -0,0 +1,38 @@
package com.betriebsratkanzlei.legalconsenthub.form_element
import com.betriebsratkanzlei.legalconsenthub_api.model.TableColumnFilterDto
import org.springframework.stereotype.Component
import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionOperator as VisibilityConditionOperatorDto
@Component
class TableColumnFilterMapper {
fun toTableColumnFilterDto(filter: TableColumnFilter): TableColumnFilterDto =
TableColumnFilterDto(
sourceColumnIndex = filter.sourceColumnIndex,
expectedValue = filter.expectedValue,
operator = filter.operator.toDto(),
)
fun toTableColumnFilter(dto: TableColumnFilterDto): TableColumnFilter =
TableColumnFilter(
sourceColumnIndex = dto.sourceColumnIndex,
expectedValue = dto.expectedValue,
operator = dto.operator?.toEntity() ?: VisibilityConditionOperator.EQUALS,
)
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
}
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
}
}

View File

@@ -0,0 +1,9 @@
package com.betriebsratkanzlei.legalconsenthub.form_element
import jakarta.persistence.Embeddable
@Embeddable
data class TableColumnMapping(
val sourceColumnIndex: Int,
val targetColumnIndex: Int,
)

View File

@@ -0,0 +1,19 @@
package com.betriebsratkanzlei.legalconsenthub.form_element
import com.betriebsratkanzlei.legalconsenthub_api.model.TableColumnMappingDto
import org.springframework.stereotype.Component
@Component
class TableColumnMappingMapper {
fun toTableColumnMappingDto(mapping: TableColumnMapping): TableColumnMappingDto =
TableColumnMappingDto(
sourceColumnIndex = mapping.sourceColumnIndex,
targetColumnIndex = mapping.targetColumnIndex,
)
fun toTableColumnMapping(dto: TableColumnMappingDto): TableColumnMapping =
TableColumnMapping(
sourceColumnIndex = dto.sourceColumnIndex ?: 0,
targetColumnIndex = dto.targetColumnIndex ?: 0,
)
}

View File

@@ -0,0 +1,11 @@
package com.betriebsratkanzlei.legalconsenthub.form_element
import jakarta.persistence.Embeddable
@Embeddable
data class TableRowConstraint(
val constraintTableReference: String? = null,
val constraintKeyColumnIndex: Int? = null,
val constraintValueColumnIndex: Int? = null,
val currentRowKeyColumnIndex: Int? = null,
)

View File

@@ -0,0 +1,23 @@
package com.betriebsratkanzlei.legalconsenthub.form_element
import com.betriebsratkanzlei.legalconsenthub_api.model.TableRowConstraintDto
import org.springframework.stereotype.Component
@Component
class TableRowConstraintMapper {
fun toTableRowConstraintDto(constraint: TableRowConstraint): TableRowConstraintDto =
TableRowConstraintDto(
constraintTableReference = constraint.constraintTableReference,
constraintKeyColumnIndex = constraint.constraintKeyColumnIndex,
constraintValueColumnIndex = constraint.constraintValueColumnIndex,
currentRowKeyColumnIndex = constraint.currentRowKeyColumnIndex,
)
fun toTableRowConstraint(dto: TableRowConstraintDto): TableRowConstraint =
TableRowConstraint(
constraintTableReference = dto.constraintTableReference,
constraintKeyColumnIndex = dto.constraintKeyColumnIndex,
constraintValueColumnIndex = dto.constraintValueColumnIndex,
currentRowKeyColumnIndex = dto.currentRowKeyColumnIndex,
)
}

View File

@@ -0,0 +1,18 @@
package com.betriebsratkanzlei.legalconsenthub.form_element
import jakarta.persistence.CollectionTable
import jakarta.persistence.ElementCollection
import jakarta.persistence.Embeddable
import jakarta.persistence.Embedded
import jakarta.persistence.JoinColumn
@Embeddable
data class TableRowPreset(
val sourceTableReference: String? = null,
@Embedded
val filterCondition: TableColumnFilter? = null,
@ElementCollection
@CollectionTable(name = "table_column_mappings", joinColumns = [JoinColumn(name = "form_element_id")])
val columnMappings: MutableList<TableColumnMapping> = mutableListOf(),
val canAddRows: Boolean? = null,
)

View File

@@ -0,0 +1,27 @@
package com.betriebsratkanzlei.legalconsenthub.form_element
import com.betriebsratkanzlei.legalconsenthub_api.model.TableRowPresetDto
import org.springframework.stereotype.Component
@Component
class TableRowPresetMapper(
private val filterMapper: TableColumnFilterMapper,
private val mappingMapper: TableColumnMappingMapper,
) {
fun toTableRowPresetDto(preset: TableRowPreset): TableRowPresetDto =
TableRowPresetDto(
sourceTableReference = preset.sourceTableReference,
filterCondition = preset.filterCondition?.let { filterMapper.toTableColumnFilterDto(it) },
columnMappings = preset.columnMappings.map { mappingMapper.toTableColumnMappingDto(it) },
canAddRows = preset.canAddRows ?: true,
)
fun toTableRowPreset(dto: TableRowPresetDto): TableRowPreset =
TableRowPreset(
sourceTableReference = dto.sourceTableReference,
filterCondition = dto.filterCondition?.let { filterMapper.toTableColumnFilter(it) },
columnMappings =
dto.columnMappings?.map { mappingMapper.toTableColumnMapping(it) }?.toMutableList() ?: mutableListOf(),
canAddRows = dto.canAddRows,
)
}

View File

@@ -53,33 +53,43 @@ create table comment
create table form_element_options create table form_element_options
( (
employee_data_category smallint not null check (employee_data_category between 0 and 3), col_config_filter_src_col_idx integer,
processing_purpose smallint not null check (processing_purpose between 0 and 3), col_config_is_checkbox boolean,
form_element_id uuid not null, col_config_is_read_only boolean,
label varchar(255) not null, col_config_source_col_idx integer,
option_value TEXT not null employee_data_category smallint not null check (employee_data_category between 0 and 3),
is_multiple_allowed boolean,
processing_purpose smallint not null check (processing_purpose between 0 and 3),
row_constraint_current_row_key_column_index integer,
row_constraint_key_column_index integer,
row_constraint_value_column_index integer,
form_element_id uuid not null,
col_config_filter_expected_val varchar(255),
col_config_filter_operator varchar(255) check (col_config_filter_operator in
('EQUALS', 'NOT_EQUALS', 'IS_EMPTY',
'IS_NOT_EMPTY')),
col_config_source_table_ref varchar(255),
label varchar(255) not null,
option_value TEXT not null,
row_constraint_table_reference varchar(255)
); );
create table form_element create table form_element
( (
form_element_order integer, can_add_rows boolean,
is_clonable boolean not null, form_element_order integer,
type smallint not null check (type between 0 and 7), is_clonable boolean not null,
form_element_sub_section_id uuid not null, row_preset_filter_src_col_idx integer,
id uuid not null, type smallint not null check (type between 0 and 8),
description varchar(255), form_element_sub_section_id uuid not null,
form_element_condition_type varchar(255) check (form_element_condition_type in ('SHOW', 'HIDE')), id uuid not null,
form_element_expected_value varchar(255), description varchar(255),
form_element_operator varchar(255) check (form_element_operator in reference varchar(255),
('EQUALS', 'NOT_EQUALS', 'IS_EMPTY', 'IS_NOT_EMPTY')), row_preset_filter_expected_val varchar(255),
reference varchar(255), row_preset_filter_operator varchar(255) check (row_preset_filter_operator in
section_spawn_condition_type varchar(255) check (section_spawn_condition_type in ('SHOW', 'HIDE')), ('EQUALS', 'NOT_EQUALS', 'IS_EMPTY', 'IS_NOT_EMPTY')),
section_spawn_expected_value varchar(255), row_preset_source_table_ref varchar(255),
section_spawn_operator varchar(255) check (section_spawn_operator in title varchar(255),
('EQUALS', 'NOT_EQUALS', 'IS_EMPTY', 'IS_NOT_EMPTY')),
source_form_element_reference varchar(255),
template_reference varchar(255),
title varchar(255),
primary key (id) primary key (id)
); );
@@ -122,6 +132,33 @@ create table notification
primary key (id) primary key (id)
); );
create table section_spawn_triggers
(
form_element_id uuid not null,
section_spawn_condition_type varchar(255) check (section_spawn_condition_type in ('SHOW', 'HIDE')),
section_spawn_expected_value varchar(255),
section_spawn_operator varchar(255) check (section_spawn_operator in
('EQUALS', 'NOT_EQUALS', 'IS_EMPTY', 'IS_NOT_EMPTY')),
template_reference varchar(255)
);
create table table_column_mappings
(
source_column_index integer,
target_column_index integer,
form_element_id uuid not null
);
create table visibility_conditions
(
form_element_id uuid not null,
form_element_condition_type varchar(255) check (form_element_condition_type in ('SHOW', 'HIDE')),
form_element_expected_value varchar(255),
form_element_operator varchar(255) check (form_element_operator in
('EQUALS', 'NOT_EQUALS', 'IS_EMPTY', 'IS_NOT_EMPTY')),
source_form_element_reference varchar(255)
);
alter table if exists application_form alter table if exists application_form
add constraint FKhtad5onoy2jknhtyfmx6cvvey add constraint FKhtad5onoy2jknhtyfmx6cvvey
foreign key (created_by_id) foreign key (created_by_id)
@@ -182,3 +219,18 @@ alter table if exists notification
add constraint FKeg1j4hnp0y4lbm0y35hgr4e8r add constraint FKeg1j4hnp0y4lbm0y35hgr4e8r
foreign key (recipient_id) foreign key (recipient_id)
references app_user; references app_user;
alter table if exists section_spawn_triggers
add constraint FK7lf0hf8cepm2o9nty147x2ahm
foreign key (form_element_id)
references form_element;
alter table if exists table_column_mappings
add constraint FK2t3a4fl5kqtqky39r7boqegf9
foreign key (form_element_id)
references form_element;
alter table if exists visibility_conditions
add constraint FK5xuf7bd179ogpq5a1m3g8q7jb
foreign key (form_element_id)
references form_element;

View File

@@ -15,6 +15,12 @@
\usepackage{xcolor} \usepackage{xcolor}
\usepackage{tcolorbox} \usepackage{tcolorbox}
\usepackage[normalem]{ulem} \usepackage[normalem]{ulem}
\usepackage{tabularx}
\usepackage{array}
\usepackage{booktabs}
% Define column type for auto-wrapping text
\newcolumntype{Y}{>{\raggedright\arraybackslash}X}
\hypersetup{ \hypersetup{
colorlinks=true, colorlinks=true,
@@ -73,12 +79,22 @@ Dieses Dokument enthält die Details der Betriebsvereinbarung "[[${applicationFo
\textit{\small [[${element.description}]]} \textit{\small [[${element.description}]]}
[/] [/]
[# th:if="${element.isTable}"]
\vspace{0.5em}
\noindent
\small
[(${element.value})]
\normalsize
\vspace{0.5em}
[/]
[# th:if="${!element.isTable}"]
\begin{tcolorbox}[colback=gray!5, colframe=gray!20, arc=0mm, boxrule=0.5pt] \begin{tcolorbox}[colback=gray!5, colframe=gray!20, arc=0mm, boxrule=0.5pt]
[[${element.value}]] [[${element.value}]]
\end{tcolorbox} \end{tcolorbox}
[/] [/]
[/] [/]
[/] [/]
[/]
\vspace{3cm} \vspace{3cm}

View File

@@ -13,6 +13,8 @@
:is="getResolvedComponent(formElementItem.formElement)" :is="getResolvedComponent(formElementItem.formElement)"
:form-options="formElementItem.formElement.options" :form-options="formElementItem.formElement.options"
:disabled="props.disabled" :disabled="props.disabled"
:all-form-elements="props.allFormElements"
:table-row-preset="formElementItem.formElement.tableRowPreset"
@update:form-options="updateFormOptions($event, formElementItem)" @update:form-options="updateFormOptions($event, formElementItem)"
/> />
<div v-if="formElementItem.formElement.isClonable && !props.disabled" class="mt-3"> <div v-if="formElementItem.formElement.isClonable && !props.disabled" class="mt-3">
@@ -102,6 +104,7 @@ const props = defineProps<{
visibilityMap: Map<string, boolean> visibilityMap: Map<string, boolean>
applicationFormId?: string applicationFormId?: string
disabled?: boolean disabled?: boolean
allFormElements?: FormElementDto[]
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{

View File

@@ -23,6 +23,7 @@
:visibility-map="visibilityMap" :visibility-map="visibilityMap"
:application-form-id="applicationFormId" :application-form-id="applicationFormId"
:disabled="disabled" :disabled="disabled"
:all-form-elements="allFormElements"
@update:model-value=" @update:model-value="
(elements) => (elements) =>
handleFormElementUpdate(elements, getSubsectionKey(currentFormElementSection, sectionIndex, subsection)) handleFormElementUpdate(elements, getSubsectionKey(currentFormElementSection, sectionIndex, subsection))
@@ -107,9 +108,9 @@ const { cloneElement } = useClonableElements()
const previousVisibilityMap = ref<Map<string, boolean>>(new Map()) const previousVisibilityMap = ref<Map<string, boolean>>(new Map())
const allFormElements = computed(() => { const allFormElements = computed(() => {
return props.formElementSections.flatMap( return props.formElementSections
(section) => section.formElementSubSections?.flatMap((subsection) => subsection.formElements) ?? [] .filter((section) => section.isTemplate !== true)
) .flatMap((section) => section.formElementSubSections?.flatMap((subsection) => subsection.formElements) ?? [])
}) })
const visibilityMap = computed(() => { const visibilityMap = computed(() => {

View File

@@ -1,8 +1,39 @@
<template> <template>
<div class="space-y-3"> <div class="space-y-3">
<UTable :data="tableData" :columns="tableColumns" class="w-full"> <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 -->
<USelectMenu
v-if="hasColumnReference(col.colIndex) && !isColumnReadOnly(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"
:placeholder="$t('applicationForms.formElements.table.selectValue')"
:multiple="isColumnMultipleAllowed(col.colIndex)"
class="w-full min-w-32"
@update:model-value="
(val: string | string[]) =>
updateCellValue((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>
<!-- Checkbox column -->
<div v-else-if="isColumnCheckbox(col.colIndex)" class="flex justify-center">
<UCheckbox
:model-value="getCellValueForCheckbox(slotProps.row as TableRow<TableRowData>, col.key)"
:disabled="disabled"
@update:model-value="
(val: boolean | 'indeterminate') =>
updateCheckboxCell((slotProps.row as TableRow<TableRowData>).index, col.colIndex, val === true)
"
/>
</div>
<!-- Regular text input -->
<UInput <UInput
v-else
:model-value="getCellValue(slotProps.row as TableRow<TableRowData>, col.key)" :model-value="getCellValue(slotProps.row as TableRow<TableRowData>, col.key)"
:disabled="disabled" :disabled="disabled"
class="w-full min-w-32" class="w-full min-w-32"
@@ -11,7 +42,7 @@
" "
/> />
</template> </template>
<template #actions-cell="{ row }"> <template v-if="canModifyRows" #actions-cell="{ row }">
<UButton <UButton
v-if="!disabled" v-if="!disabled"
icon="i-lucide-trash-2" icon="i-lucide-trash-2"
@@ -28,26 +59,61 @@
{{ $t('applicationForms.formElements.table.noData') }} {{ $t('applicationForms.formElements.table.noData') }}
</div> </div>
<UButton v-if="!disabled" variant="outline" size="sm" leading-icon="i-lucide-plus" @click="addRow"> <UButton v-if="!disabled && canModifyRows" variant="outline" size="sm" leading-icon="i-lucide-plus" @click="addRow">
{{ $t('applicationForms.formElements.table.addRow') }} {{ $t('applicationForms.formElements.table.addRow') }}
</UButton> </UButton>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { FormOptionDto } from '~~/.api-client' import type { FormElementDto, FormOptionDto, TableRowPresetDto } from '~~/.api-client'
import type { TableColumn, TableRow } from '@nuxt/ui' import type { TableColumn, TableRow } from '@nuxt/ui'
import { useTableCrossReferences } from '~/composables/useTableCrossReferences'
const props = defineProps<{ const props = defineProps<{
formOptions: FormOptionDto[] formOptions: FormOptionDto[]
disabled?: boolean disabled?: boolean
allFormElements?: FormElementDto[]
tableRowPreset?: TableRowPresetDto
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:formOptions', value: FormOptionDto[]): void (e: 'update:formOptions', value: FormOptionDto[]): void
}>() }>()
type TableRowData = Record<string, string> const { getReferencedColumnValues, getConstrainedColumnValues, applyRowPresets } = useTableCrossReferences()
const canModifyRows = computed(() => {
if (!props.tableRowPreset) return true
return props.tableRowPreset.canAddRows !== false
})
// Watch for changes in source table and apply row presets reactively
const sourceTableOptions = computed(() => {
if (!props.tableRowPreset?.sourceTableReference || !props.allFormElements) return null
const sourceTable = props.allFormElements.find(
(el) => el.reference === props.tableRowPreset?.sourceTableReference && el.type === 'TABLE'
)
return sourceTable?.options
})
watch(
sourceTableOptions,
() => {
if (!sourceTableOptions.value || !props.tableRowPreset || !props.allFormElements) return
const updatedOptions = applyRowPresets(props.tableRowPreset, props.formOptions, props.allFormElements)
const hasChanges = updatedOptions.some((opt, idx) => opt.value !== props.formOptions[idx]?.value)
if (hasChanges) {
emit('update:formOptions', updatedOptions)
}
},
{ immediate: true, deep: true }
)
type CellValue = string | string[] | boolean
type TableRowData = Record<string, CellValue>
interface DataColumn { interface DataColumn {
key: string key: string
@@ -67,7 +133,8 @@ const tableColumns = computed<TableColumn<TableRowData>[]>(() => {
header: option.label || '' header: option.label || ''
})) }))
if (!props.disabled) { // Only show actions column if not disabled AND rows can be modified
if (!props.disabled && canModifyRows.value) {
columns.push({ columns.push({
id: 'actions', id: 'actions',
header: '' header: ''
@@ -80,10 +147,21 @@ const tableColumns = computed<TableColumn<TableRowData>[]>(() => {
const tableData = computed<TableRowData[]>(() => { const tableData = computed<TableRowData[]>(() => {
if (props.formOptions.length === 0) return [] if (props.formOptions.length === 0) return []
const columnData: string[][] = props.formOptions.map((option) => { const columnData: CellValue[][] = props.formOptions.map((option, colIndex) => {
try { try {
const parsed = JSON.parse(option.value || '[]') const parsed = JSON.parse(option.value || '[]')
return Array.isArray(parsed) ? parsed : [] if (!Array.isArray(parsed)) return []
// For multi-select columns, each cell value is already an array
// For checkbox columns, each cell value is a boolean
// For single-select columns, each cell value is a string
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 { } catch {
return [] return []
} }
@@ -94,8 +172,15 @@ const tableData = computed<TableRowData[]>(() => {
const rows: TableRowData[] = [] const rows: TableRowData[] = []
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) { for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
const row: TableRowData = {} const row: TableRowData = {}
props.formOptions.forEach((_, colIndex) => { props.formOptions.forEach((option, colIndex) => {
row[`col_${colIndex}`] = columnData[colIndex]?.[rowIndex] ?? '' const cellValue = columnData[colIndex]?.[rowIndex]
if (isColumnMultipleAllowed(colIndex)) {
row[`col_${colIndex}`] = Array.isArray(cellValue) ? cellValue : []
} else if (isColumnCheckbox(colIndex)) {
row[`col_${colIndex}`] = cellValue === true
} else {
row[`col_${colIndex}`] = typeof cellValue === 'string' ? cellValue : ''
}
}) })
rows.push(row) rows.push(row)
} }
@@ -103,8 +188,81 @@ const tableData = computed<TableRowData[]>(() => {
return rows return rows
}) })
function hasColumnReference(colIndex: number): boolean {
const option = props.formOptions[colIndex]
return !!option?.columnConfig?.sourceTableReference
}
function isColumnReadOnly(colIndex: number): boolean {
const option = props.formOptions[colIndex]
return option?.columnConfig?.isReadOnly === true
}
function isColumnMultipleAllowed(colIndex: number): boolean {
const option = props.formOptions[colIndex]
return option?.columnConfig?.isMultipleAllowed === true
}
function isColumnCheckbox(colIndex: number): boolean {
const option = props.formOptions[colIndex]
return option?.columnConfig?.isCheckbox === true
}
function getColumnOptions(colIndex: number, currentRowData?: TableRowData): string[] {
const option = props.formOptions[colIndex]
if (!option?.columnConfig || !props.allFormElements) {
return []
}
const { columnConfig } = option
const { rowConstraint } = columnConfig
// If row constraint is configured, filter values based on current row's key value
if (rowConstraint?.constraintTableReference && currentRowData) {
const currentRowAsRecord: Record<string, string> = {}
for (const [key, value] of Object.entries(currentRowData)) {
currentRowAsRecord[key] =
typeof value === 'string' ? value : Array.isArray(value) ? value.join(',') : String(value)
}
return getConstrainedColumnValues(
columnConfig,
currentRowAsRecord,
rowConstraint.constraintTableReference,
rowConstraint.constraintKeyColumnIndex ?? 0,
rowConstraint.constraintValueColumnIndex ?? 1,
props.allFormElements,
rowConstraint.currentRowKeyColumnIndex
)
}
return getReferencedColumnValues(columnConfig, props.allFormElements)
}
function getCellValue(row: TableRow<TableRowData>, columnKey: string): string { function getCellValue(row: TableRow<TableRowData>, columnKey: string): string {
return row.original[columnKey] ?? '' const value = row.original[columnKey]
return typeof value === 'string' ? value : ''
}
function getCellValueForSelect(row: TableRow<TableRowData>, columnKey: string, colIndex: number): string | string[] {
const value = row.original[columnKey]
if (isColumnMultipleAllowed(colIndex)) {
return Array.isArray(value) ? value : []
}
return typeof value === 'string' ? value : ''
}
function getCellValueForCheckbox(row: TableRow<TableRowData>, columnKey: string): boolean {
const value = row.original[columnKey]
return value === true
}
function formatCellDisplay(row: TableRow<TableRowData>, columnKey: string, colIndex: number): string {
const value = row.original[columnKey]
if (isColumnMultipleAllowed(colIndex) && Array.isArray(value)) {
return value.length > 0 ? value.join(', ') : '-'
}
return (typeof value === 'string' ? value : '') || '-'
} }
function updateCell(rowIndex: number, columnKey: string, value: string) { function updateCell(rowIndex: number, columnKey: string, value: string) {
@@ -113,7 +271,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: string[] let columnValues: CellValue[]
try { try {
columnValues = JSON.parse(option.value || '[]') columnValues = JSON.parse(option.value || '[]')
if (!Array.isArray(columnValues)) columnValues = [] if (!Array.isArray(columnValues)) columnValues = []
@@ -132,9 +290,11 @@ function updateCell(rowIndex: number, columnKey: string, value: string) {
emit('update:formOptions', updatedOptions) emit('update:formOptions', updatedOptions)
} }
function addRow() { function updateCellValue(rowIndex: number, columnKey: string, colIndex: number, value: string | string[]) {
const updatedOptions = props.formOptions.map((option) => { const updatedOptions = props.formOptions.map((option, index) => {
let columnValues: string[] if (index !== colIndex) return option
let columnValues: CellValue[]
try { try {
columnValues = JSON.parse(option.value || '[]') columnValues = JSON.parse(option.value || '[]')
if (!Array.isArray(columnValues)) columnValues = [] if (!Array.isArray(columnValues)) columnValues = []
@@ -142,7 +302,61 @@ function addRow() {
columnValues = [] columnValues = []
} }
columnValues.push('') const isMultiple = isColumnMultipleAllowed(colIndex)
while (columnValues.length <= rowIndex) {
columnValues.push(isMultiple ? [] : '')
}
columnValues[rowIndex] = value
return { ...option, value: JSON.stringify(columnValues) }
})
emit('update:formOptions', updatedOptions)
}
function updateCheckboxCell(rowIndex: number, colIndex: number, value: boolean) {
const updatedOptions = props.formOptions.map((option, index) => {
if (index !== colIndex) return option
let columnValues: CellValue[]
try {
columnValues = JSON.parse(option.value || '[]')
if (!Array.isArray(columnValues)) columnValues = []
} catch {
columnValues = []
}
while (columnValues.length <= rowIndex) {
columnValues.push(false)
}
columnValues[rowIndex] = value
return { ...option, value: JSON.stringify(columnValues) }
})
emit('update:formOptions', updatedOptions)
}
function addRow() {
const updatedOptions = props.formOptions.map((option, colIndex) => {
let columnValues: CellValue[]
try {
columnValues = JSON.parse(option.value || '[]')
if (!Array.isArray(columnValues)) columnValues = []
} catch {
columnValues = []
}
// For multi-select columns, initialize with empty array
// For checkbox columns, initialize with false
// Otherwise empty string
let emptyValue: CellValue = ''
if (isColumnMultipleAllowed(colIndex)) {
emptyValue = []
} else if (isColumnCheckbox(colIndex)) {
emptyValue = false
}
columnValues.push(emptyValue)
return { ...option, value: JSON.stringify(columnValues) } return { ...option, value: JSON.stringify(columnValues) }
}) })
@@ -152,7 +366,7 @@ function addRow() {
function removeRow(rowIndex: number) { function removeRow(rowIndex: number) {
const updatedOptions = props.formOptions.map((option) => { const updatedOptions = props.formOptions.map((option) => {
let columnValues: string[] let columnValues: CellValue[]
try { try {
columnValues = JSON.parse(option.value || '[]') columnValues = JSON.parse(option.value || '[]')
if (!Array.isArray(columnValues)) columnValues = [] if (!Array.isArray(columnValues)) columnValues = []

View File

@@ -9,3 +9,4 @@ export { useUser } from './user/useUser'
export { useUserApi } from './user/useUserApi' export { useUserApi } from './user/useUserApi'
export { useSectionSpawning } from './useSectionSpawning' export { useSectionSpawning } from './useSectionSpawning'
export { useClonableElements } from './useClonableElements' export { useClonableElements } from './useClonableElements'
export { useTableCrossReferences } from './useTableCrossReferences'

View File

@@ -1,13 +1,17 @@
import type { FormElementDto, VisibilityConditionOperator } from '~~/.api-client' import type { FormElementDto, FormElementVisibilityCondition, VisibilityConditionOperator } from '~~/.api-client'
import { VisibilityConditionOperator as VCOperator, VisibilityConditionType as VCType } from '~~/.api-client' import { VisibilityConditionOperator as VCOperator, VisibilityConditionType as VCType } from '~~/.api-client'
export function useFormElementVisibility() { export function useFormElementVisibility() {
/**
* Evaluates visibility for all form elements based on their visibility conditions.
* Returns a map of element key (id or reference) to visibility status.
*/
function evaluateFormElementVisibility(allFormElements: FormElementDto[]): Map<string, boolean> { function evaluateFormElementVisibility(allFormElements: FormElementDto[]): Map<string, boolean> {
const formElementsByRef = buildFormElementsMap(allFormElements) const formElementsByRef = buildFormElementsMap(allFormElements)
const visibilityMap = new Map<string, boolean>() const visibilityMap = new Map<string, boolean>()
allFormElements.forEach((element) => { allFormElements.forEach((element) => {
const isVisible = isElementVisible(element, formElementsByRef, visibilityMap) const isVisible = isElementVisible(element, formElementsByRef)
const key = element.id || element.reference const key = element.id || element.reference
if (key) { if (key) {
visibilityMap.set(key, isVisible) visibilityMap.set(key, isVisible)
@@ -27,24 +31,33 @@ export function useFormElementVisibility() {
return map return map
} }
function isElementVisible( /**
element: FormElementDto, * Evaluates if an element is visible based on its visibility conditions.
formElementsByRef: Map<string, FormElementDto>, * Multiple conditions use AND logic - all conditions must be met for the element to be visible.
_visibilityMap: Map<string, boolean> */
): boolean { function isElementVisible(element: FormElementDto, formElementsByRef: Map<string, FormElementDto>): boolean {
if (!element.visibilityCondition) { const conditions = element.visibilityConditions
if (!conditions || conditions.length === 0) {
return true return true
} }
const condition = element.visibilityCondition // All conditions must be met (AND logic)
return conditions.every((condition) => evaluateSingleCondition(condition, formElementsByRef))
}
/**
* Evaluates a single visibility condition against the form state.
*/
function evaluateSingleCondition(
condition: FormElementVisibilityCondition,
formElementsByRef: Map<string, FormElementDto>
): boolean {
const sourceElement = formElementsByRef.get(condition.sourceFormElementReference) const sourceElement = formElementsByRef.get(condition.sourceFormElementReference)
if (!sourceElement) { if (!sourceElement) {
return false return false
} }
const sourceValue = getFormElementValue(sourceElement) const sourceValue = getFormElementValue(sourceElement)
const operator = condition.formElementOperator || VCOperator.Equals const operator = condition.formElementOperator || VCOperator.Equals
const conditionMet = evaluateCondition(sourceValue, condition.formElementExpectedValue, operator) const conditionMet = evaluateCondition(sourceValue, condition.formElementExpectedValue, operator)
@@ -61,20 +74,15 @@ export function useFormElementVisibility() {
expectedValue: string, expectedValue: string,
operator: VisibilityConditionOperator operator: VisibilityConditionOperator
): boolean { ): boolean {
let result: boolean
switch (operator) { switch (operator) {
case VCOperator.Equals: case VCOperator.Equals:
result = actualValue.toLowerCase() === expectedValue.toLowerCase() return actualValue.toLowerCase() === expectedValue.toLowerCase()
return result
case VCOperator.NotEquals: case VCOperator.NotEquals:
result = actualValue.toLowerCase() !== expectedValue.toLowerCase() return actualValue.toLowerCase() !== expectedValue.toLowerCase()
return result
case VCOperator.IsEmpty: case VCOperator.IsEmpty:
result = actualValue === '' return actualValue === ''
return result
case VCOperator.IsNotEmpty: case VCOperator.IsNotEmpty:
result = actualValue !== '' return actualValue !== ''
return result
default: default:
return false return false
} }

View File

@@ -9,35 +9,54 @@ export function useSectionSpawning() {
let resultSections = sections let resultSections = sections
for (const formElement of updatedFormElements) { for (const formElement of updatedFormElements) {
if (!formElement.sectionSpawnTrigger || !formElement.reference) { const triggers = formElement.sectionSpawnTriggers
if (!triggers || triggers.length === 0 || !formElement.reference) {
continue continue
} }
// Extract trigger configuration and current element value
const trigger = formElement.sectionSpawnTrigger
const triggerValue = getFormElementValue(formElement) const triggerValue = getFormElementValue(formElement)
const shouldSpawn = shouldSpawnSection(trigger, triggerValue)
// Use resultSections to check for existing spawned sections (in case multiple spawns happen)
const existingSpawnedSections = getSpawnedSectionsForElement(resultSections, formElement.reference)
// Handle three spawn states: // Process each trigger independently
// 1. Condition met but no section spawned yet → create new section for (const trigger of triggers) {
if (shouldSpawn && existingSpawnedSections.length === 0) { resultSections = processSingleTrigger(resultSections, formElement, trigger, triggerValue)
resultSections = spawnNewSection(resultSections, formElement, trigger, triggerValue)
}
// 2. Condition no longer met but section exists → remove spawned section
else if (!shouldSpawn && existingSpawnedSections.length > 0) {
resultSections = removeSpawnedSections(resultSections, formElement.reference)
}
// 3. Condition still met and section exists → update section titles if value changed
else if (shouldSpawn && existingSpawnedSections.length > 0 && triggerValue) {
resultSections = updateSpawnedSectionTitles(resultSections, formElement.reference, trigger, triggerValue)
} }
} }
return resultSections return resultSections
} }
function processSingleTrigger(
sections: FormElementSectionDto[],
formElement: FormElementDto,
trigger: SectionSpawnTriggerDto,
triggerValue: string
): FormElementSectionDto[] {
let resultSections = sections
const shouldSpawn = shouldSpawnSection(trigger, triggerValue)
// Find existing spawned section for this specific trigger (by template reference)
const existingSpawnedSection = findSpawnedSectionForTrigger(
resultSections,
formElement.reference!,
trigger.templateReference
)
// Handle three spawn states:
// 1. Condition met but no section spawned yet → create new section
if (shouldSpawn && !existingSpawnedSection) {
resultSections = spawnNewSection(resultSections, formElement, trigger, triggerValue)
}
// 2. Condition no longer met but section exists → remove spawned section
else if (!shouldSpawn && existingSpawnedSection) {
resultSections = removeSpawnedSectionForTrigger(resultSections, formElement.reference!, trigger.templateReference)
}
// 3. Condition still met and section exists → update section titles if value changed
else if (shouldSpawn && existingSpawnedSection && triggerValue) {
resultSections = updateSpawnedSectionTitles(resultSections, formElement.reference!, trigger, triggerValue)
}
return resultSections
}
function spawnNewSection( function spawnNewSection(
sections: FormElementSectionDto[], sections: FormElementSectionDto[],
element: FormElementDto, element: FormElementDto,
@@ -94,8 +113,30 @@ export function useSectionSpawning() {
}) })
} }
function removeSpawnedSections(sections: FormElementSectionDto[], elementReference: string): FormElementSectionDto[] { function findSpawnedSectionForTrigger(
return sections.filter((section) => section.spawnedFromElementReference !== elementReference || section.isTemplate) sections: FormElementSectionDto[],
elementReference: string,
templateReference: string
): FormElementSectionDto | undefined {
return sections.find(
(section) =>
!section.isTemplate &&
section.spawnedFromElementReference === elementReference &&
section.templateReference === templateReference
)
}
function removeSpawnedSectionForTrigger(
sections: FormElementSectionDto[],
elementReference: string,
templateReference: string
): FormElementSectionDto[] {
return sections.filter(
(section) =>
section.isTemplate ||
section.spawnedFromElementReference !== elementReference ||
section.templateReference !== templateReference
)
} }
function spawnSectionFromTemplate( function spawnSectionFromTemplate(
@@ -146,13 +187,6 @@ export function useSectionSpawning() {
return trigger.sectionSpawnConditionType === VisibilityConditionType.Show ? isConditionMet : !isConditionMet return trigger.sectionSpawnConditionType === VisibilityConditionType.Show ? isConditionMet : !isConditionMet
} }
function getSpawnedSectionsForElement(
sections: FormElementSectionDto[],
elementReference: string
): FormElementSectionDto[] {
return sections.filter((section) => !section.isTemplate && section.spawnedFromElementReference === elementReference)
}
function findTemplateSection( function findTemplateSection(
sections: FormElementSectionDto[], sections: FormElementSectionDto[],
templateReference: string templateReference: string

View File

@@ -0,0 +1,269 @@
import type {
FormElementDto,
FormOptionDto,
TableColumnConfigDto,
TableColumnFilterDto,
TableRowPresetDto
} from '~~/.api-client'
import { VisibilityConditionOperator as VCOperator } from '~~/.api-client'
export function useTableCrossReferences() {
// Get available values for a column that references another table's column
function getReferencedColumnValues(
columnConfig: TableColumnConfigDto | undefined,
allFormElements: FormElementDto[]
): string[] {
if (!columnConfig?.sourceTableReference || columnConfig.sourceColumnIndex === undefined) {
return []
}
const sourceTable = findTableElement(columnConfig.sourceTableReference, allFormElements)
if (!sourceTable) {
return []
}
const sourceColumn = sourceTable.options[columnConfig.sourceColumnIndex]
if (!sourceColumn) {
return []
}
const columnValues = parseColumnValues(sourceColumn.value)
// Apply filter if present
if (columnConfig.filterCondition) {
return filterColumnValues(columnValues, columnConfig.filterCondition, sourceTable)
}
return columnValues.filter((v) => v.trim() !== '')
}
// Get filtered values based on constraints from another table
// Used for cases like "Permission-ID can only use permissions allowed for the selected role"
function getConstrainedColumnValues(
columnConfig: TableColumnConfigDto | undefined,
currentRowData: Record<string, string>,
constraintTableReference: string,
constraintKeyColumnIndex: number,
constraintValueColumnIndex: number,
allFormElements: FormElementDto[],
currentRowKeyColumnIndex?: number
): string[] {
if (!columnConfig?.sourceTableReference) {
return []
}
const constraintTable = findTableElement(constraintTableReference, allFormElements)
if (!constraintTable) {
// No constraint found, return all values from source table column
return getReferencedColumnValues(columnConfig, allFormElements)
}
const lookupColumnIndex = currentRowKeyColumnIndex ?? constraintKeyColumnIndex
const keyValue = currentRowData[`col_${lookupColumnIndex}`]
if (!keyValue) {
// No key value to look up, return all values from source table column
return getReferencedColumnValues(columnConfig, allFormElements)
}
const allowedValuesRaw = getAllowedValuesFromConstraintTable(
constraintTable,
keyValue,
constraintKeyColumnIndex,
constraintValueColumnIndex
)
const allowedValues = allowedValuesRaw.flatMap((v) => (typeof v === 'boolean' ? String(v) : v))
// If no allowed values found, fall back to all values from source table
if (allowedValues.length === 0) {
return getReferencedColumnValues(columnConfig, allFormElements)
}
return allowedValues
}
// Apply row presets from a source table based on filter conditions
function applyRowPresets(
tableRowPreset: TableRowPresetDto | undefined,
targetOptions: FormOptionDto[],
allFormElements: FormElementDto[]
): FormOptionDto[] {
if (!tableRowPreset?.sourceTableReference) {
return targetOptions
}
const sourceTable = findTableElement(tableRowPreset.sourceTableReference, allFormElements)
if (!sourceTable) {
return targetOptions
}
// Get source table data
const sourceData = parseTableData(sourceTable.options)
// Filter rows based on filter condition
const filteredRows = tableRowPreset.filterCondition
? filterTableRows(sourceData, tableRowPreset.filterCondition, sourceTable.options)
: sourceData
// Apply column mappings to create preset rows in target
const columnMappings = tableRowPreset.columnMappings || []
const presetRowCount = filteredRows.length
return targetOptions.map((option, targetColIndex) => {
const mapping = columnMappings.find((m) => m.targetColumnIndex === targetColIndex)
// For mapped columns, use values from source
if (mapping && mapping.sourceColumnIndex !== undefined) {
const sourceColIndex = mapping.sourceColumnIndex
const presetValues = filteredRows.map((row) => String(row[sourceColIndex] ?? ''))
return {
...option,
value: JSON.stringify(presetValues)
}
}
// For non-mapped columns, ensure we have the right number of rows
const existingValues = parseColumnValues(option.value)
const isCheckboxColumn = option.columnConfig?.isCheckbox === true
// Pad or trim to match preset row count
const adjustedValues: (string | boolean)[] = []
for (let i = 0; i < presetRowCount; i++) {
if (i < existingValues.length && existingValues[i] !== undefined) {
adjustedValues.push(existingValues[i]!)
} else {
// Initialize new rows with appropriate default
adjustedValues.push(isCheckboxColumn ? false : '')
}
}
return {
...option,
value: JSON.stringify(adjustedValues)
}
})
}
function findTableElement(reference: string, allFormElements: FormElementDto[]): FormElementDto | undefined {
return allFormElements.find((el) => el.reference === reference && el.type === 'TABLE')
}
function parseColumnValues(jsonValue: string | undefined): string[] {
if (!jsonValue) return []
try {
const parsed = JSON.parse(jsonValue)
return Array.isArray(parsed) ? parsed : []
} catch {
return []
}
}
function parseTableData(options: FormOptionDto[]): (string | boolean)[][] {
const columnData = options.map((opt) => parseColumnValuesWithTypes(opt.value))
const rowCount = Math.max(...columnData.map((col) => col.length), 0)
const rows: (string | boolean)[][] = []
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
const row = columnData.map((col) => col[rowIndex] ?? '')
rows.push(row)
}
return rows
}
function parseColumnValuesWithTypes(jsonValue: string | undefined): (string | boolean)[] {
if (!jsonValue) return []
try {
const parsed = JSON.parse(jsonValue)
return Array.isArray(parsed) ? parsed : []
} catch {
return []
}
}
function filterColumnValues(
values: string[],
filterCondition: TableColumnFilterDto,
sourceTable: FormElementDto
): string[] {
if (filterCondition.sourceColumnIndex === undefined) {
return values
}
const filterColumn = sourceTable.options[filterCondition.sourceColumnIndex]
if (!filterColumn) {
return values
}
const filterColumnValues = parseColumnValues(filterColumn.value)
return values.filter((_, index) => {
const filterValue = filterColumnValues[index] || ''
return evaluateFilterCondition(filterValue, filterCondition)
})
}
function filterTableRows(
rows: (string | boolean)[][],
filterCondition: TableColumnFilterDto,
_options: FormOptionDto[]
): (string | boolean)[][] {
if (filterCondition.sourceColumnIndex === undefined) {
return rows
}
return rows.filter((row) => {
const filterValue = row[filterCondition.sourceColumnIndex!] ?? ''
return evaluateFilterCondition(filterValue, filterCondition)
})
}
function evaluateFilterCondition(actualValue: string | boolean, filterCondition: TableColumnFilterDto): boolean {
const expectedValue = filterCondition.expectedValue || ''
const operator = filterCondition.operator || VCOperator.Equals
// Handle boolean values (from checkbox columns)
const normalizedActual = typeof actualValue === 'boolean' ? String(actualValue) : actualValue
switch (operator) {
case VCOperator.Equals:
return normalizedActual.toLowerCase() === expectedValue.toLowerCase()
case VCOperator.NotEquals:
return normalizedActual.toLowerCase() !== expectedValue.toLowerCase()
case VCOperator.IsEmpty:
return normalizedActual.trim() === ''
case VCOperator.IsNotEmpty:
return normalizedActual.trim() !== ''
default:
return true
}
}
function getAllowedValuesFromConstraintTable(
constraintTable: FormElementDto,
keyValue: string,
keyColumnIndex: number,
valueColumnIndex: number
): (string | boolean | string[])[] {
const tableData = parseTableData(constraintTable.options)
const allowedValues: (string | boolean | string[])[] = []
tableData.forEach((row) => {
const keyCell = row[keyColumnIndex]
const keyCellStr = Array.isArray(keyCell) ? keyCell[0] : typeof keyCell === 'boolean' ? String(keyCell) : keyCell
if (keyCellStr?.toLowerCase() === keyValue.toLowerCase()) {
const value = row[valueColumnIndex]
if (value !== undefined && !allowedValues.includes(value)) {
allowedValues.push(value)
}
}
})
return allowedValues
}
return {
getReferencedColumnValues,
getConstrainedColumnValues,
applyRowPresets
}
}

View File

@@ -28,7 +28,8 @@
"addRow": "Zeile hinzufügen", "addRow": "Zeile hinzufügen",
"removeRow": "Zeile entfernen", "removeRow": "Zeile entfernen",
"emptyValue": "Keine Eingabe", "emptyValue": "Keine Eingabe",
"noData": "Keine Daten vorhanden" "noData": "Keine Daten vorhanden",
"selectValue": "Wert auswählen"
} }
}, },
"status": { "status": {

View File

@@ -28,7 +28,8 @@
"addRow": "Add row", "addRow": "Add row",
"removeRow": "Remove row", "removeRow": "Remove row",
"emptyValue": "No input", "emptyValue": "No input",
"noData": "No data available" "noData": "No data available",
"selectValue": "Select value"
} }
}, },
"status": { "status": {