feat(fullstack): Add per-row visibility, add read-only cells, update seeds

This commit is contained in:
2026-03-01 09:00:07 +01:00
parent f72923f945
commit fea2ca8a3b
10 changed files with 283 additions and 106 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

View File

@@ -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

View File

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

View File

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