sozsoft-platform/ui/src/views/list/shared/cascade.ts
2026-10-08 01:07:28 +03:00

195 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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'
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
}
}