2026-02-24 20:44:16 +00:00
|
|
|
|
import { Notification, toast } from '@/components/ui'
|
|
|
|
|
|
import { getList } from '@/services/form.service'
|
|
|
|
|
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
|
|
|
|
import { usePermission } from '@/utils/hooks/usePermission'
|
2026-07-13 08:39:13 +00:00
|
|
|
|
import type { FormRef } from 'devextreme-react/form'
|
2026-02-24 20:44:16 +00:00
|
|
|
|
import CustomStore from 'devextreme/data/custom_store'
|
2026-07-13 08:39:13 +00:00
|
|
|
|
import type { GroupItem } from 'devextreme/ui/form'
|
2026-02-24 20:44:16 +00:00
|
|
|
|
import { useEffect, useRef, useState } from 'react'
|
2026-07-14 20:25:04 +00:00
|
|
|
|
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
|
2026-02-24 20:44:16 +00:00
|
|
|
|
import { GridColumnData } from '../list/GridColumnData'
|
|
|
|
|
|
import { PermissionResults, RowMode, SimpleItemWithColData } from './types'
|
2026-08-06 22:54:50 +00:00
|
|
|
|
import { GridDto } from '@/proxy/form/models'
|
2026-02-24 20:44:16 +00:00
|
|
|
|
import { getAccessDeniedPath } from '@/utils/routing'
|
|
|
|
|
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
|
|
|
|
|
import { useLookupDataSource } from './useLookupDataSource'
|
|
|
|
|
|
import { layoutTypes } from '../admin/listForm/edit/types'
|
|
|
|
|
|
import { useListFormCustomDataSource } from '../list/useListFormCustomDataSource'
|
|
|
|
|
|
import { useListFormColumns } from '../list/useListFormColumns'
|
2026-08-06 22:54:50 +00:00
|
|
|
|
import { useListFormCustomSources } from '../list/shared/hooks'
|
|
|
|
|
|
import {
|
|
|
|
|
|
buildFormGroupItems,
|
|
|
|
|
|
flattenFormGroups,
|
|
|
|
|
|
getCascadeParentFields,
|
|
|
|
|
|
} from './shared/formItems'
|
2026-06-08 10:15:22 +00:00
|
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
|
const useGridData = (props: {
|
|
|
|
|
|
mode: RowMode
|
|
|
|
|
|
listFormCode: string
|
|
|
|
|
|
id?: string
|
|
|
|
|
|
level?: number
|
|
|
|
|
|
isSubForm?: boolean
|
|
|
|
|
|
onSubmitAction?: () => void
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
const { mode, listFormCode, id, isSubForm } = props
|
|
|
|
|
|
|
|
|
|
|
|
const [gridReady, setGridReady] = useState(false)
|
|
|
|
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
|
|
const [filter, setFilter] = useState<any[]>([])
|
|
|
|
|
|
const [gridDto, setGridDto] = useState<GridDto>()
|
|
|
|
|
|
const [dataSource, setDataSource] = useState<CustomStore<any, any>>()
|
|
|
|
|
|
const [commandColumnData, setCommandColumnData] = useState<GridColumnData>()
|
|
|
|
|
|
const [formDataOld, setFormDataOld] = useState<any>()
|
|
|
|
|
|
const [formData, setFormData] = useState<any>()
|
|
|
|
|
|
const [formItems, setFormItems] = useState<GroupItem[]>([])
|
|
|
|
|
|
const [permissionResults, setPermissionResults] = useState<PermissionResults>()
|
|
|
|
|
|
|
|
|
|
|
|
const refForm = useRef<FormRef>(null)
|
2026-08-13 08:41:48 +00:00
|
|
|
|
const previousFormDataRef = useRef<any | undefined>(undefined)
|
2026-02-24 20:44:16 +00:00
|
|
|
|
const [searchParams] = useSearchParams()
|
2026-07-14 20:25:04 +00:00
|
|
|
|
const location = useLocation()
|
2026-02-24 20:44:16 +00:00
|
|
|
|
const navigate = useNavigate()
|
|
|
|
|
|
const { translate } = useLocalization()
|
|
|
|
|
|
const { checkPermission } = usePermission()
|
|
|
|
|
|
const { getBandedColumns } = useListFormColumns({
|
|
|
|
|
|
gridDto,
|
|
|
|
|
|
listFormCode,
|
|
|
|
|
|
isSubForm,
|
|
|
|
|
|
gridRef: undefined,
|
|
|
|
|
|
})
|
|
|
|
|
|
const { createSelectDataSource } = useListFormCustomDataSource({} as any)
|
|
|
|
|
|
const { getLookupDataSource } = useLookupDataSource({ listFormCode, isSubForm })
|
|
|
|
|
|
|
2026-08-06 22:54:50 +00:00
|
|
|
|
useListFormCustomSources(gridDto)
|
|
|
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
|
const fetchData = async () => {
|
|
|
|
|
|
setLoading(true)
|
|
|
|
|
|
try {
|
2026-08-06 22:54:50 +00:00
|
|
|
|
const response: any = await dataSource?.load({ filter, skip: 0, take: 1 })
|
|
|
|
|
|
const record = response?.data?.[0]
|
|
|
|
|
|
setFormData(record)
|
|
|
|
|
|
setFormDataOld(record ? { ...record } : undefined)
|
2026-02-24 20:44:16 +00:00
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
toast.push(<Notification title={error.message} type="danger" />, {
|
2026-07-08 06:14:40 +00:00
|
|
|
|
placement: 'bottom-end',
|
2026-02-24 20:44:16 +00:00
|
|
|
|
})
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-06 22:54:50 +00:00
|
|
|
|
const showSaveSuccess = () =>
|
|
|
|
|
|
toast.push(
|
|
|
|
|
|
<Notification type="success" duration={2000}>
|
2026-08-14 14:03:34 +00:00
|
|
|
|
{translate('::App.Platform.FormBilgileriKaydedildi')}
|
2026-08-06 22:54:50 +00:00
|
|
|
|
</Notification>,
|
|
|
|
|
|
{ placement: 'bottom-end' },
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
/** Kaydetme sonrası yönlendirme; alt formda üst bileşene haber verilir. */
|
|
|
|
|
|
const finishSubmit = (viewPath: string) => {
|
|
|
|
|
|
if (isSubForm) {
|
|
|
|
|
|
props.onSubmitAction?.()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
navigate(viewPath)
|
|
|
|
|
|
}
|
|
|
|
|
|
showSaveSuccess()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Güncelleme isteğinin gövdesini hazırlar. */
|
|
|
|
|
|
const buildUpdateData = (formValues: Record<string, any>) => {
|
|
|
|
|
|
const sendOnlyChanged = gridDto?.gridOptions.editingOptionDto?.sendOnlyChangedFormValuesUpdate
|
|
|
|
|
|
const data =
|
|
|
|
|
|
sendOnlyChanged && formDataOld
|
|
|
|
|
|
? Object.fromEntries(
|
|
|
|
|
|
Object.keys(formValues)
|
|
|
|
|
|
.filter((key) => !Object.is(formValues[key], formDataOld[key]))
|
|
|
|
|
|
.map((key) => [key, formValues[key]]),
|
|
|
|
|
|
)
|
|
|
|
|
|
: { ...formValues }
|
|
|
|
|
|
|
|
|
|
|
|
if (gridDto?.gridOptions.keyFieldName) {
|
|
|
|
|
|
delete data[gridDto.gridOptions.keyFieldName]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return data
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
|
const handleSubmit = async (e: any) => {
|
2026-10-01 08:50:16 +00:00
|
|
|
|
// Form submit DOM olayı, DevExtreme toolbar butonu ise { event } taşıyan nesne verir.
|
|
|
|
|
|
const domEvent = e?.event ?? e
|
|
|
|
|
|
domEvent?.preventDefault?.()
|
2026-08-06 22:54:50 +00:00
|
|
|
|
if (!dataSource) return
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
2026-08-06 22:54:50 +00:00
|
|
|
|
if (!refForm.current?.instance().validate()?.isValid) {
|
2026-02-24 20:44:16 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setLoading(true)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const formValues = { ...formData }
|
2026-08-06 22:54:50 +00:00
|
|
|
|
|
2026-10-01 08:50:16 +00:00
|
|
|
|
// Store insert/update'i satır verisi olarak döndürür (Grid'in refresh sözleşmesi);
|
|
|
|
|
|
// HTTP hatası promise'i reddeder ve catch'e düşer.
|
|
|
|
|
|
const keyFieldName = gridDto?.gridOptions.keyFieldName
|
2026-02-24 20:44:16 +00:00
|
|
|
|
if (mode === 'new') {
|
|
|
|
|
|
const result = await dataSource.insert(formValues)
|
2026-10-01 08:50:16 +00:00
|
|
|
|
const newId = keyFieldName ? result?.[keyFieldName] : undefined
|
|
|
|
|
|
if (newId === undefined || newId === null) {
|
2026-08-14 14:03:34 +00:00
|
|
|
|
throw new Error(translate('::App.ListForm.FormBilgileriKaydedilemedi'))
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}
|
2026-10-01 08:50:16 +00:00
|
|
|
|
finishSubmit(
|
|
|
|
|
|
ROUTES_ENUM.protected.admin.formView
|
|
|
|
|
|
.replace(':listFormCode', encodeURIComponent(listFormCode))
|
|
|
|
|
|
.replace(':id', encodeURIComponent(String(newId))),
|
|
|
|
|
|
)
|
2026-02-24 20:44:16 +00:00
|
|
|
|
} else if (mode === 'edit') {
|
2026-10-01 08:50:16 +00:00
|
|
|
|
await dataSource.update(id, buildUpdateData(formValues))
|
2026-08-06 22:54:50 +00:00
|
|
|
|
finishSubmit(
|
|
|
|
|
|
ROUTES_ENUM.protected.admin.formView
|
|
|
|
|
|
.replace(':listFormCode', encodeURIComponent(listFormCode))
|
|
|
|
|
|
.replace(':id', encodeURIComponent(id!)),
|
|
|
|
|
|
)
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
toast.push(<Notification title={error.message} type="danger" />, {
|
2026-07-08 06:14:40 +00:00
|
|
|
|
placement: 'bottom-end',
|
2026-02-24 20:44:16 +00:00
|
|
|
|
})
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setGridReady(false)
|
|
|
|
|
|
|
2026-08-06 22:54:50 +00:00
|
|
|
|
let isActive = true
|
|
|
|
|
|
getList({ listFormCode })
|
|
|
|
|
|
.then((response: any) => {
|
|
|
|
|
|
if (isActive) setGridDto(response.data)
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch((error: unknown) => console.error('GridDto load error:', error))
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
isActive = false
|
|
|
|
|
|
}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}, [listFormCode])
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setGridReady(false)
|
2026-08-06 22:54:50 +00:00
|
|
|
|
if (!gridDto) return
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
2026-08-06 22:54:50 +00:00
|
|
|
|
const { editingOptionDto, permissionDto } = gridDto.gridOptions
|
2026-02-24 20:44:16 +00:00
|
|
|
|
setPermissionResults({
|
2026-08-06 22:54:50 +00:00
|
|
|
|
c: editingOptionDto.allowAdding === true && checkPermission(permissionDto.c),
|
|
|
|
|
|
r: checkPermission(permissionDto.r),
|
|
|
|
|
|
u: editingOptionDto.allowUpdating === true && checkPermission(permissionDto.u),
|
|
|
|
|
|
d: editingOptionDto.allowDeleting === true && checkPermission(permissionDto.d),
|
|
|
|
|
|
e: checkPermission(permissionDto.e),
|
|
|
|
|
|
i: checkPermission(permissionDto.i),
|
2026-02-24 20:44:16 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-06 22:54:50 +00:00
|
|
|
|
const columns = getBandedColumns()
|
|
|
|
|
|
setCommandColumnData(columns?.find((column) => column.type === 'buttons'))
|
|
|
|
|
|
|
|
|
|
|
|
setDataSource(
|
|
|
|
|
|
createSelectDataSource(
|
|
|
|
|
|
gridDto.gridOptions,
|
|
|
|
|
|
listFormCode,
|
|
|
|
|
|
searchParams,
|
|
|
|
|
|
layoutTypes.grid,
|
|
|
|
|
|
columns,
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
setFormItems(
|
|
|
|
|
|
buildFormGroupItems({
|
|
|
|
|
|
gridDto,
|
|
|
|
|
|
mode,
|
|
|
|
|
|
columns,
|
|
|
|
|
|
getLookupDataSource,
|
|
|
|
|
|
formData,
|
|
|
|
|
|
}),
|
2026-02-24 20:44:16 +00:00
|
|
|
|
)
|
|
|
|
|
|
setGridReady(true)
|
2026-08-06 22:54:50 +00:00
|
|
|
|
// getBandedColumns/getLookupDataSource her render'da yeniden oluşabilir.
|
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}, [gridDto])
|
|
|
|
|
|
|
2026-08-06 22:54:50 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* formData değiştiğinde yalnızca etkilenen cascading lookup dataSource'larını
|
|
|
|
|
|
* yeniden üretir; tüm form item ağacını gereksiz yere yenilemez.
|
|
|
|
|
|
*/
|
2026-02-24 20:44:16 +00:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!gridDto || !formItems.length) {
|
2026-06-08 10:15:22 +00:00
|
|
|
|
previousFormDataRef.current = formData
|
2026-02-24 20:44:16 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 10:15:22 +00:00
|
|
|
|
const previousFormData = previousFormDataRef.current
|
|
|
|
|
|
const changedFields = previousFormData
|
|
|
|
|
|
? Object.keys({ ...(previousFormData || {}), ...(formData || {}) }).filter(
|
|
|
|
|
|
(field) => !Object.is(previousFormData?.[field], formData?.[field]),
|
|
|
|
|
|
)
|
|
|
|
|
|
: []
|
|
|
|
|
|
|
2026-08-06 22:54:50 +00:00
|
|
|
|
const shouldRefreshLookup = (item: SimpleItemWithColData) =>
|
|
|
|
|
|
!previousFormData ||
|
|
|
|
|
|
getCascadeParentFields(item).some((field) => changedFields.includes(field))
|
|
|
|
|
|
|
|
|
|
|
|
const hasAffectedLookup = flattenFormGroups(formItems).some(
|
|
|
|
|
|
(item) => item.colData?.lookupDto?.dataSourceType && shouldRefreshLookup(item),
|
|
|
|
|
|
)
|
2026-06-08 10:15:22 +00:00
|
|
|
|
|
2026-08-06 22:54:50 +00:00
|
|
|
|
if (!hasAffectedLookup) {
|
|
|
|
|
|
previousFormDataRef.current = formData
|
|
|
|
|
|
return
|
2026-06-08 10:15:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 08:31:01 +00:00
|
|
|
|
const updateItems = (items: any[] = []): any[] =>
|
2026-06-08 10:15:22 +00:00
|
|
|
|
items.map((item) => {
|
2026-07-17 08:59:51 +00:00
|
|
|
|
const colData = item.colData
|
2026-08-06 22:54:50 +00:00
|
|
|
|
|
2026-06-08 10:15:22 +00:00
|
|
|
|
if (colData?.lookupDto?.dataSourceType && shouldRefreshLookup(item)) {
|
2026-05-31 18:16:41 +00:00
|
|
|
|
const currentDataSource = item.editorOptions?.dataSource
|
2026-08-06 22:54:50 +00:00
|
|
|
|
// Kullanıcı tarafından elle verilmiş (statik dizi vb.) dataSource korunur.
|
2026-05-31 18:16:41 +00:00
|
|
|
|
const keepCustomDataSource =
|
|
|
|
|
|
currentDataSource !== undefined && typeof currentDataSource?.load !== 'function'
|
|
|
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
|
return {
|
|
|
|
|
|
...item,
|
|
|
|
|
|
editorOptions: {
|
|
|
|
|
|
...item.editorOptions,
|
2026-05-31 18:16:41 +00:00
|
|
|
|
dataSource: keepCustomDataSource
|
|
|
|
|
|
? currentDataSource
|
2026-10-01 13:58:07 +00:00
|
|
|
|
: getLookupDataSource(colData, formData || null),
|
2026-06-08 10:15:22 +00:00
|
|
|
|
valueExpr:
|
|
|
|
|
|
item.editorOptions?.valueExpr ?? colData?.lookupDto?.valueExpr?.toLowerCase(),
|
2026-05-31 18:16:41 +00:00
|
|
|
|
displayExpr:
|
|
|
|
|
|
item.editorOptions?.displayExpr ?? colData?.lookupDto?.displayExpr?.toLowerCase(),
|
2026-02-24 20:44:16 +00:00
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-08 10:15:22 +00:00
|
|
|
|
|
|
|
|
|
|
if (item?.items?.length) {
|
2026-08-06 22:54:50 +00:00
|
|
|
|
return { ...item, items: updateItems(item.items) }
|
2026-06-08 10:15:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (item?.tabs?.length) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
...item,
|
2026-08-06 22:54:50 +00:00
|
|
|
|
tabs: item.tabs.map((tab: any) => ({ ...tab, items: updateItems(tab.items) })),
|
2026-06-08 10:15:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
|
return item
|
2026-06-08 10:15:22 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
previousFormDataRef.current = formData
|
2026-08-06 22:54:50 +00:00
|
|
|
|
setFormItems((currentItems) =>
|
|
|
|
|
|
currentItems.map((groupItem) => ({
|
|
|
|
|
|
...groupItem,
|
|
|
|
|
|
items: updateItems(groupItem.items as any[]),
|
|
|
|
|
|
tabs: (groupItem as any).tabs?.map((tab: any) => ({
|
|
|
|
|
|
...tab,
|
|
|
|
|
|
items: updateItems(tab.items),
|
|
|
|
|
|
})),
|
|
|
|
|
|
})),
|
|
|
|
|
|
)
|
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}, [formData, gridDto])
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-08-06 22:54:50 +00:00
|
|
|
|
if (!gridReady || mode === 'new') return
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
2026-08-06 22:54:50 +00:00
|
|
|
|
if (id) {
|
|
|
|
|
|
setFilter([gridDto?.gridOptions.keyFieldName ?? 'Id', '=', id])
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Form tipi liste formlar ID olmadan açılabilir; kayıt seçimi sunucudaki
|
|
|
|
|
|
// DefaultFilter'a bırakılır.
|
|
|
|
|
|
fetchData()
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}
|
2026-08-06 22:54:50 +00:00
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}, [id, gridReady])
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (filter?.length) {
|
|
|
|
|
|
fetchData()
|
|
|
|
|
|
}
|
2026-08-06 22:54:50 +00:00
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}, [filter])
|
|
|
|
|
|
|
2026-08-06 22:54:50 +00:00
|
|
|
|
// Yetki kontrolü
|
2026-02-24 20:44:16 +00:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!permissionResults) return
|
|
|
|
|
|
|
2026-08-06 22:54:50 +00:00
|
|
|
|
const denied =
|
|
|
|
|
|
(mode === 'new' && !permissionResults.c) ||
|
|
|
|
|
|
(mode === 'edit' && !permissionResults.u) ||
|
|
|
|
|
|
(mode === 'view' && !permissionResults.r)
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
2026-08-06 22:54:50 +00:00
|
|
|
|
if (denied) {
|
2026-02-24 20:44:16 +00:00
|
|
|
|
navigate(getAccessDeniedPath(location.pathname), { replace: true, state: { from: location } })
|
|
|
|
|
|
}
|
2026-08-06 22:54:50 +00:00
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}, [permissionResults])
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
loading,
|
|
|
|
|
|
gridDto,
|
|
|
|
|
|
dataSource,
|
|
|
|
|
|
commandColumnData,
|
|
|
|
|
|
filter,
|
|
|
|
|
|
formItems,
|
|
|
|
|
|
formData,
|
|
|
|
|
|
refForm,
|
|
|
|
|
|
permissionResults,
|
|
|
|
|
|
fetchData,
|
|
|
|
|
|
setFormData,
|
|
|
|
|
|
handleSubmit,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export { useGridData }
|