sozsoft-platform/ui/src/views/form/useFormData.tsx
2026-10-11 22:57:23 +03:00

422 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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 { Notification, toast } from '@/components/ui'
import { getList } from '@/services/form.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { usePermission } from '@/utils/hooks/usePermission'
import type { FormRef } from 'devextreme-react/form'
import CustomStore from 'devextreme/data/custom_store'
import type { GroupItem } from 'devextreme/ui/form'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import { GridColumnData } from '../list/GridColumnData'
import { PermissionResults, RowMode, SimpleItemWithColData } from './types'
import { GridDto } from '@/proxy/form/models'
import { getAccessDeniedPath } from '@/utils/routing'
import { getFormRecordUrl } 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'
const useGridData = (props: {
mode: RowMode
listFormCode: string
id?: string
relationFilter?: [string, string, unknown]
level?: number
isSubForm?: boolean
onSubmitAction?: () => void
}) => {
const { mode, listFormCode, id, relationFilter, 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>()
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
const [searchParams] = useSearchParams()
const location = useLocation()
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,
})
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)
} catch (error: any) {
toast.push(<Notification title={error.message} type="danger" />, {
placement: 'bottom-end',
})
} 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
}
const handleSubmit = async (e: any) => {
// Form submit DOM olayı, DevExtreme toolbar butonu ise { event } taşıyan nesne verir.
const domEvent = e?.event ?? e
domEvent?.preventDefault?.()
if (!dataSource) return
if (!refForm.current?.instance().validate()?.isValid) {
return
}
setLoading(true)
try {
const formValues = { ...formData }
// 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
if (mode === 'new') {
const result = await dataSource.insert(formValues)
const newId = keyFieldName ? result?.[keyFieldName] : undefined
if (newId === undefined || newId === null) {
throw new Error(translate('::App.ListForm.FormBilgileriKaydedilemedi'))
}
finishSubmit(getFormRecordUrl(listFormCode, newId))
} else if (mode === 'edit') {
// İlişkiyle açılan alt form kaydında anahtar URL'de değil yüklenen kayıttadır.
const recordKey = (keyFieldName && formDataOld?.[keyFieldName]) || id
await dataSource.update(recordKey, buildUpdateData(formValues))
finishSubmit(getFormRecordUrl(listFormCode, recordKey))
}
} catch (error: any) {
toast.push(<Notification title={error.message} type="danger" />, {
placement: 'bottom-end',
})
} 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
}
}, [listFormCode])
useEffect(() => {
setGridReady(false)
if (!gridDto) return
const { editingOptionDto, permissionDto } = gridDto.gridOptions
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),
})
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,
translate,
}),
)
setGridReady(true)
})
return () => {
isActive = false
}
// getBandedColumns/getLookupDataSource her render'da yeniden oluşabilir.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [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,
translate,
}),
)
},
// 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.
*/
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)
if (!gridDto || !formItems.length) {
previousFormDataRef.current = snapshotFormData(formData)
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)) {
const currentDataSource = item.editorOptions?.dataSource
// Kullanıcı tarafından elle verilmiş (statik dizi vb.) dataSource korunur.
const keepCustomDataSource =
currentDataSource !== undefined && typeof currentDataSource?.load !== 'function'
return {
...item,
editorOptions: {
...item.editorOptions,
dataSource: keepCustomDataSource
? currentDataSource
: getLookupDataSource(colData, formData || null),
valueExpr:
item.editorOptions?.valueExpr ?? colData?.lookupDto?.valueExpr?.toLowerCase(),
displayExpr:
item.editorOptions?.displayExpr ?? colData?.lookupDto?.displayExpr?.toLowerCase(),
},
}
}
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) })),
}
}
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
}, [formData, gridDto])
useEffect(() => {
if (!gridReady || mode === 'new') return
if (relationFilter) {
setFilter(relationFilter)
} else 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()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, JSON.stringify(relationFilter ?? null), gridReady])
useEffect(() => {
if (filter?.length) {
fetchData()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filter])
// Yetki kontrolü
useEffect(() => {
if (!permissionResults) return
const denied =
(mode === 'new' && !permissionResults.c) ||
(mode === 'edit' && !permissionResults.u) ||
(mode === 'view' && !permissionResults.r)
if (denied) {
navigate(getAccessDeniedPath(location.pathname), { replace: true, state: { from: location } })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [permissionResults])
return {
loading,
gridDto,
dataSource,
commandColumnData,
filter,
formItems,
formData,
refForm,
permissionResults,
fetchData,
setFormData,
handleSubmit,
fieldOrder,
previewFieldOrder,
saveFieldOrder,
canStoreFieldOrder: !!storageKey,
}
}
export { useGridData }