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 = {}, 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 formData: any formItems: GroupItem[] setFormData: Dispatch }) => { const { listFormCode, isSubForm, mode, refForm, formData, formItems, setFormData } = props const { translate } = useLocalization() const formDataRef = useRef(formData) const formItemsRef = useRef(formItems) const formInstanceRef = useRef() const lastContentReadyScriptKeyRef = useRef() const didAutoFocusRef = useRef(false) const [runtimeReadOnlyFields, setRuntimeReadOnlyFields] = useState>({}) const runtimeReadOnlyFieldsRef = useRef>({}) 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 ( { 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 ( {(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 ? ( ( { const newData = { ...formDataRef.current, [formItem.dataField!]: e } formDataRef.current = newData setFormData(newData) runEditorScript(formItem, e, formInstanceRef.current) }} editorOptions={getEditorOptions(formItem)} > )} label={{ text: translate('::' + formItem.colData?.captionName), className: 'font-semibold', }} > ) : formItem.editorType2 === PlatformEditorTypes.dxGridBox ? ( ( { const newData = { ...formDataRef.current, [formItem.dataField!]: e } formDataRef.current = newData setFormData(newData) runEditorScript(formItem, e, formInstanceRef.current) }} editorOptions={getEditorOptions(formItem)} > )} label={{ text: translate('::' + formItem.colData?.captionName), className: 'font-semibold', }} > ) : formItem.editorType2 === PlatformEditorTypes.dxImageUpload ? ( ( { 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', }} > ) : ( ) })} ) })} ) } export default FormDevExpress