Mobil Grid ve Tree Popup EditForm problemi

This commit is contained in:
Sedat ÖZTÜRK 2026-06-08 13:15:22 +03:00
parent 48894d2bda
commit 168768ba98
8 changed files with 423 additions and 218 deletions

View file

@ -17,5 +17,10 @@ public class GridEditingPopupDto
/// <summary> popup ekranin küçültülüp büyütülebilmesini saglar
/// </summary>
public bool ResizeEnabled { get; set; } = true;
/// <summary> popup ekranin sürüklenebilmesini saglar
public bool DragEnabled { get; set; } = true;
/// <summary> popup ekranin kapatildiktan sonra eski konumunu hatirlamasini saglar
/// </summary>
public bool RestorePosition { get; set; } = true;
}

View file

@ -111,4 +111,38 @@ div.dialog-after-open > div.dialog-content.maximized {
.dx-datagrid-header-panel {
padding: 0 0 !important;
}
}
@media (max-width: 767px), (pointer: coarse) {
.mobile-edit-popup.dx-overlay-wrapper,
.dx-overlay-wrapper.mobile-edit-popup {
align-items: flex-start !important;
justify-content: center !important;
position: fixed !important;
inset: 0 !important;
overflow: hidden !important;
}
.mobile-edit-popup .dx-overlay-content,
.mobile-edit-popup .dx-popup-normal {
position: fixed !important;
inset: 0 auto auto 0 !important;
transform: none !important;
width: 100vw !important;
max-width: 100vw !important;
height: 100dvh !important;
max-height: 100dvh !important;
margin: 0 !important;
}
.mobile-edit-popup .dx-popup-content {
overflow-y: auto !important;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
padding-bottom: max(16px, env(safe-area-inset-bottom)) !important;
}
.mobile-edit-popup .dx-popup-bottom {
flex-shrink: 0;
}
}

View file

@ -503,6 +503,8 @@ export interface GridEditingPopupDto {
fullScreen: boolean
hideOnOutsideClick: boolean
resizeEnabled: boolean
dragEnabled: boolean
restorePosition: boolean
}
export interface GridFilterRowDto {

View file

@ -149,8 +149,7 @@ function conditionNeedsValue(operator: ConditionalAction['operator']) {
function isConditionalActionReady(action: ConditionalAction) {
if (conditionNeedsSource(action.operator) && !action.source) return false
if (conditionNeedsValue(action.operator) && !action.value.trim()) return false
if (action.actionType === 'setField')
return Boolean(action.targetField && action.textValue.trim())
if (action.actionType === 'setField') return Boolean(action.targetField)
if (action.actionType === 'apiToField') return Boolean(action.targetField && action.apiUrl.trim())
if (action.actionType === 'openUrl') return Boolean(action.textValue.trim())
if (action.actionType === 'alert' || action.actionType === 'confirm')
@ -301,7 +300,9 @@ function buildScript({
if (needsRenderTemplate) {
lines.push(
' const renderTemplate = text => String(text || "").replace(/\\{value\\}/g, e?.value ?? "").replace(/\\{selected\\.([^}]+)\\}/g, (_, key) => getByPath(selectedItem, key) ?? "").replace(/\\{([^}]+)\\}/g, (_, key) => next[key] ?? "");',
' const templateOpen = String.fromCharCode(123);',
' const templateClose = String.fromCharCode(125);',
' const renderTemplate = text => String(text || "").replaceAll(templateOpen + "value" + templateClose, e?.value ?? "").replace(new RegExp("\\\\" + templateOpen + "selected\\\\.([^" + templateClose + "]+)\\\\" + templateClose, "g"), (_, key) => getByPath(selectedItem, key) ?? "").replace(new RegExp("\\\\" + templateOpen + "([^" + templateClose + "]+)\\\\" + templateClose, "g"), (_, key) => next[key] ?? "");',
)
}
@ -455,7 +456,7 @@ function buildScript({
})
if (needsSetFormData) {
lines.push(' setFormData(next);')
lines.push(' if (typeof setFormData === "function") setFormData(next);')
}
if (serviceCall.trim()) {

View file

@ -140,7 +140,9 @@ const getValueByField = (data: Record<string, any> = {}, field?: string) => {
}
const shouldRunEditorScriptOnContentReady = (script?: string) =>
Boolean(script && (script.includes('setEditorReadOnly') || script.includes('runtimeSetEditorReadOnly')))
Boolean(
script && (script.includes('setEditorReadOnly') || script.includes('runtimeSetEditorReadOnly')),
)
const FormDevExpress = (props: {
listFormCode: string
@ -158,9 +160,15 @@ const FormDevExpress = (props: {
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])
@ -173,6 +181,10 @@ const FormDevExpress = (props: {
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()
@ -202,11 +214,23 @@ const FormDevExpress = (props: {
setTimeout(() => setFormEditorReadOnly(formInstanceRef.current ?? form, field, readOnly), 0)
}
const runEditorScript = (
formItem: SimpleItemWithColData,
eventValue: any,
component?: any,
) => {
const applyEditorScriptFormData = (form: any, newData: any) => {
const nextFormData = {
...(formDataRef.current || {}),
...(newData || {}),
}
formDataRef.current = nextFormData
form?.option?.('formData', nextFormData)
Object.keys(newData || {}).forEach((field) => {
form?.getEditor?.(field)?.option?.('value', newData[field])
})
setFormData(nextFormData)
}
const runEditorScript = (formItem: SimpleItemWithColData, eventValue: any, component?: any) => {
if (!formItem?.editorScript) {
return
}
@ -239,6 +263,7 @@ const FormDevExpress = (props: {
e,
editor,
runtimeSetEditorReadOnly,
setFormData: (newData: any) => applyEditorScriptFormData(form, newData),
})
} catch (err) {
console.error('Script execution failed for', formItem.name, err)
@ -250,7 +275,9 @@ const FormDevExpress = (props: {
const prevOnValueChanged = formItem.editorOptions?.onValueChanged
return {
...(index !== undefined && mode !== 'view' ? { autoFocus: index === 1 } : {}),
...(index !== undefined && mode !== 'view' && !isTouchLikeDevice()
? { autoFocus: index === 1 }
: {}),
...(formItem.editorType === 'dxDateBox'
? {
useMaskBehavior: true,
@ -397,6 +424,7 @@ const FormDevExpress = (props: {
e,
editor,
runtimeSetEditorReadOnly,
setFormData: (newData: any) => applyEditorScriptFormData(form, newData),
})
} catch (err) {
console.error('Script execution failed on contentReady for', formItem.name, err)
@ -466,7 +494,6 @@ const FormDevExpress = (props: {
setTimeout(() => {
updateCascadeDisabledStates()
}, 0)
}}
onContentReady={(e) => {
formInstanceRef.current = e.component
@ -478,7 +505,8 @@ const FormDevExpress = (props: {
const groupItems = e.component.option('items') as any[]
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)
mode !== 'view' && editor?.focus()
}
@ -492,98 +520,100 @@ const FormDevExpress = (props: {
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) }}
/>
)
})}
{(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>
)
})}

View file

@ -19,6 +19,13 @@ import { layoutTypes } from '../admin/listForm/edit/types'
import { useListFormCustomDataSource } from '../list/useListFormCustomDataSource'
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: {
mode: RowMode
listFormCode: string
@ -41,6 +48,7 @@ const useGridData = (props: {
const [permissionResults, setPermissionResults] = useState<PermissionResults>()
const refForm = useRef<FormRef>(null)
const previousFormDataRef = useRef<any>()
const [searchParams] = useSearchParams()
const navigate = useNavigate()
const { translate } = useLocalization()
@ -306,18 +314,36 @@ const useGridData = (props: {
setGridReady(true)
}, [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(() => {
if (!gridDto || !formItems.length) {
previousFormDataRef.current = formData
return
}
// View mode'da formData olsa da olmasa da cascading alanlar için dataSource oluşturulmalı
const updatedItems = formItems.map((groupItem) => ({
...groupItem,
items: (groupItem.items as SimpleItemWithColData[])?.map((item) => {
const previousFormData = previousFormDataRef.current
const changedFields = previousFormData
? Object.keys({ ...(previousFormData || {}), ...(formData || {}) }).filter(
(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)
if (colData?.lookupDto?.dataSourceType) {
if (colData?.lookupDto?.dataSourceType && shouldRefreshLookup(item)) {
const currentDataSource = item.editorOptions?.dataSource
const keepCustomDataSource =
currentDataSource !== undefined && typeof currentDataSource?.load !== 'function'
@ -330,16 +356,55 @@ const useGridData = (props: {
dataSource: keepCustomDataSource
? currentDataSource
: getLookupDataSource(colData?.editorOptions, colData, formData || null),
valueExpr: item.editorOptions?.valueExpr ?? colData?.lookupDto?.valueExpr?.toLowerCase(),
valueExpr:
item.editorOptions?.valueExpr ?? colData?.lookupDto?.valueExpr?.toLowerCase(),
displayExpr:
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
}),
})
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)
}, [formData, gridDto])

View file

@ -15,7 +15,6 @@ import {
postListFormCustomization,
} from '@/services/list-form-customization.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
import useResponsive from '@/utils/hooks/useResponsive'
import { executeEditorScript } from '@/utils/editorScriptRuntime'
import { Template } from 'devextreme-react/core/template'
import DataGrid, {
@ -239,13 +238,22 @@ const getValueByField = (data: Record<string, any> = {}, field?: 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 { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
const { translate } = useLocalization()
const { smaller } = useResponsive()
const currentUser = useStoreState((state) => state.auth.user)
const useMobileEditPopup = useRef(isMobileViewport() || isTouchLikeDevice()).current
const gridRef = useRef<DataGridRef>()
const refListFormCode = useRef('')
@ -888,7 +896,7 @@ const Grid = (props: GridProps) => {
}
}
},
[gridDto, cascadeFieldsMap],
[gridDto, cascadeFieldsMap, parentToChildrenMap, mode],
)
const customLoadState = useCallback(() => {
@ -1181,20 +1189,20 @@ const Grid = (props: GridProps) => {
if (listFormField?.sourceDbType === DbTypeEnum.Date) {
Object.assign(defaultEditorOptions, {
type: 'date',
dateSerializationFormat: 'yyyy-MM-dd',
displayFormat: 'shortDate',
})
type: 'date',
dateSerializationFormat: 'yyyy-MM-dd',
displayFormat: 'shortDate',
})
} else if (
listFormField?.sourceDbType === DbTypeEnum.DateTime ||
listFormField?.sourceDbType === DbTypeEnum.DateTime2 ||
listFormField?.sourceDbType === DbTypeEnum.DateTimeOffset
) {
Object.assign(defaultEditorOptions, {
type: 'datetime',
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
displayFormat: 'shortDateShortTime',
})
type: 'datetime',
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
displayFormat: 'shortDateShortTime',
})
}
// Her item'a placeholder olarak captionName ekle
@ -1514,7 +1522,7 @@ const Grid = (props: GridProps) => {
/>
<Editing
refreshMode={gridDto.gridOptions.editingOptionDto?.refreshMode}
mode={smaller.md ? 'form' : gridDto.gridOptions.editingOptionDto?.mode}
mode={gridDto.gridOptions.editingOptionDto?.mode}
allowDeleting={gridDto.gridOptions.editingOptionDto?.allowDeleting}
allowUpdating={gridDto.gridOptions.editingOptionDto?.allowUpdating}
allowAdding={gridDto.gridOptions.editingOptionDto?.allowAdding}
@ -1528,16 +1536,32 @@ const Grid = (props: GridProps) => {
popup={{
deferRendering: true,
animation: {},
wrapperAttr: useMobileEditPopup
? { class: 'mobile-edit-popup' }
: undefined,
title:
(mode === 'new' ? '✚ ' : '🖊️ ') +
translate('::' + gridDto.gridOptions.editingOptionDto?.popup?.title),
showTitle: gridDto.gridOptions.editingOptionDto?.popup?.showTitle,
hideOnOutsideClick:
gridDto.gridOptions.editingOptionDto?.popup?.hideOnOutsideClick,
width: gridDto.gridOptions.editingOptionDto?.popup?.width,
height: gridDto.gridOptions.editingOptionDto?.popup?.height,
width: useMobileEditPopup
? '100%'
: gridDto.gridOptions.editingOptionDto?.popup?.width,
height: useMobileEditPopup
? '100dvh'
: gridDto.gridOptions.editingOptionDto?.popup?.height,
position: useMobileEditPopup
? {
my: 'top center',
at: 'top center',
of: typeof window !== 'undefined' ? window : undefined,
}
: undefined,
resizeEnabled: gridDto.gridOptions.editingOptionDto?.popup?.resizeEnabled,
fullScreen: isPopupFullScreen,
dragEnabled: gridDto.gridOptions.editingOptionDto?.popup?.dragEnabled,
restorePosition: gridDto.gridOptions.editingOptionDto?.popup?.restorePosition,
toolbarItems: [
{
widget: 'dxButton',
@ -1601,15 +1625,27 @@ const Grid = (props: GridProps) => {
}
const runReadOnlyScripts = () => {
const editorValues = gridDto.gridOptions.editingFormDto
.flatMap((group) => flattenEditingFormItems([group]))
.reduce<Record<string, any>>((values, formItem) => {
const formItems = gridDto.gridOptions.editingFormDto.flatMap((group) =>
flattenEditingFormItems([group]),
)
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)
if (editorInstance?.option) {
values[formItem.dataField] = editorInstance.option('value')
}
return values
}, {})
},
{},
)
const formData = {
...editingFormDataRef.current,
...(form?.option?.('formData') || {}),
@ -1617,38 +1653,37 @@ const Grid = (props: GridProps) => {
}
editingFormDataRef.current = { ...formData }
gridDto.gridOptions.editingFormDto
.flatMap((group) => flattenEditingFormItems([group]))
.filter((formItem) =>
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!, {
formData,
e,
editor,
runtimeSetEditorReadOnly,
setFormData,
})
} catch (err) {
console.error('Script exec error on contentReady', formItem.dataField, err)
scriptItems.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!, {
formData,
e,
editor,
runtimeSetEditorReadOnly,
setFormData,
})
} catch (err) {
console.error(
'Script exec error on contentReady',
formItem.dataField,
err,
)
}
})
}
runReadOnlyScripts()
@ -1704,7 +1739,6 @@ const Grid = (props: GridProps) => {
console.error('Script exec error', err)
}
}
}
},
items:
@ -1896,7 +1930,7 @@ const Grid = (props: GridProps) => {
)}
<Dialog
width={smaller.md ? '100%' : 1000}
width={useMobileEditPopup ? '100%' : 1000}
isOpen={filterData.isImportModalOpen || false}
onClose={() => filterData.setIsImportModalOpen(false)}
onRequestClose={() => filterData.setIsImportModalOpen(false)}

View file

@ -15,7 +15,6 @@ import {
postListFormCustomization,
} from '@/services/list-form-customization.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
import useResponsive from '@/utils/hooks/useResponsive'
import { captionize } from 'devextreme/core/utils/inflector'
import { Template } from 'devextreme-react/core/template'
import TreeListDx, {
@ -227,13 +226,22 @@ const getValueByField = (data: Record<string, any> = {}, field?: 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 { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
const { translate } = useLocalization()
const { smaller } = useResponsive()
const currentUser = useStoreState((state) => state.auth.user)
const useMobileEditPopup = useRef(isMobileViewport() || isTouchLikeDevice()).current
const gridRef = useRef<TreeListRef>()
const refListFormCode = useRef('')
@ -1180,7 +1188,7 @@ const Tree = (props: TreeProps) => {
<RemoteOperations filtering={true} sorting={true} grouping={false} />
<Editing
refreshMode={gridDto.gridOptions.editingOptionDto?.refreshMode}
mode={smaller.md ? 'form' : gridDto.gridOptions.editingOptionDto?.mode}
mode={gridDto.gridOptions.editingOptionDto?.mode}
allowDeleting={gridDto.gridOptions.editingOptionDto?.allowDeleting}
allowUpdating={gridDto.gridOptions.editingOptionDto?.allowUpdating}
allowAdding={gridDto.gridOptions.editingOptionDto?.allowAdding}
@ -1190,16 +1198,30 @@ const Tree = (props: TreeProps) => {
startEditAction={gridDto.gridOptions.editingOptionDto?.startEditAction}
popup={{
animation: {},
wrapperAttr: useMobileEditPopup ? { class: 'mobile-edit-popup' } : undefined,
title:
(mode === 'new' ? '✚ ' : '🖊️ ') +
translate('::' + gridDto.gridOptions.editingOptionDto?.popup?.title),
showTitle: gridDto.gridOptions.editingOptionDto?.popup?.showTitle,
hideOnOutsideClick:
gridDto.gridOptions.editingOptionDto?.popup?.hideOnOutsideClick,
width: gridDto.gridOptions.editingOptionDto?.popup?.width,
height: gridDto.gridOptions.editingOptionDto?.popup?.height,
width: useMobileEditPopup
? '100%'
: gridDto.gridOptions.editingOptionDto?.popup?.width,
height: useMobileEditPopup
? '100dvh'
: gridDto.gridOptions.editingOptionDto?.popup?.height,
position: useMobileEditPopup
? {
my: 'top center',
at: 'top center',
of: typeof window !== 'undefined' ? window : undefined,
}
: undefined,
resizeEnabled: gridDto.gridOptions.editingOptionDto?.popup?.resizeEnabled,
fullScreen: isPopupFullScreen,
dragEnabled: gridDto.gridOptions.editingOptionDto?.popup?.dragEnabled,
restorePosition: gridDto.gridOptions.editingOptionDto?.popup?.restorePosition,
toolbarItems: [
{
widget: 'dxButton',
@ -1263,15 +1285,27 @@ const Tree = (props: TreeProps) => {
}
const runReadOnlyScripts = () => {
const editorValues = gridDto.gridOptions.editingFormDto
.flatMap((group) => flattenEditingFormItems([group]))
.reduce<Record<string, any>>((values, formItem) => {
const formItems = gridDto.gridOptions.editingFormDto.flatMap((group) =>
flattenEditingFormItems([group]),
)
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)
if (editorInstance?.option) {
values[formItem.dataField] = editorInstance.option('value')
}
return values
}, {})
},
{},
)
const formData = {
...editingFormDataRef.current,
...(form?.option?.('formData') || {}),
@ -1279,38 +1313,37 @@ const Tree = (props: TreeProps) => {
}
editingFormDataRef.current = { ...formData }
gridDto.gridOptions.editingFormDto
.flatMap((group) => flattenEditingFormItems([group]))
.filter((formItem) =>
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!, {
formData,
e,
editor,
runtimeSetEditorReadOnly,
setFormData,
})
} catch (err) {
console.error('Script exec error on contentReady', formItem.dataField, err)
scriptItems.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!, {
formData,
e,
editor,
runtimeSetEditorReadOnly,
setFormData,
})
} catch (err) {
console.error(
'Script exec error on contentReady',
formItem.dataField,
err,
)
}
})
}
runReadOnlyScripts()
@ -1366,7 +1399,6 @@ const Tree = (props: TreeProps) => {
console.error('Script exec error', err)
}
}
}
},
items:
@ -1388,7 +1420,9 @@ const Tree = (props: TreeProps) => {
let parsedEditorOptions: EditorOptionsWithButtons = {}
const forcedEditorOptions: EditorOptionsWithButtons = {}
try {
parsedEditorOptions = i.editorOptions ? JSON.parse(i.editorOptions) : {}
parsedEditorOptions = i.editorOptions
? JSON.parse(i.editorOptions)
: {}
const rawFilter = searchParams?.get('filter')
if (rawFilter) {
@ -1420,20 +1454,20 @@ const Tree = (props: TreeProps) => {
if (listFormField?.sourceDbType === DbTypeEnum.Date) {
Object.assign(defaultEditorOptions, {
type: 'date',
dateSerializationFormat: 'yyyy-MM-dd',
displayFormat: 'shortDate',
})
type: 'date',
dateSerializationFormat: 'yyyy-MM-dd',
displayFormat: 'shortDate',
})
} else if (
listFormField?.sourceDbType === DbTypeEnum.DateTime ||
listFormField?.sourceDbType === DbTypeEnum.DateTime2 ||
listFormField?.sourceDbType === DbTypeEnum.DateTimeOffset
) {
Object.assign(defaultEditorOptions, {
type: 'datetime',
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
displayFormat: 'shortDateShortTime',
})
type: 'datetime',
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
displayFormat: 'shortDateShortTime',
})
}
editorOptions = {
@ -1636,7 +1670,7 @@ const Tree = (props: TreeProps) => {
)}
<Dialog
width={smaller.md ? '100%' : 1000}
width={useMobileEditPopup ? '100%' : 1000}
isOpen={filterData.isImportModalOpen || false}
onClose={() => filterData.setIsImportModalOpen(false)}
onRequestClose={() => filterData.setIsImportModalOpen(false)}