feat: Add table logic for role and permission sections

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

View File

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

View File

@@ -1,13 +1,17 @@
import type { FormElementDto, VisibilityConditionOperator } from '~~/.api-client'
import type { FormElementDto, FormElementVisibilityCondition, VisibilityConditionOperator } from '~~/.api-client'
import { VisibilityConditionOperator as VCOperator, VisibilityConditionType as VCType } from '~~/.api-client'
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
}

View File

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

View File

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