feat: Add table logic for role and permission sections
This commit is contained in:
@@ -13,6 +13,8 @@
|
||||
:is="getResolvedComponent(formElementItem.formElement)"
|
||||
:form-options="formElementItem.formElement.options"
|
||||
:disabled="props.disabled"
|
||||
:all-form-elements="props.allFormElements"
|
||||
:table-row-preset="formElementItem.formElement.tableRowPreset"
|
||||
@update:form-options="updateFormOptions($event, formElementItem)"
|
||||
/>
|
||||
<div v-if="formElementItem.formElement.isClonable && !props.disabled" class="mt-3">
|
||||
@@ -102,6 +104,7 @@ const props = defineProps<{
|
||||
visibilityMap: Map<string, boolean>
|
||||
applicationFormId?: string
|
||||
disabled?: boolean
|
||||
allFormElements?: FormElementDto[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
:visibility-map="visibilityMap"
|
||||
:application-form-id="applicationFormId"
|
||||
:disabled="disabled"
|
||||
:all-form-elements="allFormElements"
|
||||
@update:model-value="
|
||||
(elements) =>
|
||||
handleFormElementUpdate(elements, getSubsectionKey(currentFormElementSection, sectionIndex, subsection))
|
||||
@@ -107,9 +108,9 @@ const { cloneElement } = useClonableElements()
|
||||
const previousVisibilityMap = ref<Map<string, boolean>>(new Map())
|
||||
|
||||
const allFormElements = computed(() => {
|
||||
return props.formElementSections.flatMap(
|
||||
(section) => section.formElementSubSections?.flatMap((subsection) => subsection.formElements) ?? []
|
||||
)
|
||||
return props.formElementSections
|
||||
.filter((section) => section.isTemplate !== true)
|
||||
.flatMap((section) => section.formElementSubSections?.flatMap((subsection) => subsection.formElements) ?? [])
|
||||
})
|
||||
|
||||
const visibilityMap = computed(() => {
|
||||
|
||||
@@ -1,8 +1,39 @@
|
||||
<template>
|
||||
<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">
|
||||
<!-- 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
|
||||
v-else
|
||||
:model-value="getCellValue(slotProps.row as TableRow<TableRowData>, col.key)"
|
||||
:disabled="disabled"
|
||||
class="w-full min-w-32"
|
||||
@@ -11,7 +42,7 @@
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #actions-cell="{ row }">
|
||||
<template v-if="canModifyRows" #actions-cell="{ row }">
|
||||
<UButton
|
||||
v-if="!disabled"
|
||||
icon="i-lucide-trash-2"
|
||||
@@ -28,26 +59,61 @@
|
||||
{{ $t('applicationForms.formElements.table.noData') }}
|
||||
</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') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 { useTableCrossReferences } from '~/composables/useTableCrossReferences'
|
||||
|
||||
const props = defineProps<{
|
||||
formOptions: FormOptionDto[]
|
||||
disabled?: boolean
|
||||
allFormElements?: FormElementDto[]
|
||||
tableRowPreset?: TableRowPresetDto
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(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 {
|
||||
key: string
|
||||
@@ -67,7 +133,8 @@ const tableColumns = computed<TableColumn<TableRowData>[]>(() => {
|
||||
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({
|
||||
id: 'actions',
|
||||
header: ''
|
||||
@@ -80,10 +147,21 @@ const tableColumns = computed<TableColumn<TableRowData>[]>(() => {
|
||||
const tableData = computed<TableRowData[]>(() => {
|
||||
if (props.formOptions.length === 0) return []
|
||||
|
||||
const columnData: string[][] = props.formOptions.map((option) => {
|
||||
const columnData: CellValue[][] = props.formOptions.map((option, colIndex) => {
|
||||
try {
|
||||
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 {
|
||||
return []
|
||||
}
|
||||
@@ -94,8 +172,15 @@ const tableData = computed<TableRowData[]>(() => {
|
||||
const rows: TableRowData[] = []
|
||||
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
||||
const row: TableRowData = {}
|
||||
props.formOptions.forEach((_, colIndex) => {
|
||||
row[`col_${colIndex}`] = columnData[colIndex]?.[rowIndex] ?? ''
|
||||
props.formOptions.forEach((option, colIndex) => {
|
||||
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)
|
||||
}
|
||||
@@ -103,8 +188,81 @@ const tableData = computed<TableRowData[]>(() => {
|
||||
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 {
|
||||
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) {
|
||||
@@ -113,7 +271,7 @@ function updateCell(rowIndex: number, columnKey: string, value: string) {
|
||||
const updatedOptions = props.formOptions.map((option, index) => {
|
||||
if (index !== colIndex) return option
|
||||
|
||||
let columnValues: string[]
|
||||
let columnValues: CellValue[]
|
||||
try {
|
||||
columnValues = JSON.parse(option.value || '[]')
|
||||
if (!Array.isArray(columnValues)) columnValues = []
|
||||
@@ -132,9 +290,11 @@ function updateCell(rowIndex: number, columnKey: string, value: string) {
|
||||
emit('update:formOptions', updatedOptions)
|
||||
}
|
||||
|
||||
function addRow() {
|
||||
const updatedOptions = props.formOptions.map((option) => {
|
||||
let columnValues: string[]
|
||||
function updateCellValue(rowIndex: number, columnKey: string, colIndex: number, value: string | 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 = []
|
||||
@@ -142,7 +302,61 @@ function addRow() {
|
||||
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) }
|
||||
})
|
||||
@@ -152,7 +366,7 @@ function addRow() {
|
||||
|
||||
function removeRow(rowIndex: number) {
|
||||
const updatedOptions = props.formOptions.map((option) => {
|
||||
let columnValues: string[]
|
||||
let columnValues: CellValue[]
|
||||
try {
|
||||
columnValues = JSON.parse(option.value || '[]')
|
||||
if (!Array.isArray(columnValues)) columnValues = []
|
||||
|
||||
@@ -9,3 +9,4 @@ export { useUser } from './user/useUser'
|
||||
export { useUserApi } from './user/useUserApi'
|
||||
export { useSectionSpawning } from './useSectionSpawning'
|
||||
export { useClonableElements } from './useClonableElements'
|
||||
export { useTableCrossReferences } from './useTableCrossReferences'
|
||||
|
||||
@@ -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'
|
||||
|
||||
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> {
|
||||
const formElementsByRef = buildFormElementsMap(allFormElements)
|
||||
const visibilityMap = new Map<string, boolean>()
|
||||
|
||||
allFormElements.forEach((element) => {
|
||||
const isVisible = isElementVisible(element, formElementsByRef, visibilityMap)
|
||||
const isVisible = isElementVisible(element, formElementsByRef)
|
||||
const key = element.id || element.reference
|
||||
if (key) {
|
||||
visibilityMap.set(key, isVisible)
|
||||
@@ -27,24 +31,33 @@ export function useFormElementVisibility() {
|
||||
return map
|
||||
}
|
||||
|
||||
function isElementVisible(
|
||||
element: FormElementDto,
|
||||
formElementsByRef: Map<string, FormElementDto>,
|
||||
_visibilityMap: Map<string, boolean>
|
||||
): boolean {
|
||||
if (!element.visibilityCondition) {
|
||||
/**
|
||||
* Evaluates if an element is visible based on its visibility conditions.
|
||||
* Multiple conditions use AND logic - all conditions must be met for the element to be visible.
|
||||
*/
|
||||
function isElementVisible(element: FormElementDto, formElementsByRef: Map<string, FormElementDto>): boolean {
|
||||
const conditions = element.visibilityConditions
|
||||
if (!conditions || conditions.length === 0) {
|
||||
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)
|
||||
if (!sourceElement) {
|
||||
return false
|
||||
}
|
||||
|
||||
const sourceValue = getFormElementValue(sourceElement)
|
||||
|
||||
const operator = condition.formElementOperator || VCOperator.Equals
|
||||
const conditionMet = evaluateCondition(sourceValue, condition.formElementExpectedValue, operator)
|
||||
|
||||
@@ -61,20 +74,15 @@ export function useFormElementVisibility() {
|
||||
expectedValue: string,
|
||||
operator: VisibilityConditionOperator
|
||||
): boolean {
|
||||
let result: boolean
|
||||
switch (operator) {
|
||||
case VCOperator.Equals:
|
||||
result = actualValue.toLowerCase() === expectedValue.toLowerCase()
|
||||
return result
|
||||
return actualValue.toLowerCase() === expectedValue.toLowerCase()
|
||||
case VCOperator.NotEquals:
|
||||
result = actualValue.toLowerCase() !== expectedValue.toLowerCase()
|
||||
return result
|
||||
return actualValue.toLowerCase() !== expectedValue.toLowerCase()
|
||||
case VCOperator.IsEmpty:
|
||||
result = actualValue === ''
|
||||
return result
|
||||
return actualValue === ''
|
||||
case VCOperator.IsNotEmpty:
|
||||
result = actualValue !== ''
|
||||
return result
|
||||
return actualValue !== ''
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -9,35 +9,54 @@ export function useSectionSpawning() {
|
||||
let resultSections = sections
|
||||
|
||||
for (const formElement of updatedFormElements) {
|
||||
if (!formElement.sectionSpawnTrigger || !formElement.reference) {
|
||||
const triggers = formElement.sectionSpawnTriggers
|
||||
if (!triggers || triggers.length === 0 || !formElement.reference) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract trigger configuration and current element value
|
||||
const trigger = formElement.sectionSpawnTrigger
|
||||
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:
|
||||
// 1. Condition met but no section spawned yet → create new section
|
||||
if (shouldSpawn && existingSpawnedSections.length === 0) {
|
||||
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)
|
||||
// Process each trigger independently
|
||||
for (const trigger of triggers) {
|
||||
resultSections = processSingleTrigger(resultSections, formElement, trigger, triggerValue)
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
sections: FormElementSectionDto[],
|
||||
element: FormElementDto,
|
||||
@@ -94,8 +113,30 @@ export function useSectionSpawning() {
|
||||
})
|
||||
}
|
||||
|
||||
function removeSpawnedSections(sections: FormElementSectionDto[], elementReference: string): FormElementSectionDto[] {
|
||||
return sections.filter((section) => section.spawnedFromElementReference !== elementReference || section.isTemplate)
|
||||
function findSpawnedSectionForTrigger(
|
||||
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(
|
||||
@@ -146,13 +187,6 @@ export function useSectionSpawning() {
|
||||
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(
|
||||
sections: FormElementSectionDto[],
|
||||
templateReference: string
|
||||
|
||||
269
legalconsenthub/app/composables/useTableCrossReferences.ts
Normal file
269
legalconsenthub/app/composables/useTableCrossReferences.ts
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user