Mobil Grid ve Tree Popup EditForm problemi

This commit is contained in:
Sedat ÖZTÜRK 2026-06-08 11:57:13 +03:00
parent 48894d2bda
commit 88c838aac8
4 changed files with 437 additions and 216 deletions

View file

@ -140,7 +140,9 @@ const getValueByField = (data: Record<string, any> = {}, field?: string) => {
} }
const shouldRunEditorScriptOnContentReady = (script?: string) => const shouldRunEditorScriptOnContentReady = (script?: string) =>
Boolean(script && (script.includes('setEditorReadOnly') || script.includes('runtimeSetEditorReadOnly'))) Boolean(
script && (script.includes('setEditorReadOnly') || script.includes('runtimeSetEditorReadOnly')),
)
const FormDevExpress = (props: { const FormDevExpress = (props: {
listFormCode: string listFormCode: string
@ -158,9 +160,15 @@ const FormDevExpress = (props: {
const formItemsRef = useRef(formItems) const formItemsRef = useRef(formItems)
const formInstanceRef = useRef<any>() const formInstanceRef = useRef<any>()
const lastContentReadyScriptKeyRef = useRef<string>() const lastContentReadyScriptKeyRef = useRef<string>()
const didAutoFocusRef = useRef(false)
const [runtimeReadOnlyFields, setRuntimeReadOnlyFields] = useState<Record<string, boolean>>({}) const [runtimeReadOnlyFields, setRuntimeReadOnlyFields] = useState<Record<string, boolean>>({})
const runtimeReadOnlyFieldsRef = useRef<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(() => { useEffect(() => {
formDataRef.current = formData formDataRef.current = formData
}, [formData]) }, [formData])
@ -173,6 +181,10 @@ const FormDevExpress = (props: {
runtimeReadOnlyFieldsRef.current = runtimeReadOnlyFields runtimeReadOnlyFieldsRef.current = runtimeReadOnlyFields
}, [runtimeReadOnlyFields]) }, [runtimeReadOnlyFields])
useEffect(() => {
didAutoFocusRef.current = false
}, [listFormCode, mode])
const setRuntimeEditorReadOnly = (field: string, readOnly: boolean) => { const setRuntimeEditorReadOnly = (field: string, readOnly: boolean) => {
const resolvedField = findFormFieldKey(formItemsRef.current, field) const resolvedField = findFormFieldKey(formItemsRef.current, field)
const key = String(resolvedField || field || '').toLowerCase() const key = String(resolvedField || field || '').toLowerCase()
@ -202,11 +214,7 @@ const FormDevExpress = (props: {
setTimeout(() => setFormEditorReadOnly(formInstanceRef.current ?? form, field, readOnly), 0) setTimeout(() => setFormEditorReadOnly(formInstanceRef.current ?? form, field, readOnly), 0)
} }
const runEditorScript = ( const runEditorScript = (formItem: SimpleItemWithColData, eventValue: any, component?: any) => {
formItem: SimpleItemWithColData,
eventValue: any,
component?: any,
) => {
if (!formItem?.editorScript) { if (!formItem?.editorScript) {
return return
} }
@ -250,7 +258,9 @@ const FormDevExpress = (props: {
const prevOnValueChanged = formItem.editorOptions?.onValueChanged const prevOnValueChanged = formItem.editorOptions?.onValueChanged
return { return {
...(index !== undefined && mode !== 'view' ? { autoFocus: index === 1 } : {}), ...(index !== undefined && mode !== 'view' && !isTouchLikeDevice()
? { autoFocus: index === 1 }
: {}),
...(formItem.editorType === 'dxDateBox' ...(formItem.editorType === 'dxDateBox'
? { ? {
useMaskBehavior: true, useMaskBehavior: true,
@ -466,7 +476,6 @@ const FormDevExpress = (props: {
setTimeout(() => { setTimeout(() => {
updateCascadeDisabledStates() updateCascadeDisabledStates()
}, 0) }, 0)
}} }}
onContentReady={(e) => { onContentReady={(e) => {
formInstanceRef.current = e.component formInstanceRef.current = e.component
@ -478,7 +487,8 @@ const FormDevExpress = (props: {
const groupItems = e.component.option('items') as any[] const groupItems = e.component.option('items') as any[]
const firstItem = groupItems?.[0]?.items?.[0] const firstItem = groupItems?.[0]?.items?.[0]
if (firstItem?.dataField) { if (!didAutoFocusRef.current && firstItem?.dataField && !isTouchLikeDevice()) {
didAutoFocusRef.current = true
const editor = e.component.getEditor(firstItem.dataField) const editor = e.component.getEditor(firstItem.dataField)
mode !== 'view' && editor?.focus() mode !== 'view' && editor?.focus()
} }
@ -492,98 +502,100 @@ const FormDevExpress = (props: {
colSpan={formGroupItem.colSpan} colSpan={formGroupItem.colSpan}
caption={formGroupItem.caption} caption={formGroupItem.caption}
> >
{(formGroupItem.items as SimpleItemWithColData[])?.filter((formItem) => { {(formGroupItem.items as SimpleItemWithColData[])
if (mode === 'edit') return formItem.allowEditing !== false ?.filter((formItem) => {
if (mode === 'new') return formItem.allowAdding !== false if (mode === 'edit') return formItem.allowEditing !== false
return true if (mode === 'new') return formItem.allowAdding !== false
}).map((formItem, i) => { return true
return formItem.editorType2 === PlatformEditorTypes.dxTagBox ? ( })
<SimpleItemDx .map((formItem, i) => {
cssClass="font-semibold" return formItem.editorType2 === PlatformEditorTypes.dxTagBox ? (
key={getFormItemKey(formItem, i)} <SimpleItemDx
{...formItem} cssClass="font-semibold"
render={() => ( key={getFormItemKey(formItem, i)}
<TagBoxEditorComponent {...formItem}
value={formData[formItem.dataField!] || []} render={() => (
setDefaultValue={false} <TagBoxEditorComponent
values={formData} value={formData[formItem.dataField!] || []}
options={formItem.tagBoxOptions} setDefaultValue={false}
col={formItem.colData} values={formData}
onValueChanged={(e: any) => { options={formItem.tagBoxOptions}
const newData = { ...formDataRef.current, [formItem.dataField!]: e } col={formItem.colData}
formDataRef.current = newData onValueChanged={(e: any) => {
setFormData(newData) const newData = { ...formDataRef.current, [formItem.dataField!]: e }
runEditorScript(formItem, e, formInstanceRef.current) formDataRef.current = newData
}} setFormData(newData)
editorOptions={getEditorOptions(formItem)} runEditorScript(formItem, e, formInstanceRef.current)
></TagBoxEditorComponent> }}
)} editorOptions={getEditorOptions(formItem)}
label={{ ></TagBoxEditorComponent>
text: translate('::' + formItem.colData?.captionName), )}
className: 'font-semibold', label={{
}} text: translate('::' + formItem.colData?.captionName),
></SimpleItemDx> className: 'font-semibold',
) : formItem.editorType2 === PlatformEditorTypes.dxGridBox ? ( }}
<SimpleItemDx ></SimpleItemDx>
cssClass="font-semibold" ) : formItem.editorType2 === PlatformEditorTypes.dxGridBox ? (
key={getFormItemKey(formItem, i)} <SimpleItemDx
{...formItem} cssClass="font-semibold"
render={() => ( key={getFormItemKey(formItem, i)}
<GridBoxEditorComponent {...formItem}
value={formData[formItem.dataField!] || []} render={() => (
values={formData} <GridBoxEditorComponent
options={formItem.gridBoxOptions} value={formData[formItem.dataField!] || []}
col={formItem.colData} values={formData}
onValueChanged={(e: any) => { options={formItem.gridBoxOptions}
const newData = { ...formDataRef.current, [formItem.dataField!]: e } col={formItem.colData}
formDataRef.current = newData onValueChanged={(e: any) => {
setFormData(newData) const newData = { ...formDataRef.current, [formItem.dataField!]: e }
runEditorScript(formItem, e, formInstanceRef.current) formDataRef.current = newData
}} setFormData(newData)
editorOptions={getEditorOptions(formItem)} runEditorScript(formItem, e, formInstanceRef.current)
></GridBoxEditorComponent> }}
)} editorOptions={getEditorOptions(formItem)}
label={{ ></GridBoxEditorComponent>
text: translate('::' + formItem.colData?.captionName), )}
className: 'font-semibold', label={{
}} text: translate('::' + formItem.colData?.captionName),
></SimpleItemDx> className: 'font-semibold',
) : formItem.editorType2 === PlatformEditorTypes.dxImageUpload ? ( }}
<SimpleItemDx ></SimpleItemDx>
cssClass="font-semibold" ) : formItem.editorType2 === PlatformEditorTypes.dxImageUpload ? (
key={getFormItemKey(formItem, i)} <SimpleItemDx
dataField={formItem.dataField} cssClass="font-semibold"
name={formItem.name} key={getFormItemKey(formItem, i)}
colSpan={formItem.colSpan} dataField={formItem.dataField}
isRequired={formItem.isRequired} name={formItem.name}
render={() => ( colSpan={formItem.colSpan}
<ImageUploadEditorComponent isRequired={formItem.isRequired}
value={formData[formItem.dataField!]} render={() => (
options={formItem.imageUploadOptions} <ImageUploadEditorComponent
onValueChanged={(val: any) => { value={formData[formItem.dataField!]}
const newData = { ...formDataRef.current, [formItem.dataField!]: val } options={formItem.imageUploadOptions}
formDataRef.current = newData onValueChanged={(val: any) => {
setFormData(newData) const newData = { ...formDataRef.current, [formItem.dataField!]: val }
runEditorScript(formItem, val, formInstanceRef.current) formDataRef.current = newData
}} setFormData(newData)
editorOptions={getEditorOptions(formItem)} runEditorScript(formItem, val, formInstanceRef.current)
/> }}
)} editorOptions={getEditorOptions(formItem)}
label={{ />
text: translate('::' + formItem.colData?.captionName), )}
className: 'font-semibold', label={{
}} text: translate('::' + formItem.colData?.captionName),
></SimpleItemDx> className: 'font-semibold',
) : ( }}
<SimpleItemDx ></SimpleItemDx>
cssClass="font-semibold" ) : (
key={getFormItemKey(formItem, i)} <SimpleItemDx
{...formItem} cssClass="font-semibold"
editorOptions={getEditorOptions(formItem, i)} key={getFormItemKey(formItem, i)}
label={{ text: translate('::' + formItem.colData?.captionName) }} {...formItem}
/> editorOptions={getEditorOptions(formItem, i)}
) label={{ text: translate('::' + formItem.colData?.captionName) }}
})} />
)
})}
</GroupItemDx> </GroupItemDx>
) )
})} })}

View file

@ -19,6 +19,13 @@ import { layoutTypes } from '../admin/listForm/edit/types'
import { useListFormCustomDataSource } from '../list/useListFormCustomDataSource' import { useListFormCustomDataSource } from '../list/useListFormCustomDataSource'
import { useListFormColumns } from '../list/useListFormColumns' import { useListFormColumns } from '../list/useListFormColumns'
const flattenFormItems = (items: any[] = []): SimpleItemWithColData[] =>
items.flatMap((item) => [
...(item?.dataField ? [item] : []),
...flattenFormItems(item?.items || []),
...(item?.tabs || []).flatMap((tab: any) => flattenFormItems(tab?.items || [])),
])
const useGridData = (props: { const useGridData = (props: {
mode: RowMode mode: RowMode
listFormCode: string listFormCode: string
@ -41,6 +48,7 @@ const useGridData = (props: {
const [permissionResults, setPermissionResults] = useState<PermissionResults>() const [permissionResults, setPermissionResults] = useState<PermissionResults>()
const refForm = useRef<FormRef>(null) const refForm = useRef<FormRef>(null)
const previousFormDataRef = useRef<any>()
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
const navigate = useNavigate() const navigate = useNavigate()
const { translate } = useLocalization() const { translate } = useLocalization()
@ -306,18 +314,36 @@ const useGridData = (props: {
setGridReady(true) setGridReady(true)
}, [gridDto]) }, [gridDto])
// formData değiştiğinde sadece lookup datasource'ları güncelle // formData değiştiğinde sadece etkilenen cascading lookup datasource'ları güncelle
useEffect(() => { useEffect(() => {
if (!gridDto || !formItems.length) { if (!gridDto || !formItems.length) {
previousFormDataRef.current = formData
return return
} }
// View mode'da formData olsa da olmasa da cascading alanlar için dataSource oluşturulmalı const previousFormData = previousFormDataRef.current
const updatedItems = formItems.map((groupItem) => ({ const changedFields = previousFormData
...groupItem, ? Object.keys({ ...(previousFormData || {}), ...(formData || {}) }).filter(
items: (groupItem.items as SimpleItemWithColData[])?.map((item) => { (field) => !Object.is(previousFormData?.[field], formData?.[field]),
)
: []
const shouldRefreshLookup = (item: SimpleItemWithColData) => {
const cascadeParentFields = item.colData?.lookupDto?.cascadeParentFields
?.split(',')
.map((field: string) => field.trim())
.filter(Boolean)
return (
!previousFormData ||
cascadeParentFields?.some((field: string) => changedFields.includes(field))
)
}
const updateItems = (items: any[] = []) =>
items.map((item) => {
const colData = gridDto.columnFormats.find((x) => x.fieldName === item.dataField) const colData = gridDto.columnFormats.find((x) => x.fieldName === item.dataField)
if (colData?.lookupDto?.dataSourceType) { if (colData?.lookupDto?.dataSourceType && shouldRefreshLookup(item)) {
const currentDataSource = item.editorOptions?.dataSource const currentDataSource = item.editorOptions?.dataSource
const keepCustomDataSource = const keepCustomDataSource =
currentDataSource !== undefined && typeof currentDataSource?.load !== 'function' currentDataSource !== undefined && typeof currentDataSource?.load !== 'function'
@ -330,16 +356,55 @@ const useGridData = (props: {
dataSource: keepCustomDataSource dataSource: keepCustomDataSource
? currentDataSource ? currentDataSource
: getLookupDataSource(colData?.editorOptions, colData, formData || null), : getLookupDataSource(colData?.editorOptions, colData, formData || null),
valueExpr: item.editorOptions?.valueExpr ?? colData?.lookupDto?.valueExpr?.toLowerCase(), valueExpr:
item.editorOptions?.valueExpr ?? colData?.lookupDto?.valueExpr?.toLowerCase(),
displayExpr: displayExpr:
item.editorOptions?.displayExpr ?? colData?.lookupDto?.displayExpr?.toLowerCase(), item.editorOptions?.displayExpr ?? colData?.lookupDto?.displayExpr?.toLowerCase(),
}, },
} }
} }
if (item?.items?.length) {
return {
...item,
items: updateItems(item.items),
}
}
if (item?.tabs?.length) {
return {
...item,
tabs: item.tabs.map((tab: any) => ({
...tab,
items: updateItems(tab.items),
})),
}
}
return item return item
}), })
const hasAffectedLookup =
!previousFormData ||
formItems
.flatMap((group) => flattenFormItems([group]))
.some((item) => item.colData?.lookupDto?.dataSourceType && shouldRefreshLookup(item))
if (!hasAffectedLookup) {
previousFormDataRef.current = formData
return
}
const updatedItems = formItems.map((groupItem) => ({
...groupItem,
items: updateItems(groupItem.items as any[]),
tabs: (groupItem as any).tabs?.map((tab: any) => ({
...tab,
items: updateItems(tab.items),
})),
})) }))
previousFormDataRef.current = formData
setFormItems(updatedItems) setFormItems(updatedItems)
}, [formData, gridDto]) }, [formData, gridDto])

View file

@ -15,7 +15,6 @@ import {
postListFormCustomization, postListFormCustomization,
} from '@/services/list-form-customization.service' } from '@/services/list-form-customization.service'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import useResponsive from '@/utils/hooks/useResponsive'
import { executeEditorScript } from '@/utils/editorScriptRuntime' import { executeEditorScript } from '@/utils/editorScriptRuntime'
import { Template } from 'devextreme-react/core/template' import { Template } from 'devextreme-react/core/template'
import DataGrid, { import DataGrid, {
@ -239,19 +238,29 @@ const getValueByField = (data: Record<string, any> = {}, field?: string) => {
} }
const shouldRunEditorScriptOnContentReady = (script?: string) => const shouldRunEditorScriptOnContentReady = (script?: string) =>
Boolean(script && (script.includes('setEditorReadOnly') || script.includes('runtimeSetEditorReadOnly'))) Boolean(
script && (script.includes('setEditorReadOnly') || script.includes('runtimeSetEditorReadOnly')),
)
const isTouchLikeDevice = () =>
typeof window !== 'undefined' &&
(window.matchMedia?.('(pointer: coarse)').matches || window.matchMedia?.('(hover: none)').matches)
const isMobileViewport = () =>
typeof window !== 'undefined' && window.matchMedia?.('(max-width: 767px)').matches
const Grid = (props: GridProps) => { const Grid = (props: GridProps) => {
const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
const { translate } = useLocalization() const { translate } = useLocalization()
const { smaller } = useResponsive()
const currentUser = useStoreState((state) => state.auth.user) const currentUser = useStoreState((state) => state.auth.user)
const useMobileEditPopup = useRef(isMobileViewport() || isTouchLikeDevice()).current
const gridRef = useRef<DataGridRef>() const gridRef = useRef<DataGridRef>()
const refListFormCode = useRef('') const refListFormCode = useRef('')
const widgetGroupRef = useRef<HTMLDivElement>(null) const widgetGroupRef = useRef<HTMLDivElement>(null)
const editingFormDataRef = useRef<Record<string, any>>({}) const editingFormDataRef = useRef<Record<string, any>>({})
const editingFormInstanceRef = useRef<any>() const editingFormInstanceRef = useRef<any>()
const lastEditingContentReadyScriptKeyRef = useRef<string>()
// Edit popup state kaydetmeyi engellemek için flag // Edit popup state kaydetmeyi engellemek için flag
const isEditingRef = useRef(false) const isEditingRef = useRef(false)
@ -344,6 +353,29 @@ const Grid = (props: GridProps) => {
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef }) const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
const applyMobileEditorFocusGuard = useCallback(
(editorOptions: EditorOptionsWithButtons, dataField?: string) => {
if (!useMobileEditPopup || !dataField) {
return editorOptions
}
const previousOnFocusIn = editorOptions.onFocusIn
return {
...editorOptions,
autoFocus: false,
focusStateEnabled: false,
selectTextOnFocus: false,
onFocusIn: (e: any) => {
if (typeof previousOnFocusIn === 'function') {
previousOnFocusIn(e)
}
},
}
},
[useMobileEditPopup],
)
const openNotePanel = useCallback( const openNotePanel = useCallback(
(rowData: Record<string, any>) => { (rowData: Record<string, any>) => {
const keyFieldName = gridDto?.gridOptions.keyFieldName const keyFieldName = gridDto?.gridOptions.keyFieldName
@ -728,6 +760,10 @@ const Grid = (props: GridProps) => {
const onEditorPreparing = useCallback( const onEditorPreparing = useCallback(
(editor: DataGridTypes.EditorPreparingEvent<any, any>) => { (editor: DataGridTypes.EditorPreparingEvent<any, any>) => {
if (editor.parentType === 'dataRow' && editor.dataField && gridDto) { if (editor.parentType === 'dataRow' && editor.dataField && gridDto) {
if (isTouchLikeDevice()) {
editor.editorOptions = applyMobileEditorFocusGuard(editor.editorOptions, editor.dataField)
}
const formItem = gridDto.gridOptions.editingFormDto const formItem = gridDto.gridOptions.editingFormDto
.flatMap((group) => flattenEditingFormItems([group])) .flatMap((group) => flattenEditingFormItems([group]))
.find((i) => i.dataField === editor.dataField) .find((i) => i.dataField === editor.dataField)
@ -888,7 +924,13 @@ const Grid = (props: GridProps) => {
} }
} }
}, },
[gridDto, cascadeFieldsMap], [
gridDto,
cascadeFieldsMap,
parentToChildrenMap,
mode,
applyMobileEditorFocusGuard,
],
) )
const customLoadState = useCallback(() => { const customLoadState = useCallback(() => {
@ -1181,20 +1223,20 @@ const Grid = (props: GridProps) => {
if (listFormField?.sourceDbType === DbTypeEnum.Date) { if (listFormField?.sourceDbType === DbTypeEnum.Date) {
Object.assign(defaultEditorOptions, { Object.assign(defaultEditorOptions, {
type: 'date', type: 'date',
dateSerializationFormat: 'yyyy-MM-dd', dateSerializationFormat: 'yyyy-MM-dd',
displayFormat: 'shortDate', displayFormat: 'shortDate',
}) })
} else if ( } else if (
listFormField?.sourceDbType === DbTypeEnum.DateTime || listFormField?.sourceDbType === DbTypeEnum.DateTime ||
listFormField?.sourceDbType === DbTypeEnum.DateTime2 || listFormField?.sourceDbType === DbTypeEnum.DateTime2 ||
listFormField?.sourceDbType === DbTypeEnum.DateTimeOffset listFormField?.sourceDbType === DbTypeEnum.DateTimeOffset
) { ) {
Object.assign(defaultEditorOptions, { Object.assign(defaultEditorOptions, {
type: 'datetime', type: 'datetime',
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss', dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
displayFormat: 'shortDateShortTime', displayFormat: 'shortDateShortTime',
}) })
} }
// Her item'a placeholder olarak captionName ekle // Her item'a placeholder olarak captionName ekle
@ -1217,6 +1259,8 @@ const Grid = (props: GridProps) => {
...forcedEditorOptions, ...forcedEditorOptions,
} }
editorOptions = applyMobileEditorFocusGuard(editorOptions, i.dataField)
if (editorOptions?.buttons) { if (editorOptions?.buttons) {
editorOptions.buttons = (editorOptions?.buttons || []).map((btn: any) => { editorOptions.buttons = (editorOptions?.buttons || []).map((btn: any) => {
if (btn?.options?.onClick && typeof btn.options.onClick === 'string') { if (btn?.options?.onClick && typeof btn.options.onClick === 'string') {
@ -1261,7 +1305,7 @@ const Grid = (props: GridProps) => {
return item return item
}, },
[gridDto, mode, searchParams, extraFilters], [gridDto, mode, searchParams, extraFilters, applyMobileEditorFocusGuard],
) )
// WidgetGroup yüksekliğini hesapla // WidgetGroup yüksekliğini hesapla
@ -1514,7 +1558,7 @@ const Grid = (props: GridProps) => {
/> />
<Editing <Editing
refreshMode={gridDto.gridOptions.editingOptionDto?.refreshMode} refreshMode={gridDto.gridOptions.editingOptionDto?.refreshMode}
mode={smaller.md ? 'form' : gridDto.gridOptions.editingOptionDto?.mode} mode={gridDto.gridOptions.editingOptionDto?.mode}
allowDeleting={gridDto.gridOptions.editingOptionDto?.allowDeleting} allowDeleting={gridDto.gridOptions.editingOptionDto?.allowDeleting}
allowUpdating={gridDto.gridOptions.editingOptionDto?.allowUpdating} allowUpdating={gridDto.gridOptions.editingOptionDto?.allowUpdating}
allowAdding={gridDto.gridOptions.editingOptionDto?.allowAdding} allowAdding={gridDto.gridOptions.editingOptionDto?.allowAdding}
@ -1534,10 +1578,19 @@ const Grid = (props: GridProps) => {
showTitle: gridDto.gridOptions.editingOptionDto?.popup?.showTitle, showTitle: gridDto.gridOptions.editingOptionDto?.popup?.showTitle,
hideOnOutsideClick: hideOnOutsideClick:
gridDto.gridOptions.editingOptionDto?.popup?.hideOnOutsideClick, gridDto.gridOptions.editingOptionDto?.popup?.hideOnOutsideClick,
width: gridDto.gridOptions.editingOptionDto?.popup?.width, width: useMobileEditPopup
height: gridDto.gridOptions.editingOptionDto?.popup?.height, ? '100%'
resizeEnabled: gridDto.gridOptions.editingOptionDto?.popup?.resizeEnabled, : gridDto.gridOptions.editingOptionDto?.popup?.width,
fullScreen: isPopupFullScreen, height: useMobileEditPopup
? '100dvh'
: gridDto.gridOptions.editingOptionDto?.popup?.height,
resizeEnabled:
!useMobileEditPopup &&
gridDto.gridOptions.editingOptionDto?.popup?.resizeEnabled,
fullScreen: useMobileEditPopup || isPopupFullScreen,
dragEnabled: !useMobileEditPopup,
focusStateEnabled: !useMobileEditPopup,
restorePosition: !useMobileEditPopup,
toolbarItems: [ toolbarItems: [
{ {
widget: 'dxButton', widget: 'dxButton',
@ -1581,6 +1634,7 @@ const Grid = (props: GridProps) => {
}} }}
form={{ form={{
colCount: 1, colCount: 1,
focusStateEnabled: !useMobileEditPopup,
onContentReady: (e) => { onContentReady: (e) => {
editingFormInstanceRef.current = e.component editingFormInstanceRef.current = e.component
@ -1601,15 +1655,27 @@ const Grid = (props: GridProps) => {
} }
const runReadOnlyScripts = () => { const runReadOnlyScripts = () => {
const editorValues = gridDto.gridOptions.editingFormDto const formItems = gridDto.gridOptions.editingFormDto.flatMap((group) =>
.flatMap((group) => flattenEditingFormItems([group])) flattenEditingFormItems([group]),
.reduce<Record<string, any>>((values, formItem) => { )
const scriptItems = formItems.filter((formItem) =>
shouldRunEditorScriptOnContentReady(formItem.editorScript),
)
if (!scriptItems.length) {
return
}
const editorValues = formItems.reduce<Record<string, any>>(
(values, formItem) => {
const editorInstance = form?.getEditor?.(formItem.dataField) const editorInstance = form?.getEditor?.(formItem.dataField)
if (editorInstance?.option) { if (editorInstance?.option) {
values[formItem.dataField] = editorInstance.option('value') values[formItem.dataField] = editorInstance.option('value')
} }
return values return values
}, {}) },
{},
)
const formData = { const formData = {
...editingFormDataRef.current, ...editingFormDataRef.current,
...(form?.option?.('formData') || {}), ...(form?.option?.('formData') || {}),
@ -1617,38 +1683,47 @@ const Grid = (props: GridProps) => {
} }
editingFormDataRef.current = { ...formData } editingFormDataRef.current = { ...formData }
gridDto.gridOptions.editingFormDto const scriptKey = `${mode}|${String(rowKey)}|${scriptItems
.flatMap((group) => flattenEditingFormItems([group])) .map((formItem) => formItem.dataField)
.filter((formItem) => .join('|')}|${JSON.stringify(formData)}`
shouldRunEditorScriptOnContentReady(formItem.editorScript),
)
.forEach((formItem) => {
try {
const editorInstance = form?.getEditor?.(formItem.dataField)
const editorValue =
editorInstance?.option?.('value') ??
getValueByField(formData, formItem.dataField)
const editor = {
dataField: formItem.dataField,
component: grid,
}
const e = {
component: form,
dataField: formItem.dataField,
value: editorValue,
}
executeEditorScript(formItem.editorScript!, { if (lastEditingContentReadyScriptKeyRef.current === scriptKey) {
formData, return
e, }
editor,
runtimeSetEditorReadOnly, lastEditingContentReadyScriptKeyRef.current = scriptKey
setFormData,
}) scriptItems.forEach((formItem) => {
} catch (err) { try {
console.error('Script exec error on contentReady', formItem.dataField, err) const editorInstance = form?.getEditor?.(formItem.dataField)
const editorValue =
editorInstance?.option?.('value') ??
getValueByField(formData, formItem.dataField)
const editor = {
dataField: formItem.dataField,
component: grid,
} }
}) const e = {
component: form,
dataField: formItem.dataField,
value: editorValue,
}
executeEditorScript(formItem.editorScript!, {
formData,
e,
editor,
runtimeSetEditorReadOnly,
setFormData,
})
} catch (err) {
console.error(
'Script exec error on contentReady',
formItem.dataField,
err,
)
}
})
} }
runReadOnlyScripts() runReadOnlyScripts()
@ -1704,7 +1779,6 @@ const Grid = (props: GridProps) => {
console.error('Script exec error', err) console.error('Script exec error', err)
} }
} }
} }
}, },
items: items:
@ -1896,7 +1970,7 @@ const Grid = (props: GridProps) => {
)} )}
<Dialog <Dialog
width={smaller.md ? '100%' : 1000} width={useMobileEditPopup ? '100%' : 1000}
isOpen={filterData.isImportModalOpen || false} isOpen={filterData.isImportModalOpen || false}
onClose={() => filterData.setIsImportModalOpen(false)} onClose={() => filterData.setIsImportModalOpen(false)}
onRequestClose={() => filterData.setIsImportModalOpen(false)} onRequestClose={() => filterData.setIsImportModalOpen(false)}

View file

@ -15,7 +15,6 @@ import {
postListFormCustomization, postListFormCustomization,
} from '@/services/list-form-customization.service' } from '@/services/list-form-customization.service'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import useResponsive from '@/utils/hooks/useResponsive'
import { captionize } from 'devextreme/core/utils/inflector' import { captionize } from 'devextreme/core/utils/inflector'
import { Template } from 'devextreme-react/core/template' import { Template } from 'devextreme-react/core/template'
import TreeListDx, { import TreeListDx, {
@ -227,19 +226,29 @@ const getValueByField = (data: Record<string, any> = {}, field?: string) => {
} }
const shouldRunEditorScriptOnContentReady = (script?: string) => const shouldRunEditorScriptOnContentReady = (script?: string) =>
Boolean(script && (script.includes('setEditorReadOnly') || script.includes('runtimeSetEditorReadOnly'))) Boolean(
script && (script.includes('setEditorReadOnly') || script.includes('runtimeSetEditorReadOnly')),
)
const isTouchLikeDevice = () =>
typeof window !== 'undefined' &&
(window.matchMedia?.('(pointer: coarse)').matches || window.matchMedia?.('(hover: none)').matches)
const isMobileViewport = () =>
typeof window !== 'undefined' && window.matchMedia?.('(max-width: 767px)').matches
const Tree = (props: TreeProps) => { const Tree = (props: TreeProps) => {
const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
const { translate } = useLocalization() const { translate } = useLocalization()
const { smaller } = useResponsive()
const currentUser = useStoreState((state) => state.auth.user) const currentUser = useStoreState((state) => state.auth.user)
const useMobileEditPopup = useRef(isMobileViewport() || isTouchLikeDevice()).current
const gridRef = useRef<TreeListRef>() const gridRef = useRef<TreeListRef>()
const refListFormCode = useRef('') const refListFormCode = useRef('')
const widgetGroupRef = useRef<HTMLDivElement>(null) const widgetGroupRef = useRef<HTMLDivElement>(null)
const editingFormDataRef = useRef<Record<string, any>>({}) const editingFormDataRef = useRef<Record<string, any>>({})
const editingFormInstanceRef = useRef<any>() const editingFormInstanceRef = useRef<any>()
const lastEditingContentReadyScriptKeyRef = useRef<string>()
// Edit popup state kaydetmeyi engellemek için flag // Edit popup state kaydetmeyi engellemek için flag
const isEditingRef = useRef(false) const isEditingRef = useRef(false)
@ -349,6 +358,29 @@ const Tree = (props: TreeProps) => {
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef }) const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
const applyMobileEditorFocusGuard = useCallback(
(editorOptions: EditorOptionsWithButtons, dataField?: string) => {
if (!useMobileEditPopup || !dataField) {
return editorOptions
}
const previousOnFocusIn = editorOptions.onFocusIn
return {
...editorOptions,
autoFocus: false,
focusStateEnabled: false,
selectTextOnFocus: false,
onFocusIn: (e: any) => {
if (typeof previousOnFocusIn === 'function') {
previousOnFocusIn(e)
}
},
}
},
[useMobileEditPopup],
)
const openNotePanel = useCallback( const openNotePanel = useCallback(
(rowData: Record<string, any>) => { (rowData: Record<string, any>) => {
const keyFieldName = gridDto?.gridOptions.keyFieldName const keyFieldName = gridDto?.gridOptions.keyFieldName
@ -677,6 +709,10 @@ const Tree = (props: TreeProps) => {
function onEditorPreparing(editor: TreeListTypes.EditorPreparingEvent) { function onEditorPreparing(editor: TreeListTypes.EditorPreparingEvent) {
if (editor.parentType === 'dataRow' && editor.dataField && gridDto) { if (editor.parentType === 'dataRow' && editor.dataField && gridDto) {
if (isTouchLikeDevice()) {
editor.editorOptions = applyMobileEditorFocusGuard(editor.editorOptions, editor.dataField)
}
const formItem = gridDto.gridOptions.editingFormDto const formItem = gridDto.gridOptions.editingFormDto
.flatMap((group) => flattenEditingFormItems([group])) .flatMap((group) => flattenEditingFormItems([group]))
.find((i) => i.dataField === editor.dataField) .find((i) => i.dataField === editor.dataField)
@ -1180,7 +1216,7 @@ const Tree = (props: TreeProps) => {
<RemoteOperations filtering={true} sorting={true} grouping={false} /> <RemoteOperations filtering={true} sorting={true} grouping={false} />
<Editing <Editing
refreshMode={gridDto.gridOptions.editingOptionDto?.refreshMode} refreshMode={gridDto.gridOptions.editingOptionDto?.refreshMode}
mode={smaller.md ? 'form' : gridDto.gridOptions.editingOptionDto?.mode} mode={gridDto.gridOptions.editingOptionDto?.mode}
allowDeleting={gridDto.gridOptions.editingOptionDto?.allowDeleting} allowDeleting={gridDto.gridOptions.editingOptionDto?.allowDeleting}
allowUpdating={gridDto.gridOptions.editingOptionDto?.allowUpdating} allowUpdating={gridDto.gridOptions.editingOptionDto?.allowUpdating}
allowAdding={gridDto.gridOptions.editingOptionDto?.allowAdding} allowAdding={gridDto.gridOptions.editingOptionDto?.allowAdding}
@ -1196,10 +1232,19 @@ const Tree = (props: TreeProps) => {
showTitle: gridDto.gridOptions.editingOptionDto?.popup?.showTitle, showTitle: gridDto.gridOptions.editingOptionDto?.popup?.showTitle,
hideOnOutsideClick: hideOnOutsideClick:
gridDto.gridOptions.editingOptionDto?.popup?.hideOnOutsideClick, gridDto.gridOptions.editingOptionDto?.popup?.hideOnOutsideClick,
width: gridDto.gridOptions.editingOptionDto?.popup?.width, width: useMobileEditPopup
height: gridDto.gridOptions.editingOptionDto?.popup?.height, ? '100%'
resizeEnabled: gridDto.gridOptions.editingOptionDto?.popup?.resizeEnabled, : gridDto.gridOptions.editingOptionDto?.popup?.width,
fullScreen: isPopupFullScreen, height: useMobileEditPopup
? '100dvh'
: gridDto.gridOptions.editingOptionDto?.popup?.height,
resizeEnabled:
!useMobileEditPopup &&
gridDto.gridOptions.editingOptionDto?.popup?.resizeEnabled,
fullScreen: useMobileEditPopup || isPopupFullScreen,
dragEnabled: !useMobileEditPopup,
focusStateEnabled: !useMobileEditPopup,
restorePosition: !useMobileEditPopup,
toolbarItems: [ toolbarItems: [
{ {
widget: 'dxButton', widget: 'dxButton',
@ -1243,6 +1288,7 @@ const Tree = (props: TreeProps) => {
}} }}
form={{ form={{
colCount: 1, colCount: 1,
focusStateEnabled: !useMobileEditPopup,
onContentReady: (e) => { onContentReady: (e) => {
editingFormInstanceRef.current = e.component editingFormInstanceRef.current = e.component
@ -1263,15 +1309,27 @@ const Tree = (props: TreeProps) => {
} }
const runReadOnlyScripts = () => { const runReadOnlyScripts = () => {
const editorValues = gridDto.gridOptions.editingFormDto const formItems = gridDto.gridOptions.editingFormDto.flatMap((group) =>
.flatMap((group) => flattenEditingFormItems([group])) flattenEditingFormItems([group]),
.reduce<Record<string, any>>((values, formItem) => { )
const scriptItems = formItems.filter((formItem) =>
shouldRunEditorScriptOnContentReady(formItem.editorScript),
)
if (!scriptItems.length) {
return
}
const editorValues = formItems.reduce<Record<string, any>>(
(values, formItem) => {
const editorInstance = form?.getEditor?.(formItem.dataField) const editorInstance = form?.getEditor?.(formItem.dataField)
if (editorInstance?.option) { if (editorInstance?.option) {
values[formItem.dataField] = editorInstance.option('value') values[formItem.dataField] = editorInstance.option('value')
} }
return values return values
}, {}) },
{},
)
const formData = { const formData = {
...editingFormDataRef.current, ...editingFormDataRef.current,
...(form?.option?.('formData') || {}), ...(form?.option?.('formData') || {}),
@ -1279,38 +1337,47 @@ const Tree = (props: TreeProps) => {
} }
editingFormDataRef.current = { ...formData } editingFormDataRef.current = { ...formData }
gridDto.gridOptions.editingFormDto const scriptKey = `${mode}|${String(rowKey)}|${scriptItems
.flatMap((group) => flattenEditingFormItems([group])) .map((formItem) => formItem.dataField)
.filter((formItem) => .join('|')}|${JSON.stringify(formData)}`
shouldRunEditorScriptOnContentReady(formItem.editorScript),
)
.forEach((formItem) => {
try {
const editorInstance = form?.getEditor?.(formItem.dataField)
const editorValue =
editorInstance?.option?.('value') ??
getValueByField(formData, formItem.dataField)
const editor = {
dataField: formItem.dataField,
component: grid,
}
const e = {
component: form,
dataField: formItem.dataField,
value: editorValue,
}
executeEditorScript(formItem.editorScript!, { if (lastEditingContentReadyScriptKeyRef.current === scriptKey) {
formData, return
e, }
editor,
runtimeSetEditorReadOnly, lastEditingContentReadyScriptKeyRef.current = scriptKey
setFormData,
}) scriptItems.forEach((formItem) => {
} catch (err) { try {
console.error('Script exec error on contentReady', formItem.dataField, err) const editorInstance = form?.getEditor?.(formItem.dataField)
const editorValue =
editorInstance?.option?.('value') ??
getValueByField(formData, formItem.dataField)
const editor = {
dataField: formItem.dataField,
component: grid,
} }
}) const e = {
component: form,
dataField: formItem.dataField,
value: editorValue,
}
executeEditorScript(formItem.editorScript!, {
formData,
e,
editor,
runtimeSetEditorReadOnly,
setFormData,
})
} catch (err) {
console.error(
'Script exec error on contentReady',
formItem.dataField,
err,
)
}
})
} }
runReadOnlyScripts() runReadOnlyScripts()
@ -1366,7 +1433,6 @@ const Tree = (props: TreeProps) => {
console.error('Script exec error', err) console.error('Script exec error', err)
} }
} }
} }
}, },
items: items:
@ -1388,7 +1454,9 @@ const Tree = (props: TreeProps) => {
let parsedEditorOptions: EditorOptionsWithButtons = {} let parsedEditorOptions: EditorOptionsWithButtons = {}
const forcedEditorOptions: EditorOptionsWithButtons = {} const forcedEditorOptions: EditorOptionsWithButtons = {}
try { try {
parsedEditorOptions = i.editorOptions ? JSON.parse(i.editorOptions) : {} parsedEditorOptions = i.editorOptions
? JSON.parse(i.editorOptions)
: {}
const rawFilter = searchParams?.get('filter') const rawFilter = searchParams?.get('filter')
if (rawFilter) { if (rawFilter) {
@ -1420,20 +1488,20 @@ const Tree = (props: TreeProps) => {
if (listFormField?.sourceDbType === DbTypeEnum.Date) { if (listFormField?.sourceDbType === DbTypeEnum.Date) {
Object.assign(defaultEditorOptions, { Object.assign(defaultEditorOptions, {
type: 'date', type: 'date',
dateSerializationFormat: 'yyyy-MM-dd', dateSerializationFormat: 'yyyy-MM-dd',
displayFormat: 'shortDate', displayFormat: 'shortDate',
}) })
} else if ( } else if (
listFormField?.sourceDbType === DbTypeEnum.DateTime || listFormField?.sourceDbType === DbTypeEnum.DateTime ||
listFormField?.sourceDbType === DbTypeEnum.DateTime2 || listFormField?.sourceDbType === DbTypeEnum.DateTime2 ||
listFormField?.sourceDbType === DbTypeEnum.DateTimeOffset listFormField?.sourceDbType === DbTypeEnum.DateTimeOffset
) { ) {
Object.assign(defaultEditorOptions, { Object.assign(defaultEditorOptions, {
type: 'datetime', type: 'datetime',
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss', dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
displayFormat: 'shortDateShortTime', displayFormat: 'shortDateShortTime',
}) })
} }
editorOptions = { editorOptions = {
@ -1442,6 +1510,8 @@ const Tree = (props: TreeProps) => {
...forcedEditorOptions, ...forcedEditorOptions,
} }
editorOptions = applyMobileEditorFocusGuard(editorOptions, i.dataField)
if (editorOptions?.buttons) { if (editorOptions?.buttons) {
editorOptions.buttons = (editorOptions?.buttons || []).map( editorOptions.buttons = (editorOptions?.buttons || []).map(
(btn: any) => { (btn: any) => {
@ -1636,7 +1706,7 @@ const Tree = (props: TreeProps) => {
)} )}
<Dialog <Dialog
width={smaller.md ? '100%' : 1000} width={useMobileEditPopup ? '100%' : 1000}
isOpen={filterData.isImportModalOpen || false} isOpen={filterData.isImportModalOpen || false}
onClose={() => filterData.setIsImportModalOpen(false)} onClose={() => filterData.setIsImportModalOpen(false)}
onRequestClose={() => filterData.setIsImportModalOpen(false)} onRequestClose={() => filterData.setIsImportModalOpen(false)}