422 lines
14 KiB
TypeScript
422 lines
14 KiB
TypeScript
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 }
|