diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/GridEditingPopupDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/GridEditingPopupDto.cs index 3c62a3e..1debf78 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/GridEditingPopupDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/GridEditingPopupDto.cs @@ -17,5 +17,10 @@ public class GridEditingPopupDto /// popup ekranin küçültülüp büyütülebilmesini saglar /// public bool ResizeEnabled { get; set; } = true; + /// popup ekranin sürüklenebilmesini saglar + public bool DragEnabled { get; set; } = true; + /// popup ekranin kapatildiktan sonra eski konumunu hatirlamasini saglar + /// + public bool RestorePosition { get; set; } = true; } diff --git a/ui/src/index.css b/ui/src/index.css index 72eda6c..1f2a5cb 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -111,4 +111,38 @@ div.dialog-after-open > div.dialog-content.maximized { .dx-datagrid-header-panel { padding: 0 0 !important; -} \ No newline at end of file +} + +@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; + } +} diff --git a/ui/src/proxy/form/models.ts b/ui/src/proxy/form/models.ts index 4f0a8fe..cda9711 100644 --- a/ui/src/proxy/form/models.ts +++ b/ui/src/proxy/form/models.ts @@ -503,6 +503,8 @@ export interface GridEditingPopupDto { fullScreen: boolean hideOnOutsideClick: boolean resizeEnabled: boolean + dragEnabled: boolean + restorePosition: boolean } export interface GridFilterRowDto { diff --git a/ui/src/views/admin/listForm/edit/json-row-operations/EditorScriptBuilderDialog.tsx b/ui/src/views/admin/listForm/edit/json-row-operations/EditorScriptBuilderDialog.tsx index d84e0c5..5a43718 100644 --- a/ui/src/views/admin/listForm/edit/json-row-operations/EditorScriptBuilderDialog.tsx +++ b/ui/src/views/admin/listForm/edit/json-row-operations/EditorScriptBuilderDialog.tsx @@ -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()) { diff --git a/ui/src/views/form/FormDevExpress.tsx b/ui/src/views/form/FormDevExpress.tsx index bd8ae48..72c7213 100644 --- a/ui/src/views/form/FormDevExpress.tsx +++ b/ui/src/views/form/FormDevExpress.tsx @@ -140,7 +140,9 @@ const getValueByField = (data: Record = {}, 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() 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]) @@ -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 ? ( - ( - { - 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', - }} - > - ) : ( - - ) - })} + {(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', + }} + > + ) : ( + + ) + })} ) })} diff --git a/ui/src/views/form/useFormData.tsx b/ui/src/views/form/useFormData.tsx index abae5e1..c187cb4 100644 --- a/ui/src/views/form/useFormData.tsx +++ b/ui/src/views/form/useFormData.tsx @@ -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() const refForm = useRef(null) + const previousFormDataRef = useRef() 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]) diff --git a/ui/src/views/list/Grid.tsx b/ui/src/views/list/Grid.tsx index 614d38a..31a504e 100644 --- a/ui/src/views/list/Grid.tsx +++ b/ui/src/views/list/Grid.tsx @@ -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 = {}, 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() 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) => { /> { 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>((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>( + (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) => { )} filterData.setIsImportModalOpen(false)} onRequestClose={() => filterData.setIsImportModalOpen(false)} diff --git a/ui/src/views/list/Tree.tsx b/ui/src/views/list/Tree.tsx index ef4ffa8..cd9261f 100644 --- a/ui/src/views/list/Tree.tsx +++ b/ui/src/views/list/Tree.tsx @@ -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 = {}, 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() const refListFormCode = useRef('') @@ -1180,7 +1188,7 @@ 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>((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>( + (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) => { )} filterData.setIsImportModalOpen(false)} onRequestClose={() => filterData.setIsImportModalOpen(false)}