From b43fb07e5b007925b6a139d15d39cc7c230e5bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96zt=C3=BCrk?= Date: Sun, 1 Feb 2026 21:53:14 +0300 Subject: [PATCH] =?UTF-8?q?Performansl=C4=B1=20komponentler=20olu=C5=9Ftur?= =?UTF-8?q?uldu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/developerKit/EntityEditor.tsx | 4 +- ui/src/views/list/Chart.tsx | 35 +- ui/src/views/list/Grid.tsx | 1063 +++++++++-------- ui/src/views/list/Pivot.tsx | 203 ++-- ui/src/views/list/Tree.tsx | 269 +++-- ui/src/views/list/useListFormColumns.ts | 43 +- 6 files changed, 899 insertions(+), 718 deletions(-) diff --git a/ui/src/components/developerKit/EntityEditor.tsx b/ui/src/components/developerKit/EntityEditor.tsx index 7dd3411a..9cfa95b7 100644 --- a/ui/src/components/developerKit/EntityEditor.tsx +++ b/ui/src/components/developerKit/EntityEditor.tsx @@ -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) } diff --git a/ui/src/views/list/Chart.tsx b/ui/src/views/list/Chart.tsx index 6a5a0f71..56d5ebae 100644 --- a/ui/src/views/list/Chart.tsx +++ b/ui/src/views/list/Chart.tsx @@ -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 ( diff --git a/ui/src/views/list/Grid.tsx b/ui/src/views/list/Grid.tsx index d80e1118..2c0163b2 100644 --- a/ui/src/views/list/Grid.tsx +++ b/ui/src/views/list/Grid.tsx @@ -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,220 +154,249 @@ 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 grdOpt = gridDto?.gridOptions - const grd = gridRef.current?.instance() - if (!grdOpt || !grd) { - return - } + const layout = layoutTypes.grid + const { toolbarData, toolbarModalData, setToolbarModalData } = useToolbar({ + gridDto, + listFormCode, + getSelectedRowKeys, + getSelectedRowsData, + refreshData, + getFilter, + layout, + }) - // kullanicinin yetkisi varsa ve birden fazla kayit secili ise coklu silme gorunsun - if (grdOpt.editingOptionDto?.allowDeleting) { - // && abp.auth.isGranted(grdOpt.permissionDto?.d) - // kullanicinin silme yetkisi var ise - const opt = grd.option('toolbar') - const deleteSelectedRecordsIndex = opt?.items - ?.map((e: any) => e.name) - .indexOf('deleteSelectedRecords') - // deleteSelectedRecords ismindeki custom butonun index degerini bul + const onSelectionChanged = useCallback( + (data: any) => { + const grdOpt = gridDto?.gridOptions + const grd = gridRef.current?.instance() + if (!grdOpt || !grd) { + return + } - grd.option( - `toolbar.items[${deleteSelectedRecordsIndex}].options.visible`, - data.selectedRowsData.length > 1, - ) // birden fazla kayit secilmis ise gorunsun - } + // kullanicinin yetkisi varsa ve birden fazla kayit secili ise coklu silme gorunsun + if (grdOpt.editingOptionDto?.allowDeleting) { + // && abp.auth.isGranted(grdOpt.permissionDto?.d) + // kullanicinin silme yetkisi var ise + const opt = grd.option('toolbar') + const deleteSelectedRecordsIndex = opt?.items + ?.map((e: any) => e.name) + .indexOf('deleteSelectedRecords') + // deleteSelectedRecords ismindeki custom butonun index degerini bul - // SubForm'ları gösterebilmek için secili satiri formData'ya at - if (data.selectedRowsData.length) { - setFormData(data.selectedRowsData[0]) - } - } + grd.option( + `toolbar.items[${deleteSelectedRecordsIndex}].options.visible`, + data.selectedRowsData.length > 1, + ) // birden fazla kayit secilmis ise gorunsun + } - function onCellPrepared(e: any) { - const columnFormats = gridDto?.columnFormats - if (!columnFormats) { - return - } + // SubForm'ları gösterebilmek için secili satiri formData'ya at + if (data.selectedRowsData.length) { + setFormData(data.selectedRowsData[0]) + } + }, + [gridDto], + ) - // satir, hucre yada header vb. kisimlara conditional style uygulamak icin - for (let indxCol = 0; indxCol < columnFormats.length; indxCol++) { - const colFormat = columnFormats[indxCol] - for (let indxStyl = 0; indxStyl < colFormat.columnStylingDto.length; indxStyl++) { - const colStyle = colFormat.columnStylingDto[indxStyl] // uygulanacak style - if (e.rowType == colStyle.rowType) { - // header, filter, data, group, summaries ..her birisine style uygulanabilir - // style bütün satıra uygulansın olarak seçili ise yada sadece ilgili field üzerinde ise - if (colStyle.useRow || e.column.dataField == colFormat.fieldName) { - if ( - !colStyle.conditionValue || - controlStyleCondition(e.data, colFormat.fieldName, colStyle) - ) { - // css sınıf ismi var ise uygula - if (colStyle.cssClassName) { - e.cellElement.addClass(colStyle.cssClassName) - } - // css inline style var ise uygula - if (colStyle.cssStyles) { - e.cellElement.attr('style', e.cellElement.attr('style') + ';' + colStyle.cssStyles) + const onCellPrepared = useCallback( + (e: any) => { + const columnFormats = gridDto?.columnFormats + if (!columnFormats) { + return + } + + // satir, hucre yada header vb. kisimlara conditional style uygulamak icin + for (let indxCol = 0; indxCol < columnFormats.length; indxCol++) { + const colFormat = columnFormats[indxCol] + for (let indxStyl = 0; indxStyl < colFormat.columnStylingDto.length; indxStyl++) { + const colStyle = colFormat.columnStylingDto[indxStyl] // uygulanacak style + if (e.rowType == colStyle.rowType) { + // header, filter, data, group, summaries ..her birisine style uygulanabilir + // style bütün satıra uygulansın olarak seçili ise yada sadece ilgili field üzerinde ise + if (colStyle.useRow || e.column.dataField == colFormat.fieldName) { + if ( + !colStyle.conditionValue || + controlStyleCondition(e.data, colFormat.fieldName, colStyle) + ) { + // css sınıf ismi var ise uygula + if (colStyle.cssClassName) { + e.cellElement.addClass(colStyle.cssClassName) + } + // css inline style var ise uygula + if (colStyle.cssStyles) { + e.cellElement.attr( + 'style', + e.cellElement.attr('style') + ';' + colStyle.cssStyles, + ) + } } } } } } - } - } + }, + [gridDto], + ) - function onInitNewRow(e: any) { - if (!gridDto?.columnFormats) { - return - } - - setMode('new') - setIsPopupFullScreen(gridDto?.gridOptions.editingOptionDto?.popup?.fullScreen ?? false) - - for (const colFormat of gridDto?.columnFormats) { - if (!colFormat.fieldName) { - continue + const onInitNewRow = useCallback( + (e: any) => { + if (!gridDto?.columnFormats) { + return } - // Grid'den gelen columnFormat'ları kullanarak default değerleri set et - if (colFormat.defaultValue != null) { - if ( - typeof colFormat.defaultValue === 'string' && - colFormat.defaultValue === '@AUTONUMBER' - ) { - e.data[colFormat.fieldName] = autoNumber() - } else { - e.data[colFormat.fieldName] = colFormat.defaultValue + setMode('new') + setIsPopupFullScreen(gridDto?.gridOptions.editingOptionDto?.popup?.fullScreen ?? false) + + for (const colFormat of gridDto?.columnFormats) { + if (!colFormat.fieldName) { + continue } - } - // ExtraFilters içerisinde ilgili Field varsa, default değerleri set etme - if (extraFilters.some((f) => f.fieldName === colFormat.fieldName)) { - continue - } + // Grid'den gelen columnFormat'ları kullanarak default değerleri set et + if (colFormat.defaultValue != null) { + if ( + typeof colFormat.defaultValue === 'string' && + colFormat.defaultValue === '@AUTONUMBER' + ) { + e.data[colFormat.fieldName] = autoNumber() + } else { + e.data[colFormat.fieldName] = colFormat.defaultValue + } + } - // URL'den veya Component Prop'dan gelen parametreleri set et - if (!searchParams) { - continue - } + // ExtraFilters içerisinde ilgili Field varsa, default değerleri set etme + if (extraFilters.some((f) => f.fieldName === colFormat.fieldName)) { + continue + } - const rawFilter = searchParams?.get('filter') - if (rawFilter) { - const parsed = JSON.parse(rawFilter) - const filters = extractSearchParamsFields(parsed) - const fieldMatch = filters.find(([field]) => field === colFormat.fieldName) + // URL'den veya Component Prop'dan gelen parametreleri set et + if (!searchParams) { + continue + } - if (fieldMatch) { - const val = fieldMatch[2] //value - const dType = colFormat.dataType as DataType + const rawFilter = searchParams?.get('filter') + if (rawFilter) { + const parsed = JSON.parse(rawFilter) + const filters = extractSearchParamsFields(parsed) + const fieldMatch = filters.find(([field]) => field === colFormat.fieldName) - switch (dType) { - case 'date': - case 'datetime': - e.data[colFormat.fieldName] = new Date(val) - break - case 'number': - e.data[colFormat.fieldName] = Number(val) - break - case 'boolean': - e.data[colFormat.fieldName] = val === true || val === 'true' - break - case 'object': - try { - e.data[colFormat.fieldName] = JSON.parse(val) - } catch {} - break - default: - e.data[colFormat.fieldName] = val - break + if (fieldMatch) { + const val = fieldMatch[2] //value + const dType = colFormat.dataType as DataType + + switch (dType) { + case 'date': + case 'datetime': + e.data[colFormat.fieldName] = new Date(val) + break + case 'number': + e.data[colFormat.fieldName] = Number(val) + break + case 'boolean': + e.data[colFormat.fieldName] = val === true || val === 'true' + break + case 'object': + try { + e.data[colFormat.fieldName] = JSON.parse(val) + } catch {} + break + default: + e.data[colFormat.fieldName] = val + break + } } } } - } - } + }, + [gridDto, searchParams, extraFilters], + ) - function onRowInserting(e: DataGridTypes.RowInsertingEvent) { + const onRowInserting = useCallback((e: DataGridTypes.RowInsertingEvent) => { e.data = setFormEditingExtraItemValues(e.data) - } + }, []) - function onRowUpdating(e: DataGridTypes.RowUpdatingEvent) { - if (gridDto?.gridOptions.editingOptionDto?.sendOnlyChangedFormValuesUpdate) { - if (Object.keys(e.newData).some((a) => a.includes(':'))) { - Object.keys(e.oldData).forEach((col) => { - if (col.includes(':')) { - e.newData[col] = e.newData[col] ?? e.oldData[col] + const onRowUpdating = useCallback( + (e: DataGridTypes.RowUpdatingEvent) => { + if (gridDto?.gridOptions.editingOptionDto?.sendOnlyChangedFormValuesUpdate) { + if (Object.keys(e.newData).some((a) => a.includes(':'))) { + Object.keys(e.oldData).forEach((col) => { + if (col.includes(':')) { + e.newData[col] = e.newData[col] ?? e.oldData[col] + } + }) + } + e.newData = setFormEditingExtraItemValues(e.newData) + } else { + let newData = { ...e.oldData, ...e.newData } + newData = setFormEditingExtraItemValues(newData) + Object.keys(newData).forEach((key) => { + if (key.includes(':')) { + delete newData[key] } }) + e.newData = newData } - e.newData = setFormEditingExtraItemValues(e.newData) - } else { - let newData = { ...e.oldData, ...e.newData } - newData = setFormEditingExtraItemValues(newData) - Object.keys(newData).forEach((key) => { - if (key.includes(':')) { - delete newData[key] + + if (gridDto?.gridOptions.keyFieldName) { + delete e.newData[gridDto?.gridOptions.keyFieldName] + } + }, + [gridDto], + ) + + const onEditingStart = useCallback( + (e: DataGridTypes.EditingStartEvent) => { + isEditingRef.current = true + setMode('edit') + setIsPopupFullScreen(gridDto?.gridOptions.editingOptionDto?.popup?.fullScreen ?? false) + const columns = e.component.option('columns') as GridColumnData[] + // FormEditingExtraItem field ise datayı doldur + columns?.forEach((col) => { + if (!col.dataField?.includes(':')) { + return } + const field = col.dataField.split(':') + if (!e.data[field[0]]) { + return + } + const json = JSON.parse(e.data[field[0]]) + e.data[col.dataField] = json[field[1]] }) - e.newData = newData - } + }, + [gridDto, mode], + ) - if (gridDto?.gridOptions.keyFieldName) { - delete e.newData[gridDto?.gridOptions.keyFieldName] - } - } - - function onEditingStart(e: DataGridTypes.EditingStartEvent) { - isEditingRef.current = true - setMode('edit') - setIsPopupFullScreen(gridDto?.gridOptions.editingOptionDto?.popup?.fullScreen ?? false) - const columns = e.component.option('columns') as GridColumnData[] - // FormEditingExtraItem field ise datayı doldur - columns?.forEach((col) => { - if (!col.dataField?.includes(':')) { - return - } - const field = col.dataField.split(':') - if (!e.data[field[0]]) { - return - } - const json = JSON.parse(e.data[field[0]]) - e.data[col.dataField] = json[field[1]] - }) - } - - function onDataErrorOccurred(e: DataGridTypes.DataErrorOccurredEvent) { + const onDataErrorOccurred = useCallback((e: DataGridTypes.DataErrorOccurredEvent) => { toast.push( {e.error?.message} @@ -387,133 +405,152 @@ const Grid = (props: GridProps) => { placement: 'top-end', }, ) - } + }, []) - function onEditorPreparing(editor: DataGridTypes.EditorPreparingEvent) { - if (editor.parentType === 'dataRow' && editor.dataField && gridDto) { - const formItem = gridDto.gridOptions.editingFormDto - .flatMap((group) => group.items || []) - .find((i) => i.dataField === editor.dataField) + // Cascade parent fields mapping'i memoize et - popup açılışını hızlandırır + const cascadeFieldsMap = useMemo(() => { + if (!gridDto) return new Map() - // 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 prevHandler = editor.editorOptions.onValueChanged - - editor.editorOptions.onValueChanged = (e: any) => { - if (prevHandler) prevHandler(e) - - // Parent field değiştiğinde tüm cascade childları kontrol et - const grid = editor.component - const rowKey = grid.option('editing.editRowKey') - const rowIndex = grid.getRowIndexByKey(rowKey) - 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 (rowIndex >= 0) { - grid.cellValue(rowIndex, childField, null) - } - }) - } - - // Tüm cascade fieldlerin disabled durumlarını güncelle - 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]) - - try { - const editorInstance = formInstance.getEditor(col.fieldName!) - if (editorInstance) { - editorInstance.option('disabled', shouldDisable) - } - } catch (err) { - console.debug('Cascade disabled update skipped for', col.fieldName, err) - } - } - }) - } - } - } - - // İlk açılışta disabled durumunu kontrol et - const grid = editor.component - const rowKey = grid.option('editing.editRowKey') - const rowData = grid.getVisibleRows().find((r) => r.key === rowKey)?.data || {} - const shouldDisable = parentFields.some((pf: string) => !rowData[pf]) - - if (shouldDisable) { - editor.editorOptions.disabled = true - } + const map = new Map< + string, + { + parentFields: string[] + childFields?: string[] } + >() - if (formItem?.editorScript) { - const prevHandler = editor.editorOptions.onValueChanged + 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()), + }) + } + }) - editor.editorOptions.onValueChanged = (e: any) => { - if (prevHandler) prevHandler(e) + return map + }, [gridDto]) - try { + const onEditorPreparing = useCallback( + (editor: DataGridTypes.EditorPreparingEvent) => { + 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 cascadeInfo = cascadeFieldsMap.get(editor.dataField) + if (cascadeInfo) { + const parentFields = cascadeInfo.parentFields + + const prevHandler = editor.editorOptions.onValueChanged + + editor.editorOptions.onValueChanged = (e: any) => { + if (prevHandler) prevHandler(e) + + // Parent field değiştiğinde tüm cascade childları kontrol et const grid = editor.component const rowKey = grid.option('editing.editRowKey') const rowIndex = grid.getRowIndexByKey(rowKey) + const rowData = grid.getVisibleRows().find((r) => r.key === rowKey)?.data || {} - const formData = grid.getVisibleRows().find((r) => r.key === rowKey)?.data || {} + // Bu field bir parent ise, child fieldleri temizle + if (cascadeInfo.childFields) { + cascadeInfo.childFields.forEach((childField: string) => { + if (rowIndex >= 0) { + grid.cellValue(rowIndex, childField, null) + } + }) + } - const setFormData = (newData: any) => { - if (rowIndex >= 0) { - Object.keys(newData).forEach((field) => { - grid.cellValue(rowIndex, field, newData[field]) + // 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) { + // 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(fieldName) + if (editorInstance) { + editorInstance.option('disabled', shouldDisable) + } + } catch (err) { + console.debug('Cascade disabled update skipped for', fieldName, err) + } + } }) } } + } - eval(formItem.editorScript!) - } catch (err) { - console.error('Script exec error', formItem.dataField, err) + // İlk açılışta disabled durumunu kontrol et + const grid = editor.component + const rowKey = grid.option('editing.editRowKey') + const rowData = grid.getVisibleRows().find((r) => r.key === rowKey)?.data || {} + const shouldDisable = parentFields.some((pf: string) => !rowData[pf]) + + if (shouldDisable) { + editor.editorOptions.disabled = true } } - } - if (editor.editorOptions?.buttons) { - editor.editorOptions.buttons = editor.editorOptions.buttons.map((btn: any) => { - if (btn?.options?.onClick && typeof btn.options.onClick === 'function') { - const origClick = btn.options.onClick - btn.options.onClick = (e: any) => { + if (formItem?.editorScript) { + const prevHandler = editor.editorOptions.onValueChanged + + editor.editorOptions.onValueChanged = (e: any) => { + if (prevHandler) prevHandler(e) + + try { const grid = editor.component const rowKey = grid.option('editing.editRowKey') const rowIndex = grid.getRowIndexByKey(rowKey) + const formData = grid.getVisibleRows().find((r) => r.key === rowKey)?.data || {} - origClick({ - ...e, - formData, - fieldName: editor.dataField, - rowKey, - rowIndex, - }) + const setFormData = (newData: any) => { + if (rowIndex >= 0) { + Object.keys(newData).forEach((field) => { + grid.cellValue(rowIndex, field, newData[field]) + }) + } + } + + eval(formItem.editorScript!) + } catch (err) { + console.error('Script exec error', formItem.dataField, err) } } - return btn - }) + } + + if (editor.editorOptions?.buttons) { + editor.editorOptions.buttons = editor.editorOptions.buttons.map((btn: any) => { + if (btn?.options?.onClick && typeof btn.options.onClick === 'function') { + const origClick = btn.options.onClick + btn.options.onClick = (e: any) => { + const grid = editor.component + const rowKey = grid.option('editing.editRowKey') + const rowIndex = grid.getRowIndexByKey(rowKey) + const formData = grid.getVisibleRows().find((r) => r.key === rowKey)?.data || {} + + origClick({ + ...e, + formData, + fieldName: editor.dataField, + rowKey, + rowIndex, + }) + } + } + return btn + }) + } } - } - } + }, + [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 diff --git a/ui/src/views/list/Pivot.tsx b/ui/src/views/list/Pivot.tsx index 7a442153..adbba732 100644 --- a/ui/src/views/list/Pivot.tsx +++ b/ui/src/views/list/Pivot.tsx @@ -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() // PivotGrid’i 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( + + {translate('::App.Common.ExportError') ?? 'Dışa aktarma sırasında hata oluştu.'} + , + { 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 = 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,31 +350,37 @@ 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) { - const pivotInstance = gridRef?.current?.instance() - const chartInstance = chartRef?.current?.instance() - if (pivotInstance && chartInstance) { - pivotInstance.bindChart(chartInstance, { - dataFieldsDisplayMode: 'splitPanes', - alternateDataFields: false, - }) + instance.option('dataSource', dataSource) + + // 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) { + pivotInstance.bindChart(chartInstance, { + dataFieldsDisplayMode: 'splitPanes', + 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} > + { enabled={gridDto.gridOptions.pivotOptionDto.columnChooserEnabled} height={500} /> - diff --git a/ui/src/views/list/Tree.tsx b/ui/src/views/list/Tree.tsx index 7bd58da5..b754d524 100644 --- a/ui/src/views/list/Tree.tsx +++ b/ui/src/views/list/Tree.tsx @@ -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,81 +192,103 @@ 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 treeOpt = gridDto?.gridOptions - const tree = gridRef.current?.instance() - if (!treeOpt || !tree) { - return - } + const layout = layoutTypes.tree + const { toolbarData, toolbarModalData, setToolbarModalData } = useToolbar({ + gridDto, + listFormCode, + getSelectedRowKeys, + getSelectedRowsData, + refreshData, + expandAll, + collapseAll, + getFilter, + layout, + }) - if (treeOpt.editingOptionDto?.allowDeleting) { - try { - const opt = tree.option('toolbar') - if (opt && opt.items && Array.isArray(opt.items)) { - const deleteSelectedRecordsIndex = opt.items - .map((e: any) => e.name) - .indexOf('deleteSelectedRecords') - - if (deleteSelectedRecordsIndex >= 0) { - tree.option( - `toolbar.items[${deleteSelectedRecordsIndex}].options.visible`, - data.selectedRowsData.length > 1, - ) - } - } - } catch (error) { - console.error('Error updating toolbar items:', error) + const onSelectionChanged = useCallback( + (data: any) => { + const treeOpt = gridDto?.gridOptions + const tree = gridRef.current?.instance() + if (!treeOpt || !tree) { + return } - } - if (data.selectedRowsData.length) { - setFormData(data.selectedRowsData[0]) - } - } + if (treeOpt.editingOptionDto?.allowDeleting) { + try { + const opt = tree.option('toolbar') + if (opt && opt.items && Array.isArray(opt.items)) { + const deleteSelectedRecordsIndex = opt.items + .map((e: any) => e.name) + .indexOf('deleteSelectedRecords') - function onCellPrepared(e: any) { - const columnFormats = gridDto?.columnFormats - if (!columnFormats) { - return - } + if (deleteSelectedRecordsIndex >= 0) { + tree.option( + `toolbar.items[${deleteSelectedRecordsIndex}].options.visible`, + data.selectedRowsData.length > 1, + ) + } + } + } catch (error) { + console.error('Error updating toolbar items:', error) + } + } - for (let indxCol = 0; indxCol < columnFormats.length; indxCol++) { - const colFormat = columnFormats[indxCol] - for (let indxStyl = 0; indxStyl < colFormat.columnStylingDto.length; indxStyl++) { - const colStyle = colFormat.columnStylingDto[indxStyl] - if (e.rowType == colStyle.rowType) { - if (colStyle.useRow || e.column.dataField == colFormat.fieldName) { - if ( - !colStyle.conditionValue || - controlStyleCondition(e.data, colFormat.fieldName, colStyle) - ) { - if (colStyle.cssClassName) { - e.cellElement.addClass(colStyle.cssClassName) - } - if (colStyle.cssStyles) { - e.cellElement.attr('style', e.cellElement.attr('style') + ';' + colStyle.cssStyles) + if (data.selectedRowsData.length) { + setFormData(data.selectedRowsData[0]) + } + }, + [gridDto], + ) + + const onCellPrepared = useCallback( + (e: any) => { + const columnFormats = gridDto?.columnFormats + if (!columnFormats) { + return + } + + for (let indxCol = 0; indxCol < columnFormats.length; indxCol++) { + const colFormat = columnFormats[indxCol] + for (let indxStyl = 0; indxStyl < colFormat.columnStylingDto.length; indxStyl++) { + const colStyle = colFormat.columnStylingDto[indxStyl] + if (e.rowType == colStyle.rowType) { + if (colStyle.useRow || e.column.dataField == colFormat.fieldName) { + if ( + !colStyle.conditionValue || + controlStyleCondition(e.data, colFormat.fieldName, colStyle) + ) { + if (colStyle.cssClassName) { + e.cellElement.addClass(colStyle.cssClassName) + } + if (colStyle.cssStyles) { + e.cellElement.attr( + 'style', + e.cellElement.attr('style') + ';' + colStyle.cssStyles, + ) + } } } } } } - } - } + }, + [gridDto], + ) function onInitNewRow(e: any) { if (!gridDto?.columnFormats) { @@ -349,55 +358,61 @@ 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) { - if (gridDto?.gridOptions.editingOptionDto?.sendOnlyChangedFormValuesUpdate) { - if (Object.keys(e.newData).some((a) => a.includes(':'))) { - Object.keys(e.oldData).forEach((col) => { - if (col.includes(':')) { - e.newData[col] = e.newData[col] ?? e.oldData[col] + 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) => { + if (col.includes(':')) { + e.newData[col] = e.newData[col] ?? e.oldData[col] + } + }) + } + e.newData = setFormEditingExtraItemValues(e.newData) + } else { + let newData = { ...e.oldData, ...e.newData } + newData = setFormEditingExtraItemValues(newData) + Object.keys(newData).forEach((key) => { + if (key.includes(':')) { + delete newData[key] } }) + e.newData = newData } - e.newData = setFormEditingExtraItemValues(e.newData) - } else { - let newData = { ...e.oldData, ...e.newData } - newData = setFormEditingExtraItemValues(newData) - Object.keys(newData).forEach((key) => { - if (key.includes(':')) { - delete newData[key] + + if (gridDto?.gridOptions.keyFieldName) { + delete e.newData[gridDto?.gridOptions.keyFieldName] + } + }, + [gridDto], + ) + + const onEditingStart = useCallback( + (e: TreeListTypes.EditingStartEvent) => { + isEditingRef.current = true + setMode('edit') + setIsPopupFullScreen(gridDto?.gridOptions.editingOptionDto?.popup?.fullScreen ?? false) + const columns = e.component.option('columns') as GridColumnData[] + columns?.forEach((col) => { + if (!col.dataField?.includes(':')) { + return } + const field = col.dataField.split(':') + if (!e.data[field[0]]) { + return + } + const json = JSON.parse(e.data[field[0]]) + e.data[col.dataField] = json[field[1]] }) - e.newData = newData - } + }, + [gridDto], + ) - if (gridDto?.gridOptions.keyFieldName) { - delete e.newData[gridDto?.gridOptions.keyFieldName] - } - } - - function onEditingStart(e: TreeListTypes.EditingStartEvent) { - isEditingRef.current = true - setMode('edit') - setIsPopupFullScreen(gridDto?.gridOptions.editingOptionDto?.popup?.fullScreen ?? false) - const columns = e.component.option('columns') as GridColumnData[] - columns?.forEach((col) => { - if (!col.dataField?.includes(':')) { - return - } - const field = col.dataField.split(':') - if (!e.data[field[0]]) { - return - } - const json = JSON.parse(e.data[field[0]]) - e.data[col.dataField] = json[field[1]] - }) - } - - function onDataErrorOccurred(e: TreeListTypes.DataErrorOccurredEvent) { + const onDataErrorOccurred = useCallback((e: TreeListTypes.DataErrorOccurredEvent) => { toast.push( {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]) diff --git a/ui/src/views/list/useListFormColumns.ts b/ui/src/views/list/useListFormColumns.ts index 6dff99d7..2ba29eb6 100644 --- a/ui/src/views/list/useListFormColumns.ts +++ b/ui/src/views/list/useListFormColumns.ts @@ -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>() +// lookup cache (module scope) - cache süresini ve boyutunu yönet +const __lookupCache = new Map; 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) => { - 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) => { __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,