Performanslı komponentler oluşturuldu

This commit is contained in:
Sedat Öztürk 2026-02-01 21:53:14 +03:00
parent 58d3e3c940
commit b43fb07e5b
6 changed files with 899 additions and 718 deletions

View file

@ -67,8 +67,8 @@ const EntityEditor: React.FC = () => {
if (response.data && Array.isArray(response.data)) {
const options = response.data.map((menuItem: any) => ({
value: menuItem.shortName || menuItem.code,
label: menuItem.displayName || menuItem.code,
value: menuItem.shortName,
label: translate('::' + menuItem.displayName),
}))
setMenuOptions(options)
}

View file

@ -4,7 +4,7 @@ import { Container } from '@/components/shared'
import { DX_CLASSNAMES } from '@/constants/app.constant'
import { useLocalization } from '@/utils/hooks/useLocalization'
import DxChart from 'devextreme-react/chart'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Helmet } from 'react-helmet'
import { useParams, useSearchParams } from 'react-router-dom'
import { GridDto } from '@/proxy/form/models'
@ -98,11 +98,9 @@ const Chart = (props: ChartProps) => {
}
}, [gridDto, userName])
useEffect(() => {
if (!gridDto) return
if (!initialized) return
const memoizedChartOptions = useMemo(() => {
if (!gridDto || !initialized) return undefined
// Chart her zaman currentSeries'e göre render edilir
const seriesDto = currentSeries
const gridOptions = {
@ -117,7 +115,7 @@ const Chart = (props: ChartProps) => {
layoutTypes.chart,
)
const options = {
return {
dataSource: dataSource,
adjustOnZoom: gridDto.gridOptions.commonDto?.adjustOnZoom ?? true,
@ -126,7 +124,6 @@ const Chart = (props: ChartProps) => {
disabled: gridDto.gridOptions.commonDto?.disabled ?? false,
palette: gridDto.gridOptions.commonDto?.palette ?? 'Material',
paletteExtensionMode: gridDto.gridOptions.commonDto?.paletteExtensionMode ?? 'blend',
//theme: s(chartDto.commonDto?.theme, 'generic.light'),
title: gridDto.gridOptions.titleDto,
size: gridDto.gridOptions.sizeDto?.useSize
@ -170,9 +167,13 @@ const Chart = (props: ChartProps) => {
enabled: true,
},
}
}, [gridDto, currentSeries, initialized, createSelectDataSource, listFormCode, urlSearchParams, openDrawer])
setChartOptions(options)
}, [gridDto, currentSeries, initialized, searchParams, urlSearchParams, openDrawer])
useEffect(() => {
if (memoizedChartOptions) {
setChartOptions(memoizedChartOptions)
}
}, [memoizedChartOptions])
const onFilter = useCallback(
(value?: string) => {
@ -220,7 +221,7 @@ const Chart = (props: ChartProps) => {
[gridDto, urlSearchParams, searchText],
)
const getFields = async () => {
const getFields = useCallback(async () => {
if (!props.listFormCode) return
try {
const resp = await getListFormFields({
@ -246,24 +247,24 @@ const Chart = (props: ChartProps) => {
{ placement: 'top-end' },
)
}
}
}, [props.listFormCode, translate])
useEffect(() => {
if (props.listFormCode) getFields()
}, [props.listFormCode, config])
const handlePreviewChange = (series: ChartSeriesDto[]) => {
const handlePreviewChange = useCallback((series: ChartSeriesDto[]) => {
// Preview değişikliklerini anında chart'a yansıt
setCurrentSeries(series)
}
}, [])
const handleDrawerClose = () => {
const handleDrawerClose = useCallback(() => {
setOpenDrawer(false)
// İptal - kaydedilmiş serilere geri dön
setCurrentSeries(savedSeries)
}
}, [savedSeries])
const onSave = async (newSeries: ChartSeriesDto[]) => {
const onSave = useCallback(async (newSeries: ChartSeriesDto[]) => {
// 1. Silinecek serileri bul (savedSeries var ama newSeries yok)
const toDelete = savedSeries.filter((old: ChartSeriesDto) => !newSeries.some((s) => s.index === old.index))
@ -299,7 +300,7 @@ const Chart = (props: ChartProps) => {
setSavedSeries(newSeries)
setCurrentSeries(newSeries)
}
}
}, [savedSeries, id, props])
return (
<Container className={DX_CLASSNAMES}>

View file

@ -44,7 +44,7 @@ import { Item } from 'devextreme-react/toolbar'
import { DataType } from 'devextreme/common'
import { captionize } from 'devextreme/core/utils/inflector'
import CustomStore from 'devextreme/data/custom_store'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Helmet } from 'react-helmet'
import SubForms from '../form/SubForms'
import { RowMode, SimpleItemWithColData } from '../form/types'
@ -140,17 +140,6 @@ const Grid = (props: GridProps) => {
}
}, [searchParams])
const layout = layoutTypes.grid
const { toolbarData, toolbarModalData, setToolbarModalData } = useToolbar({
gridDto,
listFormCode,
getSelectedRowKeys,
getSelectedRowsData,
refreshData,
getFilter,
layout,
})
const { filterToolbarData, ...filterData } = useFilters({
gridDto,
gridRef,
@ -165,38 +154,50 @@ const Grid = (props: GridProps) => {
gridRef,
})
async function getSelectedRowKeys() {
const getSelectedRowKeys = useCallback(async () => {
const grd = gridRef.current?.instance()
if (!grd) {
return []
}
return await grd.getSelectedRowKeys()
}
}, [])
function getSelectedRowsData() {
const getSelectedRowsData = useCallback(() => {
const grd = gridRef.current?.instance()
if (!grd) {
return []
}
return grd.getSelectedRowsData()
}
}, [])
function refreshData() {
const refreshData = useCallback(() => {
gridRef.current?.instance()?.refresh()
}
}, [])
function getFilter() {
const getFilter = useCallback(() => {
const grd = gridRef.current?.instance()
if (!grd) {
return
}
return grd.getCombinedFilter()
}
}, [])
function onSelectionChanged(data: any) {
const layout = layoutTypes.grid
const { toolbarData, toolbarModalData, setToolbarModalData } = useToolbar({
gridDto,
listFormCode,
getSelectedRowKeys,
getSelectedRowsData,
refreshData,
getFilter,
layout,
})
const onSelectionChanged = useCallback(
(data: any) => {
const grdOpt = gridDto?.gridOptions
const grd = gridRef.current?.instance()
if (!grdOpt || !grd) {
@ -223,9 +224,12 @@ const Grid = (props: GridProps) => {
if (data.selectedRowsData.length) {
setFormData(data.selectedRowsData[0])
}
}
},
[gridDto],
)
function onCellPrepared(e: any) {
const onCellPrepared = useCallback(
(e: any) => {
const columnFormats = gridDto?.columnFormats
if (!columnFormats) {
return
@ -250,16 +254,22 @@ const Grid = (props: GridProps) => {
}
// css inline style var ise uygula
if (colStyle.cssStyles) {
e.cellElement.attr('style', e.cellElement.attr('style') + ';' + colStyle.cssStyles)
}
e.cellElement.attr(
'style',
e.cellElement.attr('style') + ';' + colStyle.cssStyles,
)
}
}
}
}
}
}
},
[gridDto],
)
function onInitNewRow(e: any) {
const onInitNewRow = useCallback(
(e: any) => {
if (!gridDto?.columnFormats) {
return
}
@ -327,13 +337,16 @@ const Grid = (props: GridProps) => {
}
}
}
}
},
[gridDto, searchParams, extraFilters],
)
function onRowInserting(e: DataGridTypes.RowInsertingEvent<any, any>) {
const onRowInserting = useCallback((e: DataGridTypes.RowInsertingEvent<any, any>) => {
e.data = setFormEditingExtraItemValues(e.data)
}
}, [])
function onRowUpdating(e: DataGridTypes.RowUpdatingEvent<any, any>) {
const onRowUpdating = useCallback(
(e: DataGridTypes.RowUpdatingEvent<any, any>) => {
if (gridDto?.gridOptions.editingOptionDto?.sendOnlyChangedFormValuesUpdate) {
if (Object.keys(e.newData).some((a) => a.includes(':'))) {
Object.keys(e.oldData).forEach((col) => {
@ -357,9 +370,12 @@ const Grid = (props: GridProps) => {
if (gridDto?.gridOptions.keyFieldName) {
delete e.newData[gridDto?.gridOptions.keyFieldName]
}
}
},
[gridDto],
)
function onEditingStart(e: DataGridTypes.EditingStartEvent<any, any>) {
const onEditingStart = useCallback(
(e: DataGridTypes.EditingStartEvent<any, any>) => {
isEditingRef.current = true
setMode('edit')
setIsPopupFullScreen(gridDto?.gridOptions.editingOptionDto?.popup?.fullScreen ?? false)
@ -376,9 +392,11 @@ const Grid = (props: GridProps) => {
const json = JSON.parse(e.data[field[0]])
e.data[col.dataField] = json[field[1]]
})
}
},
[gridDto, mode],
)
function onDataErrorOccurred(e: DataGridTypes.DataErrorOccurredEvent<any, any>) {
const onDataErrorOccurred = useCallback((e: DataGridTypes.DataErrorOccurredEvent<any, any>) => {
toast.push(
<Notification type="danger" duration={2000}>
{e.error?.message}
@ -387,20 +405,43 @@ const Grid = (props: GridProps) => {
placement: 'top-end',
},
)
}
}, [])
function onEditorPreparing(editor: DataGridTypes.EditorPreparingEvent<any, any>) {
// Cascade parent fields mapping'i memoize et - popup açılışını hızlandırır
const cascadeFieldsMap = useMemo(() => {
if (!gridDto) return new Map()
const map = new Map<
string,
{
parentFields: string[]
childFields?: string[]
}
>()
gridDto.columnFormats.forEach((col) => {
if (col.lookupDto?.cascadeParentFields && col.fieldName) {
map.set(col.fieldName, {
parentFields: col.lookupDto.cascadeParentFields.split(',').map((f: string) => f.trim()),
childFields: col.lookupDto.cascadeEmptyFields?.split(',').map((f: string) => f.trim()),
})
}
})
return map
}, [gridDto])
const onEditorPreparing = useCallback(
(editor: DataGridTypes.EditorPreparingEvent<any, any>) => {
if (editor.parentType === 'dataRow' && editor.dataField && gridDto) {
const formItem = gridDto.gridOptions.editingFormDto
.flatMap((group) => group.items || [])
.find((i) => i.dataField === editor.dataField)
// Cascade disabled mantığı
const colFormat = gridDto.columnFormats.find((c) => c.fieldName === editor.dataField)
if (colFormat?.lookupDto?.cascadeParentFields) {
const parentFields = colFormat.lookupDto.cascadeParentFields
.split(',')
.map((f: string) => f.trim())
const cascadeInfo = cascadeFieldsMap.get(editor.dataField)
if (cascadeInfo) {
const parentFields = cascadeInfo.parentFields
const prevHandler = editor.editorOptions.onValueChanged
@ -414,36 +455,30 @@ const Grid = (props: GridProps) => {
const rowData = grid.getVisibleRows().find((r) => r.key === rowKey)?.data || {}
// Bu field bir parent ise, child fieldleri temizle
if (colFormat.lookupDto?.cascadeEmptyFields) {
const childFields = colFormat.lookupDto.cascadeEmptyFields
.split(',')
.map((f: string) => f.trim())
childFields.forEach((childField: string) => {
if (cascadeInfo.childFields) {
cascadeInfo.childFields.forEach((childField: string) => {
if (rowIndex >= 0) {
grid.cellValue(rowIndex, childField, null)
}
})
}
// Tüm cascade fieldlerin disabled durumlarını güncelle
// Tüm cascade fieldlerin disabled durumlarını güncelle - sadece değişen field için
const popup = grid.option('editing.popup')
if (popup) {
const formInstance = grid.option('editing.form') as any
if (formInstance && formInstance.getEditor) {
gridDto.columnFormats.forEach((col) => {
if (col.lookupDto?.cascadeParentFields) {
const colParentFields = col.lookupDto.cascadeParentFields
.split(',')
.map((f: string) => f.trim())
const shouldDisable = colParentFields.some((pf: string) => !rowData[pf])
// Sadece bu field'den etkilenen childları güncelle
cascadeFieldsMap.forEach((info, fieldName) => {
if (info.parentFields.includes(editor.dataField!)) {
const shouldDisable = info.parentFields.some((pf: string) => !rowData[pf])
try {
const editorInstance = formInstance.getEditor(col.fieldName!)
const editorInstance = formInstance.getEditor(fieldName)
if (editorInstance) {
editorInstance.option('disabled', shouldDisable)
}
} catch (err) {
console.debug('Cascade disabled update skipped for', col.fieldName, err)
console.debug('Cascade disabled update skipped for', fieldName, err)
}
}
})
@ -513,7 +548,9 @@ const Grid = (props: GridProps) => {
})
}
}
}
},
[gridDto, cascadeFieldsMap],
)
const customSaveState = useCallback(
(state: any) => {
@ -595,106 +632,8 @@ const Grid = (props: GridProps) => {
}
}, [gridDto])
// Kolonları oluştur - dil değiştiğinde güncelle
useEffect(() => {
if (!gridDto || !config) return
const cols = getBandedColumns()
cols?.forEach((col) => {
const eo = col.editorOptions
// Sadece phoneGlobal formatlı kolonlarda çalış
if (eo?.format === 'phoneGlobal') {
// DevExtreme bazen string tipinde formatter'ı çağırmaz
// Bu yüzden her durumda çalışması için customizeText ekleyeceğiz
col.dataType = 'string'
const formatter = (value: any) => {
if (!value) return ''
// string'e dönüştür ve sadece rakamları al
let digits = String(value).replace(/\D/g, '')
// +90, 0090, 0 gibi ülke kodu veya ön ekleri atla
if (digits.startsWith('90') && digits.length > 10) digits = digits.slice(-10)
if (digits.startsWith('0') && digits.length > 10) digits = digits.slice(-10)
if (digits.length > 10) digits = digits.slice(-10)
// 🔒 Eğer 10 haneli değilse geçersiz → boş göster
if (digits.length !== 10) return ''
// (XXX) XXX-XXXX formatında göster
const match = digits.match(/^(\d{3})(\d{3})(\d{4})$/)
return match ? `(${match[1]}) ${match[2]}-${match[3]}` : digits
}
// 1⃣ Normal format nesnesi
col.format = { formatter }
// 2⃣ CustomizeText fallback — bazı durumlarda zorunlu
col.customizeText = (cellInfo: any) => formatter(cellInfo?.value)
}
})
setColumnData(cols)
}, [gridDto, config])
// DataSource oluştur
useEffect(() => {
if (!gridDto) return
const dataSource = createSelectDataSource(
gridDto.gridOptions,
listFormCode,
searchParams,
layoutTypes.grid,
columnData,
)
setGridDataSource(dataSource)
}, [gridDto, searchParams])
useEffect(() => {
if (!columnData) return
refListFormCode.current = listFormCode
if (!gridRef?.current) {
return
}
const instance = gridRef?.current?.instance()
if (instance) {
instance.option('columns', columnData)
instance.option('remoteOperations', {
groupPaging: true,
filtering: true,
sorting: true,
paging: true,
grouping: true,
summary: true,
})
instance.option('dataSource', gridDataSource)
instance.state(null)
const stateStoring: IStateStoringProps = {
enabled: gridDto?.gridOptions.stateStoringDto?.enabled,
type: gridDto?.gridOptions.stateStoringDto?.type,
savingTimeout: gridDto?.gridOptions.stateStoringDto?.savingTimeout,
storageKey: gridDto?.gridOptions.stateStoringDto?.storageKey,
}
if (
gridDto?.gridOptions.stateStoringDto?.enabled &&
gridDto?.gridOptions.stateStoringDto?.type === 'custom'
) {
stateStoring.customSave = customSaveState
stateStoring.customLoad = customLoadState
}
instance.option('stateStoring', stateStoring)
}
}, [columnData])
useEffect(() => {
// extraFilters değişikliklerini useMemo ile optimize et
const filterParams = useMemo(() => {
const activeFilters = extraFilters.filter((f) => f.value)
let base: any = null
@ -736,19 +675,269 @@ const Grid = (props: GridProps) => {
}, null as any)
}
if (filter) {
searchParams?.set('filter', JSON.stringify(filter))
return filter
}, [extraFilters])
// Filter değiştiğinde searchParams'ı güncelle (side effect)
// NOT: searchParams'ı dependency'e koymuyoruz çünkü sonsuz döngü yaratır
useEffect(() => {
if (filterParams) {
searchParams?.set('filter', JSON.stringify(filterParams))
} else {
searchParams?.delete('filter')
}
// Grid'i yenile
gridRef.current?.instance()?.refresh()
}, [extraFilters])
}, [filterParams])
// Kolonları oluştur - dil değiştiğinde güncelle
const memoizedColumns = useMemo(() => {
if (!gridDto || !config) return undefined
const cols = getBandedColumns()
cols?.forEach((col: any) => {
const eo = col.editorOptions
// Sadece phoneGlobal formatlı kolonlarda çalış
if (eo?.format === 'phoneGlobal') {
// DevExtreme bazen string tipinde formatter'ı çağırmaz
// Bu yüzden her durumda çalışması için customizeText ekleyeceğiz
col.dataType = 'string'
const formatter = (value: any) => {
if (!value) return ''
// string'e dönüştür ve sadece rakamları al
let digits = String(value).replace(/\D/g, '')
// +90, 0090, 0 gibi ülke kodu veya ön ekleri atla
if (digits.startsWith('90') && digits.length > 10) digits = digits.slice(-10)
if (digits.startsWith('0') && digits.length > 10) digits = digits.slice(-10)
if (digits.length > 10) digits = digits.slice(-10)
// 🔒 Eğer 10 haneli değilse geçersiz → boş göster
if (digits.length !== 10) return ''
// (XXX) XXX-XXXX formatında göster
const match = digits.match(/^(\d{3})(\d{3})(\d{4})$/)
return match ? `(${match[1]}) ${match[2]}-${match[3]}` : digits
}
// 1⃣ Normal format nesnesi
col.format = { formatter }
// 2⃣ CustomizeText fallback — bazı durumlarda zorunlu
col.customizeText = (cellInfo: any) => formatter(cellInfo?.value)
}
})
return cols
}, [gridDto, config])
useEffect(() => {
if (memoizedColumns) {
setColumnData(memoizedColumns)
}
}, [memoizedColumns])
// DataSource oluştur - sadece gridDto değiştiğinde
const memoizedDataSource = useMemo(() => {
if (!gridDto) return undefined
return createSelectDataSource(
gridDto.gridOptions,
listFormCode,
searchParams,
layoutTypes.grid,
undefined, // columnData yerine undefined - datasource oluşturulurken columns henüz set edilmeyebilir
)
}, [gridDto, listFormCode, createSelectDataSource])
useEffect(() => {
if (memoizedDataSource) {
setGridDataSource(memoizedDataSource)
}
}, [memoizedDataSource])
// Grid columns'ı set et - sadece columnData değiştiğinde
useEffect(() => {
if (!columnData || !gridRef?.current) return
const instance = gridRef?.current?.instance()
if (instance) {
instance.option('columns', columnData)
}
}, [columnData])
// Grid dataSource'u set et - sadece gridDataSource değiştiğinde
useEffect(() => {
if (!gridDataSource || !gridRef?.current) return
const instance = gridRef?.current?.instance()
if (instance) {
instance.option('remoteOperations', {
groupPaging: true,
filtering: true,
sorting: true,
paging: true,
grouping: true,
summary: true,
})
instance.option('dataSource', gridDataSource)
}
}, [gridDataSource])
// listFormCode'u ref'e kaydet
useEffect(() => {
refListFormCode.current = listFormCode
}, [listFormCode])
// StateStoring fonksiyonlarını ref'e kaydet - her render'da yeniden oluşturulmasın
const customSaveStateRef = useRef(customSaveState)
const customLoadStateRef = useRef(customLoadState)
useEffect(() => {
customSaveStateRef.current = customSaveState
customLoadStateRef.current = customLoadState
}, [customSaveState, customLoadState])
// StateStoring'i sadece gridDto değiştiğinde ayarla
useEffect(() => {
if (!gridDto || !gridRef?.current) return
const instance = gridRef?.current?.instance()
if (instance) {
const stateStoring: IStateStoringProps = {
enabled: gridDto?.gridOptions.stateStoringDto?.enabled,
type: gridDto?.gridOptions.stateStoringDto?.type,
savingTimeout: gridDto?.gridOptions.stateStoringDto?.savingTimeout,
storageKey: gridDto?.gridOptions.stateStoringDto?.storageKey,
}
if (
gridDto?.gridOptions.stateStoringDto?.enabled &&
gridDto?.gridOptions.stateStoringDto?.type === 'custom'
) {
// Ref'i kullan, direkt fonksiyon yerine
stateStoring.customSave = (state: any) => customSaveStateRef.current(state)
stateStoring.customLoad = () => customLoadStateRef.current()
}
instance.option('stateStoring', stateStoring)
instance.state(null)
}
}, [gridDto]) // Sadece gridDto'ya bağlı
useEffect(() => {
refListFormCode.current = listFormCode
}, [listFormCode])
// Form items mapper'ı memoize et - popup açılışını hızlandırır
const mapFormItem = useCallback(
(i: EditingFormItemDto) => {
let editorOptions: EditorOptionsWithButtons = {}
try {
editorOptions = i.editorOptions && JSON.parse(i.editorOptions)
if (editorOptions?.buttons) {
editorOptions.buttons = (editorOptions?.buttons || []).map((btn: any) => {
if (btn?.options?.onClick && typeof btn.options.onClick === 'string') {
btn.options.onClick = eval(`(${btn.options.onClick})`)
}
return btn
})
}
const rawFilter = searchParams?.get('filter')
if (rawFilter) {
const parsed = JSON.parse(rawFilter)
const filters = extractSearchParamsFields(parsed)
const hasFilter = filters.some(([field, op, val]) => field === i.dataField)
if (hasFilter) {
const existsInExtra = extraFilters.some((f) => f.fieldName === i.dataField && !!f.value)
if (!existsInExtra) {
editorOptions = {
...editorOptions,
readOnly: true,
}
}
}
}
} catch {}
const fieldName = i.dataField.split(':')[0]
const listFormField = gridDto?.columnFormats.find((x: any) => x.fieldName === fieldName)
if (listFormField?.sourceDbType === DbTypeEnum.Date) {
editorOptions = {
...{
type: 'date',
dateSerializationFormat: 'yyyy-MM-dd',
displayFormat: 'shortDate',
},
...editorOptions,
}
} else if (
listFormField?.sourceDbType === DbTypeEnum.DateTime ||
listFormField?.sourceDbType === DbTypeEnum.DateTime2 ||
listFormField?.sourceDbType === DbTypeEnum.DateTimeOffset
) {
editorOptions = {
...{
type: 'datetime',
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
displayFormat: 'shortDateShortTime',
},
...editorOptions,
}
}
// Set defaultValue for @AUTONUMBER fields
if (
typeof listFormField?.defaultValue === 'string' &&
listFormField?.defaultValue === '@AUTONUMBER' &&
mode === 'new'
) {
editorOptions = {
...editorOptions,
value: autoNumber(),
}
}
const item: SimpleItemWithColData = {
canRead: listFormField?.canRead ?? false,
canUpdate: listFormField?.canUpdate ?? false,
canCreate: listFormField?.canCreate ?? false,
canExport: listFormField?.canExport ?? false,
dataField: i.dataField,
name: i.dataField,
editorType2: i.editorType2,
editorType:
i.editorType2 == PlatformEditorTypes.dxGridBox ? 'dxDropDownBox' : i.editorType2,
colSpan: i.colSpan,
isRequired: i.isRequired,
editorOptions,
editorScript: i.editorScript,
}
if (i.dataField.indexOf(':') >= 0) {
item.label = { text: captionize(i.dataField.split(':')[1]) }
}
if ((mode == 'edit' && !item.canUpdate) || (mode == 'new' && !item.canCreate)) {
item.editorOptions = {
...item.editorOptions,
readOnly: true,
}
}
return item
},
[gridDto, mode, searchParams, extraFilters],
)
// WidgetGroup yüksekliğini hesapla
useEffect(() => {
const calculateWidgetHeight = () => {
@ -1053,126 +1242,6 @@ const Grid = (props: GridProps) => {
)
const result: any[] = []
// Helper function: item mapper
const mapFormItem = (i: EditingFormItemDto) => {
let editorOptions: EditorOptionsWithButtons = {}
try {
editorOptions = i.editorOptions && JSON.parse(i.editorOptions)
if (editorOptions?.buttons) {
editorOptions.buttons = (editorOptions?.buttons || []).map(
(btn: any) => {
if (
btn?.options?.onClick &&
typeof btn.options.onClick === 'string'
) {
btn.options.onClick = eval(`(${btn.options.onClick})`)
}
return btn
},
)
}
const rawFilter = searchParams?.get('filter')
if (rawFilter) {
const parsed = JSON.parse(rawFilter)
const filters = extractSearchParamsFields(parsed)
const hasFilter = filters.some(
([field, op, val]) => field === i.dataField,
)
if (hasFilter) {
const existsInExtra = extraFilters.some(
(f) => f.fieldName === i.dataField && !!f.value,
)
if (!existsInExtra) {
editorOptions = {
...editorOptions,
readOnly: true,
}
}
}
}
} catch {}
const fieldName = i.dataField.split(':')[0]
const listFormField = gridDto.columnFormats.find(
(x: any) => x.fieldName === fieldName,
)
if (listFormField?.sourceDbType === DbTypeEnum.Date) {
editorOptions = {
...{
type: 'date',
dateSerializationFormat: 'yyyy-MM-dd',
displayFormat: 'shortDate',
},
...editorOptions,
}
} else if (
listFormField?.sourceDbType === DbTypeEnum.DateTime ||
listFormField?.sourceDbType === DbTypeEnum.DateTime2 ||
listFormField?.sourceDbType === DbTypeEnum.DateTimeOffset
) {
editorOptions = {
...{
type: 'datetime',
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
displayFormat: 'shortDateShortTime',
},
...editorOptions,
}
}
// Set defaultValue for @AUTONUMBER fields
if (
typeof listFormField?.defaultValue === 'string' &&
listFormField?.defaultValue === '@AUTONUMBER' &&
mode === 'new'
) {
editorOptions = {
...editorOptions,
value: autoNumber(),
}
}
const item: SimpleItemWithColData = {
canRead: listFormField?.canRead ?? false,
canUpdate: listFormField?.canUpdate ?? false,
canCreate: listFormField?.canCreate ?? false,
canExport: listFormField?.canExport ?? false,
dataField: i.dataField,
name: i.dataField,
editorType2: i.editorType2,
editorType:
i.editorType2 == PlatformEditorTypes.dxGridBox
? 'dxDropDownBox'
: i.editorType2,
colSpan: i.colSpan,
isRequired: i.isRequired,
editorOptions,
editorScript: i.editorScript,
}
if (i.dataField.indexOf(':') >= 0) {
item.label = { text: captionize(i.dataField.split(':')[1]) }
}
if (
(mode == 'edit' && !item.canUpdate) ||
(mode == 'new' && !item.canCreate)
) {
item.editorOptions = {
...item.editorOptions,
readOnly: true,
}
}
return item
}
sortedFormDto.forEach((e: any) => {
if (e.itemType !== 'tabbed') {
// Backend'den gelen colCount ve colSpan değerlerini kullan

View file

@ -13,9 +13,11 @@ import Chart, {
Tooltip,
} from 'devextreme-react/chart'
import PivotGrid, {
Export,
FieldChooser,
FieldPanel,
HeaderFilter,
LoadPanel,
PivotGridRef,
PivotGridTypes,
Scrolling,
@ -24,7 +26,7 @@ import PivotGrid, {
} from 'devextreme-react/pivot-grid'
import CustomStore from 'devextreme/data/custom_store'
import PivotGridDataSource, { Field } from 'devextreme/ui/pivot_grid/data_source'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Helmet } from 'react-helmet'
import { GridColumnData } from './GridColumnData'
import {
@ -36,7 +38,7 @@ import {
} from './Utils'
import { useFilters } from './useFilters'
import WidgetGroup from '@/components/common/WidgetGroup'
import { Button } from '@/components/ui'
import { Button, Notification, toast } from '@/components/ui'
import { FaCog, FaTimes, FaUndo } from 'react-icons/fa'
import { usePermission } from '@/utils/hooks/usePermission'
import { ROUTES_ENUM } from '@/routes/route.constant'
@ -86,7 +88,7 @@ const Pivot = (props: PivotProps) => {
gridRef,
})
function onCellPrepared(e: any) {
const onCellPrepared = useCallback((e: any) => {
const columnFormats = gridDto?.columnFormats
if (!columnFormats) {
return
@ -118,9 +120,9 @@ const Pivot = (props: PivotProps) => {
}
}
}
}
}, [gridDto])
const clearPivotFilters = () => {
const clearPivotFilters = useCallback(() => {
const grid = gridRef.current?.instance()
if (!grid) return
@ -133,9 +135,9 @@ const Pivot = (props: PivotProps) => {
})
ds.reload()
}
}
}, [])
const moveAllFieldsToFilterArea = () => {
const moveAllFieldsToFilterArea = useCallback(() => {
const grid = gridRef.current?.instance()
if (!grid) return
@ -152,9 +154,9 @@ const Pivot = (props: PivotProps) => {
ds.fields(fields)
ds.reload() // PivotGridi yeniden yükle
grid.repaint() // UI güncelle
}
}, [])
const resetPivotGridState = async () => {
const resetPivotGridState = useCallback(async () => {
const grid = gridRef.current?.instance()
if (grid) {
// kullaniciya ait kayitli grid state i sil customizationData boşalt silinsin.
@ -169,8 +171,47 @@ const Pivot = (props: PivotProps) => {
clearPivotFilters()
moveAllFieldsToFilterArea()
}
}
}, [listFormCode, props, clearPivotFilters, moveAllFieldsToFilterArea])
const onExporting = useCallback(async (e: PivotGridTypes.ExportingEvent) => {
e.cancel = true
const pivot = gridRef?.current?.instance()
if (!pivot) return
try {
// PivotGrid sadece Excel export destekliyor
const [{ Workbook }, { saveAs }, { exportPivotGrid }] = await Promise.all([
import('exceljs'),
import('file-saver'),
import('devextreme/excel_exporter'),
])
const workbook = new Workbook()
const worksheet = workbook.addWorksheet(`${listFormCode}_pivot`)
await exportPivotGrid({
component: pivot as any,
worksheet,
})
const buffer = await workbook.xlsx.writeBuffer()
saveAs(
new Blob([buffer], { type: 'application/octet-stream' }),
`${listFormCode}_pivot_export.xlsx`,
)
} catch (err) {
console.error('Pivot export error:', err)
toast.push(
<Notification type="danger" duration={2500}>
{translate('::App.Common.ExportError') ?? 'Dışa aktarma sırasında hata oluştu.'}
</Notification>,
{ placement: 'top-end' },
)
}
}, [listFormCode, translate])
// StateStoring fonksiyonlarını ref'e kaydet
const customSaveState = useCallback(
(state: any) => {
return postListFormCustomization({
@ -201,15 +242,16 @@ const Pivot = (props: PivotProps) => {
[listFormCode],
)
const customSaveStateRef = useRef(customSaveState)
const customLoadStateRef = useRef(customLoadState)
useEffect(() => {
if (gridRef?.current) {
const instance = gridRef?.current?.instance()
if (instance) {
instance.option('remoteOperations', false)
instance.option('dataSource', undefined)
instance.option('stateStoring', undefined)
}
}
customSaveStateRef.current = customSaveState
customLoadStateRef.current = customLoadState
}, [customSaveState, customLoadState])
useEffect(() => {
refListFormCode.current = listFormCode
}, [listFormCode])
useEffect(() => {
@ -231,34 +273,67 @@ const Pivot = (props: PivotProps) => {
}
}, [gridDto])
useEffect(() => {
if (!gridDto) {
return
}
// Kolonları memoize et
const memoizedColumns = useMemo(() => {
if (!gridDto || !config) return undefined
// Set columns
const cols = getBandedColumns()
setColumnData(cols?.filter((a) => a.colData?.pivotSettingsDto.isPivot))
return cols?.filter((a) => a.colData?.pivotSettingsDto.isPivot)
}, [gridDto, config])
// Set data source
const dataSource: CustomStore<any, any> = createSelectDataSource(
// DataSource'u memoize et
const memoizedDataSource = useMemo(() => {
if (!gridDto) return undefined
const cols = getBandedColumns()
return createSelectDataSource(
gridDto.gridOptions,
listFormCode,
searchParams,
layoutTypes.pivot,
cols,
)
setGridDataSource(dataSource)
}, [gridDto, config, searchParams])
}, [gridDto, listFormCode, createSelectDataSource])
useEffect(() => {
refListFormCode.current = listFormCode
if (!gridRef?.current) {
if (memoizedColumns) {
setColumnData(memoizedColumns)
}
}, [memoizedColumns])
useEffect(() => {
if (memoizedDataSource) {
setGridDataSource(memoizedDataSource)
}
}, [memoizedDataSource])
// Pivot dataSource ve fields'i set et + StateStoring ayarla
useEffect(() => {
if (!columnData || !gridDataSource || !gridRef?.current || !gridDto) {
return
}
const fields: any = columnData?.map((b) => {
const instance = gridRef?.current?.instance()
if (!instance) return
// 1. StateStoring'i ÖNCE ayarla
const stateStoring: any = {
enabled: gridDto?.gridOptions.stateStoringDto?.enabled,
type: gridDto?.gridOptions.stateStoringDto?.type,
savingTimeout: gridDto?.gridOptions.stateStoringDto?.savingTimeout,
storageKey: gridDto?.gridOptions.stateStoringDto?.storageKey,
}
if (
gridDto?.gridOptions.stateStoringDto?.enabled &&
gridDto?.gridOptions.stateStoringDto?.type === 'custom'
) {
stateStoring.customSave = (state: any) => customSaveStateRef.current(state)
stateStoring.customLoad = () => customLoadStateRef.current()
}
instance.option('stateStoring', stateStoring)
// 2. Default fields'i hazırla
const defaultFields: any = columnData?.map((b) => {
return {
dataField: b.dataField,
caption: b.caption,
@ -275,21 +350,28 @@ const Pivot = (props: PivotProps) => {
} as Field
})
PivotGridDataSource
// 3. DataSource'u ayarla - fields olmadan
const dataSource: PivotGridTypes.Properties['dataSource'] = {
remoteOperations: true,
store: gridDataSource,
fields,
fields: defaultFields,
}
const instance = gridRef?.current?.instance()
if (instance) {
instance.option('dataSource', dataSource)
instance.option('state', null)
}
//chart Integration
if (gridRef?.current && chartRef?.current) {
// 4. DataSource set edildikten SONRA state'i yükle - state fields'i override edecek
setTimeout(() => {
const ds = instance.getDataSource()
if (ds && typeof ds.state === 'function') {
ds.state(null) // Bu saved state'teki fields'i kullanacak
}
}, 50)
}, [columnData, gridDataSource, gridDto])
// Chart binding - sadece bir kez
useEffect(() => {
if (!gridRef?.current || !chartRef?.current) return
const pivotInstance = gridRef?.current?.instance()
const chartInstance = chartRef?.current?.instance()
if (pivotInstance && chartInstance) {
@ -298,8 +380,7 @@ const Pivot = (props: PivotProps) => {
alternateDataFields: false,
})
}
}
}, [columnData])
}, [gridDto])
return (
<>
@ -383,7 +464,9 @@ const Pivot = (props: PivotProps) => {
rtlEnabled={gridDto.gridOptions.columnOptionDto?.rtlEnabled}
hoverStateEnabled={gridDto.gridOptions.columnOptionDto?.hoverStateEnabled}
onCellPrepared={onCellPrepared}
onExporting={onExporting}
>
<Export enabled={gridDto.gridOptions.exportDto?.enabled} />
<HeaderFilter
allowSelectAll={gridDto.gridOptions.selectionDto.allowSelectAll}
width={gridDto.gridOptions.headerFilterDto.width}
@ -406,23 +489,9 @@ const Pivot = (props: PivotProps) => {
enabled={gridDto.gridOptions.pivotOptionDto.columnChooserEnabled}
height={500}
/>
<StateStoring
enabled={gridDto?.gridOptions.stateStoringDto?.enabled}
type={gridDto?.gridOptions.stateStoringDto?.type}
savingTimeout={gridDto?.gridOptions.stateStoringDto?.savingTimeout}
storageKey={gridDto?.gridOptions.stateStoringDto?.storageKey}
customSave={
gridDto?.gridOptions.stateStoringDto?.enabled &&
gridDto?.gridOptions.stateStoringDto?.type === 'custom'
? customSaveState
: undefined
}
customLoad={
gridDto?.gridOptions.stateStoringDto?.enabled &&
gridDto?.gridOptions.stateStoringDto?.type === 'custom'
? customLoadState
: undefined
}
<LoadPanel
enabled={gridDto.gridOptions.pagerOptionDto?.loadPanelEnabled as boolean | undefined}
text={gridDto.gridOptions.pagerOptionDto?.loadPanelText}
/>
<Scrolling mode={gridDto.gridOptions.pagerOptionDto.scrollingMode} />
</PivotGrid>

View file

@ -38,7 +38,7 @@ import TreeListDx, {
} from 'devextreme-react/tree-list'
import { Item } from 'devextreme-react/toolbar'
import CustomStore from 'devextreme/data/custom_store'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Helmet } from 'react-helmet'
import { RowMode, SimpleItemWithColData } from '../form/types'
import { GridColumnData } from './GridColumnData'
@ -132,19 +132,6 @@ const Tree = (props: TreeProps) => {
}
}, [searchParams])
const layout = layoutTypes.tree
const { toolbarData, toolbarModalData, setToolbarModalData } = useToolbar({
gridDto,
listFormCode,
getSelectedRowKeys,
getSelectedRowsData,
refreshData,
expandAll,
collapseAll,
getFilter,
layout,
})
const { filterToolbarData, ...filterData } = useFilters({
gridDto,
gridRef,
@ -159,7 +146,7 @@ const Tree = (props: TreeProps) => {
gridRef,
})
function extractSearchParamsFields(filter: any): [string, string, any][] {
const extractSearchParamsFields = useCallback((filter: any): [string, string, any][] => {
if (!Array.isArray(filter)) return []
if (filter.length === 3 && typeof filter[0] === 'string') {
@ -167,27 +154,27 @@ const Tree = (props: TreeProps) => {
}
return filter.flatMap((f) => extractSearchParamsFields(f))
}
}, [])
async function getSelectedRowKeys() {
const getSelectedRowKeys = useCallback(async () => {
const tree = gridRef.current?.instance()
if (!tree) {
return []
}
return await tree.getSelectedRowKeys()
}
}, [])
function getSelectedRowsData() {
const getSelectedRowsData = useCallback(() => {
const tree = gridRef.current?.instance()
if (!tree) {
return []
}
return tree.getSelectedRowsData()
}
}, [])
function expandAll() {
const expandAll = useCallback(() => {
const tree = gridRef.current?.instance()
if (!tree) return
tree.forEachNode((node: any) => {
@ -195,9 +182,9 @@ const Tree = (props: TreeProps) => {
tree.expandRow(node.key)
}
})
}
}, [])
function collapseAll() {
const collapseAll = useCallback(() => {
const tree = gridRef.current?.instance()
if (!tree) return
tree.forEachNode((node: any) => {
@ -205,22 +192,36 @@ const Tree = (props: TreeProps) => {
tree.collapseRow(node.key)
}
})
}
}, [])
function refreshData() {
const refreshData = useCallback(() => {
gridRef.current?.instance().refresh()
}
}, [])
function getFilter() {
const getFilter = useCallback(() => {
const tree = gridRef.current?.instance()
if (!tree) {
return
}
return tree.getCombinedFilter()
}
}, [])
function onSelectionChanged(data: any) {
const layout = layoutTypes.tree
const { toolbarData, toolbarModalData, setToolbarModalData } = useToolbar({
gridDto,
listFormCode,
getSelectedRowKeys,
getSelectedRowsData,
refreshData,
expandAll,
collapseAll,
getFilter,
layout,
})
const onSelectionChanged = useCallback(
(data: any) => {
const treeOpt = gridDto?.gridOptions
const tree = gridRef.current?.instance()
if (!treeOpt || !tree) {
@ -250,9 +251,12 @@ const Tree = (props: TreeProps) => {
if (data.selectedRowsData.length) {
setFormData(data.selectedRowsData[0])
}
}
},
[gridDto],
)
function onCellPrepared(e: any) {
const onCellPrepared = useCallback(
(e: any) => {
const columnFormats = gridDto?.columnFormats
if (!columnFormats) {
return
@ -272,14 +276,19 @@ const Tree = (props: TreeProps) => {
e.cellElement.addClass(colStyle.cssClassName)
}
if (colStyle.cssStyles) {
e.cellElement.attr('style', e.cellElement.attr('style') + ';' + colStyle.cssStyles)
}
e.cellElement.attr(
'style',
e.cellElement.attr('style') + ';' + colStyle.cssStyles,
)
}
}
}
}
}
}
},
[gridDto],
)
function onInitNewRow(e: any) {
if (!gridDto?.columnFormats) {
@ -349,11 +358,12 @@ const Tree = (props: TreeProps) => {
}
}
function onRowInserting(e: TreeListTypes.RowInsertingEvent) {
const onRowInserting = useCallback((e: TreeListTypes.RowInsertingEvent) => {
e.data = setFormEditingExtraItemValues(e.data)
}
}, [])
function onRowUpdating(e: TreeListTypes.RowUpdatingEvent) {
const onRowUpdating = useCallback(
(e: TreeListTypes.RowUpdatingEvent) => {
if (gridDto?.gridOptions.editingOptionDto?.sendOnlyChangedFormValuesUpdate) {
if (Object.keys(e.newData).some((a) => a.includes(':'))) {
Object.keys(e.oldData).forEach((col) => {
@ -377,9 +387,12 @@ const Tree = (props: TreeProps) => {
if (gridDto?.gridOptions.keyFieldName) {
delete e.newData[gridDto?.gridOptions.keyFieldName]
}
}
},
[gridDto],
)
function onEditingStart(e: TreeListTypes.EditingStartEvent) {
const onEditingStart = useCallback(
(e: TreeListTypes.EditingStartEvent) => {
isEditingRef.current = true
setMode('edit')
setIsPopupFullScreen(gridDto?.gridOptions.editingOptionDto?.popup?.fullScreen ?? false)
@ -395,9 +408,11 @@ const Tree = (props: TreeProps) => {
const json = JSON.parse(e.data[field[0]])
e.data[col.dataField] = json[field[1]]
})
}
},
[gridDto],
)
function onDataErrorOccurred(e: TreeListTypes.DataErrorOccurredEvent) {
const onDataErrorOccurred = useCallback((e: TreeListTypes.DataErrorOccurredEvent) => {
toast.push(
<Notification type="danger" duration={2000}>
{e.error?.message}
@ -406,7 +421,7 @@ const Tree = (props: TreeProps) => {
placement: 'top-end',
},
)
}
}, [])
function onEditorPreparing(editor: TreeListTypes.EditorPreparingEvent) {
if (editor.parentType === 'dataRow' && editor.dataField && gridDto) {
@ -566,6 +581,15 @@ const Tree = (props: TreeProps) => {
[listFormCode],
)
// StateStoring fonksiyonlarını ref'e kaydet - Grid'deki gibi
const customSaveStateRef = useRef(customSaveState)
const customLoadStateRef = useRef(customLoadState)
useEffect(() => {
customSaveStateRef.current = customSaveState
customLoadStateRef.current = customLoadState
}, [customSaveState, customLoadState])
useEffect(() => {
if (gridRef?.current) {
gridRef?.current?.instance().option('columns', undefined)
@ -723,8 +747,9 @@ const Tree = (props: TreeProps) => {
gridDto?.gridOptions.stateStoringDto?.enabled &&
gridDto?.gridOptions.stateStoringDto?.type === 'custom'
) {
stateStoring.customSave = customSaveState
stateStoring.customLoad = customLoadState
// Ref pattern kullan - Grid'deki gibi
stateStoring.customSave = (state: any) => customSaveStateRef.current(state)
stateStoring.customLoad = () => customLoadStateRef.current()
}
gridRef?.current?.instance().option('stateStoring', stateStoring)
}, [columnData])

View file

@ -2,7 +2,7 @@ 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 { useEffect } from 'react'
import { useCallback, useEffect } from 'react'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { usePermission } from '@/utils/hooks/usePermission'
import { usePWA } from '@/utils/hooks/usePWA'
@ -105,11 +105,27 @@ function calculateFilterExpressionMultiValue(
}
}
// lookup cache (module scope)
const __lookupCache = new Map<string, Promise<any[]>>()
// 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[]>) => {
if (__lookupCache.has(key)) return __lookupCache.get(key)!
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 ?? [])
@ -117,7 +133,8 @@ const cachedLoader = (key: string, loader: () => Promise<any[]>) => {
__lookupCache.delete(key) // hata olursa tekrar denenebilsin
throw err
})
__lookupCache.set(key, p)
__lookupCache.set(key, { promise: p, timestamp: now })
return p
}
@ -142,7 +159,7 @@ const useListFormColumns = ({
__lookupCache.clear()
}, [listFormCode])
const lookupDataSource = (options: any, colData: any, listFormCode: string) => {
const lookupDataSource = useCallback((options: any, colData: any, listFormCode: string) => {
const { lookupDto } = colData
const filters = []
if (lookupDto.cascadeParentFields) {
@ -184,7 +201,7 @@ const useListFormColumns = ({
store: [],
}
}
}
}, [listFormCode])
const createLookupStaticDataSource = (
load: () => any,
@ -289,7 +306,7 @@ const useListFormColumns = ({
})
}
const getCommandColumn = (): GridColumnData | undefined => {
const getCommandColumn = useCallback((): GridColumnData | undefined => {
if (!gridDto) {
return
}
@ -399,9 +416,9 @@ const useListFormColumns = ({
}
return column as GridColumnData
}
}, [gridDto, checkPermission, translate, listFormCode, isPwaMode, dialog, gridRef])
const getColumns = (columnFormats: ColumnFormatDto[]) => {
const getColumns = useCallback((columnFormats: ColumnFormatDto[]) => {
const columns: GridColumnData[] = []
if (!gridDto || !columnFormats) {
@ -550,9 +567,9 @@ const useListFormColumns = ({
})
return columns
}
}, [gridDto, lookupDataSource, translate, checkPermission, dialog, isPwaMode, listFormCode])
const getBandedColumns = () => {
const getBandedColumns = useCallback(() => {
if (!gridDto) {
return
}
@ -635,7 +652,7 @@ const useListFormColumns = ({
}
return columns
}
}, [gridDto, getColumns, getCommandColumn])
return {
getBandedColumns,