sozsoft-platform/ui/src/views/list/shared/cascade.ts

196 lines
6.5 KiB
TypeScript
Raw Normal View History

/**
* Lookup cascade (bağımlı açılır liste) davranışı.
* Grid, Tree ve form sayfası aynı kuralları paylaşır.
*/
import type { ColumnFormatDto, GridDto } from '@/proxy/form/models'
2026-10-07 22:07:28 +00:00
import { getActiveEditingForm, getEditingRow } from './editingForm'
type CascadeInfo = {
parentFields: string[]
}
export type CascadeMaps = {
/** child fieldName -> cascade bilgisi */
cascadeFieldsMap: Map<string, CascadeInfo>
/** parentField -> etkilediği child alan adları */
parentToChildrenMap: Map<string, Set<string>>
/** fieldName -> değişince boşaltılacak alanlar */
clearFieldsMap: Map<string, string[]>
}
const splitFields = (value?: string | null) =>
(value ?? '')
.split(',')
.map((field) => field.trim())
.filter(Boolean)
export const getCascadeParentFieldsOf = (colFormat?: Pick<ColumnFormatDto, 'lookupDto'>) =>
splitFields(colFormat?.lookupDto?.cascadeParentFields)
/**
* Bir alan değiştiğinde boşaltılacak alanlar: ebeveyni bu alan olan çocuklar
* (`CascadeParentFields`) ile alanın kendi `CascadeEmptyFields` listesi.
*/
export const getCascadeClearFields = (
columns: (Pick<ColumnFormatDto, 'fieldName' | 'lookupDto'> | undefined)[],
fieldName: string,
) => {
const fields = new Set<string>()
columns.forEach((column) => {
if (!column?.fieldName) return
if (column.fieldName === fieldName) {
splitFields(column.lookupDto?.cascadeEmptyFields).forEach((field) => fields.add(field))
} else if (getCascadeParentFieldsOf(column).includes(fieldName)) {
fields.add(column.fieldName)
}
})
fields.delete(fieldName)
return [...fields]
}
/** Ebeveynlerinden biri boşsa çocuk kilitlenir (bool ebeveynde `false` da boştur). */
export const isCascadeParentEmpty = (parentFields: string[], data?: Record<string, any>) =>
parentFields.some((parentField) => !data?.[parentField])
/** Ekranın cascade ilişkilerini tek geçişte çıkarır; `clearFieldsMap` `getCascadeClearFields` ile aynıdır. */
export const createCascadeMaps = (gridDto?: GridDto): CascadeMaps => {
const cascadeFieldsMap = new Map<string, CascadeInfo>()
const parentToChildrenMap = new Map<string, Set<string>>()
const clearFieldSets = new Map<string, Set<string>>()
const addClearField = (fieldName: string, clearField: string) => {
if (fieldName === clearField) return
const fields = clearFieldSets.get(fieldName) ?? new Set<string>()
fields.add(clearField)
clearFieldSets.set(fieldName, fields)
}
gridDto?.columnFormats.forEach((col) => {
const fieldName = col.fieldName
if (!fieldName) return
splitFields(col.lookupDto?.cascadeEmptyFields).forEach((field) =>
addClearField(fieldName, field),
)
const parentFields = getCascadeParentFieldsOf(col)
if (!parentFields.length) return
cascadeFieldsMap.set(fieldName, { parentFields })
parentFields.forEach((parentField) => {
const children = parentToChildrenMap.get(parentField) ?? new Set<string>()
children.add(fieldName)
parentToChildrenMap.set(parentField, children)
addClearField(parentField, fieldName)
})
})
const clearFieldsMap = new Map<string, string[]>(
[...clearFieldSets].map(([fieldName, fields]) => [fieldName, [...fields]]),
)
return { cascadeFieldsMap, parentToChildrenMap, clearFieldsMap }
}
/** Alan cascade zincirinde (ebeveyn ya da çocuk olarak) yer alıyor mu? */
export const hasCascadeBehavior = (maps: CascadeMaps, fieldName: string) =>
maps.cascadeFieldsMap.has(fieldName) ||
maps.parentToChildrenMap.has(fieldName) ||
maps.clearFieldsMap.has(fieldName)
/** Parent değiştikten sonra etkilenen çocuk editörlerin kilidini yeniler. */
const updateChildDisabledStates = (
grid: any,
gridDto: GridDto,
maps: CascadeMaps,
children: Iterable<string>,
rowData: Record<string, any>,
) => {
const form = getActiveEditingForm(grid)
if (!form?.getEditor) return
for (const childFieldName of children) {
const childInfo = maps.cascadeFieldsMap.get(childFieldName)
if (!childInfo) continue
try {
const childColumn = gridDto.columnFormats.find(
(column) => column.fieldName === childFieldName,
)
const childEditorOptions = childColumn?.editorOptions
? JSON.parse(childColumn.editorOptions)
: {}
form
.getEditor(childFieldName)
?.option(
'disabled',
childEditorOptions?.disabled === true ||
isCascadeParentEmpty(childInfo.parentFields, rowData),
)
} catch (err) {
console.error(`Cascade child editor güncellenemedi: "${childFieldName}"`, err)
}
}
}
/**
* Bir alanın değeri değiştiğinde cascade kurallarını düzenlenen satıra uygular:
* bağımlı alanları boşaltır ve çocuk editörlerin kilidini yeniler. Grid editörleri
* (`onEditorPreparing`) ve platform editörleri (TagBox, GridBox…) aynı yoldan geçer.
*/
export const applyCascadeChange = (
grid: any,
gridDto: GridDto,
maps: CascadeMaps,
fieldName: string,
value: any,
) => {
if (!grid?.getVisibleRows) return
const { rowIndex, rowData } = getEditingRow(grid)
const clearFields = maps.clearFieldsMap.get(fieldName) ?? []
if (rowIndex >= 0) {
clearFields.forEach((childField) => grid.cellValue(rowIndex, childField, null))
}
const affectedChildren = maps.parentToChildrenMap.get(fieldName)
if (!affectedChildren?.size || !rowData) return
const nextRowData = {
...rowData,
[fieldName]: value,
...Object.fromEntries(clearFields.map((field) => [field, null])),
}
updateChildDisabledStates(grid, gridDto, maps, affectedChildren, nextRowData)
}
type AttachCascadeParams = {
editor: any
gridDto: GridDto
maps: CascadeMaps
}
/**
* `onEditorPreparing` içinde cascade davranışını editöre bağlar:
* - değer değişince bağımlı alanları temizler ve çocukların kilidini yeniler,
* - form ilk açıldığında parent'ı boş olan alanı devre dışı bırakır.
*/
export const attachCascadeBehavior = ({ editor, gridDto, maps }: AttachCascadeParams) => {
const fieldName = editor.dataField as string
const previousHandler = editor.editorOptions.onValueChanged
editor.editorOptions.onValueChanged = (e: any) => {
previousHandler?.(e)
applyCascadeChange(editor.component, gridDto, maps, fieldName, e.value)
}
const cascadeInfo = maps.cascadeFieldsMap.get(fieldName)
const { rowData } = getEditingRow(editor.component)
if (cascadeInfo && rowData && isCascadeParentEmpty(cascadeInfo.parentFields, rowData)) {
editor.editorOptions.disabled = true
}
}