sozsoft-platform/ui/src/views/form/useFormData.tsx

424 lines
14 KiB
TypeScript
Raw Normal View History

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'
import { useCallback, 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'
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'
import { useListFormCustomSources } from '../list/shared/hooks'
import { useListFormStateStoring } from '../list/useListFormStateStoring'
import {
buildFormGroupItems,
flattenFormGroups,
FormFieldOrder,
getCascadeParentFields,
} from './shared/formItems'
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 [fieldOrder, setFieldOrder] = useState<FormFieldOrder>()
2026-02-24 20:44:16 +00:00
const refForm = useRef<FormRef>(null)
const previousFormDataRef = useRef<any | undefined>(undefined)
// Alan sırası kaydedilince form o anki veriyle yeniden kurulur.
const currentFormDataRef = useRef<any>(formData)
currentFormDataRef.current = formData
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 })
useListFormCustomSources(gridDto)
// Alan sırası grid/pivot görünümleriyle aynı ListForm state'inde, kullanıcı bazında tutulur.
const stateStoring = gridDto?.gridOptions?.stateStoringDto
const { customLoadState, customSaveState, storageKey } = useListFormStateStoring({
listFormCode,
storageKey: stateStoring?.enabled ? stateStoring.storageKey : undefined,
filterPrefix: 'form',
showSaveToast: false,
saveDebounceMs: 0,
})
2026-02-24 20:44:16 +00:00
const fetchData = async () => {
setLoading(true)
try {
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)
}
}
const showSaveSuccess = () =>
toast.push(
<Notification type="success" duration={2000}>
{translate('::App.Platform.FormBilgileriKaydedildi')}
</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?.()
if (!dataSource) return
2026-02-24 20:44:16 +00:00
if (!refForm.current?.instance().validate()?.isValid) {
2026-02-24 20:44:16 +00:00
return
}
setLoading(true)
try {
const formValues = { ...formData }
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) {
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))
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)
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)
if (!gridDto) return
2026-02-24 20:44:16 +00:00
const { editingOptionDto, permissionDto } = gridDto.gridOptions
2026-02-24 20:44:16 +00:00
setPermissionResults({
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
})
const columns = getBandedColumns()
setCommandColumnData(columns?.find((column) => column.type === 'buttons'))
setDataSource(
createSelectDataSource(
gridDto.gridOptions,
listFormCode,
searchParams,
layoutTypes.grid,
columns,
),
)
// Kayıtlı alan sırası form kurulmadan okunur; aksi hâlde form önce varsayılan sırayla çizilip yer değiştirirdi.
let isActive = true
customLoadState()
.catch(() => null)
.then((state: any) => {
if (!isActive) return
const savedOrder: FormFieldOrder | undefined = state?.fieldOrder ?? undefined
setFieldOrder(savedOrder)
setFormItems(
buildFormGroupItems({
gridDto,
mode,
columns,
getLookupDataSource,
formData,
fieldOrder: savedOrder,
}),
)
setGridReady(true)
})
return () => {
isActive = false
}
// 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])
/** Formu verilen alan sırasıyla yeniden kurar; kaydetmez (sürükleme sırasında canlı önizleme). */
const previewFieldOrder = useCallback(
(order: FormFieldOrder | undefined) => {
if (!gridDto) return
setFormItems(
buildFormGroupItems({
gridDto,
mode,
columns: getBandedColumns(),
getLookupDataSource,
formData: currentFormDataRef.current,
fieldOrder: order,
}),
)
},
// getBandedColumns/getLookupDataSource her render'da yeniden oluşabilir.
// eslint-disable-next-line react-hooks/exhaustive-deps
[gridDto, mode],
)
/** Alan sırasını kullanıcının state'ine yazar; `undefined` fabrika sırasına (`EditOrderNo`) döner. */
const saveFieldOrder = useCallback(
async (order: FormFieldOrder | undefined) => {
await customSaveState(order ? { fieldOrder: order } : null)
setFieldOrder(order)
previewFieldOrder(order)
},
[customSaveState, previewFieldOrder],
)
/**
* 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(() => {
// DevExtreme formData nesnesine yerinde yazar; referans tutulursa önceki değer de yeni
// değere dönüşür ve değişen parent alan hiç bulunamaz.
const snapshotFormData = (data: any) => (data ? { ...data } : data)
2026-02-24 20:44:16 +00:00
if (!gridDto || !formItems.length) {
previousFormDataRef.current = snapshotFormData(formData)
2026-02-24 20:44:16 +00:00
return
}
const previousFormData = previousFormDataRef.current
const changedFields = previousFormData
? Object.keys({ ...(previousFormData || {}), ...(formData || {}) }).filter(
(field) => !Object.is(previousFormData?.[field], formData?.[field]),
)
: []
const shouldRefreshLookup = (item: SimpleItemWithColData) =>
!previousFormData ||
getCascadeParentFields(item).some((field) => changedFields.includes(field))
const hasAffectedLookup = flattenFormGroups(formItems).some(
(item) => item.colData?.lookupDto?.dataSourceType && shouldRefreshLookup(item),
)
if (!hasAffectedLookup) {
previousFormDataRef.current = snapshotFormData(formData)
return
}
const updateItems = (items: any[] = []): any[] =>
items.map((item) => {
const colData = item.colData
if (colData?.lookupDto?.dataSourceType && shouldRefreshLookup(item)) {
2026-05-31 18:16:41 +00:00
const currentDataSource = item.editorOptions?.dataSource
// 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
: getLookupDataSource(colData, formData || null),
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
},
}
}
if (item?.items?.length) {
return { ...item, items: updateItems(item.items) }
}
if (item?.tabs?.length) {
return {
...item,
tabs: item.tabs.map((tab: any) => ({ ...tab, items: updateItems(tab.items) })),
}
}
2026-02-24 20:44:16 +00:00
return item
})
previousFormDataRef.current = snapshotFormData(formData)
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(() => {
if (!gridReady || mode === 'new') return
2026-02-24 20:44:16 +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
}
// eslint-disable-next-line react-hooks/exhaustive-deps
2026-02-24 20:44:16 +00:00
}, [id, gridReady])
useEffect(() => {
if (filter?.length) {
fetchData()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
2026-02-24 20:44:16 +00:00
}, [filter])
// Yetki kontrolü
2026-02-24 20:44:16 +00:00
useEffect(() => {
if (!permissionResults) return
const denied =
(mode === 'new' && !permissionResults.c) ||
(mode === 'edit' && !permissionResults.u) ||
(mode === 'view' && !permissionResults.r)
2026-02-24 20:44:16 +00:00
if (denied) {
2026-02-24 20:44:16 +00:00
navigate(getAccessDeniedPath(location.pathname), { replace: true, state: { from: location } })
}
// 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,
fieldOrder,
previewFieldOrder,
saveFieldOrder,
canStoreFieldOrder: !!storageKey,
2026-02-24 20:44:16 +00:00
}
}
export { useGridData }