sozsoft-platform/ui/src/views/list/Grid.tsx
2026-06-01 23:51:59 +03:00

1882 lines
70 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 { APP_NAME, DX_CLASSNAMES } from '@/constants/app.constant'
import {
DbTypeEnum,
EditingFormItemDto,
FieldCustomValueTypeEnum,
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 { executeEditorScript } from '@/utils/editorScriptRuntime'
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 { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
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 { getList, getNextSequenceValue } 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 { workflowService } from '@/services/workflow.service'
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 isTemporaryDxKey = (key: unknown) => typeof key === 'string' && key.startsWith('_DX_KEY_')
const getPersistedInsertedKey = (
e: DataGridTypes.RowInsertedEvent<any, any>,
keyFieldName?: string,
) => {
const dataKey = keyFieldName ? e.data?.[keyFieldName] : undefined
if (dataKey !== undefined && dataKey !== null && !isTemporaryDxKey(dataKey)) {
return dataKey
}
if (e.key !== undefined && e.key !== null && !isTemporaryDxKey(e.key)) {
return e.key
}
return undefined
}
const updateReadOnlyInFormItems = (items: any[] = [], field: string, readOnly: boolean) => {
let changed = false
const expected = String(field || '').toLowerCase()
const nextItems = items.map((item) => {
const key = item?.dataField || item?.name
let nextItem = item
if (key && String(key).toLowerCase() === expected) {
const editorOptions = nextItem.editorOptions || {}
if (editorOptions.readOnly !== readOnly) {
changed = true
nextItem = {
...nextItem,
editorOptions: {
...editorOptions,
readOnly,
},
}
}
}
if (nextItem?.items?.length) {
const childResult = updateReadOnlyInFormItems(nextItem.items, field, readOnly)
if (childResult.changed) {
changed = true
nextItem = { ...nextItem, items: childResult.items }
}
}
if (nextItem?.tabs?.length) {
const tabs = nextItem.tabs.map((tab: any) => {
const tabResult = updateReadOnlyInFormItems(tab.items, field, readOnly)
if (tabResult.changed) {
changed = true
return { ...tab, items: tabResult.items }
}
return tab
})
nextItem = tabs === nextItem.tabs ? nextItem : { ...nextItem, tabs }
}
return nextItem
})
return { items: nextItems, changed }
}
const findFormFieldKey = (items: any[] = [], field: string): string => {
const expected = String(field || '').toLowerCase()
for (const item of items || []) {
const key = item?.dataField || item?.name
if (key && String(key).toLowerCase() === expected) {
return key
}
const childKey = findFormFieldKey(item?.items || [], field)
if (childKey) {
return childKey
}
for (const tab of item?.tabs || []) {
const tabKey = findFormFieldKey(tab?.items || [], field)
if (tabKey) {
return tabKey
}
}
}
return field
}
const setFormEditorReadOnly = (form: any, field: string, readOnly: boolean) => {
if (!form?.option) return false
const apply = () => {
const formItems = form.option('items') || []
const resolvedField = findFormFieldKey(formItems, field)
const editor = form.getEditor?.(resolvedField) ?? form.getEditor?.(field)
const result = updateReadOnlyInFormItems(formItems, resolvedField, readOnly)
if (result.changed) {
try {
const item = form.itemOption?.(resolvedField) ?? form.itemOption?.(field)
if (item) {
form.itemOption?.(resolvedField, 'editorOptions', {
...(item.editorOptions || {}),
readOnly,
})
} else {
form.option('items', result.items)
}
} catch {
form.option('items', result.items)
}
}
const activeEditor = editor ?? form.getEditor?.(resolvedField) ?? form.getEditor?.(field)
if (activeEditor?.option?.('readOnly') !== readOnly) {
activeEditor?.option?.('readOnly', readOnly)
}
}
apply()
setTimeout(apply, 0)
return true
}
const getActiveEditingForm = (component: any) => {
const editForm = component?.getView?.('editingView')?._editForm
return editForm?.getEditor || editForm?.itemOption ? editForm : undefined
}
const flattenEditingFormItems = (items: any[] = []): EditingFormItemDto[] =>
items.flatMap((item) => [
...(item?.dataField ? [item] : []),
...flattenEditingFormItems(item?.items || []),
...(item?.tabs || []).flatMap((tab: any) => flattenEditingFormItems(tab?.items || [])),
])
const getValueByField = (data: Record<string, any> = {}, field?: string) => {
if (!field) return undefined
if (Object.prototype.hasOwnProperty.call(data, field)) return data[field]
const key = Object.keys(data).find(
(itemKey) => itemKey.toLowerCase() === String(field).toLowerCase(),
)
return key ? data[key] : undefined
}
const shouldRunEditorScriptOnContentReady = (script?: string) =>
Boolean(script && (script.includes('setEditorReadOnly') || script.includes('runtimeSetEditorReadOnly')))
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 editingFormDataRef = useRef<Record<string, any>>({})
const editingFormInstanceRef = useRef<any>()
// Edit popup state kaydetmeyi engellemek için flag
const isEditingRef = useRef(false)
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)
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])
// 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(
<Notification type="success" duration={2000}>
{translate('::ListForms.ListForm.GridStateSaved')}
</Notification>,
{
placement: 'top-end',
},
)
})
.catch((err) => {
toast.push(
<Notification type="danger" duration={2500}>
{translate('::ListForms.ListForm.GridStateSaveError')}
</Notification>,
{
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 updateWorkflowApprovalButtons = useCallback(
(component?: any, selectedRowsData?: Record<string, unknown>[]) => {
const grd = component ?? gridRef.current?.instance()
if (!grd) {
return
}
updateWorkflowApprovalToolbarItems(
grd,
gridDto?.gridOptions.workflowDto,
selectedRowsData ?? grd.getSelectedRowsData(),
config?.currentUser,
)
},
[config?.currentUser, gridDto],
)
const refreshData = useCallback(() => {
const grd = gridRef.current?.instance()
const refreshResult = grd?.refresh()
Promise.resolve(refreshResult).finally(() => updateWorkflowApprovalButtons(grd))
}, [updateWorkflowApprovalButtons])
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
updateWorkflowApprovalButtons(grd, data.selectedRowsData)
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) {
editingFormDataRef.current = { ...(e.data || {}) }
return
}
setMode('new')
setIsPopupFullScreen(gridDto?.gridOptions.editingOptionDto?.popup?.fullScreen ?? false)
e.promise = (async () => {
const rawFilter = searchParams?.get('filter')
let filters: any[] = []
if (rawFilter) {
try {
const parsed = JSON.parse(rawFilter)
filters = extractSearchParamsFields(parsed)
} catch (error) {
console.error('Filter parse edilemedi:', error)
}
}
for (const colFormat of gridDto.columnFormats) {
const fieldName = colFormat.fieldName
if (!fieldName) {
continue
}
// ExtraFilters içerisinde ilgili Field varsa, default değerleri set etme
if (extraFilters.some((f) => f.fieldName === fieldName)) {
continue
}
// 1) Önce grid default value set et
if (colFormat.defaultValue !== null && colFormat.defaultValue !== undefined) {
if (
typeof colFormat.defaultValue === 'string' &&
colFormat.defaultValue === '@AUTONUMBER'
) {
e.data[fieldName] = autoNumber()
} else if (colFormat.defaultValueType === FieldCustomValueTypeEnum.Sequence) {
try {
const response = await getNextSequenceValue(String(colFormat.defaultValue))
e.data[fieldName] = response.data
} catch (error) {
console.error('Sequence default value alınamadı:', {
fieldName,
defaultValue: colFormat.defaultValue,
error,
})
}
} else {
e.data[fieldName] = colFormat.defaultValue
}
}
// 2) Sonra URL / prop filter varsa üzerine yaz
const fieldMatch = filters.find(([field]) => field === fieldName)
if (fieldMatch) {
const val = fieldMatch[2]
const dType = colFormat.dataType as DataType
switch (dType) {
case 'date':
case 'datetime':
e.data[fieldName] = new Date(val)
break
case 'number':
e.data[fieldName] = Number(val)
break
case 'boolean':
e.data[fieldName] = val === true || val === 'true'
break
case 'object':
try {
e.data[fieldName] = JSON.parse(val)
} catch {
e.data[fieldName] = val
}
break
default:
e.data[fieldName] = val
break
}
}
}
editingFormDataRef.current = { ...(e.data || {}) }
})()
},
[gridDto, searchParams, extraFilters, getNextSequenceValue],
)
const onRowInserting = useCallback(
(e: DataGridTypes.RowInsertingEvent<any, any>) => {
if (!gridDto?.columnFormats) {
e.data = setFormEditingExtraItemValues(e.data)
return
}
const allowedFields = gridDto.columnFormats
.filter((f) => f.allowAdding)
.map((f) => f.fieldName)
const filteredData: any = {}
for (const key of allowedFields) {
if (e.data.hasOwnProperty(key) && key) {
filteredData[key] = e.data[key]
}
}
e.data = setFormEditingExtraItemValues(filteredData)
},
[gridDto],
)
const onRowUpdating = useCallback(
(e: DataGridTypes.RowUpdatingEvent<any, any>) => {
if (!gridDto?.columnFormats) return
const allowedFields = gridDto.columnFormats
.filter((f) => f.allowEditing)
.map((f) => f.fieldName)
let newData = { ...e.oldData, ...e.newData }
// Remove keys not allowed
Object.keys(newData).forEach((key) => {
if (!allowedFields.includes(key) || key.includes(':')) {
delete newData[key]
}
})
newData = setFormEditingExtraItemValues(newData)
if (gridDto?.gridOptions.keyFieldName) {
delete newData[gridDto?.gridOptions.keyFieldName]
}
e.newData = newData
},
[gridDto],
)
const onEditingStart = useCallback(
(e: DataGridTypes.EditingStartEvent<any, any>) => {
isEditingRef.current = true
editingFormDataRef.current = { ...(e.data || {}) }
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<any, any>) => {
toast.push(
<Notification type="danger" duration={2000}>
{e.error?.message}
</Notification>,
{
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<string, Set<string>>()
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<any, any>) => {
if (editor.parentType === 'dataRow' && editor.dataField && gridDto) {
const formItem = gridDto.gridOptions.editingFormDto
.flatMap((group) => flattenEditingFormItems([group]))
.find((i) => i.dataField === editor.dataField)
const fieldName = editor.dataField.split(':')[0]
const columnFormat = gridDto.columnFormats.find((column) => column.fieldName === fieldName)
const isNewRow = Boolean((editor as any).row?.isNewRow) || mode === 'new'
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
}
}
// 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 {
const childFormItem = gridDto.gridOptions.editingFormDto
.flatMap((group) => flattenEditingFormItems([group]))
.find((i) => i.dataField === childFieldName)
const childEditorOptions = childFormItem?.editorOptions
? JSON.parse(childFormItem.editorOptions)
: {}
const childEditor = formInstance.getEditor(childFieldName)
if (childEditorOptions?.disabled === true) {
childEditor?.option('disabled', true)
} else {
childEditor?.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
const editorDataField = editor.dataField
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 || {}),
[editorDataField]: e.value,
}
const runtimeSetEditorReadOnly = (field: string, readOnly: boolean) =>
setFormEditorReadOnly(
editingFormInstanceRef.current ?? getActiveEditingForm(grid),
field,
readOnly,
)
const setFormData = (newData: any) => {
if (rowIndex >= 0) {
Object.keys(newData).forEach((field) => {
grid.cellValue(rowIndex, field, newData[field])
})
}
}
executeEditorScript(formItem.editorScript!, {
formData,
e,
editor,
runtimeSetEditorReadOnly,
setFormData,
})
} 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 = {}
let parsedEditorOptions: EditorOptionsWithButtons = {}
const forcedEditorOptions: EditorOptionsWithButtons = {}
try {
parsedEditorOptions = i.editorOptions ? JSON.parse(i.editorOptions) : {}
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) {
forcedEditorOptions.readOnly = true
}
}
}
} catch {}
const fieldName = i.dataField.split(':')[0]
const listFormField = gridDto?.columnFormats.find((x: any) => x.fieldName === fieldName)
const defaultEditorOptions: EditorOptionsWithButtons = {}
if (listFormField?.sourceDbType === DbTypeEnum.Date) {
Object.assign(defaultEditorOptions, {
type: 'date',
dateSerializationFormat: 'yyyy-MM-dd',
displayFormat: 'shortDate',
})
} else if (
listFormField?.sourceDbType === DbTypeEnum.DateTime ||
listFormField?.sourceDbType === DbTypeEnum.DateTime2 ||
listFormField?.sourceDbType === DbTypeEnum.DateTimeOffset
) {
Object.assign(defaultEditorOptions, {
type: 'datetime',
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
displayFormat: 'shortDateShortTime',
})
}
// Her item'a placeholder olarak captionName ekle
if (listFormField?.placeHolder) {
defaultEditorOptions.placeholder = translate('::' + listFormField.placeHolder)
}
// Set defaultValue for @AUTONUMBER fields
if (
typeof listFormField?.defaultValue === 'string' &&
listFormField?.defaultValue === '@AUTONUMBER' &&
mode === 'new'
) {
defaultEditorOptions.value = autoNumber()
}
editorOptions = {
...defaultEditorOptions,
...parsedEditorOptions,
...forcedEditorOptions,
}
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 item: SimpleItemWithColData = {
canRead: listFormField?.canRead ?? false,
canUpdate: listFormField?.canUpdate ?? false,
canCreate: listFormField?.canCreate ?? false,
canExport: listFormField?.canExport ?? false,
allowEditing: listFormField?.allowEditing ?? true,
allowAdding: listFormField?.allowAdding ?? true,
dataField: i.dataField,
name: i.dataField,
editorType2: i.editorType2,
editorType:
i.editorType2 == PlatformEditorTypes.dxGridBox
? 'dxDropDownBox'
: i.editorType2 == PlatformEditorTypes.dxImageUpload
? undefined
: 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) => {
// 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') {
// 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(
<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} className="mt-2">
<WidgetGroup widgetGroups={gridDto?.widgets ?? []} />
</div>
<Container className={DX_CLASSNAMES}>
{!isSubForm && (
<Helmet
titleTemplate={`%s | ${APP_NAME}`}
title={translate('::' + gridDto?.gridOptions.title)}
defaultTitle={APP_NAME}
></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}
id={'Grid-' + listFormCode}
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}
onContentReady={(e) => updateWorkflowApprovalButtons(e.component)}
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={(e) => {
const insertedKey = getPersistedInsertedKey(e, gridDto.gridOptions.keyFieldName)
if (
gridDto.gridOptions.workflowDto?.approvalStatusFieldName &&
insertedKey !== undefined
) {
workflowService
.startWorkflow(listFormCode, [insertedKey])
.then(() => gridRef.current?.instance()?.refresh())
.catch(console.error)
}
props.refreshData?.()
}}
onRowUpdated={() => {
props.refreshData?.()
}}
onRowRemoved={() => {
props.refreshData?.()
}}
onEditorPreparing={onEditorPreparing}
>
<Export
enabled={gridDto.gridOptions.exportDto?.enabled}
allowExportSelectedData={gridDto.gridOptions.exportDto?.allowExportSelectedData}
formats={['pdf', 'xlsx']}
/>
<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={{
deferRendering: true,
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,
onContentReady: (e) => {
editingFormInstanceRef.current = e.component
const form = e.component
const grid = gridRef.current?.instance()
const rowKey = grid?.option('editing.editRowKey')
const rowIndex = rowKey !== undefined ? grid?.getRowIndexByKey(rowKey) : -1
const runtimeSetEditorReadOnly = (field: string, readOnly: boolean) =>
setFormEditorReadOnly(form, field, readOnly)
const setFormData = (newData: any) => {
form?.option?.('formData', newData)
editingFormDataRef.current = { ...newData }
if (grid && rowIndex !== undefined && rowIndex >= 0) {
Object.keys(newData).forEach((field) => {
grid.cellValue(rowIndex, field, newData[field])
})
}
}
const runReadOnlyScripts = () => {
const editorValues = gridDto.gridOptions.editingFormDto
.flatMap((group) => flattenEditingFormItems([group]))
.reduce<Record<string, any>>((values, formItem) => {
const editorInstance = form?.getEditor?.(formItem.dataField)
if (editorInstance?.option) {
values[formItem.dataField] = editorInstance.option('value')
}
return values
}, {})
const formData = {
...editingFormDataRef.current,
...(form?.option?.('formData') || {}),
...editorValues,
}
editingFormDataRef.current = { ...formData }
gridDto.gridOptions.editingFormDto
.flatMap((group) => flattenEditingFormItems([group]))
.filter((formItem) =>
shouldRunEditorScriptOnContentReady(formItem.editorScript),
)
.forEach((formItem) => {
try {
const editorInstance = form?.getEditor?.(formItem.dataField)
const editorValue =
editorInstance?.option?.('value') ??
getValueByField(formData, formItem.dataField)
const editor = {
dataField: formItem.dataField,
component: grid,
}
const e = {
component: form,
dataField: formItem.dataField,
value: editorValue,
}
executeEditorScript(formItem.editorScript!, {
formData,
e,
editor,
runtimeSetEditorReadOnly,
setFormData,
})
} catch (err) {
console.error('Script exec error on contentReady', formItem.dataField, err)
}
})
}
runReadOnlyScripts()
},
onFieldDataChanged: (e) => {
if (e.dataField) {
const previousValue = editingFormDataRef.current?.[e.dataField]
editingFormDataRef.current = {
...(e.component?.option?.('formData') || {}),
}
const formItem = gridDto.gridOptions.editingFormDto
.flatMap((group) => flattenEditingFormItems([group]))
.find(
(i) =>
String(i.dataField || '').toLowerCase() ===
String(e.dataField || '').toLowerCase(),
)
if (formItem?.editorScript) {
try {
const grid = gridRef.current?.instance()
const rowKey = grid?.option('editing.editRowKey')
const rowIndex =
rowKey !== undefined ? grid?.getRowIndexByKey(rowKey) : -1
const formData = {
...(e.component?.option?.('formData') || {}),
[e.dataField]: e.value,
}
editingFormDataRef.current = { ...formData }
const runtimeSetEditorReadOnly = (field: string, readOnly: boolean) =>
setFormEditorReadOnly(e.component, field, readOnly)
const setFormData = (newData: any) => {
e.component?.option?.('formData', newData)
editingFormDataRef.current = { ...newData }
if (grid && rowIndex !== undefined && rowIndex >= 0) {
Object.keys(newData).forEach((field) => {
grid.cellValue(rowIndex, field, newData[field])
})
}
}
executeEditorScript(formItem.editorScript, {
formData,
e,
editor: {
dataField: e.dataField,
component: grid,
},
runtimeSetEditorReadOnly,
setFormData,
})
} 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.allowAdding
} else if (mode === 'edit') {
return a.canUpdate && a.allowEditing
} 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.allowAdding
} else if (mode === 'edit') {
return a.canUpdate && a.allowEditing
} else {
return false
}
}),
}
}),
})
}
})
return result
})()
: undefined,
}}
></Editing>
<Template name={'cellEditTagBox'} render={TagBoxEditorComponent} />
<Template name={'cellEditGridBox'} render={GridBoxEditorComponent} />
<Template name={'cellEditImageUpload'} render={ImageUploadEditorComponent} />
<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 &&
gridDto?.gridOptions?.subFormsListFormType === SubFormTabTypeEnum.List && (
<>
<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)}
>
<Dialog.Body className="flex flex-col">
<ImportDashboard gridDto={gridDto} />
</Dialog.Body>
</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