195 lines
6.5 KiB
TypeScript
195 lines
6.5 KiB
TypeScript
/**
|
||
* 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
|
||
}
|
||
}
|