erp-platform/ui/src/views/list/Grid.tsx
2026-01-17 22:08:21 +03:00

1344 lines
52 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Container from '@/components/shared/Container'
import { Dialog, Notification, toast } from '@/components/ui'
import { DX_CLASSNAMES } from '@/constants/app.constant'
import {
DbTypeEnum,
EditingFormItemDto,
GridDto,
ListFormCustomizationTypeEnum,
PlatformEditorTypes,
} 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,
StateStoring,
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, 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'
import { locale, loadMessages } from 'devextreme/localization'
interface GridProps {
listFormCode: string
searchParams?: URLSearchParams
isSubForm?: boolean
level?: number
refreshData?: () => Promise<void>
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<DataGridRef>()
const refListFormCode = useRef('')
const widgetGroupRef = useRef<HTMLDivElement>(null)
const [gridDataSource, setGridDataSource] = useState<CustomStore<any, any>>()
const [columnData, setColumnData] = useState<GridColumnData[]>()
const [formData, setFormData] = useState<any>()
const [mode, setMode] = useState<RowMode>('view')
const [extraFilters, setExtraFilters] = useState<GridExtraFilterState[]>([])
const [gridDto, setGridDto] = useState<GridDto>()
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
const [widgetGroupHeight, setWidgetGroupHeight] = useState(0)
const preloadExportLibs = () => {
import('exceljs')
import('file-saver')
import('devextreme/excel_exporter')
import('jspdf')
import('devextreme/pdf_exporter')
}
type EditorOptionsWithButtons = {
buttons?: any[]
} & Record<string, any>
const defaultSearchParams = useRef<string | null>(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])
const layout = layoutTypes.grid
const { toolbarData, toolbarModalData, setToolbarModalData } = useToolbar({
gridDto,
listFormCode,
getSelectedRowKeys,
getSelectedRowsData,
refreshData,
getFilter,
layout,
})
const { filterToolbarData, ...filterData } = useFilters({
gridDto,
gridRef,
listFormCode,
})
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
const { getBandedColumns } = useListFormColumns({
gridDto,
listFormCode,
isSubForm,
gridRef,
})
async function getSelectedRowKeys() {
const grd = gridRef.current?.instance()
if (!grd) {
return []
}
return await grd.getSelectedRowKeys()
}
function getSelectedRowsData() {
const grd = gridRef.current?.instance()
if (!grd) {
return []
}
return grd.getSelectedRowsData()
}
function refreshData() {
gridRef.current?.instance()?.refresh()
}
function getFilter() {
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
}
// 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])
}
}
function onCellPrepared(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)
}
}
}
}
}
}
}
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
}
// 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
}
}
}
}
}
function onRowInserting(e: DataGridTypes.RowInsertingEvent<any, any>) {
e.data = setFormEditingExtraItemValues(e.data)
}
function onRowUpdating(e: DataGridTypes.RowUpdatingEvent<any, any>) {
if (gridDto?.gridOptions.editingOptionDto?.sendOnlyChangedFormValuesUpdate) {
if (Object.keys(e.newData).some((a) => a.includes(':'))) {
Object.keys(e.oldData).forEach((col) => {
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]
}
}
function onEditingStart(e: DataGridTypes.EditingStartEvent<any, any>) {
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<any, any>) {
toast.push(
<Notification type="danger" duration={2000}>
{e.error?.message}
</Notification>,
{
placement: 'top-end',
},
)
}
function onEditorPreparing(editor: DataGridTypes.EditorPreparingEvent<any, any>) {
if (editor.parentType === 'dataRow' && editor.dataField && gridDto) {
const formItem = gridDto.gridOptions.editingFormDto
.flatMap((group) => group.items || [])
.find((i) => i.dataField === editor.dataField)
// Cascade disabled mantığı
const colFormat = gridDto.columnFormats.find((c) => c.fieldName === editor.dataField)
if (colFormat?.lookupDto?.cascadeParentFields) {
const parentFields = colFormat.lookupDto.cascadeParentFields
.split(',')
.map((f: string) => f.trim())
const 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
setTimeout(() => {
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)
}
}
})
}
}
}, 50)
}
// İ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 (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
})
}
}
}
const customSaveState = useCallback(
(state: any) => {
return postListFormCustomization({
listFormCode: listFormCode,
customizationType: ListFormCustomizationTypeEnum.GridState,
filterName: `list-${gridRef.current?.instance()?.option('stateStoring')?.storageKey ?? ''}`,
customizationData: JSON.stringify(state),
}).then(() => {
setGridPanelColor(statedGridPanelColor)
})
},
[listFormCode],
)
const customLoadState = useCallback(() => {
return getListFormCustomization(
listFormCode,
ListFormCustomizationTypeEnum.GridState,
`list-${gridRef.current?.instance()?.option('stateStoring')?.storageKey ?? ''}`,
).then((response: any) => {
setGridPanelColor(statedGridPanelColor)
if (response.data?.length > 0) {
return JSON.parse(response.data[0].customizationData)
}
})
}, [listFormCode])
useEffect(() => {
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])
// 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 || !columnData) return
const dataSource = createSelectDataSource(
gridDto.gridOptions,
listFormCode,
searchParams,
layoutTypes.grid,
columnData,
)
setGridDataSource(dataSource)
}, [gridDto, searchParams, columnData])
useEffect(() => {
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)
}
if (filter) {
searchParams?.set('filter', JSON.stringify(filter))
} else {
searchParams?.delete('filter')
}
gridRef.current?.instance()?.refresh()
}, [extraFilters])
useEffect(() => {
refListFormCode.current = listFormCode
}, [listFormCode])
// 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) => {
// DevExtremein 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' || e.format === 'csv') {
// exceljs + file-saver + devextreme excel exporter => ihtiyaç anında yükle
const [{ Workbook }, { saveAs }, { exportDataGrid: exportDataExcel }] = await Promise.all([
import('exceljs'),
import('file-saver'),
import('devextreme/excel_exporter'),
])
const workbook = new Workbook()
const worksheet = workbook.addWorksheet(`${listFormCode}_sheet`)
await exportDataExcel({
component: grid as any,
worksheet,
autoFilterEnabled: true,
})
if (e.format === 'xlsx') {
const buffer = await workbook.xlsx.writeBuffer()
saveAs(
new Blob([buffer], { type: 'application/octet-stream' }),
`${listFormCode}_export.xlsx`,
)
} else {
const buffer = await workbook.csv.writeBuffer()
saveAs(
new Blob([buffer], { type: 'application/octet-stream' }),
`${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(
<Notification type="danger" duration={2500}>
{translate('::App.Common.ExportError') ?? 'Dışa aktarma sırasında hata oluştu.'}
</Notification>,
{ placement: 'top-end' },
)
}
}
return (
<>
<div ref={widgetGroupRef}>
<WidgetGroup widgetGroups={gridDto?.widgets ?? []} />
</div>
<Container className={DX_CLASSNAMES}>
{!isSubForm && (
<Helmet
titleTemplate="%s | Erp Platform"
title={translate('::' + gridDto?.gridOptions.title)}
defaultTitle="Erp Platform"
></Helmet>
)}
{!gridDto && (
<div className="p-4">
<Loading loading>Loading grid configuration...</Loading>
</div>
)}
{gridDto && !columnData && (
<div className="p-4">
<Loading loading>Loading columns...</Loading>
</div>
)}
{gridDto && columnData && !gridDataSource && (
<div className="p-4">
<Loading loading>Loading data source...</Loading>
</div>
)}
{gridDto &&
columnData &&
gridDataSource &&
(() => {
return true
})() && (
<>
<div className="p-1">
<DataGrid
ref={gridRef as any}
key={`Grid-${listFormCode}-${gridDataSource ? 'loaded' : 'loading'}`}
id={'Grid-' + listFormCode}
dataSource={gridDataSource}
columns={columnData}
remoteOperations={{
groupPaging: true,
filtering: true,
sorting: true,
paging: true,
grouping: true,
summary: true,
}}
height={
gridDto.gridOptions.height > 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}
onEditCanceled={() => {
setMode('view')
setIsPopupFullScreen(false)
}}
onSaved={() => {
setMode('view')
setIsPopupFullScreen(false)
}}
onRowInserted={() => {
props.refreshData?.()
}}
onRowUpdated={() => {
props.refreshData?.()
}}
onRowRemoved={() => {
props.refreshData?.()
}}
onEditorPreparing={onEditorPreparing}
>
<StateStoring
enabled={gridDto.gridOptions.stateStoringDto?.enabled}
type={gridDto.gridOptions.stateStoringDto?.type}
savingTimeout={gridDto.gridOptions.stateStoringDto?.savingTimeout}
storageKey={gridDto.gridOptions.stateStoringDto?.storageKey}
customSave={
gridDto?.gridOptions.stateStoringDto?.enabled &&
gridDto?.gridOptions.stateStoringDto?.type === 'custom'
? customSaveState
: undefined
}
customLoad={
gridDto?.gridOptions.stateStoringDto?.enabled &&
gridDto?.gridOptions.stateStoringDto?.type === 'custom'
? customLoadState
: undefined
}
/>
<Export
enabled={gridDto.gridOptions.exportDto?.enabled}
allowExportSelectedData={gridDto.gridOptions.exportDto?.allowExportSelectedData}
formats={['pdf', 'xlsx', 'csv']}
/>
<Editing
refreshMode={gridDto.gridOptions.editingOptionDto?.refreshMode}
mode={smaller.md ? 'form' : gridDto.gridOptions.editingOptionDto?.mode}
allowDeleting={gridDto.gridOptions.editingOptionDto?.allowDeleting}
allowUpdating={gridDto.gridOptions.editingOptionDto?.allowUpdating}
allowAdding={gridDto.gridOptions.editingOptionDto?.allowAdding}
useIcons={gridDto.gridOptions.editingOptionDto?.useIcons}
confirmDelete={gridDto.gridOptions.editingOptionDto?.confirmDelete}
newRowPosition={gridDto.gridOptions.editingOptionDto?.newRowPosition}
selectTextOnEditStart={
gridDto.gridOptions.editingOptionDto?.selectTextOnEditStart
}
startEditAction={gridDto.gridOptions.editingOptionDto?.startEditAction}
popup={{
animation: {},
title:
(mode === 'new' ? '✚ ' : '🖊️ ') +
translate('::' + gridDto.gridOptions.editingOptionDto?.popup?.title),
showTitle: gridDto.gridOptions.editingOptionDto?.popup?.showTitle,
hideOnOutsideClick:
gridDto.gridOptions.editingOptionDto?.popup?.hideOnOutsideClick,
width: gridDto.gridOptions.editingOptionDto?.popup?.width,
height: gridDto.gridOptions.editingOptionDto?.popup?.height,
resizeEnabled: gridDto.gridOptions.editingOptionDto?.popup?.resizeEnabled,
fullScreen: isPopupFullScreen,
toolbarItems: [
{
widget: 'dxButton',
toolbar: 'bottom',
location: 'after',
options: {
text: translate('::Save'),
type: 'default',
onClick: () => {
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[] = []
// 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
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,
}}
></Editing>
<Template name={'cellEditTagBox'} render={TagBoxEditorComponent} />
<Template name={'cellEditGridBox'} render={GridBoxEditorComponent} />
<Template name="extraFilters">
<GridExtraFilterToolbar
filters={gridDto?.gridOptions.extraFilterDto ?? []}
extraFilters={extraFilters}
setExtraFilters={setExtraFilters}
/>
</Template>
<Toolbar visible={toolbarData.length > 0 || filterToolbarData.length > 0}>
{toolbarData.map((item) => (
<Item key={item.name} {...item}></Item>
))}
{filterToolbarData.map((item) => (
<Item key={item.name} {...item}></Item>
))}
{/* burada özel filtre alanını Template ile bağla */}
{gridDto?.gridOptions.extraFilterDto?.length ? (
<Item location="before" template="extraFilters" cssClass="no-default" />
) : null}
</Toolbar>
<Sorting mode={gridDto.gridOptions?.sortMode}></Sorting>
<FilterRow
visible={gridDto.gridOptions.filterRowDto?.visible}
applyFilter={gridDto.gridOptions.filterRowDto?.applyFilter}
></FilterRow>
<FilterPanel visible={gridDto.gridOptions.filterPanelDto.visible}></FilterPanel>
<HeaderFilter
visible={gridDto.gridOptions.headerFilterDto.visible}
></HeaderFilter>
<SearchPanel
visible={gridDto.gridOptions.searchPanelDto.visible}
width={gridDto.gridOptions.searchPanelDto.width}
placeholder={translate('::App.Search')}
></SearchPanel>
<GroupPanel
visible={gridDto.gridOptions.groupPanelDto?.visible}
emptyPanelText={translate('::App.GroupPanel.EmptyPanelText')}
></GroupPanel>
<Grouping
autoExpandAll={gridDto.gridOptions.groupPanelDto?.autoExpandAll}
></Grouping>
<Selection
mode={gridDto.gridOptions.selectionDto?.mode}
allowSelectAll={gridDto.gridOptions.selectionDto?.allowSelectAll}
selectAllMode={gridDto.gridOptions.selectionDto?.selectAllMode}
showCheckBoxesMode={gridDto.gridOptions.selectionDto?.showCheckBoxesMode}
></Selection>
<Paging defaultPageSize={gridDto.gridOptions.pageSize ?? 20} />
<Pager
visible={gridDto.gridOptions.pagerOptionDto?.visible}
allowedPageSizes={gridDto.gridOptions.pagerOptionDto?.allowedPageSizes
?.split(',')
.map((a: any) => +a)}
showPageSizeSelector={gridDto.gridOptions.pagerOptionDto?.showPageSizeSelector}
showInfo={false}
showNavigationButtons={
gridDto.gridOptions.pagerOptionDto?.showNavigationButtons
}
displayMode={gridDto.gridOptions.pagerOptionDto?.displayMode}
></Pager>
<ColumnChooser
enabled={gridDto.gridOptions.columnOptionDto?.columnChooserEnabled}
mode={gridDto.gridOptions.columnOptionDto?.columnChooserMode}
></ColumnChooser>
<ColumnFixing
enabled={gridDto.gridOptions.columnOptionDto?.columnFixingEnabled}
></ColumnFixing>
<Scrolling mode={gridDto.gridOptions.pagerOptionDto?.scrollingMode}></Scrolling>
<LoadPanel
enabled={gridDto.gridOptions.pagerOptionDto?.loadPanelEnabled}
text={gridDto.gridOptions.pagerOptionDto?.loadPanelText}
></LoadPanel>
<Summary>
{gridDto.columnFormats
.filter((x: any) => !!x.columnTotalSummaryDto?.summaryType)
.map((x: any) => (
<TotalItem
key={`Total_${x.fieldName}`}
column={x.fieldName}
summaryType={x.columnTotalSummaryDto.summaryType as any}
showInColumn={x.columnTotalSummaryDto.showInColumn}
valueFormat={x.columnTotalSummaryDto.valueFormat}
displayFormat={x.columnTotalSummaryDto.displayFormat}
/>
))}
{gridDto.columnFormats
.filter((x: any) => !!x.columnGroupSummaryDto?.summaryType)
.map((x: any) => (
<GroupItemDx
key={`Group_${x.fieldName}`}
column={x.fieldName}
summaryType={x.columnGroupSummaryDto.summaryType as any}
showInColumn={x.columnGroupSummaryDto.showInColumn}
valueFormat={x.columnGroupSummaryDto.valueFormat}
displayFormat={x.columnGroupSummaryDto.displayFormat}
/>
))}
</Summary>
</DataGrid>
{gridDto?.gridOptions?.subFormsDto?.length > 0 && (
<>
<hr className="my-2" />
<SubForms gridDto={gridDto!} formData={formData} level={level ?? 0} />
</>
)}
<Dialog
width={smaller.md ? '100%' : 1000}
isOpen={filterData.isImportModalOpen || false}
onClose={() => filterData.setIsImportModalOpen(false)}
onRequestClose={() => filterData.setIsImportModalOpen(false)}
>
<ImportDashboard gridDto={gridDto} />
</Dialog>
</div>
</>
)}
<Dialog
isOpen={toolbarModalData?.open || false}
onClose={() => setToolbarModalData(undefined)}
onRequestClose={() => setToolbarModalData(undefined)}
>
{toolbarModalData?.content}
</Dialog>
<GridFilterDialogs gridRef={gridRef} listFormCode={listFormCode} {...filterData} />
</Container>
</>
)
}
export default Grid