feat(fullstack): Add per-row visibility, add read-only cells, update seeds
This commit is contained in:
@@ -22,6 +22,8 @@
|
||||
:disabled="disabled"
|
||||
:can-modify-rows="canModifyRows"
|
||||
:get-column-options="getColumnOptions"
|
||||
:read-only-column-indices="readOnlyColumnIndices"
|
||||
:is-cell-visible="isCellVisible"
|
||||
@update:cell="updateCell"
|
||||
@update:cell-value="updateCellValue"
|
||||
@update:checkbox-cell="updateCheckboxCell"
|
||||
@@ -60,6 +62,8 @@
|
||||
:disabled="disabled"
|
||||
:can-modify-rows="canModifyRows"
|
||||
:get-column-options="getColumnOptions"
|
||||
:read-only-column-indices="readOnlyColumnIndices"
|
||||
:is-cell-visible="isCellVisible"
|
||||
add-row-button-class="mt-4"
|
||||
@update:cell="updateCell"
|
||||
@update:cell-value="updateCellValue"
|
||||
@@ -151,6 +155,44 @@ const visibleColumns = computed<VisibleColumn[]>(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const readOnlyColumnIndices = computed<Set<number>>(() => {
|
||||
if (!props.allFormElements) return new Set()
|
||||
|
||||
return new Set(
|
||||
props.formOptions
|
||||
.map((option, index) => ({ option, index }))
|
||||
.filter(({ option }) => {
|
||||
const conditions = option.columnConfig?.readOnlyConditions
|
||||
return conditions && isFormOptionVisible(conditions, props.allFormElements!)
|
||||
})
|
||||
.map(({ index }) => index)
|
||||
)
|
||||
})
|
||||
|
||||
// When columns become read-only, reset their values to the configured default
|
||||
watch(
|
||||
readOnlyColumnIndices,
|
||||
(currentSet, previousSet) => {
|
||||
const newlyReadOnlyIndices = [...currentSet].filter((i) => !previousSet?.has(i))
|
||||
if (newlyReadOnlyIndices.length === 0) return
|
||||
|
||||
const updatedOptions = props.formOptions.map((option, colIndex) => {
|
||||
if (!newlyReadOnlyIndices.includes(colIndex)) return option
|
||||
|
||||
const columnValues = parseColumnValues(option.value)
|
||||
const defaultValue = isColumnCheckbox(colIndex) ? false : (option.columnConfig?.readOnlyDefaultValue ?? '')
|
||||
const newValue = JSON.stringify(columnValues.map(() => defaultValue))
|
||||
|
||||
return newValue !== option.value ? { ...option, value: newValue } : option
|
||||
})
|
||||
|
||||
if (updatedOptions.some((opt, i) => opt !== props.formOptions[i])) {
|
||||
emit('update:formOptions', updatedOptions)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const dataColumns = computed<DataColumn[]>(() =>
|
||||
visibleColumns.value.map(({ originalIndex }) => ({
|
||||
key: `col_${originalIndex}`,
|
||||
@@ -179,23 +221,16 @@ const tableData = computed<TableRowData[]>(() => {
|
||||
if (props.formOptions.length === 0) return []
|
||||
|
||||
const columnData: CellValue[][] = props.formOptions.map((option, colIndex) => {
|
||||
try {
|
||||
const parsed = JSON.parse(option.value || '[]')
|
||||
if (!Array.isArray(parsed)) return []
|
||||
const parsed = parseColumnValues(option.value)
|
||||
|
||||
// For multi-select columns, each cell value is already an array
|
||||
// For checkbox columns, each cell value is a boolean
|
||||
// For single-select columns, each cell value is a string
|
||||
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 []
|
||||
// Normalize cell values based on column type
|
||||
if (isColumnMultipleAllowed(colIndex)) {
|
||||
return parsed.map((val) => (Array.isArray(val) ? val : []))
|
||||
}
|
||||
if (isColumnCheckbox(colIndex)) {
|
||||
return parsed.map((val) => val === true)
|
||||
}
|
||||
return parsed
|
||||
})
|
||||
|
||||
const rowCount = Math.max(...columnData.map((col) => col.length), 0)
|
||||
@@ -266,14 +301,7 @@ function updateCell(rowIndex: number, columnKey: string, value: string) {
|
||||
const updatedOptions = props.formOptions.map((option, index) => {
|
||||
if (index !== colIndex) return option
|
||||
|
||||
let columnValues: CellValue[]
|
||||
try {
|
||||
columnValues = JSON.parse(option.value || '[]')
|
||||
if (!Array.isArray(columnValues)) columnValues = []
|
||||
} catch {
|
||||
columnValues = []
|
||||
}
|
||||
|
||||
const columnValues = parseColumnValues(option.value)
|
||||
while (columnValues.length <= rowIndex) {
|
||||
columnValues.push('')
|
||||
}
|
||||
@@ -289,14 +317,7 @@ function updateCellValue(rowIndex: number, _columnKey: string, colIndex: number,
|
||||
const updatedOptions = props.formOptions.map((option, index) => {
|
||||
if (index !== colIndex) return option
|
||||
|
||||
let columnValues: CellValue[]
|
||||
try {
|
||||
columnValues = JSON.parse(option.value || '[]')
|
||||
if (!Array.isArray(columnValues)) columnValues = []
|
||||
} catch {
|
||||
columnValues = []
|
||||
}
|
||||
|
||||
const columnValues = parseColumnValues(option.value)
|
||||
const isMultiple = isColumnMultipleAllowed(colIndex)
|
||||
while (columnValues.length <= rowIndex) {
|
||||
columnValues.push(isMultiple ? [] : '')
|
||||
@@ -313,14 +334,7 @@ function updateCheckboxCell(rowIndex: number, colIndex: number, value: boolean)
|
||||
const updatedOptions = props.formOptions.map((option, index) => {
|
||||
if (index !== colIndex) return option
|
||||
|
||||
let columnValues: CellValue[]
|
||||
try {
|
||||
columnValues = JSON.parse(option.value || '[]')
|
||||
if (!Array.isArray(columnValues)) columnValues = []
|
||||
} catch {
|
||||
columnValues = []
|
||||
}
|
||||
|
||||
const columnValues = parseColumnValues(option.value)
|
||||
while (columnValues.length <= rowIndex) {
|
||||
columnValues.push(false)
|
||||
}
|
||||
@@ -334,24 +348,18 @@ function updateCheckboxCell(rowIndex: number, colIndex: number, value: boolean)
|
||||
|
||||
function addRow() {
|
||||
const updatedOptions = props.formOptions.map((option, colIndex) => {
|
||||
let columnValues: CellValue[]
|
||||
try {
|
||||
columnValues = JSON.parse(option.value || '[]')
|
||||
if (!Array.isArray(columnValues)) columnValues = []
|
||||
} catch {
|
||||
columnValues = []
|
||||
}
|
||||
const columnValues = parseColumnValues(option.value)
|
||||
|
||||
// For multi-select columns, initialize with empty array
|
||||
// For checkbox columns, initialize with false
|
||||
// Otherwise empty string
|
||||
let emptyValue: CellValue = ''
|
||||
if (isColumnMultipleAllowed(colIndex)) {
|
||||
emptyValue = []
|
||||
// Determine initial value based on column type
|
||||
let initialValue: CellValue = ''
|
||||
if (readOnlyColumnIndices.value.has(colIndex)) {
|
||||
initialValue = isColumnCheckbox(colIndex) ? false : (option.columnConfig?.readOnlyDefaultValue ?? '')
|
||||
} else if (isColumnMultipleAllowed(colIndex)) {
|
||||
initialValue = []
|
||||
} else if (isColumnCheckbox(colIndex)) {
|
||||
emptyValue = false
|
||||
initialValue = false
|
||||
}
|
||||
columnValues.push(emptyValue)
|
||||
columnValues.push(initialValue)
|
||||
|
||||
return { ...option, value: JSON.stringify(columnValues) }
|
||||
})
|
||||
@@ -361,19 +369,44 @@ function addRow() {
|
||||
|
||||
function removeRow(rowIndex: number) {
|
||||
const updatedOptions = props.formOptions.map((option) => {
|
||||
let columnValues: CellValue[]
|
||||
try {
|
||||
columnValues = JSON.parse(option.value || '[]')
|
||||
if (!Array.isArray(columnValues)) columnValues = []
|
||||
} catch {
|
||||
columnValues = []
|
||||
}
|
||||
|
||||
const columnValues = parseColumnValues(option.value)
|
||||
columnValues.splice(rowIndex, 1)
|
||||
|
||||
return { ...option, value: JSON.stringify(columnValues) }
|
||||
})
|
||||
|
||||
emit('update:formOptions', updatedOptions)
|
||||
}
|
||||
|
||||
function isCellVisible(colIndex: number, rowData: TableRowData): boolean {
|
||||
const option = props.formOptions[colIndex]
|
||||
const rowVisibility = option?.columnConfig?.rowVisibilityCondition
|
||||
if (!rowVisibility) return true
|
||||
|
||||
const { sourceColumnIndex, expectedValues, operator } = rowVisibility
|
||||
const sourceKey = `col_${sourceColumnIndex}`
|
||||
const cellValue = rowData[sourceKey]
|
||||
|
||||
let sourceValues: string[] = []
|
||||
if (Array.isArray(cellValue)) {
|
||||
sourceValues = cellValue
|
||||
} else if (typeof cellValue === 'string' && cellValue) {
|
||||
sourceValues = cellValue.split(',').map((v) => v.trim())
|
||||
}
|
||||
|
||||
if (operator === 'CONTAINS' || operator === 'EQUALS') {
|
||||
return (expectedValues ?? []).some((expected) =>
|
||||
sourceValues.some((v) => v.toLowerCase() === expected.toLowerCase())
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function parseColumnValues(value: string | undefined): CellValue[] {
|
||||
try {
|
||||
const parsed = JSON.parse(value || '[]')
|
||||
return Array.isArray(parsed) ? parsed : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,54 +2,64 @@
|
||||
<div>
|
||||
<UTable :data="tableData" :columns="tableColumns" class="w-full" :ui="{ td: 'p-2' }">
|
||||
<template v-for="col in dataColumns" :key="col.key" #[`${col.key}-cell`]="slotProps">
|
||||
<!-- Column with cross-reference -->
|
||||
<USelectMenu
|
||||
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"
|
||||
: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)
|
||||
<span
|
||||
v-if="
|
||||
props.isCellVisible &&
|
||||
!props.isCellVisible(col.colIndex, (slotProps.row as TableRow<TableRowData>).original)
|
||||
"
|
||||
/>
|
||||
<!-- 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)"
|
||||
<template v-else>
|
||||
<!-- Column with cross-reference -->
|
||||
<USelectMenu
|
||||
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"
|
||||
:placeholder="$t('applicationForms.formElements.table.selectValue')"
|
||||
:multiple="isColumnMultipleAllowed(col.colIndex)"
|
||||
class="w-full min-w-32"
|
||||
@update:model-value="
|
||||
(val: boolean | 'indeterminate') =>
|
||||
$emit(
|
||||
'update:checkboxCell',
|
||||
(slotProps.row as TableRow<TableRowData>).index,
|
||||
col.colIndex,
|
||||
val === true
|
||||
)
|
||||
(val: string | string[]) =>
|
||||
$emit('update:cellValue', (slotProps.row as TableRow<TableRowData>).index, col.key, col.colIndex, val)
|
||||
"
|
||||
/>
|
||||
</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))
|
||||
"
|
||||
/>
|
||||
<!-- 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') =>
|
||||
$emit(
|
||||
'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 v-if="canModifyRows" #actions-cell="{ row }">
|
||||
<UButton
|
||||
@@ -102,6 +112,8 @@ const props = defineProps<{
|
||||
canModifyRows: boolean
|
||||
addRowButtonClass?: string
|
||||
getColumnOptions: (colIndex: number, currentRowData?: TableRowData) => string[]
|
||||
readOnlyColumnIndices?: Set<number>
|
||||
isCellVisible?: (colIndex: number, rowData: TableRowData) => boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
@@ -119,7 +131,7 @@ function hasColumnReference(colIndex: number): boolean {
|
||||
|
||||
function isColumnReadOnly(colIndex: number): boolean {
|
||||
const option = props.formOptions[colIndex]
|
||||
return option?.columnConfig?.isReadOnly === true
|
||||
return option?.columnConfig?.isReadOnly === true || (props.readOnlyColumnIndices?.has(colIndex) ?? false)
|
||||
}
|
||||
|
||||
function isColumnMultipleAllowed(colIndex: number): boolean {
|
||||
|
||||
Reference in New Issue
Block a user