feat(fullstack): Add per-row visibility, add read-only cells, update seeds
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -251,6 +251,12 @@ formElementSubSections:
|
|||||||
employeeDataCategory: SENSITIVE
|
employeeDataCategory: SENSITIVE
|
||||||
columnConfig:
|
columnConfig:
|
||||||
isCheckbox: true
|
isCheckbox: true
|
||||||
|
rowVisibilityCondition:
|
||||||
|
sourceColumnIndex: 5
|
||||||
|
expectedValues:
|
||||||
|
- "Aggregiert (Team)"
|
||||||
|
- "Aggregiert (Abteilung)"
|
||||||
|
operator: CONTAINS
|
||||||
- value: '["N/A", "Min. 5 Personen im Vergleich", "Min. 10 Personen pro Auswertung"]'
|
- 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
|
||||||
|
|||||||
@@ -76,6 +76,29 @@ formElementSubSections:
|
|||||||
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
|
||||||
@@ -405,6 +428,12 @@ formElementSubSections:
|
|||||||
employeeDataCategory: SENSITIVE
|
employeeDataCategory: SENSITIVE
|
||||||
columnConfig:
|
columnConfig:
|
||||||
isCheckbox: true
|
isCheckbox: true
|
||||||
|
rowVisibilityCondition:
|
||||||
|
sourceColumnIndex: 5
|
||||||
|
expectedValues:
|
||||||
|
- "Aggregiert (Team)"
|
||||||
|
- "Aggregiert (Abteilung)"
|
||||||
|
operator: CONTAINS
|
||||||
- value: '[]'
|
- value: '[]'
|
||||||
label: Mindestgruppe/Schwelle
|
label: Mindestgruppe/Schwelle
|
||||||
processingPurpose: DATA_ANALYSIS
|
processingPurpose: DATA_ANALYSIS
|
||||||
|
|||||||
@@ -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
|
|
||||||
// For single-select columns, each cell value is a string
|
|
||||||
if (isColumnMultipleAllowed(colIndex)) {
|
if (isColumnMultipleAllowed(colIndex)) {
|
||||||
return parsed.map((val: CellValue) => (Array.isArray(val) ? val : []))
|
return parsed.map((val) => (Array.isArray(val) ? val : []))
|
||||||
}
|
}
|
||||||
if (isColumnCheckbox(colIndex)) {
|
if (isColumnCheckbox(colIndex)) {
|
||||||
return parsed.map((val: CellValue) => val === true)
|
return parsed.map((val) => val === true)
|
||||||
}
|
}
|
||||||
return parsed
|
return parsed
|
||||||
} catch {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
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>
|
||||||
|
|||||||
@@ -2,6 +2,15 @@
|
|||||||
<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">
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
props.isCellVisible &&
|
||||||
|
!props.isCellVisible(col.colIndex, (slotProps.row as TableRow<TableRowData>).original)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</span>
|
||||||
|
<template v-else>
|
||||||
<!-- Column with cross-reference -->
|
<!-- Column with cross-reference -->
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
v-if="hasColumnReference(col.colIndex)"
|
v-if="hasColumnReference(col.colIndex)"
|
||||||
@@ -51,6 +60,7 @@
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
</template>
|
||||||
<template v-if="canModifyRows" #actions-cell="{ row }">
|
<template v-if="canModifyRows" #actions-cell="{ row }">
|
||||||
<UButton
|
<UButton
|
||||||
v-if="!disabled"
|
v-if="!disabled"
|
||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user