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

595 lines
20 KiB
TypeScript
Raw Normal View History

2026-02-24 20:44:16 +00:00
import { DX_CLASSNAMES } from '@/constants/app.constant'
import { executeEditorScript } from '@/utils/editorScriptRuntime'
2026-02-24 20:44:16 +00:00
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'
2026-02-24 20:44:16 +00:00
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
2026-02-24 20:44:16 +00:00
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
import { RowMode, SimpleItemWithColData } from './types'
import { PlatformEditorTypes } from '@/proxy/form/models'
import { useLocalization } from '@/utils/hooks/useLocalization'
2026-06-01 13:47:38 +00:00
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')))
2026-02-24 20:44:16 +00:00
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)
2026-06-01 13:47:38 +00:00
const formInstanceRef = useRef<any>()
const lastContentReadyScriptKeyRef = useRef<string>()
const [runtimeReadOnlyFields, setRuntimeReadOnlyFields] = useState<Record<string, boolean>>({})
const runtimeReadOnlyFieldsRef = useRef<Record<string, boolean>>({})
2026-02-24 20:44:16 +00:00
useEffect(() => {
formDataRef.current = formData
}, [formData])
useEffect(() => {
formItemsRef.current = formItems
}, [formItems])
useEffect(() => {
runtimeReadOnlyFieldsRef.current = runtimeReadOnlyFields
}, [runtimeReadOnlyFields])
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' ? { 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)}`
}
2026-02-24 20:44:16 +00:00
// formItems değiştiğinde (özellikle cascading alanlar için) editörlerin dataSource'larını güncelle
useEffect(() => {
if (!refForm.current?.instance()) return
2026-06-01 13:47:38 +00:00
const allItems = formItems.flatMap((group) => flattenFormItems([group]))
2026-02-24 20:44:16 +00:00
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
2026-06-01 13:47:38 +00:00
const allItems = formItemsRef.current.flatMap((group) => flattenFormItems([group]))
2026-02-24 20:44:16 +00:00
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') {
2026-05-31 18:16:41 +00:00
if (item.editorOptions?.disabled === true) {
editor.option('disabled', true)
return
}
2026-02-24 20:44:16 +00:00
// 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])
2026-02-24 20:44:16 +00:00
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)
2026-02-24 20:44:16 +00:00
// Cascading child field'leri temizle (parent field değiştiğinde)
2026-06-01 13:47:38 +00:00
const allItems = formItemsRef.current.flatMap((group) => flattenFormItems([group]))
2026-02-24 20:44:16 +00:00
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
}
2026-02-24 20:44:16 +00:00
})
if (hasChanges) {
formDataRef.current = newFormData
setFormData(newFormData)
}
2026-02-24 20:44:16 +00:00
// Cascade disabled durumlarını güncelle (setTimeout ile editor güncellemesinden sonra çalışsın)
setTimeout(() => {
updateCascadeDisabledStates()
}, 0)
}}
onContentReady={(e) => {
2026-06-01 13:47:38 +00:00
formInstanceRef.current = e.component
const form = e.component
runReadOnlyScripts(form)
2026-06-01 13:47:38 +00:00
2026-02-24 20:44:16 +00:00
const groupItems = e.component.option('items') as any[]
const firstItem = groupItems?.[0]?.items?.[0]
if (firstItem?.dataField) {
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) => {
2026-02-24 20:44:16 +00:00
return formItem.editorType2 === PlatformEditorTypes.dxTagBox ? (
<SimpleItemDx
cssClass="font-semibold"
key={getFormItemKey(formItem, i)}
2026-02-24 20:44:16 +00:00
{...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)
2026-02-24 20:44:16 +00:00
}}
editorOptions={getEditorOptions(formItem)}
2026-02-24 20:44:16 +00:00
></TagBoxEditorComponent>
)}
label={{
text: translate('::' + formItem.colData?.captionName),
className: 'font-semibold',
}}
2026-02-24 20:44:16 +00:00
></SimpleItemDx>
) : formItem.editorType2 === PlatformEditorTypes.dxGridBox ? (
<SimpleItemDx
cssClass="font-semibold"
key={getFormItemKey(formItem, i)}
2026-02-24 20:44:16 +00:00
{...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)
2026-02-24 20:44:16 +00:00
}}
editorOptions={getEditorOptions(formItem)}
2026-02-24 20:44:16 +00:00
></GridBoxEditorComponent>
)}
label={{
text: translate('::' + formItem.colData?.captionName),
className: 'font-semibold',
}}
2026-02-24 20:44:16 +00:00
></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>
2026-02-24 20:44:16 +00:00
) : (
<SimpleItemDx
cssClass="font-semibold"
key={getFormItemKey(formItem, i)}
2026-02-24 20:44:16 +00:00
{...formItem}
editorOptions={getEditorOptions(formItem, i)}
2026-02-24 20:44:16 +00:00
label={{ text: translate('::' + formItem.colData?.captionName) }}
/>
)
})}
</GroupItemDx>
)
})}
</FormDx>
)
}
export default FormDevExpress