2026-08-06 22:54:50 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Lookup cascade (bağımlı açılır liste) davranışı.
|
2026-10-07 18:08:10 +00:00
|
|
|
|
* Grid, Tree ve form sayfası aynı kuralları paylaşır.
|
2026-08-06 22:54:50 +00:00
|
|
|
|
*/
|
2026-10-07 18:08:10 +00:00
|
|
|
|
import type { ColumnFormatDto, GridDto } from '@/proxy/form/models'
|
2026-10-07 22:07:28 +00:00
|
|
|
|
import { getActiveEditingForm, getEditingRow } from './editingForm'
|
2026-08-06 22:54:50 +00:00
|
|
|
|
|
2026-10-07 18:08:10 +00:00
|
|
|
|
type CascadeInfo = {
|
2026-08-06 22:54:50 +00:00
|
|
|
|
parentFields: string[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export type CascadeMaps = {
|
2026-10-07 18:08:10 +00:00
|
|
|
|
/** child fieldName -> cascade bilgisi */
|
2026-08-06 22:54:50 +00:00
|
|
|
|
cascadeFieldsMap: Map<string, CascadeInfo>
|
|
|
|
|
|
/** parentField -> etkilediği child alan adları */
|
|
|
|
|
|
parentToChildrenMap: Map<string, Set<string>>
|
2026-10-07 18:08:10 +00:00
|
|
|
|
/** fieldName -> değişince boşaltılacak alanlar */
|
|
|
|
|
|
clearFieldsMap: Map<string, string[]>
|
2026-08-06 22:54:50 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const splitFields = (value?: string | null) =>
|
|
|
|
|
|
(value ?? '')
|
|
|
|
|
|
.split(',')
|
|
|
|
|
|
.map((field) => field.trim())
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
|
2026-10-07 18:08:10 +00:00
|
|
|
|
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. */
|
2026-08-06 22:54:50 +00:00
|
|
|
|
export const createCascadeMaps = (gridDto?: GridDto): CascadeMaps => {
|
|
|
|
|
|
const cascadeFieldsMap = new Map<string, CascadeInfo>()
|
|
|
|
|
|
const parentToChildrenMap = new Map<string, Set<string>>()
|
2026-10-07 18:08:10 +00:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
2026-08-06 22:54:50 +00:00
|
|
|
|
|
|
|
|
|
|
gridDto?.columnFormats.forEach((col) => {
|
2026-10-07 18:08:10 +00:00
|
|
|
|
const fieldName = col.fieldName
|
|
|
|
|
|
if (!fieldName) return
|
2026-08-06 22:54:50 +00:00
|
|
|
|
|
2026-10-07 18:08:10 +00:00
|
|
|
|
splitFields(col.lookupDto?.cascadeEmptyFields).forEach((field) =>
|
|
|
|
|
|
addClearField(fieldName, field),
|
|
|
|
|
|
)
|
2026-08-06 22:54:50 +00:00
|
|
|
|
|
2026-10-07 18:08:10 +00:00
|
|
|
|
const parentFields = getCascadeParentFieldsOf(col)
|
|
|
|
|
|
if (!parentFields.length) return
|
2026-08-06 22:54:50 +00:00
|
|
|
|
|
2026-10-07 18:08:10 +00:00
|
|
|
|
cascadeFieldsMap.set(fieldName, { parentFields })
|
2026-08-06 22:54:50 +00:00
|
|
|
|
parentFields.forEach((parentField) => {
|
|
|
|
|
|
const children = parentToChildrenMap.get(parentField) ?? new Set<string>()
|
2026-10-07 18:08:10 +00:00
|
|
|
|
children.add(fieldName)
|
2026-08-06 22:54:50 +00:00
|
|
|
|
parentToChildrenMap.set(parentField, children)
|
2026-10-07 18:08:10 +00:00
|
|
|
|
addClearField(parentField, fieldName)
|
2026-08-06 22:54:50 +00:00
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-10-07 18:08:10 +00:00
|
|
|
|
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)
|
2026-08-06 22:54:50 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type AttachCascadeParams = {
|
|
|
|
|
|
editor: any
|
|
|
|
|
|
gridDto: GridDto
|
|
|
|
|
|
maps: CascadeMaps
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* `onEditorPreparing` içinde cascade davranışını editöre bağlar:
|
2026-10-07 18:08:10 +00:00
|
|
|
|
* - değer değişince bağımlı alanları temizler ve çocukların kilidini yeniler,
|
2026-08-06 22:54:50 +00:00
|
|
|
|
* - form ilk açıldığında parent'ı boş olan alanı devre dışı bırakır.
|
|
|
|
|
|
*/
|
2026-10-07 18:08:10 +00:00
|
|
|
|
export const attachCascadeBehavior = ({ editor, gridDto, maps }: AttachCascadeParams) => {
|
|
|
|
|
|
const fieldName = editor.dataField as string
|
2026-08-06 22:54:50 +00:00
|
|
|
|
const previousHandler = editor.editorOptions.onValueChanged
|
|
|
|
|
|
|
|
|
|
|
|
editor.editorOptions.onValueChanged = (e: any) => {
|
|
|
|
|
|
previousHandler?.(e)
|
2026-10-07 18:08:10 +00:00
|
|
|
|
applyCascadeChange(editor.component, gridDto, maps, fieldName, e.value)
|
2026-08-06 22:54:50 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-07 18:08:10 +00:00
|
|
|
|
const cascadeInfo = maps.cascadeFieldsMap.get(fieldName)
|
|
|
|
|
|
const { rowData } = getEditingRow(editor.component)
|
|
|
|
|
|
if (cascadeInfo && rowData && isCascadeParentEmpty(cascadeInfo.parentFields, rowData)) {
|
2026-08-06 22:54:50 +00:00
|
|
|
|
editor.editorOptions.disabled = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|