import Container from '@/components/shared/Container' import { Dialog, Notification, toast } from '@/components/ui' import { APP_NAME, DX_CLASSNAMES } from '@/constants/app.constant' import { DbTypeEnum, EditingFormItemDto, GridDto, ListFormCustomizationTypeEnum, PlatformEditorTypes, SubFormTabTypeEnum, } from '@/proxy/form/models' import { getListFormCustomization, postListFormCustomization, } from '@/services/list-form-customization.service' import { useLocalization } from '@/utils/hooks/useLocalization' import useResponsive from '@/utils/hooks/useResponsive' import { Template } from 'devextreme-react/core/template' import DataGrid, { ColumnChooser, ColumnFixing, DataGridRef, 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 { DataType } from 'devextreme/common' import { captionize } from 'devextreme/core/utils/inflector' import CustomStore from 'devextreme/data/custom_store' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Helmet } from 'react-helmet' import SubForms from '../form/SubForms' import { RowMode, SimpleItemWithColData } from '../form/types' import { GridColumnData } from './GridColumnData' import GridFilterDialogs from './GridFilterDialogs' import { addCss, addJs, autoNumber, controlStyleCondition, extractSearchParamsFields, GridExtraFilterState, setFormEditingExtraItemValues, setGridPanelColor, } from './Utils' import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent' import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent' import { useFilters } from './useFilters' import { useToolbar } from './useToolbar' import { ImportDashboard } from '@/components/importManager/ImportDashboard' import WidgetGroup from '@/components/ui/Widget/WidgetGroup' import { GridExtraFilterToolbar } from './GridExtraFilterToolbar' import { getList } from '@/services/form.service' import { layoutTypes } from '../admin/listForm/edit/types' import { useListFormCustomDataSource } from './useListFormCustomDataSource' import { useListFormColumns } from './useListFormColumns' import { Loading } from '@/components/shared' import { useStoreState } from '@/store' interface GridProps { listFormCode: string searchParams?: URLSearchParams isSubForm?: boolean level?: number refreshData?: () => Promise gridDto?: GridDto } const statedGridPanelColor = 'rgba(50, 200, 200, 0.5)' // kullanici tanimli gridState ile islem gormus gridin paneline ait renk const Grid = (props: GridProps) => { const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props const { translate } = useLocalization() const { smaller } = useResponsive() const config = useStoreState((state) => state.abpConfig.config) const gridRef = useRef() const refListFormCode = useRef('') const widgetGroupRef = useRef(null) // Edit popup state kaydetmeyi engellemek için flag const isEditingRef = useRef(false) const [gridDataSource, setGridDataSource] = useState>() const [columnData, setColumnData] = useState() const [formData, setFormData] = useState() const [mode, setMode] = useState('view') const [extraFilters, setExtraFilters] = useState([]) const [gridDto, setGridDto] = useState() const [isPopupFullScreen, setIsPopupFullScreen] = useState(false) const [widgetGroupHeight, setWidgetGroupHeight] = useState(0) type EditorOptionsWithButtons = { buttons?: any[] } & Record const defaultSearchParams = useRef(null) useEffect(() => { const initializeGrid = async () => { const response = await getList({ listFormCode }) setGridDto(response.data) } if (extGridDto === undefined) { initializeGrid() } else { setGridDto(extGridDto) } }, [listFormCode, extGridDto]) useEffect(() => { if (!defaultSearchParams.current) { defaultSearchParams.current = searchParams?.get('filter') ?? null } }, [searchParams]) // StateStoring için storageKey'i memoize et const storageKey = useMemo(() => { return gridDto?.gridOptions.stateStoringDto?.storageKey ?? '' }, [gridDto?.gridOptions.stateStoringDto?.storageKey]) const customSaveState = useCallback( (state: any) => { if (isEditingRef.current) { return Promise.resolve() } return postListFormCustomization({ listFormCode: listFormCode, customizationType: ListFormCustomizationTypeEnum.GridState, filterName: `list-${storageKey}`, customizationData: JSON.stringify(state), }) .then(() => { setGridPanelColor(statedGridPanelColor) toast.push( {translate('::ListForms.ListForm.GridStateSaved')} , { placement: 'top-end', }, ) }) .catch((err) => { toast.push( {translate('::ListForms.ListForm.GridStateSaveError')} , { placement: 'top-end', }, ) throw err }) }, [listFormCode, storageKey, translate], ) const { filterToolbarData, ...filterData } = useFilters({ gridDto, gridRef, listFormCode, saveGridState: customSaveState, }) const { createSelectDataSource } = useListFormCustomDataSource({ gridRef }) const { getBandedColumns } = useListFormColumns({ gridDto, listFormCode, isSubForm, gridRef, }) const getSelectedRowKeys = useCallback(async () => { const grd = gridRef.current?.instance() if (!grd) { return [] } return await grd.getSelectedRowKeys() }, []) const getSelectedRowsData = useCallback(() => { const grd = gridRef.current?.instance() if (!grd) { return [] } return grd.getSelectedRowsData() }, []) const refreshData = useCallback(() => { gridRef.current?.instance()?.refresh() }, []) const getFilter = useCallback(() => { const grd = gridRef.current?.instance() if (!grd) { return } return grd.getCombinedFilter() }, []) const layout = layoutTypes.grid const { toolbarData, toolbarModalData, setToolbarModalData } = useToolbar({ gridDto, listFormCode, getSelectedRowKeys, getSelectedRowsData, refreshData, getFilter, layout, }) const onSelectionChanged = useCallback( (data: any) => { const grdOpt = gridDto?.gridOptions const grd = gridRef.current?.instance() if (!grdOpt || !grd) { return } // 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 grd.option( `toolbar.items[${deleteSelectedRecordsIndex}].options.visible`, data.selectedRowsData.length > 1, ) // birden fazla kayit secilmis ise gorunsun } // SubForm'ları gösterebilmek için secili satiri formData'ya at if (data.selectedRowsData.length) { setFormData(data.selectedRowsData[0]) } }, [gridDto], ) 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], ) const onInitNewRow = useCallback( (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 } // 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 } } // ExtraFilters içerisinde ilgili Field varsa, default değerleri set etme if (extraFilters.some((f) => f.fieldName === colFormat.fieldName)) { continue } // URL'den veya Component Prop'dan gelen parametreleri set et if (!searchParams) { 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) 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], ) const onRowInserting = useCallback((e: DataGridTypes.RowInsertingEvent) => { e.data = setFormEditingExtraItemValues(e.data) }, []) 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 } 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]] }) }, [gridDto, mode], ) const onDataErrorOccurred = useCallback((e: DataGridTypes.DataErrorOccurredEvent) => { toast.push( {e.error?.message} , { placement: 'top-end', }, ) }, []) // Cascade parent fields mapping'i memoize et - popup açılışını hızlandırır // Ayrıca parent -> child ilişkisi için reverse map oluştur (performans için) const { cascadeFieldsMap, parentToChildrenMap } = useMemo(() => { if (!gridDto) return { cascadeFieldsMap: new Map(), parentToChildrenMap: new Map() } const cascadeMap = new Map< string, { parentFields: string[] childFields?: string[] } >() const parentChildMap = new Map>() gridDto.columnFormats.forEach((col) => { if (col.lookupDto?.cascadeParentFields && col.fieldName) { const parentFields = col.lookupDto.cascadeParentFields .split(',') .map((f: string) => f.trim()) const childFields = col.lookupDto.cascadeEmptyFields ?.split(',') .map((f: string) => f.trim()) cascadeMap.set(col.fieldName, { parentFields, childFields, }) // Her parent field için, hangi childları etkilediğini kaydet parentFields.forEach((parentField) => { if (!parentChildMap.has(parentField)) { parentChildMap.set(parentField, new Set()) } if (col.fieldName) { parentChildMap.get(parentField)!.add(col.fieldName) } }) } }) return { cascadeFieldsMap: cascadeMap, parentToChildrenMap: parentChildMap } }, [gridDto]) 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 mantığı const cascadeInfo = cascadeFieldsMap.get(editor.dataField) if (cascadeInfo) { const parentFields = cascadeInfo.parentFields const childFields = cascadeInfo.childFields const affectedChildren = parentToChildrenMap.get(editor.dataField!) const prevHandler = editor.editorOptions.onValueChanged editor.editorOptions.onValueChanged = (e: any) => { prevHandler?.(e) const grid = editor.component const rowKey = grid.option('editing.editRowKey') const rowIndex = grid.getRowIndexByKey(rowKey) // Parent field değiştiğinde child fieldleri temizle if (childFields && rowIndex >= 0) { childFields.forEach((childField: string) => { grid.cellValue(rowIndex, childField, null) }) } // Sadece bu parent'tan etkilenen childların disabled durumunu güncelle if (affectedChildren?.size) { const formInstance = grid.option('editing.form') as any if (formInstance?.getEditor) { const visibleRows = grid.getVisibleRows() const rowData = visibleRows.find((r) => r.key === rowKey)?.data if (rowData) { affectedChildren.forEach((childFieldName: string) => { const childInfo = cascadeFieldsMap.get(childFieldName) if (childInfo) { const shouldDisable = childInfo.parentFields.some( (pf: string) => !rowData[pf], ) try { formInstance.getEditor(childFieldName)?.option('disabled', shouldDisable) } catch {} } }) } } } } // İlk açılışta disabled durumunu kontrol et const grid = editor.component const rowKey = grid.option('editing.editRowKey') const visibleRows = grid.getVisibleRows() const rowData = visibleRows.find((r) => r.key === rowKey)?.data if (rowData && parentFields.some((pf: string) => !rowData[pf])) { editor.editorOptions.disabled = true } } 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 || {} 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) } } } 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 customLoadState = useCallback(() => { return getListFormCustomization( listFormCode, ListFormCustomizationTypeEnum.GridState, `list-${storageKey}`, ).then((response: any) => { if (response.data?.length > 0) { setGridPanelColor(statedGridPanelColor) return JSON.parse(response.data[0].customizationData) } // Veri yoksa null dön (DevExtreme bunu default state olarak algılar) return null }) }, [listFormCode, storageKey]) useEffect(() => { // React state'i sıfırla - eski değerlerin customLoadState'i erken tetiklemesini önle setGridDataSource(undefined) setColumnData(undefined) if (gridRef?.current) { const instance = gridRef?.current?.instance() if (instance) { instance.option('columns', undefined) instance.option('remoteOperations', false) instance.option('dataSource', undefined) instance.state(null) } } if (refListFormCode.current !== listFormCode) { setExtraFilters([]) } }, [listFormCode]) useEffect(() => { if (!gridDto) { return } // Set js and css const grdOpt = gridDto.gridOptions if (grdOpt.customJsSources.length) { for (const js of grdOpt.customJsSources) { addJs(js) } } if (grdOpt.customStyleSources.length) { for (const css of grdOpt.customStyleSources) { addCss(css) } } if (gridDto?.gridOptions.extraFilterDto) { setExtraFilters( gridDto.gridOptions.extraFilterDto.map((f) => ({ fieldName: f.fieldName, operator: f.operator, controlType: f.controlType, value: f.defaultValue ?? '', })), ) } if (gridDto.gridOptions.editingOptionDto?.popup) { setIsPopupFullScreen(gridDto.gridOptions.editingOptionDto?.popup?.fullScreen ?? false) } }, [gridDto]) // extraFilters değişikliklerini useMemo ile optimize et const filterParams = useMemo(() => { const activeFilters = extraFilters.filter((f) => f.value) let base: any = null if (defaultSearchParams.current) { base = JSON.parse(defaultSearchParams.current) } // Default filter tripletlerini çıkar const baseTriplets = extractSearchParamsFields(base) // Extra filter tripletleri hazırla const extraTriplets = activeFilters.map( (f) => [f.fieldName, f.operator, f.value] as [string, string, any], ) // Tripletleri birleştir, aynı field+op varsa sonuncuyu al const merged = [...baseTriplets, ...extraTriplets].reduce( (acc, cur) => { const [field, op] = cur const idx = acc.findIndex((a) => a[0] === field && a[1] === op) if (idx >= 0) { acc[idx] = cur // varsa üzerine yaz } else { acc.push(cur) } return acc }, [] as [string, string, any][], ) // Tek filtre varsa direkt yaz let filter: any = null if (merged.length === 1) { filter = merged[0] } else if (merged.length > 1) { filter = merged.reduce((acc, f, idx) => { if (idx === 0) return f return [acc, 'and', f] }, null as any) } 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() }, [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(() => { 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(() => { 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]) // listFormCode'u ref'e kaydet useEffect(() => { refListFormCode.current = listFormCode }, [listFormCode]) // State yükle + DataSource'u tek seferde set et → tek bir veri çekimi // customLoadState önce çalışır, state grid'e uygulandıktan SONRA dataSource set edilir useEffect(() => { if (!gridDto || !gridRef?.current || !gridDataSource || !columnData) return const instance = gridRef?.current?.instance() if (!instance) return const remoteOps = { paging: true, filtering: true, sorting: true, grouping: false, summary: false, } customLoadState() .then((state) => { instance.option('remoteOperations', remoteOps) // State varsa dataSource set edilmeden ÖNCE uygula → tek veri çekimi if (state) { instance.state(state) } instance.option('dataSource', gridDataSource) }) .catch((err) => { console.error('State load error:', err) instance.option('remoteOperations', remoteOps) instance.option('dataSource', gridDataSource) }) }, [gridDto, gridDataSource, columnData]) // StateStoring'i devre dışı bırak - manuel kaydetme kullanılacak useEffect(() => { if (!gridDto || !gridRef?.current) return const instance = gridRef?.current?.instance() if (instance) { // Otomatik state kaydetme/yükleme kapalı instance.option('stateStoring', { enabled: false, }) } }, [gridDto]) 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 = () => { if (widgetGroupRef.current) { const height = widgetGroupRef.current.offsetHeight setWidgetGroupHeight(height) } } // İlk render'da hesapla calculateWidgetHeight() // Resize durumunda tekrar hesapla const resizeObserver = new ResizeObserver(calculateWidgetHeight) if (widgetGroupRef.current) { resizeObserver.observe(widgetGroupRef.current) } return () => { resizeObserver.disconnect() } }, [gridDto?.widgets]) const onExporting = async (e: DataGridTypes.ExportingEvent) => { // DevExtreme’in varsayılan export davranışını iptal ediyoruz; kendi akışımızı çalıştıracağız e.cancel = true const grid = gridRef?.current?.instance() if (!grid) return try { if (e.format === 'xlsx') { // Sadece Excel için gerekli kütüphaneleri yükle const [{ Workbook }, { saveAs }, { exportDataGrid }] = await Promise.all([ import('exceljs'), import('file-saver'), import('devextreme/excel_exporter'), ]) const workbook = new Workbook() const worksheet = workbook.addWorksheet(`${listFormCode}_sheet`) await exportDataGrid({ component: grid as any, worksheet, autoFilterEnabled: true, }) const buffer = await workbook.xlsx.writeBuffer() saveAs( new Blob([buffer], { type: 'application/octet-stream' }), `${listFormCode}_export.xlsx`, ) } else if (e.format === 'csv') { // Sadece CSV için gerekli kütüphaneleri yükle (exceljs CSV desteği için) const [{ Workbook }, { saveAs }] = await Promise.all([ import('exceljs'), import('file-saver'), ]) const workbook = new Workbook() const worksheet = workbook.addWorksheet(`${listFormCode}_sheet`) // CSV için basit data export const dataSource = grid.getDataSource() const items = dataSource?.items() || [] const columns = grid.getVisibleColumns().filter((c: any) => c.dataField) // Header ekle worksheet.addRow(columns.map((c: any) => c.caption || c.dataField)) // Data ekle items.forEach((item: any) => { worksheet.addRow(columns.map((c: any) => item[c.dataField!])) }) const buffer = await workbook.csv.writeBuffer() saveAs(new Blob([buffer], { type: 'text/csv' }), `${listFormCode}_export.csv`) } else if (e.format === 'pdf') { // jspdf + devextreme pdf exporter => ihtiyaç anında yükle const [jspdfMod, { exportDataGrid: exportDataPdf }] = await Promise.all([ import('jspdf'), import('devextreme/pdf_exporter'), ]) // jsPDF bazı paketlemelerde default, bazılarında named export olarak gelir const JsPDFCtor = (jspdfMod as any).default ?? (jspdfMod as any).jsPDF const doc = new JsPDFCtor({}) await exportDataPdf({ jsPDFDocument: doc, component: grid as any, indent: 5, }) doc.save(`${listFormCode}_export.pdf`) } } catch (err) { console.error('Export error:', err) toast.push( {translate('::App.Common.ExportError') ?? 'Dışa aktarma sırasında hata oluştu.'} , { placement: 'top-end' }, ) } } return ( <>
{!isSubForm && ( )} {!gridDto && (
Loading grid configuration...
)} {gridDto && !columnData && (
Loading columns...
)} {gridDto && columnData && !gridDataSource && (
Loading data source...
)} {gridDto && columnData && gridDataSource && (() => { return true })() && ( <>
0 ? gridDto.gridOptions.height : gridDto.gridOptions.fullHeight ? `calc(100vh - ${170 + widgetGroupHeight}px)` : undefined } width={gridDto.gridOptions.width || '100%'} allowColumnResizing={gridDto.gridOptions.columnOptionDto?.allowColumnResizing} allowColumnReordering={gridDto.gridOptions.columnOptionDto?.allowColumnReordering} showBorders={gridDto.gridOptions.columnOptionDto?.showBorders} showRowLines={gridDto.gridOptions.columnOptionDto?.showRowLines} showColumnLines={gridDto.gridOptions.columnOptionDto?.showColumnLines} columnResizingMode={gridDto.gridOptions.columnOptionDto?.columnResizingMode} columnAutoWidth={gridDto.gridOptions.columnOptionDto?.columnAutoWidth} rtlEnabled={gridDto.gridOptions.columnOptionDto?.rtlEnabled} rowAlternationEnabled={gridDto.gridOptions.columnOptionDto?.rowAlternationEnabled} onRowPrepared={(e) => { //header, filter, data, group, summaries if (e.rowType === 'data') { e.rowElement.style.height = gridDto.gridOptions?.rowDto.rowHeight e.rowElement.style.whiteSpace = gridDto.gridOptions?.rowDto.whiteSpace e.rowElement.style.overflowWrap = gridDto.gridOptions?.rowDto.overflowWrap } }} hoverStateEnabled={gridDto.gridOptions.columnOptionDto?.hoverStateEnabled} columnHidingEnabled={gridDto.gridOptions.columnOptionDto?.columnHidingEnabled} focusedRowEnabled={gridDto.gridOptions.columnOptionDto?.focusedRowEnabled} showColumnHeaders={gridDto.gridOptions.columnOptionDto?.showColumnHeaders} filterSyncEnabled={true} onSelectionChanged={onSelectionChanged} onInitNewRow={onInitNewRow} onCellPrepared={onCellPrepared} onRowInserting={onRowInserting} onRowUpdating={onRowUpdating} onEditingStart={onEditingStart} onDataErrorOccurred={onDataErrorOccurred} onExporting={onExporting} onEditCanceling={() => { setMode('view') }} onEditCanceled={() => { isEditingRef.current = false setMode('view') setIsPopupFullScreen(false) }} onSaved={() => { isEditingRef.current = false setMode('view') setIsPopupFullScreen(false) }} onRowInserted={() => { props.refreshData?.() }} onRowUpdated={() => { props.refreshData?.() }} onRowRemoved={() => { props.refreshData?.() }} onEditorPreparing={onEditorPreparing} > { const grid = gridRef.current?.instance() grid?.saveEditData() }, }, }, { widget: 'dxButton', toolbar: 'bottom', location: 'after', options: { text: translate('::Cancel'), onClick: () => { const grid = gridRef.current?.instance() grid?.cancelEditData() }, }, }, { widget: 'dxButton', toolbar: 'top', location: 'after', options: { icon: isPopupFullScreen ? 'collapse' : 'fullscreen', hint: isPopupFullScreen ? translate('::Normal Boyut') : translate('::Tam Ekran'), stylingMode: 'text', onClick: () => setIsPopupFullScreen(!isPopupFullScreen), }, }, ], }} form={{ colCount: 1, onFieldDataChanged: (e) => { if (e.dataField) { const formItem = gridDto.gridOptions.editingFormDto .flatMap((group) => group.items || []) .find((i) => i.dataField === e.dataField) if (formItem?.editorScript) { try { eval(formItem.editorScript) } catch (err) { console.error('Script exec error', err) } } } }, items: gridDto.gridOptions.editingFormDto?.length > 0 ? (() => { const sortedFormDto = gridDto.gridOptions.editingFormDto .slice() .sort((a: any, b: any) => (a.order >= b.order ? 1 : -1)) // Tabbed item'ları grupla const tabbedItems = sortedFormDto.filter( (e: any) => e.itemType === 'tabbed', ) const result: any[] = [] sortedFormDto.forEach((e: any) => { if (e.itemType !== 'tabbed') { // Backend'den gelen colCount ve colSpan değerlerini kullan const effectiveColCount = e.colCount || 1 const effectiveColSpan = e.colSpan || 1 result.push({ itemType: e.itemType, colCount: effectiveColCount, colSpan: effectiveColSpan, caption: e.caption, // Group'larda caption olmalı items: e.items ?.sort((a: any, b: any) => (a.order >= b.order ? 1 : -1)) .map(mapFormItem) .filter((a: any) => { if (mode === 'view') { return a.canRead } else if (mode === 'new') { return a.canCreate || a.canRead } else if (mode === 'edit') { return a.canUpdate || a.canRead } else { return false } }), }) } else if (tabbedItems.length > 0 && e === tabbedItems[0]) { // Tabbed için caption OLMAMALI - sadece tabs array içinde title kullan result.push({ itemType: 'tabbed', colCount: 1, colSpan: 1, // caption kullanma! Tabs içindeki title'lar yeterli tabs: tabbedItems.map((tabbedItem: any) => { // Backend'den gelen colCount ve colSpan değerlerini kullan const effectiveColCount = tabbedItem.colCount || 1 return { title: tabbedItem.caption, // Her tab'ın title'ı colCount: effectiveColCount, items: tabbedItem.items ?.sort((a: any, b: any) => (a.order >= b.order ? 1 : -1)) .map(mapFormItem) .filter((a: any) => { if (mode === 'view') { return a.canRead } else if (mode === 'new') { return a.canCreate || a.canRead } else if (mode === 'edit') { return a.canUpdate || a.canRead } else { return false } }), } }), }) } }) return result })() : undefined, }} >