606 lines
21 KiB
TypeScript
606 lines
21 KiB
TypeScript
import { DX_CLASSNAMES } from '@/constants/app.constant'
|
||
import { executeEditorScript } from '@/utils/editorScriptRuntime'
|
||
import {
|
||
Form as FormDx,
|
||
FormRef,
|
||
GroupItem as GroupItemDx,
|
||
SimpleItem as SimpleItemDx,
|
||
} from 'devextreme-react/form'
|
||
import { FieldDataChangedEvent, GroupItem } from 'devextreme/ui/form'
|
||
import { Dispatch, RefObject, useEffect, useRef, useState } from 'react'
|
||
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
|
||
import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
|
||
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
|
||
import { RowMode, SimpleItemWithColData } from './types'
|
||
import { PlatformEditorTypes } from '@/proxy/form/models'
|
||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||
|
||
const flattenFormItems = (items: any[] = []): SimpleItemWithColData[] =>
|
||
items.flatMap((item) => [
|
||
...(item?.dataField ? [item] : []),
|
||
...flattenFormItems(item?.items || []),
|
||
...(item?.tabs || []).flatMap((tab: any) => flattenFormItems(tab?.items || [])),
|
||
])
|
||
|
||
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()
|
||
return true
|
||
}
|
||
|
||
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 FormDevExpress = (props: {
|
||
listFormCode: string
|
||
isSubForm?: boolean
|
||
mode: RowMode
|
||
refForm: RefObject<FormRef>
|
||
formData: any
|
||
formItems: GroupItem[]
|
||
setFormData: Dispatch<any>
|
||
}) => {
|
||
const { listFormCode, isSubForm, mode, refForm, formData, formItems, setFormData } = props
|
||
const { translate } = useLocalization()
|
||
|
||
const formDataRef = useRef(formData)
|
||
const formItemsRef = useRef(formItems)
|
||
const formInstanceRef = useRef<any>()
|
||
const lastContentReadyScriptKeyRef = useRef<string>()
|
||
const didAutoFocusRef = useRef(false)
|
||
const [runtimeReadOnlyFields, setRuntimeReadOnlyFields] = useState<Record<string, boolean>>({})
|
||
const runtimeReadOnlyFieldsRef = useRef<Record<string, boolean>>({})
|
||
|
||
const isTouchLikeDevice = () =>
|
||
typeof window !== 'undefined' &&
|
||
(window.matchMedia?.('(pointer: coarse)').matches ||
|
||
window.matchMedia?.('(hover: none)').matches)
|
||
|
||
useEffect(() => {
|
||
formDataRef.current = formData
|
||
}, [formData])
|
||
|
||
useEffect(() => {
|
||
formItemsRef.current = formItems
|
||
}, [formItems])
|
||
|
||
useEffect(() => {
|
||
runtimeReadOnlyFieldsRef.current = runtimeReadOnlyFields
|
||
}, [runtimeReadOnlyFields])
|
||
|
||
useEffect(() => {
|
||
didAutoFocusRef.current = false
|
||
}, [listFormCode, mode])
|
||
|
||
const setRuntimeEditorReadOnly = (field: string, readOnly: boolean) => {
|
||
const resolvedField = findFormFieldKey(formItemsRef.current, field)
|
||
const key = String(resolvedField || field || '').toLowerCase()
|
||
if (!key || runtimeReadOnlyFieldsRef.current[key] === readOnly) {
|
||
return
|
||
}
|
||
|
||
runtimeReadOnlyFieldsRef.current = {
|
||
...runtimeReadOnlyFieldsRef.current,
|
||
[key]: readOnly,
|
||
}
|
||
setRuntimeReadOnlyFields(runtimeReadOnlyFieldsRef.current)
|
||
}
|
||
|
||
const getRuntimeEditorReadOnly = (formItem: SimpleItemWithColData) => {
|
||
const field = formItem.dataField || formItem.name
|
||
const resolvedField = findFormFieldKey(formItemsRef.current, field || '')
|
||
const key = String(resolvedField || field || '').toLowerCase()
|
||
return Object.prototype.hasOwnProperty.call(runtimeReadOnlyFields, key)
|
||
? runtimeReadOnlyFields[key]
|
||
: undefined
|
||
}
|
||
|
||
const applyEditorReadOnly = (form: any, field: string, readOnly: boolean) => {
|
||
setRuntimeEditorReadOnly(field, readOnly)
|
||
setFormEditorReadOnly(form, field, readOnly)
|
||
setTimeout(() => setFormEditorReadOnly(formInstanceRef.current ?? form, field, readOnly), 0)
|
||
}
|
||
|
||
const runEditorScript = (formItem: SimpleItemWithColData, eventValue: any, component?: any) => {
|
||
if (!formItem?.editorScript) {
|
||
return
|
||
}
|
||
|
||
const form = formInstanceRef.current ?? component
|
||
const dataField = formItem.dataField
|
||
const nextFormData = {
|
||
...(formDataRef.current || {}),
|
||
...(form?.option?.('formData') || {}),
|
||
...(dataField ? { [dataField]: eventValue } : {}),
|
||
}
|
||
formDataRef.current = nextFormData
|
||
|
||
try {
|
||
const editor = {
|
||
dataField,
|
||
component: form,
|
||
}
|
||
const formData = nextFormData
|
||
const e = {
|
||
component: form,
|
||
dataField,
|
||
value: eventValue,
|
||
}
|
||
const runtimeSetEditorReadOnly = (field: string, readOnly: boolean) =>
|
||
applyEditorReadOnly(form, field, readOnly)
|
||
|
||
executeEditorScript(formItem.editorScript, {
|
||
formData,
|
||
e,
|
||
editor,
|
||
runtimeSetEditorReadOnly,
|
||
})
|
||
} catch (err) {
|
||
console.error('Script execution failed for', formItem.name, err)
|
||
}
|
||
}
|
||
|
||
const getEditorOptions = (formItem: SimpleItemWithColData, index?: number) => {
|
||
const runtimeReadOnly = getRuntimeEditorReadOnly(formItem)
|
||
const prevOnValueChanged = formItem.editorOptions?.onValueChanged
|
||
|
||
return {
|
||
...(index !== undefined && mode !== 'view' && !isTouchLikeDevice()
|
||
? { autoFocus: index === 1 }
|
||
: {}),
|
||
...(formItem.editorType === 'dxDateBox'
|
||
? {
|
||
useMaskBehavior: true,
|
||
openOnFieldClick: true,
|
||
showClearButton: true,
|
||
}
|
||
: {}),
|
||
...(formItem.colData?.placeHolder
|
||
? { placeholder: translate('::' + formItem.colData.placeHolder) }
|
||
: {}),
|
||
...formItem.editorOptions,
|
||
...(runtimeReadOnly !== undefined ? { readOnly: runtimeReadOnly } : {}),
|
||
...(mode === 'view' ? { readOnly: true } : {}),
|
||
...(formItem.editorScript
|
||
? {
|
||
onValueChanged: (e: any) => {
|
||
if (typeof prevOnValueChanged === 'function') {
|
||
prevOnValueChanged(e)
|
||
}
|
||
if (formItem.dataField) {
|
||
const nextFormData = {
|
||
...(formDataRef.current || {}),
|
||
...(formInstanceRef.current?.option?.('formData') || {}),
|
||
[formItem.dataField]: e?.value,
|
||
}
|
||
formDataRef.current = nextFormData
|
||
formInstanceRef.current?.option?.('formData', nextFormData)
|
||
setFormData(nextFormData)
|
||
}
|
||
runEditorScript(formItem, e?.value, formInstanceRef.current)
|
||
},
|
||
}
|
||
: {}),
|
||
buttons: (formItem.editorOptions?.buttons || []).map((btn: any) => {
|
||
if (btn?.options?.onClick && typeof btn.options.onClick === 'string') {
|
||
const origClick = eval(`(${btn.options.onClick})`)
|
||
btn.options.onClick = (e: any) => {
|
||
origClick({
|
||
...e,
|
||
formData: formDataRef.current,
|
||
fieldName: formItem.dataField,
|
||
mode,
|
||
})
|
||
}
|
||
}
|
||
return btn
|
||
}),
|
||
}
|
||
}
|
||
|
||
const getFormItemKey = (formItem: SimpleItemWithColData, index: number) => {
|
||
const runtimeReadOnly = getRuntimeEditorReadOnly(formItem)
|
||
return `formItem-${formItem.dataField || formItem.name || index}-${String(runtimeReadOnly)}`
|
||
}
|
||
|
||
// formItems değiştiğinde (özellikle cascading alanlar için) editörlerin dataSource'larını güncelle
|
||
useEffect(() => {
|
||
if (!refForm.current?.instance()) return
|
||
|
||
const allItems = formItems.flatMap((group) => flattenFormItems([group]))
|
||
|
||
allItems.forEach((item) => {
|
||
if (item.colData?.lookupDto?.dataSourceType && item.editorOptions?.dataSource) {
|
||
try {
|
||
const editor = refForm.current?.instance().getEditor(item.dataField!)
|
||
if (editor) {
|
||
editor.option('dataSource', item.editorOptions.dataSource)
|
||
}
|
||
} catch (err) {
|
||
// Editor henüz oluşmamış olabilir, sessizce devam et
|
||
console.debug('Editor update skipped for', item.dataField, err)
|
||
}
|
||
}
|
||
})
|
||
}, [formItems])
|
||
|
||
// Cascade fieldlerin disabled durumunu güncelle
|
||
const updateCascadeDisabledStates = () => {
|
||
if (!refForm.current?.instance()) return
|
||
|
||
const allItems = formItemsRef.current.flatMap((group) => flattenFormItems([group]))
|
||
|
||
allItems.forEach((item) => {
|
||
const cascadeParentFields = item.colData?.lookupDto?.cascadeParentFields
|
||
if (cascadeParentFields) {
|
||
const parentFields = cascadeParentFields.split(',').map((f: string) => f.trim())
|
||
|
||
try {
|
||
const editor = refForm.current?.instance().getEditor(item.dataField!)
|
||
if (editor && mode !== 'view') {
|
||
if (item.editorOptions?.disabled === true) {
|
||
editor.option('disabled', true)
|
||
return
|
||
}
|
||
|
||
// Parent fieldlerden en az biri boşsa disabled olmalı
|
||
const shouldDisable = parentFields.some((parentField: string) => {
|
||
return !formDataRef.current || !formDataRef.current[parentField]
|
||
})
|
||
|
||
editor.option('disabled', shouldDisable)
|
||
}
|
||
} catch (err) {
|
||
console.debug('Cascade disabled update skipped for', item.dataField, err)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// formData değiştiğinde cascade disabled durumlarını güncelle
|
||
useEffect(() => {
|
||
updateCascadeDisabledStates()
|
||
}, [formData, mode])
|
||
|
||
const runReadOnlyScripts = (form: any) => {
|
||
if (!form) return
|
||
|
||
const currentFormData = {
|
||
...(formDataRef.current || {}),
|
||
...(form?.option?.('formData') || {}),
|
||
}
|
||
formDataRef.current = currentFormData
|
||
|
||
formItemsRef.current
|
||
.flatMap((group) => flattenFormItems([group]))
|
||
.filter((formItem) => shouldRunEditorScriptOnContentReady(formItem.editorScript))
|
||
.forEach((formItem) => {
|
||
try {
|
||
const editor = {
|
||
dataField: formItem.dataField,
|
||
component: form,
|
||
}
|
||
const formData = currentFormData
|
||
const e = {
|
||
component: form,
|
||
dataField: formItem.dataField,
|
||
value: getValueByField(currentFormData, formItem.dataField),
|
||
}
|
||
const runtimeSetEditorReadOnly = (field: string, readOnly: boolean) =>
|
||
applyEditorReadOnly(form, field, readOnly)
|
||
|
||
executeEditorScript(formItem.editorScript!, {
|
||
formData,
|
||
e,
|
||
editor,
|
||
runtimeSetEditorReadOnly,
|
||
})
|
||
} catch (err) {
|
||
console.error('Script execution failed on contentReady for', formItem.name, err)
|
||
}
|
||
})
|
||
}
|
||
|
||
useEffect(() => {
|
||
const form = formInstanceRef.current
|
||
if (!form || mode === 'view' || !formData || !formItems?.length) {
|
||
return
|
||
}
|
||
|
||
const scriptFields = formItemsRef.current
|
||
.flatMap((group) => flattenFormItems([group]))
|
||
.filter((formItem) => shouldRunEditorScriptOnContentReady(formItem.editorScript))
|
||
.map((formItem) => formItem.dataField)
|
||
.join('|')
|
||
|
||
if (!scriptFields) {
|
||
return
|
||
}
|
||
|
||
const scriptKey = `${mode}|${scriptFields}|${JSON.stringify(formData)}`
|
||
if (lastContentReadyScriptKeyRef.current === scriptKey) {
|
||
return
|
||
}
|
||
|
||
lastContentReadyScriptKeyRef.current = scriptKey
|
||
setTimeout(() => runReadOnlyScripts(form), 0)
|
||
}, [formData, formItems, mode])
|
||
|
||
return (
|
||
<FormDx
|
||
ref={refForm}
|
||
className={`${DX_CLASSNAMES} ${!isSubForm ? 'px-2' : ''} pb-2`}
|
||
formData={formData}
|
||
onFieldDataChanged={async (e: FieldDataChangedEvent) => {
|
||
if (!e.dataField) {
|
||
return
|
||
}
|
||
|
||
const newFormData = { ...formData, [e.dataField]: e.value }
|
||
let hasChanges = !Object.is(formData?.[e.dataField], e.value)
|
||
|
||
// Cascading child field'leri temizle (parent field değiştiğinde)
|
||
const allItems = formItemsRef.current.flatMap((group) => flattenFormItems([group]))
|
||
const cascadingChildren = allItems.filter((item) => {
|
||
const parentFields = item.colData?.lookupDto?.cascadeParentFields?.split(',') || []
|
||
return parentFields.some((field) => field.trim() === e.dataField)
|
||
})
|
||
|
||
// Parent field değiştiğinde child field'leri temizle
|
||
cascadingChildren.forEach((child) => {
|
||
if (!Object.is(newFormData[child.dataField!], null)) {
|
||
newFormData[child.dataField!] = null
|
||
hasChanges = true
|
||
}
|
||
})
|
||
|
||
if (hasChanges) {
|
||
formDataRef.current = newFormData
|
||
setFormData(newFormData)
|
||
}
|
||
|
||
// Cascade disabled durumlarını güncelle (setTimeout ile editor güncellemesinden sonra çalışsın)
|
||
setTimeout(() => {
|
||
updateCascadeDisabledStates()
|
||
}, 0)
|
||
}}
|
||
onContentReady={(e) => {
|
||
formInstanceRef.current = e.component
|
||
|
||
const form = e.component
|
||
|
||
runReadOnlyScripts(form)
|
||
|
||
const groupItems = e.component.option('items') as any[]
|
||
const firstItem = groupItems?.[0]?.items?.[0]
|
||
|
||
if (!didAutoFocusRef.current && firstItem?.dataField && !isTouchLikeDevice()) {
|
||
didAutoFocusRef.current = true
|
||
const editor = e.component.getEditor(firstItem.dataField)
|
||
mode !== 'view' && editor?.focus()
|
||
}
|
||
}}
|
||
>
|
||
{formItems.map((formGroupItem, i) => {
|
||
return (
|
||
<GroupItemDx
|
||
key={'formGroupItem-' + i}
|
||
colCount={formGroupItem.colCount}
|
||
colSpan={formGroupItem.colSpan}
|
||
caption={formGroupItem.caption}
|
||
>
|
||
{(formGroupItem.items as SimpleItemWithColData[])
|
||
?.filter((formItem) => {
|
||
if (mode === 'edit') return formItem.allowEditing !== false
|
||
if (mode === 'new') return formItem.allowAdding !== false
|
||
return true
|
||
})
|
||
.map((formItem, i) => {
|
||
return formItem.editorType2 === PlatformEditorTypes.dxTagBox ? (
|
||
<SimpleItemDx
|
||
cssClass="font-semibold"
|
||
key={getFormItemKey(formItem, i)}
|
||
{...formItem}
|
||
render={() => (
|
||
<TagBoxEditorComponent
|
||
value={formData[formItem.dataField!] || []}
|
||
setDefaultValue={false}
|
||
values={formData}
|
||
options={formItem.tagBoxOptions}
|
||
col={formItem.colData}
|
||
onValueChanged={(e: any) => {
|
||
const newData = { ...formDataRef.current, [formItem.dataField!]: e }
|
||
formDataRef.current = newData
|
||
setFormData(newData)
|
||
runEditorScript(formItem, e, formInstanceRef.current)
|
||
}}
|
||
editorOptions={getEditorOptions(formItem)}
|
||
></TagBoxEditorComponent>
|
||
)}
|
||
label={{
|
||
text: translate('::' + formItem.colData?.captionName),
|
||
className: 'font-semibold',
|
||
}}
|
||
></SimpleItemDx>
|
||
) : formItem.editorType2 === PlatformEditorTypes.dxGridBox ? (
|
||
<SimpleItemDx
|
||
cssClass="font-semibold"
|
||
key={getFormItemKey(formItem, i)}
|
||
{...formItem}
|
||
render={() => (
|
||
<GridBoxEditorComponent
|
||
value={formData[formItem.dataField!] || []}
|
||
values={formData}
|
||
options={formItem.gridBoxOptions}
|
||
col={formItem.colData}
|
||
onValueChanged={(e: any) => {
|
||
const newData = { ...formDataRef.current, [formItem.dataField!]: e }
|
||
formDataRef.current = newData
|
||
setFormData(newData)
|
||
runEditorScript(formItem, e, formInstanceRef.current)
|
||
}}
|
||
editorOptions={getEditorOptions(formItem)}
|
||
></GridBoxEditorComponent>
|
||
)}
|
||
label={{
|
||
text: translate('::' + formItem.colData?.captionName),
|
||
className: 'font-semibold',
|
||
}}
|
||
></SimpleItemDx>
|
||
) : formItem.editorType2 === PlatformEditorTypes.dxImageUpload ? (
|
||
<SimpleItemDx
|
||
cssClass="font-semibold"
|
||
key={getFormItemKey(formItem, i)}
|
||
dataField={formItem.dataField}
|
||
name={formItem.name}
|
||
colSpan={formItem.colSpan}
|
||
isRequired={formItem.isRequired}
|
||
render={() => (
|
||
<ImageUploadEditorComponent
|
||
value={formData[formItem.dataField!]}
|
||
options={formItem.imageUploadOptions}
|
||
onValueChanged={(val: any) => {
|
||
const newData = { ...formDataRef.current, [formItem.dataField!]: val }
|
||
formDataRef.current = newData
|
||
setFormData(newData)
|
||
runEditorScript(formItem, val, formInstanceRef.current)
|
||
}}
|
||
editorOptions={getEditorOptions(formItem)}
|
||
/>
|
||
)}
|
||
label={{
|
||
text: translate('::' + formItem.colData?.captionName),
|
||
className: 'font-semibold',
|
||
}}
|
||
></SimpleItemDx>
|
||
) : (
|
||
<SimpleItemDx
|
||
cssClass="font-semibold"
|
||
key={getFormItemKey(formItem, i)}
|
||
{...formItem}
|
||
editorOptions={getEditorOptions(formItem, i)}
|
||
label={{ text: translate('::' + formItem.colData?.captionName) }}
|
||
/>
|
||
)
|
||
})}
|
||
</GroupItemDx>
|
||
)
|
||
})}
|
||
</FormDx>
|
||
)
|
||
}
|
||
|
||
export default FormDevExpress
|