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

468 lines
17 KiB
TypeScript
Raw Normal View History

2026-02-24 20:44:16 +00:00
import { DX_CLASSNAMES } from '@/constants/app.constant'
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 } from 'react'
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>()
2026-02-24 20:44:16 +00:00
useEffect(() => {
formDataRef.current = formData
}, [formData])
useEffect(() => {
formItemsRef.current = formItems
}, [formItems])
// 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])
return (
<FormDx
ref={refForm}
className={`${DX_CLASSNAMES} ${!isSubForm ? 'px-2' : ''} pb-2`}
formData={formData}
onFieldDataChanged={async (e: FieldDataChangedEvent) => {
const newFormData = { ...formData, [e.dataField!]: e.value }
// 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) => {
newFormData[child.dataField!] = null
})
setFormData(newFormData)
// Cascade disabled durumlarını güncelle (setTimeout ile editor güncellemesinden sonra çalışsın)
setTimeout(() => {
updateCascadeDisabledStates()
}, 0)
//Dinamik script
const changeItem = formItemsRef.current
2026-06-01 13:47:38 +00:00
.flatMap((group) => flattenFormItems([group]))
.find(
(i: SimpleItemWithColData) =>
String(i.dataField || '').toLowerCase() === String(e.dataField || '').toLowerCase(),
)
2026-02-24 20:44:16 +00:00
if (changeItem?.editorScript) {
try {
2026-06-01 13:47:38 +00:00
const form = e.component
const editor = {
dataField: changeItem.dataField,
component: form,
}
const formData = newFormData
formDataRef.current = newFormData
const runtimeSetEditorReadOnly = (field: string, readOnly: boolean) =>
setFormEditorReadOnly(formInstanceRef.current ?? form, field, readOnly)
2026-02-24 20:44:16 +00:00
//setFormData({...formData, Path: e.value});
//UiEvalService.ApiGenerateBackgroundWorkers();
//setFormData({ ...formData, Path: (v => v === '1' ? '1-deneme' : v === '0' ? '0-deneme' : '')(e.value) })
eval(changeItem.editorScript)
} catch (err) {
console.error('Script execution failed for', changeItem.name, err)
}
}
2026-06-01 13:47:38 +00:00
2026-02-24 20:44:16 +00:00
}}
onContentReady={(e) => {
2026-06-01 13:47:38 +00:00
formInstanceRef.current = e.component
const form = e.component
const runtimeSetEditorReadOnly = (field: string, readOnly: boolean) =>
setFormEditorReadOnly(form, field, readOnly)
const runReadOnlyScripts = () => {
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),
}
eval(formItem.editorScript!)
} catch (err) {
console.error('Script execution failed on contentReady for', formItem.name, err)
}
})
}
runReadOnlyScripts()
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={'formItem-' + i}
{...formItem}
render={() => (
<TagBoxEditorComponent
value={formData[formItem.dataField!] || []}
setDefaultValue={false}
values={formData}
options={formItem.tagBoxOptions}
col={formItem.colData}
onValueChanged={(e: any) => {
setFormData({ ...formData, [formItem.dataField!]: e })
}}
editorOptions={{
...formItem.editorOptions,
...(mode === 'view' ? { readOnly: true } : {}),
}}
></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={'formItem-' + i}
{...formItem}
render={() => (
<GridBoxEditorComponent
value={formData[formItem.dataField!] || []}
values={formData}
options={formItem.gridBoxOptions}
col={formItem.colData}
onValueChanged={(e: any) => {
setFormData({ ...formData, [formItem.dataField!]: e })
}}
editorOptions={{
...formItem.editorOptions,
...(mode === 'view' ? { readOnly: true } : {}),
}}
></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={'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) => {
setFormData({ ...formData, [formItem.dataField!]: val })
}}
editorOptions={{
...formItem.editorOptions,
...(mode === 'view' ? { readOnly: true } : {}),
}}
/>
)}
label={{
text: translate('::' + formItem.colData?.captionName),
className: 'font-semibold',
}}
></SimpleItemDx>
2026-02-24 20:44:16 +00:00
) : (
<SimpleItemDx
cssClass="font-semibold"
key={'formItem-' + i}
{...formItem}
editorOptions={{
2026-05-31 18:16:41 +00:00
...(mode === 'view' ? {} : { autoFocus: i === 1 }),
2026-02-24 20:44:16 +00:00
...(formItem.editorType === 'dxDateBox'
? {
useMaskBehavior: true,
openOnFieldClick: true,
showClearButton: true,
}
: {}),
...(formItem.colData?.placeHolder
? { placeholder: translate('::' + formItem.colData.placeHolder) }
: {}),
2026-05-31 18:16:41 +00:00
...formItem.editorOptions,
...(mode === 'view' ? { readOnly: true } : {}),
2026-02-24 20:44:16 +00:00
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
}),
}}
label={{ text: translate('::' + formItem.colData?.captionName) }}
/>
)
})}
</GroupItemDx>
)
})}
</FormDx>
)
}
export default FormDevExpress