import Container from '@/components/shared/Container' import '@/utils/registerDevExtremeEditors' import { Dialog, Notification, toast } from '@/components/ui' import { DX_CLASSNAMES } from '@/constants/app.constant' import { GridDto, SubFormTabTypeEnum } from '@/proxy/form/models' import { useLocalization } from '@/utils/hooks/useLocalization' import { Template } from 'devextreme-react/core/template' import DataGrid, { ColumnChooser, ColumnFixing, type DataGridRef, type DataGridTypes, Editing, Export, FilterPanel, FilterRow, Grouping, GroupItem as GroupItemDx, GroupPanel, HeaderFilter, LoadPanel, Pager, Paging, Scrolling, SearchPanel, Selection, Sorting, Summary, Toolbar, TotalItem, } from 'devextreme-react/data-grid' import { Item } from 'devextreme-react/toolbar' import CustomStore from 'devextreme/data/custom_store' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import PageTitle from '@/components/shared/PageTitle' import SubForms from '../form/SubForms' import { RowMode } from '../form/types' import { GridColumnData } from './GridColumnData' import GridFilterDialogs from './GridFilterDialogs' import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent' import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent' import { ImageViewerEditorComponent } from './editors/ImageViewerEditorComponent' import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent' import { useFilters } from './useFilters' import { updateWorkflowApprovalToolbarItems, useToolbar } from './useToolbar' import { ImportDashboard } from '@/components/importManager/ImportDashboard' import WidgetGroup from '@/components/ui/Widget/WidgetGroup' import { GridExtraFilterToolbar } from './GridExtraFilterToolbar' import { layoutTypes } from '../admin/listForm/edit/types' import { useListFormCustomDataSource } from './useListFormCustomDataSource' import { useListFormColumns } from './useListFormColumns' import { orderListToolbarItems } from './toolbarOrder' import { Loading } from '@/components/shared' import { useStoreState } from '@/store' import { NotePanel } from '../form/notes/NotePanel' import { useListFormStateStoring } from './useListFormStateStoring' import { exportListView } from './shared/exportListView' import { buildEditPopupOptions, shouldUseMobileEditPopup } from './shared/editPopup' import { applyNewRowDefaults, buildEditingFormItems, buildInsertPayload, buildUpdatePayload, createFormItemMapper, expandJsonFormFields, getActiveEditingForm, getValueByField, } from './shared/editingForm' import { attachEditorButtonContext, attachEditorValueChangedScript, createEditingFormHandlers, } from './shared/editingFormScripts' import { attachCascadeBehavior, createCascadeMaps } from './shared/cascade' import { createConditionalCellStyleHandler } from './shared/cellStyles' import { useExtraFilters, useListFormCustomSources, useListFormGridDto, useListFormWidgets, useWidgetGroupHeight, } from './shared/hooks' import { startWorkflowForInsertedRow } from './shared/workflowRows' import { applyPhoneGlobalFormat } from './shared/columnFormatters' interface GridProps { listFormCode: string searchParams?: URLSearchParams isSubForm?: boolean level?: number refreshData?: () => Promise gridDto?: GridDto } const remoteOperations = { paging: true, filtering: true, sorting: true, grouping: false, summary: false, } const Grid = (props: GridProps) => { const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props const { translate } = useLocalization() const currentUser = useStoreState((state) => state.auth.user) const currentCulture = useStoreState( (state) => state.abpConfig.config?.localization.currentCulture.cultureName, ) const localizationTexts = useStoreState((state) => state.abpConfig.texts) const useMobileEditPopup = shouldUseMobileEditPopup() const gridRef = useRef(undefined) const widgetGroupRef = useRef(null) const editingFormDataRef = useRef>({}) const editingFormInstanceRef = useRef(undefined) // Edit popup açıkken grid state'inin kaydedilmesini engeller. const isEditingRef = useRef(false) const [gridDataSource, setGridDataSource] = useState>() const [columnData, setColumnData] = useState() const [formData, setFormData] = useState() const [mode, setMode] = useState('view') const gridDto = useListFormGridDto(listFormCode, extGridDto) const [isPopupFullScreen, setIsPopupFullScreen] = useState(false) const [notePanelTarget, setNotePanelTarget] = useState<{ entityName: string entityId: string } | null>(null) const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto, isSubForm) const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets) useListFormCustomSources(gridDto) const getGridInstance = useCallback(() => gridRef.current?.instance(), []) const { customSaveState, customLoadState } = useListFormStateStoring({ listFormCode, storageKey: gridDto?.gridOptions?.stateStoringDto?.storageKey, filterPrefix: 'list', skipSaveRef: isEditingRef, }) const { filterToolbarData, ...filterData } = useFilters({ gridDto, gridRef, listFormCode, saveGridState: customSaveState, searchParams, }) const { createSelectDataSource } = useListFormCustomDataSource({ gridRef }) const refreshGrid = useCallback(() => { getGridInstance()?.refresh() }, [getGridInstance]) const { extraFilters, setExtraFilters } = useExtraFilters({ gridDto, listFormCode, searchParams, onFilterChanged: refreshGrid, }) const openNotePanel = useCallback( (rowData: Record) => { const entityId = getValueByField(rowData, gridDto?.gridOptions.keyFieldName) if (entityId === undefined || entityId === null || entityId === '') { return } setNotePanelTarget({ entityName: gridDto?.gridOptions.listFormCode ?? listFormCode, entityId: String(entityId), }) }, [gridDto, listFormCode], ) const { getBandedColumns, refreshLookupsForRow } = useListFormColumns({ gridDto, listFormCode, isSubForm, gridRef, onShowNote: openNotePanel, }) /* ======================= TOOLBAR ======================= */ const updateWorkflowApprovalButtons = useCallback( (component?: any, selectedRowsData?: Record[]) => { const grid = component ?? getGridInstance() if (!grid) return updateWorkflowApprovalToolbarItems( grid, gridDto?.gridOptions.workflowDto, selectedRowsData ?? grid.getSelectedRowsData(), currentUser, ) }, [currentUser, getGridInstance, gridDto], ) const refreshData = useCallback(() => { const grid = getGridInstance() Promise.resolve(grid?.refresh()).finally(() => updateWorkflowApprovalButtons(grid)) void refreshWidgets() }, [getGridInstance, updateWorkflowApprovalButtons, refreshWidgets]) const getSelectedRowKeys = useCallback( async () => (await getGridInstance()?.getSelectedRowKeys()) ?? [], [getGridInstance], ) const getSelectedRowsData = useCallback( () => getGridInstance()?.getSelectedRowsData() ?? [], [getGridInstance], ) const getFilter = useCallback(() => getGridInstance()?.getCombinedFilter(), [getGridInstance]) const { toolbarData, toolbarModalData, setToolbarModalData } = useToolbar({ gridDto, listFormCode, getSelectedRowKeys, getSelectedRowsData, refreshData, getFilter, layout: layoutTypes.grid, }) const orderedToolbarData = useMemo( () => orderListToolbarItems([...toolbarData, ...filterToolbarData]), [toolbarData, filterToolbarData], ) /* ======================= GRID EVENTS ======================= */ const onSelectionChanged = useCallback( (data: any) => { const grid = getGridInstance() if (!gridDto?.gridOptions || !grid) return // Birden fazla kayıt seçiliyse ve silme yetkisi varsa toplu silme butonunu göster. if (gridDto.gridOptions.editingOptionDto?.allowDeleting) { const items = grid.option('toolbar')?.items ?? [] const deleteSelectedIndex = items.findIndex( (item: any) => item.name === 'deleteSelectedRecords', ) if (deleteSelectedIndex >= 0) { grid.option( `toolbar.items[${deleteSelectedIndex}].options.visible`, data.selectedRowsData.length > 1, ) } } updateWorkflowApprovalButtons(grid, data.selectedRowsData) // SubForm'ları besleyebilmek için seçili ilk satırı sakla. if (data.selectedRowsData.length) { setFormData(data.selectedRowsData[0]) } }, [getGridInstance, gridDto, updateWorkflowApprovalButtons], ) const onCellPrepared = useMemo(() => createConditionalCellStyleHandler(gridDto), [gridDto]) const onInitNewRow = useCallback( (e: DataGridTypes.InitNewRowEvent) => { if (!gridDto?.columnFormats) { editingFormDataRef.current = { ...(e.data || {}) } return } setMode('new') setIsPopupFullScreen( useMobileEditPopup || (gridDto.gridOptions.editingOptionDto?.popup?.fullScreen ?? false), ) e.promise = applyNewRowDefaults( gridDto, e.data, searchParams?.get('filter'), extraFilters, ).then((data) => { editingFormDataRef.current = { ...data } }) }, [gridDto, searchParams, extraFilters, useMobileEditPopup], ) const onRowInserting = useCallback( (e: DataGridTypes.RowInsertingEvent) => { e.data = buildInsertPayload(gridDto, e.data) }, [gridDto], ) const onRowUpdating = useCallback( (e: DataGridTypes.RowUpdatingEvent) => { if (!gridDto?.columnFormats) return e.newData = buildUpdatePayload(gridDto, e.oldData, e.newData) }, [gridDto], ) const onEditingStart = useCallback( (e: DataGridTypes.EditingStartEvent) => { isEditingRef.current = true editingFormDataRef.current = { ...(e.data || {}) } setMode('edit') setIsPopupFullScreen( useMobileEditPopup || (gridDto?.gridOptions.editingOptionDto?.popup?.fullScreen ?? false), ) expandJsonFormFields(e.component.option('columns') as GridColumnData[], e.data) }, [gridDto, useMobileEditPopup], ) const onEditFinished = useCallback(() => { isEditingRef.current = false setMode('view') setIsPopupFullScreen(false) }, []) const onDataErrorOccurred = useCallback((e: DataGridTypes.DataErrorOccurredEvent) => { toast.push( {e.error?.message} , { placement: 'bottom-end' }, ) }, []) const cascadeMaps = useMemo(() => createCascadeMaps(gridDto), [gridDto]) const onEditorPreparing = useCallback( (editor: DataGridTypes.EditorPreparingEvent) => { if (editor.parentType !== 'dataRow' || !editor.dataField || !gridDto) return const columnFormat = gridDto.columnFormats.find( (column) => column.fieldName === editor.dataField, ) const isNewRow = Boolean((editor as any).row?.isNewRow) || mode === 'new' if (columnFormat?.editorOptions) { try { editor.editorOptions = { ...editor.editorOptions, ...JSON.parse(columnFormat.editorOptions), } } catch (err) { console.error('EditorOptions parse error:', columnFormat.fieldName, err) } } const hasReadOnlyEditorOption = editor.editorOptions?.readOnly === true const hasDisabledEditorOption = editor.editorOptions?.disabled === true if ( (isNewRow && columnFormat?.allowAdding === false) || (!isNewRow && columnFormat?.allowEditing === false) ) { editor.editorOptions.readOnly = true } else if (isNewRow && columnFormat?.allowAdding === true) { if (!hasReadOnlyEditorOption) editor.editorOptions.readOnly = false if (!hasDisabledEditorOption) editor.editorOptions.disabled = false } const cascadeInfo = cascadeMaps.cascadeFieldsMap.get(editor.dataField) if (cascadeInfo) { attachCascadeBehavior({ editor, gridDto, cascadeInfo, maps: cascadeMaps }) } // Yalnızca düzenleme formunda yer alan alanların script'i çalıştırılır. if (columnFormat?.editOrderNo != null && columnFormat.editorScript) { attachEditorValueChangedScript({ editor, editorScript: columnFormat.editorScript, fieldName: columnFormat.fieldName, editingFormInstanceRef, getActiveEditingForm, }) } attachEditorButtonContext(editor) }, [gridDto, cascadeMaps, mode], ) /* ======================= COLUMNS & DATASOURCE ======================= */ // listFormCode değişiminde eski kolon/veri kaynağını temizle. useEffect(() => { setGridDataSource(undefined) setColumnData(undefined) const instance = getGridInstance() if (instance) { instance.option('columns', undefined) instance.option('remoteOperations', false) instance.option('dataSource', undefined) instance.state(null) } }, [listFormCode, getGridInstance]) // Kolonlar dil değiştiğinde de yeniden üretilir. const memoizedColumns = useMemo(() => { if (!gridDto) return undefined const columns = getBandedColumns() columns?.forEach(applyPhoneGlobalFormat) return columns // getBandedColumns her render'da yeni referans üretebildiği için bağımlılığa alınmaz. // eslint-disable-next-line react-hooks/exhaustive-deps }, [gridDto, currentCulture, localizationTexts]) const memoizedDataSource = useMemo(() => { if (!gridDto) return undefined return createSelectDataSource( gridDto.gridOptions, listFormCode, searchParams, layoutTypes.grid, // Kolonlar henüz grid'e set edilmemiş olabilir; store instance üzerinden okur. undefined, ) }, [gridDto, listFormCode, searchParams, createSelectDataSource]) useEffect(() => setColumnData(memoizedColumns), [memoizedColumns]) useEffect(() => setGridDataSource(memoizedDataSource), [memoizedDataSource]) useEffect(() => { const instance = getGridInstance() if (columnData && instance) { instance.option('columns', columnData) } }, [columnData, getGridInstance]) // Otomatik state kaydetme/yükleme kapalı; manuel akış kullanılır. useEffect(() => { if (!gridDto) return getGridInstance()?.option('stateStoring', { enabled: false }) }, [gridDto, getGridInstance]) /** * State önce yüklenir, ardından dataSource set edilir. Böylece kayıtlı * filtre/sıralama tek bir veri çekimiyle uygulanır. */ useEffect(() => { const instance = getGridInstance() if (!gridDto || !instance || !gridDataSource || !columnData) return const applyDataSource = (state?: any) => { instance.option('remoteOperations', remoteOperations) if (state) instance.state(state) instance.option('dataSource', gridDataSource) } customLoadState() .then(applyDataSource) .catch((err) => { console.error('State load error:', err) applyDataSource() }) }, [gridDto, gridDataSource, columnData, customLoadState, getGridInstance]) /* ======================= EDITING FORM ======================= */ const mapFormItem = useMemo( () => createFormItemMapper({ mode, translate, rawFilter: searchParams?.get('filter'), extraFilters, getEditingFormData: () => editingFormDataRef.current, }), [mode, translate, searchParams, extraFilters], ) const editingFormItems = useMemo( () => buildEditingFormItems(gridDto, mapFormItem, mode, translate), [gridDto, mapFormItem, mode, translate], ) const editingFormHandlers = useMemo( () => createEditingFormHandlers({ getGridDto: () => gridDto, getGridInstance, editingFormDataRef, editingFormInstanceRef, }), [gridDto, getGridInstance], ) const editPopupOptions = useMemo( () => buildEditPopupOptions({ popupOptions: gridDto?.gridOptions.editingOptionDto?.popup, mode, isPopupFullScreen, useMobileEditPopup, baseWrapperClass: 'dx-datagrid-edit-popup', translate, handlers: { onSave: () => getGridInstance()?.saveEditData(), onCancel: () => getGridInstance()?.cancelEditData(), onToggleFullScreen: () => setIsPopupFullScreen((current) => !current), }, }), [gridDto, mode, isPopupFullScreen, useMobileEditPopup, translate, getGridInstance], ) const onExporting = useCallback( (e: DataGridTypes.ExportingEvent) => { e.cancel = true const grid = getGridInstance() if (!grid) return void exportListView({ component: grid, format: e.format, fileName: listFormCode, onError: () => toast.push( {translate('::App.Common.ExportError')} , { placement: 'bottom-end' }, ), }) }, [getGridInstance, listFormCode, translate], ) /* ======================= RENDER ======================= */ const gridOptions = gridDto?.gridOptions const loadingMessage = !gridDto ? 'Loading grid configuration...' : !columnData ? 'Loading columns...' : !gridDataSource ? 'Loading data source...' : undefined return ( <>
0 ? 'mt-2' : ''}>
{!isSubForm && } {loadingMessage && (
{loadingMessage}
)} {gridDto && gridOptions && columnData && gridDataSource && (
0 ? gridOptions.height : gridOptions.fullHeight ? `calc(100vh - ${170 + widgetGroupHeight}px)` : undefined } width={gridOptions.width || '100%'} allowColumnResizing={gridOptions.columnOptionDto?.allowColumnResizing} allowColumnReordering={gridOptions.columnOptionDto?.allowColumnReordering} showBorders={gridOptions.columnOptionDto?.showBorders} showRowLines={gridOptions.columnOptionDto?.showRowLines} showColumnLines={gridOptions.columnOptionDto?.showColumnLines} columnResizingMode={gridOptions.columnOptionDto?.columnResizingMode} columnAutoWidth={gridOptions.columnOptionDto?.columnAutoWidth} rtlEnabled={gridOptions.columnOptionDto?.rtlEnabled} rowAlternationEnabled={gridOptions.columnOptionDto?.rowAlternationEnabled} hoverStateEnabled={gridOptions.columnOptionDto?.hoverStateEnabled} columnHidingEnabled={gridOptions.columnOptionDto?.columnHidingEnabled} focusedRowEnabled={gridOptions.columnOptionDto?.focusedRowEnabled} showColumnHeaders={gridOptions.columnOptionDto?.showColumnHeaders} filterSyncEnabled onRowPrepared={(e) => { if (e.rowType === 'data') { e.rowElement.style.height = gridOptions.rowDto.rowHeight e.rowElement.style.whiteSpace = gridOptions.rowDto.whiteSpace e.rowElement.style.overflowWrap = gridOptions.rowDto.overflowWrap } }} onSelectionChanged={onSelectionChanged} onContentReady={(e) => updateWorkflowApprovalButtons(e.component)} onInitNewRow={onInitNewRow} onCellPrepared={onCellPrepared} onRowInserting={onRowInserting} onRowUpdating={onRowUpdating} onEditingStart={onEditingStart} onDataErrorOccurred={onDataErrorOccurred} onExporting={onExporting} onEditCanceling={() => setMode('view')} onEditCanceled={onEditFinished} onSaved={onEditFinished} onRowInserted={(e) => { startWorkflowForInsertedRow(e, gridDto, listFormCode, refreshGrid) props.refreshData?.() void refreshWidgets() void refreshLookupsForRow(e.component, e.data) }} onRowUpdated={(e) => { props.refreshData?.() void refreshWidgets() void refreshLookupsForRow(e.component, e.data) }} onRowRemoved={() => { props.refreshData?.() void refreshWidgets() }} onEditorPreparing={onEditorPreparing} >