sozsoft-platform/ui/src/views/list/useListFormColumns.ts

743 lines
24 KiB
TypeScript
Raw Normal View History

2026-02-24 20:44:16 +00:00
import { DataGridTypes } from 'devextreme-react/data-grid'
import { DataType, HorizontalEdge, SortOrder, ValidationRule } from 'devextreme/common'
import CustomStore from 'devextreme/data/custom_store'
import { SelectedFilterOperation } from 'devextreme/ui/data_grid'
import { useCallback, useEffect } from 'react'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { usePermission } from '@/utils/hooks/usePermission'
import { usePWA } from '@/utils/hooks/usePWA'
import { useDialogContext } from '../shared/DialogContext'
import { dynamicFetch } from '@/services/form.service'
import { GridColumnData } from './GridColumnData'
import { useLocation } from 'react-router-dom'
import {
ColumnFormatDto,
EditingFormItemDto,
GridDto,
PlatformEditorTypes,
UiCommandButtonPositionTypeEnum,
UiLookupDataSourceTypeEnum,
} from '@/proxy/form/models'
import { addCss } from './Utils'
const cellTemplateMultiValue = (
cellElement: HTMLElement,
cellInfo: DataGridTypes.ColumnCellTemplateData<any, any>,
) => {
if (cellInfo?.value) {
const values = Array.isArray(cellInfo.value)
? cellInfo.value.map((a: any) => {
const { lookup } = cellInfo.column
if (lookup && lookup.calculateCellValue) {
return lookup.calculateCellValue(a)
}
return ''
})
: [cellInfo.value]
// Badge benzeri HTML üret
const html = values
.filter((v) => v)
.map(
(v) => `
<div
style="
display:inline-block;
background-color:#dc3545;
color:#fff;
border-radius:12px;
padding:2px 8px;
margin:2px 2px;
font-size:12px;
font-weight:300;
line-height:1.4;
white-space:nowrap;
"
>
${v}
</div>
`,
)
.join('')
cellElement.innerHTML = html
cellElement.title = values.join(',')
}
}
function calculateFilterExpressionMultiValue(
this: DataGridTypes.Column,
filterValue: any,
selectedFilterOperation: string | null,
): string | Array<any> {
if (filterValue) {
if (selectedFilterOperation == '=' || selectedFilterOperation === null) {
return [this.dataField, 'contains', filterValue]
} else if (selectedFilterOperation == '<>') {
return [this.dataField, 'notcontains', filterValue]
} else {
return [this.dataField, selectedFilterOperation, filterValue]
}
} else {
// filterValue null ise isblank veya isnotblank secilmistir
if (selectedFilterOperation === '=') {
return [
this.dataField,
selectedFilterOperation,
filterValue,
'or',
this.dataField,
selectedFilterOperation,
' ',
]
} else if (selectedFilterOperation === '<>') {
return [
this.dataField,
selectedFilterOperation,
filterValue,
'and',
this.dataField,
selectedFilterOperation,
' ',
]
} else {
return [this.dataField, selectedFilterOperation, filterValue]
}
}
}
// lookup cache (module scope) - cache süresini ve boyutunu yönet
const __lookupCache = new Map<string, { promise: Promise<any[]>; timestamp: number }>()
const CACHE_DURATION = 5 * 60 * 1000 // 5 dakika
const MAX_CACHE_SIZE = 100 // Maksimum cache entry sayısı
const cachedLoader = (key: string, loader: () => Promise<any[]>) => {
const now = Date.now()
const cached = __lookupCache.get(key)
// Cache'de var ve süresi dolmamışsa kullan
if (cached && now - cached.timestamp < CACHE_DURATION) {
return cached.promise
}
// Cache boyutu limitini aşarsa en eskiyi temizle
if (__lookupCache.size >= MAX_CACHE_SIZE) {
const oldestKey = Array.from(__lookupCache.entries()).sort(
(a, b) => a[1].timestamp - b[1].timestamp,
)[0][0]
__lookupCache.delete(oldestKey)
}
const p = Promise.resolve()
.then(() => loader())
.then((res) => res ?? [])
.catch((err) => {
__lookupCache.delete(key) // hata olursa tekrar denenebilsin
throw err
})
__lookupCache.set(key, { promise: p, timestamp: now })
return p
}
const useListFormColumns = ({
gridDto,
listFormCode,
isSubForm,
gridRef,
}: {
gridDto?: GridDto
listFormCode: string
isSubForm?: boolean
gridRef?: any
}) => {
const dialog: any = useDialogContext()
const { translate } = useLocalization()
const { checkPermission } = usePermission()
const isPwaMode = usePWA()
const location = useLocation()
useEffect(() => {
// listFormCode değişince lookup cache temizlensin (farklı form farklı lookuplar)
__lookupCache.clear()
}, [listFormCode])
const lookupDataSource = useCallback(
(options: any, colData: any, listFormCode: string) => {
const { lookupDto } = colData
const filters = []
if (lookupDto.cascadeParentFields) {
if (lookupDto.dataSourceType == UiLookupDataSourceTypeEnum.StaticData) {
filters.push([
lookupDto?.cascadeRelationField,
lookupDto?.cascadeFilterOperator,
options?.data[lookupDto?.cascadeParentField],
])
//TODO: Statik data test edilecek
} else {
const data = options?.data ?? options
for (const cascadeParentField of lookupDto.cascadeParentFields.split(',')) {
filters.push(data[cascadeParentField])
}
}
}
//UiLookupDataSourceTypeEnum :
// Data = 1 (Statik Data),
// Query = 2 (API'den geliyor fakat API query çalıştırıyor)
// WebService = 3 (API servisten geliyor)
if (lookupDto.dataSourceType == UiLookupDataSourceTypeEnum.StaticData) {
return createLookupStaticDataSource(
() => JSON.parse(lookupDto?.lookupQuery),
filters.length ? filters : null,
`static:${listFormCode}:${colData.fieldName}`, // cache key
)
} else if (lookupDto.dataSourceType == UiLookupDataSourceTypeEnum.Query) {
return createLookupQueryDataSource(listFormCode, colData.fieldName, filters)
} else if (lookupDto.dataSourceType == UiLookupDataSourceTypeEnum.WebService) {
return createLookupApiDataSource(
listFormCode,
lookupDto?.lookupQuery,
filters,
colData.lookupDto?.valueExpr?.toLowerCase(),
)
} else {
return {
store: [],
}
}
},
[listFormCode],
)
const createLookupStaticDataSource = (
load: () => any,
filter: any = null,
key: any = 'static',
sort: any = 'name',
) => ({
store: new CustomStore({
key,
loadMode: 'raw',
load: async (loadOptions) => {
// load fonksiyonu sync sonuç döndürüyor olabilir, o yüzden Promise.resolve ile sar
return cachedLoader(`static:${key}`, () => Promise.resolve(load()))
},
}),
sort,
filter,
})
const createLookupQueryDataSource = (
listFormCode?: string,
listFormFieldName?: string,
filters?: any[],
) => {
return new CustomStore({
loadMode: 'raw',
load: async (loadOptions) => {
if (!isSubForm && listFormCode && !window.location.pathname.includes(listFormCode)) {
return
}
try {
const cacheKey = `query:${listFormCode}:${listFormFieldName}:${JSON.stringify(filters ?? null)}`
return cachedLoader(cacheKey, async () => {
const response = await dynamicFetch('list-form-select/lookup', 'POST', null, {
listFormCode,
listFormFieldName,
filters,
})
return (response.data ?? []).map((a: any) => ({
key: a.Key,
name: a.Name,
group: a.Group,
...a,
}))
})
} catch (error: any) {
return null
}
},
})
}
const createLookupApiDataSource = (
listFormCode?: string,
lookupQuery?: string,
filters?: any[],
keyName?: string,
) => {
return new CustomStore({
key: keyName,
loadMode: 'raw',
load: async (loadOptions) => {
if (!isSubForm && listFormCode && !window.location.pathname.includes(listFormCode)) {
return
}
if (!lookupQuery) {
return
}
const [method, url, body, keySelector, nameSelector, groupSelector] = lookupQuery.split(';')
// body içindeki @paramN'leri filtrelerle değiştir
let resolvedBody = body
if (filters?.length) {
for (let i = 0; i < filters.length; i++) {
resolvedBody = resolvedBody.replace(new RegExp(`@param${i}`, 'g'), filters[i])
}
}
try {
const cacheKey = `api:${lookupQuery}:${JSON.stringify(filters ?? null)}`
return cachedLoader(cacheKey, async () => {
const response = await dynamicFetch(url, method, null, resolvedBody)
let { data } = response
if (!data) return []
if (!Array.isArray(data)) data = [data]
return data.map((a: any) => ({
key: eval(keySelector),
name: eval(nameSelector),
group: eval(groupSelector),
...a,
}))
})
} catch {
return
}
},
})
}
const getCommandColumn = useCallback((): GridColumnData | undefined => {
if (!gridDto) {
return
}
const hasUpdate =
gridDto.gridOptions.editingOptionDto.allowEditing &&
checkPermission(gridDto.gridOptions.permissionDto.u)
const hasDelete =
gridDto.gridOptions.editingOptionDto.allowDeleting &&
checkPermission(gridDto.gridOptions.permissionDto.d)
const hasDetail =
gridDto.gridOptions.editingOptionDto.allowDetail &&
checkPermission(gridDto.gridOptions.permissionDto.u)
2026-03-30 03:15:42 +00:00
const hasDuplicate =
gridDto.gridOptions.editingOptionDto.allowDuplicate &&
checkPermission(gridDto.gridOptions.permissionDto.i)
2026-02-24 20:44:16 +00:00
const hasCommandButtons = gridDto.gridOptions.commandColumnDto.length > 0
// Eğer hiçbir buton eklenecek durumda değilse: çık
if (!hasUpdate && !hasDelete && !hasCommandButtons) {
return
}
const buttons: any[] = []
if (hasUpdate) {
buttons.push({
name: 'edit',
text: translate('::App.Platform.Edit'),
})
}
if (hasDelete) {
buttons.push({
name: 'delete',
text: translate('::App.Platform.Delete'),
})
}
if (hasDetail) {
const item = {
name: 'detail',
text: translate('::App.Platform.Detail'),
onClick: (e: any) => {
if (typeof e.event?.preventDefault === 'function') {
e.event.preventDefault()
}
if (!gridDto.gridOptions.keyFieldName) return
const url =
location.pathname?.replace('/list/', '/form/') +
`/${e.row.data[gridDto.gridOptions.keyFieldName]}`
window.open(url, isPwaMode ? '_self' : '_blank')
},
}
buttons.push(item)
}
2026-03-30 03:15:42 +00:00
if (hasDuplicate) {
const item = {
name: 'duplicate',
text: translate('::App.Platform.Duplicate'),
onClick: async (e: any) => {
if (typeof e.event?.preventDefault === 'function') {
e.event.preventDefault()
}
2026-03-30 09:16:28 +00:00
// Onay penceresi
const confirmed = window.confirm(
translate('::App.Platform.DuplicateConfirm') || 'Kopyalama işlemini onaylıyor musunuz?',
)
if (!confirmed) return
2026-03-30 03:15:42 +00:00
if (!gridDto.gridOptions.keyFieldName) return
const id = e.row.data[gridDto.gridOptions.keyFieldName]
if (!id) return
// Backend'e duplicate isteği gönder
try {
await dynamicFetch('list-form-data/duplicate', 'POST', null, {
listFormCode,
keys: [id],
2026-03-30 09:16:28 +00:00
data: [gridDto.gridOptions.keyFieldName],
2026-03-30 03:15:42 +00:00
})
2026-03-30 09:16:28 +00:00
2026-03-30 03:15:42 +00:00
// Başarılı ise grid'i yenile
if (gridRef?.current?.instance()) {
gridRef.current.instance().refresh()
}
} catch (err) {
// Hata yönetimi
2026-03-30 09:16:28 +00:00
alert(translate('::App.Platform.DuplicateError'))
2026-03-30 03:15:42 +00:00
}
},
}
buttons.push(item)
}
2026-02-24 20:44:16 +00:00
gridDto.gridOptions.commandColumnDto.forEach((action) => {
if (action.buttonPosition !== UiCommandButtonPositionTypeEnum.CommandColumn) return
if (!checkPermission(action.authName)) return
// visibleExpression varsa dinamik fonksiyon oluştur, yoksa statik değer kullan
let visibleFunc: ((e: any) => boolean) | boolean = action.isVisible
if (action.visibleExpression) {
try {
// visibleExpression string'ini fonksiyona çevir
// Örnek: "(e) => !e.row.isEditing" veya "(e) => e.row.data.Status === 'Active'"
visibleFunc = eval(action.visibleExpression)
} catch (error) {
console.error('VisibleExpression evaluation error:', error, action.visibleExpression)
visibleFunc = action.isVisible // Hata durumunda varsayılan değeri kullan
}
}
const item = {
visible: visibleFunc,
hint: action.hint,
icon: action.icon,
text: translate('::' + action.text),
onClick: (e: any) => {
if (typeof e.event?.preventDefault === 'function') {
e.event.preventDefault()
}
if (action.url) {
let url = action.url?.replace('@LISTFORMCODE', listFormCode)
gridDto.columnFormats.forEach((field) => {
if (field.fieldName) {
url = url.replace(`@${field.fieldName}`, e.row.data[field.fieldName])
}
})
window.open(url, isPwaMode ? '_self' : action.urlTarget)
} else if (action.dialogName) {
if (action.dialogParameters) {
const dynamicMap = JSON.parse(action.dialogParameters)
for (const [key, value] of Object.entries<string>(dynamicMap)) {
dynamicMap[key] = value.startsWith('@') ? e.row.data[value.replace('@', '')] : value
}
dialog.setConfig({
component: action.dialogName,
props: dynamicMap,
onClose: () => {
// Dialog kapandığında grid'i yenile
if (gridRef?.current?.instance()) {
gridRef?.current?.instance().refresh()
}
},
})
}
} else if (action.onClick) {
eval(action.onClick)
}
},
}
buttons.push(item)
})
// Buton sayısına göre dinamik genişlik hesapla
// Her buton için ~35-40px + padding
const calculatedWidth = Math.min(buttons.length * 40 + 50, 200)
const column = {
type: 'buttons',
width: calculatedWidth,
minWidth: calculatedWidth,
buttons,
allowResizing: true,
}
return column as GridColumnData
}, [gridDto, checkPermission, translate, listFormCode, isPwaMode, dialog, gridRef])
const getColumns = useCallback(
(columnFormats: ColumnFormatDto[]) => {
const columns: GridColumnData[] = []
if (!gridDto || !columnFormats) {
return columns
}
columnFormats.forEach((colData) => {
if (!colData.canRead || !colData.isActive) {
return
}
const column: GridColumnData = {}
column.colData = colData // Onemli: Baska event-callback lerde kullanmak icin eklendi, colData.lookupDto?.editorTemplateType
//column.showEditorAlways = true
column.dataField = colData.fieldName
if (colData.dataType) column.dataType = colData.dataType as DataType
if (colData.captionName) column.caption = translate('::' + colData.captionName)
if (colData.width > 0) column.width = colData.width
column.visible = colData.visible
column.alignment = colData.alignment
column.format = colData.format
let editorOptions: any = {}
if (colData.editorOptions) {
try {
editorOptions =
typeof colData.editorOptions === 'string'
? JSON.parse(colData.editorOptions)
: colData.editorOptions
} catch {
editorOptions = {}
}
}
column.editorOptions = { ...editorOptions }
// Format bilgisini öncelik sırasına göre ata
if (column.editorOptions.displayFormat) {
column.format = column.editorOptions.displayFormat
} else if (column.editorOptions.format) {
column.format = column.editorOptions.format
}
// columnCustomizationDto
column.fixed = colData.columnCustomizationDto?.fixed
column.fixedPosition = colData.columnCustomizationDto?.fixedPosition as HorizontalEdge
column.allowReordering = colData.columnCustomizationDto?.allowReordering
// sort
if (colData.sortIndex >= 0) {
column.sortIndex = colData.sortIndex
column.sortOrder = colData.sortDirection as SortOrder
}
// filterRow
column.allowFiltering = colData.columnFilterDto?.allowFiltering
column.selectedFilterOperation = colData.columnFilterDto
?.selectedFilterOperation as SelectedFilterOperation
column.filterValue = colData.columnFilterDto?.filterValue
// headerFilter
column.allowHeaderFiltering = colData.columnHeaderDto?.allowHeaderFiltering
if (column.allowHeaderFiltering == true) {
column.headerFilter = {}
column.headerFilter.allowSearch = colData.columnHeaderDto?.allowSearch
column.headerFilter.dataSource = colData.columnHeaderDto?.dataSource
}
// search
column.allowSearch = colData.allowSearch
//export
column.allowExporting = colData.canExport
// grouping
column.allowGrouping = colData.columnGroupingDto?.allowGrouping
column.autoExpandGroup = colData.columnGroupingDto?.autoExpandGroup
if (colData.columnGroupingDto.groupIndex)
column.groupIndex = colData.columnGroupingDto?.groupIndex
// constsa dinamik olarak css verilerini ekle
if (colData.columnCssClass) {
column.cssClass = colData.columnCssClass
if (colData.columnCssValue) {
addCss(colData.columnCssValue)
}
}
2026-03-15 19:40:19 +00:00
column.allowEditing = colData?.allowEditing
2026-02-24 20:44:16 +00:00
// #region lookup ayarlari
if (colData.lookupDto?.dataSourceType) {
// UiColumnEditorTemplateTypeEnum : None:0, Table:1, TagBox:2
const allItems = gridDto.gridOptions.editingFormDto.flatMap((group) => group.items)
const formItem = allItems.find((a) => a?.dataField === colData.fieldName)
if (formItem?.editorType2 === PlatformEditorTypes.dxTagBox) {
column.extras = {
multiValue: true,
editorOptions: formItem.editorOptions,
tagBoxOptions: formItem.tagBoxOptions,
}
column.editCellTemplate = 'cellEditTagBox'
column.calculateFilterExpression = calculateFilterExpressionMultiValue
column.cellTemplate = cellTemplateMultiValue
} else if (formItem?.editorType2 === PlatformEditorTypes.dxGridBox) {
column.extras = {
multiValue: false,
editorOptions: formItem.editorOptions,
gridBoxOptions: formItem.gridBoxOptions,
}
column.editCellTemplate = 'cellEditGridBox'
column.cellTemplate = cellTemplateMultiValue
if (formItem.gridBoxOptions?.selectionMode === 'multiple') {
column.calculateFilterExpression = calculateFilterExpressionMultiValue
column.extras.multiValue = true
}
}
column.lookup = {
valueExpr: colData.lookupDto?.valueExpr?.toLowerCase(),
displayExpr: colData.lookupDto?.displayExpr?.toLowerCase(),
dataSource: (o) => lookupDataSource(o, colData, listFormCode),
}
//column.lookup.dataSource = lookupDataSource(null, colData)
//cascadeEmptyFields verisi dolu ise bu kolon/field bir parent field dir
if (colData.lookupDto.cascadeEmptyFields) {
// parent field guncellendigi zaman bu fonksiyon cagrilir
column.setCellValue = function (rowData: any, value: any) {
if (!colData.fieldName) return
//console.log({ rowData, value, colData })
rowData[colData.fieldName] = Array.isArray(value) ? value[0] : value
// cascadeEmptyFields alani aralarinda virgul olacak sekilde bosaltilmak istenen alanlari saklar
colData?.lookupDto?.cascadeEmptyFields?.split(',').forEach((emptyField: any) => {
rowData[emptyField] = null
})
}
}
}
// #endregion
if (colData.validationRuleDto) {
// for server side validation : https://js.devexpress.com/Demos/WidgetsGallery/Demo/DataGrid/DataValidation/jQuery/Light/
column.validationRules = colData.validationRuleDto as ValidationRule[]
}
columns.push(column)
})
return columns
},
[gridDto, lookupDataSource, translate, checkPermission, dialog, isPwaMode, listFormCode],
)
const getBandedColumns = useCallback(() => {
if (!gridDto) {
return
}
const columns: GridColumnData[] = []
const insertedColumns: (string | undefined)[] = []
const commandColumn = getCommandColumn()
if (commandColumn) {
columns.push(commandColumn)
}
for (const col of gridDto.columnFormats) {
if (!col.fieldName) {
return
}
if (insertedColumns.some((a) => a === col.fieldName)) {
// kolon zaten eklenmis ise islem yapma
return
}
if (col.bandName) {
// banded kolon ise; en fazla iki kirilima kadar eklenir
const bands = col.bandName.split(':') // ic ice banded kolon const ise aralarinda : kullanilir
if (bands.length > 1) {
// band + band + column seklinde ise :
let topBand = columns.find((e) => e.caption == bands[0] && e.isBand == true)
if (!topBand) {
// en ustteki band ilk defa ekleniyor ise
topBand = { caption: bands[0], columns: [] as GridColumnData[], isBand: true }
columns.push(topBand)
}
topBand.columns ??= [] as GridColumnData[]
const band2 = { caption: bands[1], columns: [] as GridColumnData[], isBand: true }
topBand.columns.push(band2)
const rData = gridDto.columnFormats.filter((e) => e.bandName == col.bandName)
const cols = getColumns(rData) as GridColumnData[]
band2.columns.push(...cols)
insertedColumns.push(...cols.map((e) => e.dataField))
} else {
// band + column
const band = { caption: bands[0], columns: [] as GridColumnData[], isBand: true }
const rData = gridDto.columnFormats.filter((e) => e.bandName == bands[0])
const cols = getColumns(rData) as GridColumnData[]
band.columns.push(...cols)
columns.push(band)
insertedColumns.push(...cols.map((e) => e.dataField))
}
} else {
// band a bagli olmayan kolonlar
const cols = getColumns([col])
//console.log({ col, cols: JSON.stringify(cols) })
columns.push(...cols)
insertedColumns.push(...cols.map((e) => e.dataField))
}
}
// FormEditingExtraItem
// Devexpress Gridde kaydete basılınca
// formda gözükecek olan EditingFormDto.Items elemanları gelmiyor
// Sadece grid'de tanımlanmış columnları getiriyor.
// Bu elemanları da getirmesi için aşağıdaki şekilde,
// columns'da olmayan alanları da gizli olarak ekliyoruz
if (columns?.length) {
gridDto.gridOptions.editingFormDto.forEach((group) => {
group.items?.forEach((item: EditingFormItemDto) => {
if (!columns.some((a) => a.dataField === item.dataField)) {
columns.push({
dataField: item.dataField,
visible: false,
showInColumnChooser: false,
})
insertedColumns.push(item.dataField)
}
})
})
}
return columns
}, [gridDto, getColumns, getCommandColumn])
return {
getBandedColumns,
}
}
export { useListFormColumns }