sozsoft-platform/ui/src/views/form/FormDevExpress.tsx
2026-06-08 11:46:21 +03:00

606 lines
21 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 { 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