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 TreeListDx, { ColumnChooser, ColumnFixing, Editing, FilterPanel, FilterRow, HeaderFilter, LoadPanel, Pager, Paging, RemoteOperations, Scrolling, SearchPanel, Selection, Sorting, Toolbar, type TreeListRef, type TreeListTypes, } from 'devextreme-react/tree-list' 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 { 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 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 { useStoreState } from '@/store/store' import SubForms from '../form/SubForms' import { ImportDashboard } from '@/components/importManager/ImportDashboard' import { NotePanel } from '../form/notes/NotePanel' import { useListFormStateStoring } from './useListFormStateStoring' 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, useWidgetGroupHeight, } from './shared/hooks' import { startWorkflowForInsertedRow } from './shared/workflowRows' interface TreeProps { listFormCode: string searchParams?: URLSearchParams isSubForm?: boolean level?: number refreshData?: () => Promise gridDto?: GridDto } const defaultPageSizes = [10, 20, 50, 100] const Tree = (props: TreeProps) => { 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 [treeListDataSource, setTreeListDataSource] = 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 [expandedRowKeys, setExpandedRowKeys] = useState([]) const [notePanelTarget, setNotePanelTarget] = useState<{ entityName: string entityId: string } | null>(null) const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, gridDto?.widgets) useListFormCustomSources(gridDto) const getTreeInstance = useCallback(() => gridRef.current?.instance(), []) const { customSaveState, customLoadState } = useListFormStateStoring({ listFormCode, storageKey: gridDto?.gridOptions.stateStoringDto?.storageKey, filterPrefix: 'tree', skipSaveRef: isEditingRef, }) const { filterToolbarData, ...filterData } = useFilters({ gridDto, gridRef, listFormCode, saveGridState: customSaveState, searchParams, }) const { createSelectDataSource } = useListFormCustomDataSource({ gridRef }) const refreshTree = useCallback(() => { getTreeInstance()?.refresh() }, [getTreeInstance]) const { extraFilters, setExtraFilters } = useExtraFilters({ gridDto, listFormCode, searchParams, onFilterChanged: refreshTree, }) 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 } = useListFormColumns({ gridDto, listFormCode, isSubForm, gridRef, onShowNote: openNotePanel, }) /* ======================= TOOLBAR ======================= */ const updateWorkflowApprovalButtons = useCallback( (component?: any, selectedRowsData?: Record[]) => { const tree = component ?? getTreeInstance() if (!tree) return updateWorkflowApprovalToolbarItems( tree, gridDto?.gridOptions.workflowDto, selectedRowsData ?? tree.getSelectedRowsData(), currentUser, ) }, [currentUser, getTreeInstance, gridDto], ) const refreshData = useCallback(() => { const tree = getTreeInstance() Promise.resolve(tree?.refresh()).finally(() => updateWorkflowApprovalButtons(tree)) }, [getTreeInstance, updateWorkflowApprovalButtons]) const getSelectedRowKeys = useCallback( async () => (await getTreeInstance()?.getSelectedRowKeys()) ?? [], [getTreeInstance], ) const getSelectedRowsData = useCallback( () => getTreeInstance()?.getSelectedRowsData() ?? [], [getTreeInstance], ) const getFilter = useCallback(() => getTreeInstance()?.getCombinedFilter(), [getTreeInstance]) const toggleAllNodes = useCallback( (expand: boolean) => { const tree = getTreeInstance() if (!tree) return tree.forEachNode((node: any) => { if (!node.hasChildren) return if (expand) { tree.expandRow(node.key) } else { tree.collapseRow(node.key) } }) }, [getTreeInstance], ) const expandAll = useCallback(() => toggleAllNodes(true), [toggleAllNodes]) const collapseAll = useCallback(() => toggleAllNodes(false), [toggleAllNodes]) const { toolbarData, toolbarModalData, setToolbarModalData } = useToolbar({ gridDto, listFormCode, getSelectedRowKeys, getSelectedRowsData, refreshData, expandAll, collapseAll, getFilter, layout: layoutTypes.tree, }) const orderedToolbarData = useMemo( () => orderListToolbarItems([...toolbarData, ...filterToolbarData]), [toolbarData, filterToolbarData], ) /* ======================= TREE EVENTS ======================= */ const onSelectionChanged = useCallback( (data: any) => { const tree = getTreeInstance() if (!gridDto?.gridOptions || !tree) return if (gridDto.gridOptions.editingOptionDto?.allowDeleting) { const items = tree.option('toolbar')?.items ?? [] const deleteSelectedIndex = items.findIndex( (item: any) => item.name === 'deleteSelectedRecords', ) if (deleteSelectedIndex >= 0) { tree.option( `toolbar.items[${deleteSelectedIndex}].options.visible`, data.selectedRowsData.length > 1, ) } } updateWorkflowApprovalButtons(tree, data.selectedRowsData) if (data.selectedRowsData.length) { setFormData(data.selectedRowsData[0]) } }, [getTreeInstance, gridDto, updateWorkflowApprovalButtons], ) const onCellPrepared = useMemo(() => createConditionalCellStyleHandler(gridDto), [gridDto]) const onInitNewRow = useCallback( (e: TreeListTypes.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: TreeListTypes.RowInsertingEvent) => { e.data = buildInsertPayload(gridDto, e.data) }, [gridDto], ) const onRowUpdating = useCallback( (e: TreeListTypes.RowUpdatingEvent) => { if (!gridDto?.columnFormats) return e.newData = buildUpdatePayload(gridDto, e.oldData, e.newData) }, [gridDto], ) const onEditingStart = useCallback( (e: TreeListTypes.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: TreeListTypes.DataErrorOccurredEvent) => { toast.push( {e.error?.message} , { placement: 'bottom-end' }, ) }, []) const cascadeMaps = useMemo(() => createCascadeMaps(gridDto), [gridDto]) const onEditorPreparing = useCallback( (editor: TreeListTypes.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 }) } if (columnFormat?.editOrderNo != null && columnFormat.editorScript) { attachEditorValueChangedScript({ editor, editorScript: columnFormat.editorScript, fieldName: columnFormat.fieldName, editingFormInstanceRef, getActiveEditingForm, }) } attachEditorButtonContext(editor) }, [gridDto, cascadeMaps, mode], ) /* ======================= COLUMNS & DATASOURCE ======================= */ useEffect(() => { setTreeListDataSource(undefined) setColumnData(undefined) const instance = getTreeInstance() if (instance) { instance.option('columns', undefined) instance.option('dataSource', undefined) instance.state(null) } }, [listFormCode, getTreeInstance]) useEffect(() => { if (!gridDto) return const columns = getBandedColumns() setColumnData(columns) setTreeListDataSource( createSelectDataSource( gridDto.gridOptions, listFormCode, searchParams, layoutTypes.tree, 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, listFormCode, searchParams, currentCulture, localizationTexts]) // Başlangıçta açık olacak düğümler. useEffect(() => { setExpandedRowKeys(gridDto?.gridOptions.treeOptionDto?.expandedRowKeys ?? []) }, [gridDto]) useEffect(() => { const instance = getTreeInstance() if (columnData && instance) { instance.option('columns', columnData as any) } }, [columnData, getTreeInstance]) useEffect(() => { const instance = getTreeInstance() if (treeListDataSource && instance) { instance.option('dataSource', treeListDataSource) } }, [treeListDataSource, getTreeInstance]) // Otomatik state kaydetme/yükleme kapalı; manuel akış kullanılır. useEffect(() => { if (!gridDto) return getTreeInstance()?.option('stateStoring', { enabled: false }) }, [gridDto, getTreeInstance]) useEffect(() => { const instance = getTreeInstance() if (!gridDto || !instance || !treeListDataSource || !columnData) return customLoadState() .then((state) => { if (state) instance.state(state) }) .catch((err) => console.error('State load error:', err)) }, [gridDto, treeListDataSource, columnData, customLoadState, getTreeInstance]) /* ======================= 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), [gridDto, mapFormItem, mode], ) const editingFormHandlers = useMemo( () => createEditingFormHandlers({ getGridDto: () => gridDto, getGridInstance: getTreeInstance, editingFormDataRef, editingFormInstanceRef, }), [gridDto, getTreeInstance], ) const editPopupOptions = useMemo( () => buildEditPopupOptions({ popupOptions: gridDto?.gridOptions.editingOptionDto?.popup, mode, isPopupFullScreen, useMobileEditPopup, translate, handlers: { onSave: () => getTreeInstance()?.saveEditData(), onCancel: () => getTreeInstance()?.cancelEditData(), onToggleFullScreen: () => setIsPopupFullScreen((current) => !current), }, }), [gridDto, mode, isPopupFullScreen, useMobileEditPopup, translate, getTreeInstance], ) /* ======================= RENDER ======================= */ const gridOptions = gridDto?.gridOptions const treeOptions = gridOptions?.treeOptionDto const autoExpandAll = treeOptions?.autoExpandAll ?? false return ( <>
0 ? 'mt-2' : ''}>
{!isSubForm && } {gridDto && gridOptions && columnData && (
0 ? gridOptions.height : gridOptions.fullHeight ? `calc(100vh - ${170 + widgetGroupHeight}px)` : undefined } width={gridOptions.width || '100%'} dataStructure="plain" keyExpr={treeOptions?.keyExpr} parentIdExpr={treeOptions?.parentIdExpr} hasItemsExpr={treeOptions?.hasItemsExpr} rootValue={ treeOptions?.rootValue === '' || treeOptions?.rootValue === undefined ? null : treeOptions.rootValue } {...(!autoExpandAll && { expandedRowKeys, onRowExpanding: (e: any) => setExpandedRowKeys((prev) => [...prev, e.key]), onRowCollapsing: (e: any) => setExpandedRowKeys((prev) => prev.filter((key) => key !== e.key)), })} autoExpandAll={autoExpandAll} 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} onInitNewRow={onInitNewRow} onCellPrepared={onCellPrepared} onRowInserting={onRowInserting} onRowUpdating={onRowUpdating} onEditingStart={onEditingStart} onDataErrorOccurred={onDataErrorOccurred} onEditCanceled={onEditFinished} onSaved={onEditFinished} onRowInserted={(e) => { startWorkflowForInsertedRow(e, gridDto, listFormCode, refreshTree) props.refreshData?.() }} onRowUpdated={() => props.refreshData?.()} onRowRemoved={() => props.refreshData?.()} onEditorPreparing={onEditorPreparing} onContentReady={(e) => { updateWorkflowApprovalButtons(e.component) // Veri yenilendikten sonra açık düğümleri geri yükle. if (!autoExpandAll && expandedRowKeys.length > 0) { e.component.option('expandedRowKeys', expandedRowKeys) } }} >