@nocobase/client-v2 2.2.0-beta.1 → 2.2.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/es/BaseApplication.d.ts +3 -0
  2. package/es/PluginSettingsManager.d.ts +33 -0
  3. package/es/RouteRepository.d.ts +29 -6
  4. package/es/RouterManager.d.ts +15 -0
  5. package/es/authRedirect.d.ts +1 -0
  6. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  7. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  8. package/es/components/form/JsonTextArea.d.ts +2 -1
  9. package/es/components/form/VariableJsonTextArea.d.ts +19 -0
  10. package/es/components/form/index.d.ts +1 -0
  11. package/es/components/index.d.ts +1 -0
  12. package/es/entry-actions/EntryActionManager.d.ts +24 -0
  13. package/es/entry-actions/index.d.ts +9 -0
  14. package/es/flow/actions/afterSuccess.d.ts +8 -1
  15. package/es/flow/actions/index.d.ts +1 -1
  16. package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
  17. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
  18. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  19. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  20. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
  21. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  22. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -0
  23. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
  24. package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
  25. package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
  26. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  27. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
  28. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  29. package/es/flow/models/base/ActionModelCore.d.ts +2 -0
  30. package/es/flow/models/base/GridModel.d.ts +1 -1
  31. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
  32. package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
  33. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  34. package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
  35. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
  36. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +30 -0
  37. package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
  38. package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
  39. package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
  40. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  41. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
  42. package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
  43. package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
  44. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
  45. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
  46. package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
  47. package/es/flow/routeTransientInputArgs.d.ts +14 -0
  48. package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
  49. package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
  50. package/es/flow/utils/formValueDeps.d.ts +32 -0
  51. package/es/flow-compat/FieldValidation.d.ts +2 -1
  52. package/es/index.d.ts +4 -1
  53. package/es/index.mjs +315 -167
  54. package/es/utils/markdownSanitize.d.ts +11 -0
  55. package/lib/index.js +315 -167
  56. package/package.json +8 -7
  57. package/src/BaseApplication.tsx +12 -5
  58. package/src/PluginSettingsManager.ts +53 -0
  59. package/src/RouteRepository.ts +151 -24
  60. package/src/RouterManager.tsx +142 -1
  61. package/src/__tests__/PluginSettingsManager.test.ts +13 -0
  62. package/src/__tests__/RouteRepository.test.ts +239 -0
  63. package/src/__tests__/RouterManager.test.ts +125 -0
  64. package/src/__tests__/app.test.tsx +55 -0
  65. package/src/__tests__/authRedirect.test.ts +17 -0
  66. package/src/__tests__/exports.test.ts +16 -0
  67. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +199 -0
  68. package/src/__tests__/plugin-manager.test.tsx +44 -2
  69. package/src/__tests__/settings-center.test.tsx +40 -1
  70. package/src/authRedirect.ts +38 -0
  71. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  72. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  73. package/src/collection-manager/field-configure.ts +1 -1
  74. package/src/collection-manager/interfaces/id.ts +1 -1
  75. package/src/collection-manager/interfaces/m2m.tsx +2 -2
  76. package/src/collection-manager/interfaces/m2o.tsx +2 -2
  77. package/src/collection-manager/interfaces/nanoid.ts +1 -1
  78. package/src/collection-manager/interfaces/o2m.tsx +2 -2
  79. package/src/collection-manager/interfaces/obo.tsx +2 -2
  80. package/src/collection-manager/interfaces/oho.tsx +2 -2
  81. package/src/collection-manager/interfaces/properties/index.ts +2 -2
  82. package/src/collection-manager/interfaces/uuid.ts +1 -1
  83. package/src/components/AppComponents.tsx +19 -3
  84. package/src/components/form/JsonTextArea.tsx +21 -11
  85. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +145 -2
  86. package/src/components/form/ScanInput/useCodeScanner.ts +154 -2
  87. package/src/components/form/VariableJsonTextArea.tsx +175 -0
  88. package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
  89. package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
  90. package/src/components/form/index.tsx +1 -0
  91. package/src/components/index.ts +1 -0
  92. package/src/entry-actions/EntryActionManager.ts +76 -0
  93. package/src/entry-actions/index.ts +10 -0
  94. package/src/flow/FlowPage.tsx +38 -3
  95. package/src/flow/__tests__/FlowPage.test.tsx +201 -27
  96. package/src/flow/__tests__/FlowRoute.test.tsx +1088 -105
  97. package/src/flow/__tests__/getKey.test.ts +7 -0
  98. package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
  99. package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
  100. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +120 -0
  101. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1022 -0
  102. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
  103. package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
  104. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
  105. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
  106. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  107. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +164 -0
  108. package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
  109. package/src/flow/actions/afterSuccess.tsx +142 -3
  110. package/src/flow/actions/customVariable.tsx +1 -2
  111. package/src/flow/actions/dataScopeFilter.ts +20 -1
  112. package/src/flow/actions/dateTimeFormat.tsx +42 -28
  113. package/src/flow/actions/index.ts +1 -1
  114. package/src/flow/actions/linkageRules.tsx +123 -35
  115. package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
  116. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  117. package/src/flow/actions/openView.tsx +59 -5
  118. package/src/flow/actions/runjs.tsx +2 -14
  119. package/src/flow/actions/validation.tsx +62 -30
  120. package/src/flow/admin-shell/BaseLayoutModel.tsx +149 -3
  121. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +187 -42
  122. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1020 -110
  123. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +74 -13
  124. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +486 -0
  125. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +64 -8
  126. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  127. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  128. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  129. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  130. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
  131. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
  132. package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
  133. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
  134. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
  135. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  136. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +518 -2
  137. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +214 -2
  138. package/src/flow/admin-shell/admin-layout/index.ts +1 -0
  139. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  140. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  141. package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
  142. package/src/flow/common/Markdown/Display.tsx +14 -3
  143. package/src/flow/common/Markdown/Edit.tsx +19 -7
  144. package/src/flow/components/DefaultValue.tsx +1 -2
  145. package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
  146. package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
  147. package/src/flow/components/FieldAssignValueInput.tsx +15 -11
  148. package/src/flow/components/FlowRoute.tsx +186 -25
  149. package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
  150. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
  151. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +134 -0
  152. package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
  153. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  154. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  155. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  156. package/src/flow/components/fieldAssignOptions.ts +155 -27
  157. package/src/flow/flows/editMarkdownFlow.tsx +1 -1
  158. package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
  159. package/src/flow/index.ts +1 -1
  160. package/src/flow/internal/components/Markdown/util.ts +2 -1
  161. package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
  162. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  163. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  164. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  165. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  166. package/src/flow/models/base/ActionModel.tsx +21 -8
  167. package/src/flow/models/base/ActionModelCore.tsx +11 -4
  168. package/src/flow/models/base/GridModel.tsx +38 -7
  169. package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
  170. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
  171. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  172. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
  173. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  174. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  175. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +32 -3
  176. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
  177. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  178. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
  179. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +111 -0
  180. package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
  181. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
  182. package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
  183. package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
  184. package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
  185. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  186. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  187. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  188. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  189. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
  190. package/src/flow/models/blocks/form/submitHandler.ts +17 -3
  191. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +940 -75
  192. package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
  193. package/src/flow/models/blocks/form/value-runtime/runtime.ts +348 -13
  194. package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
  195. package/src/flow/models/blocks/js-block/JSBlock.tsx +225 -9
  196. package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
  197. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  198. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +38 -14
  199. package/src/flow/models/blocks/table/TableBlockModel.tsx +38 -17
  200. package/src/flow/models/blocks/table/TableColumnModel.tsx +42 -5
  201. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +111 -1
  202. package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
  203. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  204. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
  205. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +210 -1
  206. package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
  207. package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
  208. package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
  209. package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
  210. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +30 -6
  211. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +61 -17
  212. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
  213. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
  214. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -0
  215. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
  216. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
  217. package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
  218. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
  219. package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
  220. package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
  221. package/src/flow/models/fields/InputFieldModel.tsx +40 -2
  222. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  223. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  224. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  225. package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
  226. package/src/flow/models/fields/TextareaFieldModel.tsx +72 -3
  227. package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
  228. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +117 -1
  229. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
  230. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +131 -0
  231. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
  232. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
  233. package/src/flow/models/topbar/TopbarActionModel.tsx +168 -10
  234. package/src/flow/resolveViewParamsToViewList.tsx +11 -3
  235. package/src/flow/routeTransientInputArgs.ts +27 -0
  236. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +300 -0
  237. package/src/flow/utils/dataScopeFormValueClear.ts +218 -81
  238. package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
  239. package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
  240. package/src/flow/utils/formValueDeps.ts +170 -0
  241. package/src/flow-compat/FieldValidation.tsx +122 -60
  242. package/src/flow-compat/Popover.tsx +43 -4
  243. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  244. package/src/index.ts +10 -1
  245. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  246. package/src/nocobase-buildin-plugin/index.tsx +51 -15
  247. package/src/settings-center/AdminSettingsLayout.tsx +10 -2
  248. package/src/settings-center/SystemSettingsPage.tsx +1 -2
  249. package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
  250. package/src/settings-center/plugin-manager/index.tsx +3 -0
  251. package/src/settings-center/utils.tsx +0 -6
  252. package/src/utils/markdownSanitize.ts +88 -0
@@ -10,7 +10,7 @@
10
10
  import { FlowEngine } from '@nocobase/flow-engine';
11
11
  import React from 'react';
12
12
  import { beforeEach, describe, expect, it, vi } from 'vitest';
13
- import { GridModel } from '../GridModel';
13
+ import { GridModel, transformRowsToSingleColumn } from '../GridModel';
14
14
 
15
15
  const createMockRect = ({ top, left, width, height }: { top: number; left: number; width: number; height: number }) => {
16
16
  return {
@@ -66,6 +66,83 @@ const createGridContainer = () => {
66
66
  return container;
67
67
  };
68
68
 
69
+ const createGridContainerWithEmptyColumn = () => {
70
+ const container = document.createElement('div');
71
+ container.setAttribute('data-grid-root', '');
72
+ const row = document.createElement('div');
73
+ row.setAttribute('data-grid-row-id', 'row-1');
74
+ container.appendChild(row);
75
+
76
+ const firstColumn = document.createElement('div');
77
+ firstColumn.setAttribute('data-grid-column-row-id', 'row-1');
78
+ firstColumn.setAttribute('data-grid-column-index', '0');
79
+ row.appendChild(firstColumn);
80
+
81
+ const item = document.createElement('div');
82
+ item.setAttribute('data-grid-item-row-id', 'row-1');
83
+ item.setAttribute('data-grid-column-index', '0');
84
+ item.setAttribute('data-grid-item-index', '0');
85
+ item.setAttribute('data-grid-item-uid', 'item-1');
86
+ firstColumn.appendChild(item);
87
+
88
+ const secondColumn = document.createElement('div');
89
+ secondColumn.setAttribute('data-grid-column-row-id', 'row-1');
90
+ secondColumn.setAttribute('data-grid-column-index', '1');
91
+ row.appendChild(secondColumn);
92
+
93
+ mockRect(container, { top: 0, left: 0, width: 480, height: 280 });
94
+ mockRect(row, { top: 20, left: 20, width: 440, height: 120 });
95
+ mockRect(firstColumn, { top: 20, left: 20, width: 220, height: 120 });
96
+ mockRect(item, { top: 30, left: 30, width: 200, height: 100 });
97
+ mockRect(secondColumn, { top: 20, left: 260, width: 200, height: 120 });
98
+ return container;
99
+ };
100
+
101
+ const createProjectedMobileGridContainer = (rows: Record<string, string[][]>) => {
102
+ const container = document.createElement('div');
103
+ container.setAttribute('data-grid-root', '');
104
+ mockRect(container, { top: 0, left: 0, width: 480, height: 320 });
105
+
106
+ let top = 20;
107
+ Object.entries(transformRowsToSingleColumn(rows)).forEach(([rowId, columns]) => {
108
+ const row = document.createElement('div');
109
+ row.setAttribute('data-grid-row-id', rowId);
110
+ container.appendChild(row);
111
+
112
+ const column = document.createElement('div');
113
+ column.setAttribute('data-grid-column-row-id', rowId);
114
+ column.setAttribute('data-grid-column-index', '0');
115
+ row.appendChild(column);
116
+
117
+ const items = columns[0] || [];
118
+ items.forEach((itemUid, itemIndex) => {
119
+ const item = document.createElement('div');
120
+ item.setAttribute('data-grid-item-row-id', rowId);
121
+ item.setAttribute('data-grid-column-index', '0');
122
+ item.setAttribute('data-grid-item-index', String(itemIndex));
123
+ item.setAttribute('data-grid-item-uid', itemUid);
124
+ column.appendChild(item);
125
+ mockRect(item, { top: top + 10 + itemIndex * 36, left: 30, width: 420, height: 32 });
126
+ });
127
+
128
+ mockRect(row, { top, left: 20, width: 440, height: 52 });
129
+ mockRect(column, { top, left: 20, width: 440, height: 52 });
130
+ top += 72;
131
+ });
132
+
133
+ return container;
134
+ };
135
+
136
+ const setMobileGridItems = (engine: FlowEngine, model: GridModel, itemUids: string[]) => {
137
+ (model as any).subModels = {
138
+ items: itemUids.map((itemUid) => engine.createModel({ use: 'FlowModel', uid: itemUid })),
139
+ };
140
+ };
141
+
142
+ const getSavedTopLevelItemOrder = (model: GridModel) => {
143
+ return Object.values(model.props.rows || {}).map((columns: string[][]) => columns[0]?.[0]);
144
+ };
145
+
69
146
  describe('GridModel drag snapshot container', () => {
70
147
  let engine: FlowEngine;
71
148
 
@@ -106,6 +183,167 @@ describe('GridModel drag snapshot container', () => {
106
183
  model.handleDragCancel({} as any);
107
184
  });
108
185
 
186
+ it('filters multi-column drop slots from mobile drag snapshots', () => {
187
+ const model = engine.createModel<GridModel>({
188
+ use: 'GridModel',
189
+ uid: 'grid-mobile-drag-slots',
190
+ props: {
191
+ rows: {
192
+ 'row-1': [['item-1'], []],
193
+ },
194
+ sizes: {
195
+ 'row-1': [12, 12],
196
+ },
197
+ },
198
+ structure: {} as any,
199
+ });
200
+ model.context.defineProperty('isMobileLayout', { value: true });
201
+
202
+ const container = createGridContainerWithEmptyColumn();
203
+ (model.gridContainerRef as any).current = container;
204
+
205
+ model.handleDragStart({
206
+ active: { id: 'item-1' },
207
+ activatorEvent: { clientX: 80, clientY: 80 },
208
+ } as any);
209
+
210
+ const slotTypes = ((model as any).dragState?.slots || []).map((slot) => slot.type);
211
+ expect(slotTypes).toContain('column');
212
+ expect(slotTypes).toContain('row-gap');
213
+ expect(slotTypes).not.toContain('column-edge');
214
+ expect(slotTypes).not.toContain('item-edge');
215
+ expect(slotTypes).not.toContain('empty-column');
216
+ model.handleDragCancel({} as any);
217
+ });
218
+
219
+ it('keeps empty-row slot available for the first mobile grid item', () => {
220
+ const model = engine.createModel<GridModel>({
221
+ use: 'GridModel',
222
+ uid: 'grid-mobile-empty-drag-slots',
223
+ props: {},
224
+ structure: {} as any,
225
+ });
226
+ model.context.defineProperty('isMobileLayout', { value: true });
227
+
228
+ const container = document.createElement('div');
229
+ mockRect(container, { top: 0, left: 0, width: 480, height: 280 });
230
+ (model.gridContainerRef as any).current = container;
231
+
232
+ model.handleDragStart({
233
+ active: { id: 'item-1' },
234
+ activatorEvent: { clientX: 80, clientY: 80 },
235
+ } as any);
236
+
237
+ expect(((model as any).dragState?.slots || []).map((slot) => slot.type)).toEqual(['empty-row']);
238
+ model.handleDragCancel({} as any);
239
+ });
240
+
241
+ it('saves mobile vertical sorting when moving the second item above the first one', () => {
242
+ const rows = {
243
+ 'row-a': [['item-a']],
244
+ 'row-b': [['item-b']],
245
+ };
246
+ const model = engine.createModel<GridModel>({
247
+ use: 'GridModel',
248
+ uid: 'grid-mobile-sort-second-above-first',
249
+ props: {
250
+ rows,
251
+ sizes: {
252
+ 'row-a': [24],
253
+ 'row-b': [24],
254
+ },
255
+ rowOrder: ['row-a', 'row-b'],
256
+ },
257
+ structure: {} as any,
258
+ });
259
+ model.context.defineProperty('isMobileLayout', { value: true });
260
+ setMobileGridItems(engine, model, ['item-a', 'item-b']);
261
+ (model as any).saveStepParams = vi.fn();
262
+
263
+ const container = createProjectedMobileGridContainer(rows);
264
+ (model.gridContainerRef as any).current = container;
265
+
266
+ model.handleDragStart({
267
+ active: { id: 'item-b' },
268
+ activatorEvent: { clientX: 240, clientY: 118 },
269
+ } as any);
270
+ model.handleDragEnd({
271
+ delta: { x: 0, y: -112 },
272
+ } as any);
273
+
274
+ expect(getSavedTopLevelItemOrder(model)).toEqual(['item-b', 'item-a']);
275
+ });
276
+
277
+ it('saves mobile vertical sorting when moving the first item below the last one', () => {
278
+ const rows = {
279
+ 'row-a': [['item-a']],
280
+ 'row-b': [['item-b']],
281
+ 'row-c': [['item-c']],
282
+ };
283
+ const model = engine.createModel<GridModel>({
284
+ use: 'GridModel',
285
+ uid: 'grid-mobile-sort-first-below-last',
286
+ props: {
287
+ rows,
288
+ sizes: {
289
+ 'row-a': [24],
290
+ 'row-b': [24],
291
+ 'row-c': [24],
292
+ },
293
+ rowOrder: ['row-a', 'row-b', 'row-c'],
294
+ },
295
+ structure: {} as any,
296
+ });
297
+ model.context.defineProperty('isMobileLayout', { value: true });
298
+ setMobileGridItems(engine, model, ['item-a', 'item-b', 'item-c']);
299
+ (model as any).saveStepParams = vi.fn();
300
+
301
+ const container = createProjectedMobileGridContainer(rows);
302
+ (model.gridContainerRef as any).current = container;
303
+
304
+ model.handleDragStart({
305
+ active: { id: 'item-a' },
306
+ activatorEvent: { clientX: 240, clientY: 46 },
307
+ } as any);
308
+ model.handleDragEnd({
309
+ delta: { x: 0, y: 184 },
310
+ } as any);
311
+
312
+ expect(getSavedTopLevelItemOrder(model)).toEqual(['item-b', 'item-c', 'item-a']);
313
+ });
314
+
315
+ it('does not filter multi-column drop slots from desktop drag snapshots', () => {
316
+ const model = engine.createModel<GridModel>({
317
+ use: 'GridModel',
318
+ uid: 'grid-desktop-drag-slots',
319
+ props: {
320
+ rows: {
321
+ 'row-1': [['item-1'], []],
322
+ },
323
+ sizes: {
324
+ 'row-1': [12, 12],
325
+ },
326
+ },
327
+ structure: {} as any,
328
+ });
329
+
330
+ const container = createGridContainerWithEmptyColumn();
331
+ (model.gridContainerRef as any).current = container;
332
+
333
+ model.handleDragStart({
334
+ active: { id: 'item-1' },
335
+ activatorEvent: { clientX: 80, clientY: 80 },
336
+ } as any);
337
+
338
+ const slotTypes = ((model as any).dragState?.slots || []).map((slot) => slot.type);
339
+ expect(slotTypes).toContain('column');
340
+ expect(slotTypes).toContain('row-gap');
341
+ expect(slotTypes).toContain('column-edge');
342
+ expect(slotTypes).toContain('item-edge');
343
+ expect(slotTypes).toContain('empty-column');
344
+ model.handleDragCancel({} as any);
345
+ });
346
+
109
347
  it('uses native pointer position instead of scroll-adjusted drag delta', () => {
110
348
  const model = engine.createModel<GridModel>({
111
349
  use: 'GridModel',
@@ -35,6 +35,36 @@ describe('transformRowsToSingleColumn', () => {
35
35
  expect(all).toEqual([['a'], ['b', 'c'], ['d'], ['e']]);
36
36
  });
37
37
 
38
+ it('should preserve row ids for ordinary single-column mobile rows', () => {
39
+ const rows = {
40
+ row_a: [['a']],
41
+ row_b: [['b']],
42
+ };
43
+
44
+ expect(transformRowsToSingleColumn(rows)).toEqual(rows);
45
+ });
46
+
47
+ it('should avoid adding another mobile prefix to ordinary single-column projected rows', () => {
48
+ const rows = {
49
+ 'mobile:row_a:0': [['a']],
50
+ };
51
+
52
+ expect(Object.keys(transformRowsToSingleColumn(rows))).toEqual(['mobile:row_a:0']);
53
+ });
54
+
55
+ it('should keep split multi-column row keys stable and unique', () => {
56
+ const rows = {
57
+ row_a: [['a'], ['b']],
58
+ };
59
+ const result = transformRowsToSingleColumn(rows);
60
+ const keys = Object.keys(result);
61
+
62
+ expect(keys).toHaveLength(2);
63
+ expect(new Set(keys).size).toBe(2);
64
+ expect(keys.every((key) => key.startsWith('mobile:'))).toBe(true);
65
+ expect(Object.values(result).map((row) => row[0])).toEqual([['a'], ['b']]);
66
+ });
67
+
38
68
  it('should skip columns that only contain EMPTY_COLUMN_UID', () => {
39
69
  const rows = buildRows([[[EMPTY_COLUMN_UID], ['a']]]);
40
70
  const result = transformRowsToSingleColumn(rows);
@@ -55,4 +85,22 @@ describe('transformRowsToSingleColumn', () => {
55
85
  const all = Object.values(result).map((r) => r[0]);
56
86
  expect(all).toEqual([['x'], ['y']]);
57
87
  });
88
+
89
+ it('should keep generated row keys stable for the same input', () => {
90
+ const rows = buildRows([
91
+ [['a'], ['b']],
92
+ [['c'], ['d']],
93
+ ]);
94
+
95
+ expect(Object.keys(transformRowsToSingleColumn(rows))).toEqual(Object.keys(transformRowsToSingleColumn(rows)));
96
+ });
97
+
98
+ it('should keep existing row keys stable when appending new mobile rows', () => {
99
+ const before = buildRows([[['a'], ['b']]]);
100
+ const after = buildRows([[['a'], ['b']], [['c']]]);
101
+ const beforeKeys = Object.keys(transformRowsToSingleColumn(before));
102
+ const afterKeys = Object.keys(transformRowsToSingleColumn(after));
103
+
104
+ expect(afterKeys.slice(0, beforeKeys.length)).toEqual(beforeKeys);
105
+ });
58
106
  });
@@ -17,7 +17,6 @@ import {
17
17
  Droppable,
18
18
  isRunJSValue,
19
19
  normalizeRunJSValue,
20
- runjsWithSafeGlobals,
21
20
  tExpr,
22
21
  FlowModelRenderer,
23
22
  FlowSettingsButton,
@@ -231,6 +230,7 @@ export class FilterFormBlockModel extends FilterBlockModel<{
231
230
  private initialDefaultsPromise?: Promise<void>;
232
231
  private initialRefreshHandledTargetIds = new Set<string>();
233
232
  private lastDefaultValueByFieldName = new Map<string, any>();
233
+ private userEditedFieldNames = new Set<string>();
234
234
  private defaultValuesRefreshSeq = 0;
235
235
 
236
236
  get form() {
@@ -431,11 +431,34 @@ export class FilterFormBlockModel extends FilterBlockModel<{
431
431
 
432
432
  private canApplyFormDefaultValue(name: string, current: any, force?: boolean) {
433
433
  if (force) return true;
434
+ if (this.userEditedFieldNames.has(name)) return false;
434
435
  if (isEmptyValue(current)) return true;
435
436
  if (!this.lastDefaultValueByFieldName.has(name)) return false;
436
437
  return isEqual(current, this.lastDefaultValueByFieldName.get(name));
437
438
  }
438
439
 
440
+ private canApplyFormOverrideValue(name: string, force?: boolean) {
441
+ if (force) return true;
442
+ return !this.userEditedFieldNames.has(name);
443
+ }
444
+
445
+ private normalizeFieldValueMode(mode: unknown): 'default' | 'assign' | 'override' {
446
+ if (mode === 'assign') return 'assign';
447
+ if (mode === 'override') return 'override';
448
+ return 'default';
449
+ }
450
+
451
+ private markFilterFormUserEditedFields(changedValues: any) {
452
+ if (!changedValues || typeof changedValues !== 'object' || Array.isArray(changedValues)) return;
453
+ for (const name of Object.keys(changedValues)) {
454
+ this.userEditedFieldNames.add(String(name));
455
+ }
456
+ }
457
+
458
+ private resetFilterFormUserEditedFields() {
459
+ this.userEditedFieldNames.clear();
460
+ }
461
+
439
462
  private async matchDefaultValueCondition(condition: any) {
440
463
  if (!condition) return true;
441
464
 
@@ -458,6 +481,10 @@ export class FilterFormBlockModel extends FilterBlockModel<{
458
481
  if (!form) return appliedValues;
459
482
 
460
483
  const force = options?.force === true;
484
+ if (force) {
485
+ this.resetFilterFormUserEditedFields();
486
+ }
487
+
461
488
  const params = this.getStepParams?.('formFilterBlockModelSettings', 'defaultValues');
462
489
  const rules = (params?.value || []) as any[];
463
490
  if (!Array.isArray(rules) || rules.length === 0) return appliedValues;
@@ -466,7 +493,7 @@ export class FilterFormBlockModel extends FilterBlockModel<{
466
493
  // RunJS support
467
494
  if (isRunJSValue(raw)) {
468
495
  const { code, version } = normalizeRunJSValue(raw);
469
- const ret = await runjsWithSafeGlobals(this.context, code, { version });
496
+ const ret = await this.context.runjs(code, undefined, { version });
470
497
  return ret?.success ? ret.value : undefined;
471
498
  }
472
499
 
@@ -499,8 +526,9 @@ export class FilterFormBlockModel extends FilterBlockModel<{
499
526
 
500
527
  const operator = getDefaultOperator(itemModel as any);
501
528
  const normalized = normalizeFilterValueByOperator(operator, resolved);
502
- const mode = String(rule.mode || 'default') === 'assign' ? 'assign' : 'default';
529
+ const mode = this.normalizeFieldValueMode(rule.mode);
503
530
  if (mode === 'default' && !this.canApplyFormDefaultValue(String(name), current, force)) continue;
531
+ if (mode === 'override' && !this.canApplyFormOverrideValue(String(name), force)) continue;
504
532
  if (isEqual(current, normalized)) {
505
533
  if (mode === 'default') {
506
534
  this.lastDefaultValueByFieldName.set(String(name), normalized);
@@ -527,6 +555,7 @@ export class FilterFormBlockModel extends FilterBlockModel<{
527
555
  }
528
556
 
529
557
  private handleFilterFormValuesChange(changedValues: any, allValues: any) {
558
+ this.markFilterFormUserEditedFields(changedValues);
530
559
  const refreshSeq = ++this.defaultValuesRefreshSeq;
531
560
  void (async () => {
532
561
  const appliedValues = await this.applyFormDefaultValues({ refreshSeq });
@@ -62,9 +62,25 @@ function isRecord(value: unknown): value is Record<string, unknown> {
62
62
  return !!value && typeof value === 'object' && !Array.isArray(value);
63
63
  }
64
64
 
65
+ interface CollectionFieldWithAssociationMarker {
66
+ isAssociationField?: () => boolean;
67
+ target?: unknown;
68
+ }
69
+
70
+ function hasAssociationMarker(value: unknown): value is CollectionFieldWithAssociationMarker {
71
+ return isRecord(value) && (typeof value.isAssociationField === 'function' || 'target' in value);
72
+ }
73
+
74
+ const isAssociationCollectionField = (collectionField: unknown) => {
75
+ if (!hasAssociationMarker(collectionField)) {
76
+ return false;
77
+ }
78
+ return collectionField.isAssociationField ? collectionField.isAssociationField() : Boolean(collectionField.target);
79
+ };
80
+
65
81
  const normalizeAssociationDefaultFilterValue = (value: any, fieldModel: any) => {
66
82
  const collectionField = fieldModel?.context?.collectionField;
67
- if (!collectionField?.isAssociationField?.()) {
83
+ if (!isAssociationCollectionField(collectionField)) {
68
84
  return value;
69
85
  }
70
86
 
@@ -171,8 +187,7 @@ const buildFilterFormFieldItem = ({
171
187
  if (!binding) {
172
188
  return;
173
189
  }
174
- const isAssociation =
175
- typeof field?.isAssociationField === 'function' ? field.isAssociationField() : Boolean(field?.target);
190
+ const isAssociation = isAssociationCollectionField(field);
176
191
  const fieldModel =
177
192
  isAssociation && ctxWithFlags.engine?.getModelClass?.('FilterFormRecordSelectFieldModel')
178
193
  ? 'FilterFormRecordSelectFieldModel'
@@ -507,10 +522,7 @@ export class FilterFormItemModel extends FilterableItemModel<{
507
522
  const formValue = this.context.form?.getFieldValue(this.props.name);
508
523
  const modelValue = fieldModel.getFilterValue ? fieldModel.getFilterValue() : formValue;
509
524
  const collectionField = (fieldModel as any)?.context?.collectionField;
510
- const isAssociationField =
511
- typeof collectionField?.isAssociationField === 'function'
512
- ? collectionField.isAssociationField()
513
- : !!collectionField?.target;
525
+ const isAssociationField = isAssociationCollectionField(collectionField);
514
526
  const shouldUseFormValue =
515
527
  isAssociationField &&
516
528
  !this.mounted &&
@@ -547,11 +559,7 @@ export class FilterFormItemModel extends FilterableItemModel<{
547
559
  return value;
548
560
  }
549
561
  const collectionField = (fieldModel as any)?.context?.collectionField;
550
- const isAssociation =
551
- typeof collectionField?.isAssociationField === 'function'
552
- ? collectionField.isAssociationField()
553
- : !!collectionField?.target;
554
- if (!isAssociation) {
562
+ if (!isAssociationCollectionField(collectionField)) {
555
563
  return value;
556
564
  }
557
565
  const normalizedFieldNames = normalizeAssociationFieldNames(
@@ -798,6 +806,13 @@ FilterFormItemModel.registerFlow({
798
806
  },
799
807
  defaultOperator: {
800
808
  use: 'defaultOperator',
809
+ hideInSettings(ctx) {
810
+ const collectionField =
811
+ ctx.collectionField ||
812
+ ctx.model?.context?.collectionField ||
813
+ ctx.model?.subModels?.field?.context?.collectionField;
814
+ return isAssociationCollectionField(collectionField);
815
+ },
801
816
  },
802
817
  operatorComponentProps: {
803
818
  use: 'operatorComponentProps',
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { createSafeDocument, createSafeWindow, createSafeNavigator, tExpr } from '@nocobase/flow-engine';
10
+ import { tExpr } from '@nocobase/flow-engine';
11
11
  import { CodeEditor } from '../../../components/code-editor';
12
12
  import { FilterFormActionModel } from './FilterFormActionModel';
13
13
  import { resolveRunJsParams } from '../../utils/resolveRunJsParams';
@@ -60,12 +60,7 @@ FilterFormJSActionModel.registerFlow({
60
60
  },
61
61
  async handler(ctx, params) {
62
62
  const { code, version } = resolveRunJsParams(ctx, params);
63
- const navigator = createSafeNavigator();
64
- await ctx.runjs(
65
- code,
66
- { window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
67
- { version },
68
- );
63
+ await ctx.runjs(code, undefined, { version });
69
64
  },
70
65
  },
71
66
  },
@@ -33,6 +33,42 @@ class DummyCollectionBlockModel extends CollectionBlockModel {
33
33
  }
34
34
 
35
35
  describe('FilterFormItemModel defineChildren association fields', () => {
36
+ it('hides default operator setting for association filter fields', () => {
37
+ const engine = new FlowEngine();
38
+ engine.registerModels({
39
+ FilterFormItemModel,
40
+ });
41
+
42
+ const filterItem = engine.createModel<FilterFormItemModel>({
43
+ uid: 'association-filter-item-settings',
44
+ use: 'FilterFormItemModel',
45
+ });
46
+
47
+ const defaultOperatorStep = filterItem.getFlow('filterFormItemSettings')?.steps?.defaultOperator as {
48
+ hideInSettings?: (ctx: {
49
+ collectionField?: unknown;
50
+ model?: {
51
+ subModels?: {
52
+ field?: {
53
+ context?: {
54
+ collectionField?: unknown;
55
+ };
56
+ };
57
+ };
58
+ };
59
+ }) => boolean;
60
+ };
61
+ expect(defaultOperatorStep?.hideInSettings?.({ collectionField: { isAssociationField: () => true } })).toBe(true);
62
+ expect(
63
+ defaultOperatorStep?.hideInSettings?.({
64
+ model: { subModels: { field: { context: { collectionField: { target: 'departments' } } } } },
65
+ }),
66
+ ).toBe(true);
67
+ expect(defaultOperatorStep?.hideInSettings?.({ collectionField: { interface: 'input', type: 'string' } })).toBe(
68
+ false,
69
+ );
70
+ });
71
+
36
72
  it('groups association target fields and supports recursive paths', async () => {
37
73
  const engine = new FlowEngine();
38
74
  engine.registerModels({
@@ -50,6 +50,7 @@ function createFilterFormDefaultValuesModel(rules: any[], initialValues: Record<
50
50
  const model = {
51
51
  defaultValuesRefreshSeq: 0,
52
52
  lastDefaultValueByFieldName: new Map<string, any>(),
53
+ userEditedFieldNames: new Set<string>(),
53
54
  form: {
54
55
  getFieldsValue: () => ({ ...values }),
55
56
  getFieldValue: (name: string) => values[name],
@@ -86,6 +87,10 @@ function createFilterFormDefaultValuesModel(rules: any[], initialValues: Record<
86
87
  return undefined;
87
88
  }),
88
89
  canApplyFormDefaultValue: (FilterFormBlockModel.prototype as any).canApplyFormDefaultValue,
90
+ canApplyFormOverrideValue: (FilterFormBlockModel.prototype as any).canApplyFormOverrideValue,
91
+ normalizeFieldValueMode: (FilterFormBlockModel.prototype as any).normalizeFieldValueMode,
92
+ markFilterFormUserEditedFields: (FilterFormBlockModel.prototype as any).markFilterFormUserEditedFields,
93
+ resetFilterFormUserEditedFields: (FilterFormBlockModel.prototype as any).resetFilterFormUserEditedFields,
89
94
  matchDefaultValueCondition: (FilterFormBlockModel.prototype as any).matchDefaultValueCondition,
90
95
  applyFormDefaultValues: FilterFormBlockModel.prototype.applyFormDefaultValues,
91
96
  handleFilterFormValuesChange: (FilterFormBlockModel.prototype as any).handleFilterFormValuesChange,
@@ -296,6 +301,40 @@ describe('filter-form defaultValues wiring', () => {
296
301
  expect(values.username_user).toBe('Manual');
297
302
  });
298
303
 
304
+ it('does not reapply a filter form default value after user clears the field', async () => {
305
+ const { model, values } = createFilterFormDefaultValuesModel([
306
+ {
307
+ key: 'username-default',
308
+ enable: true,
309
+ targetPath: 'username',
310
+ mode: 'default',
311
+ value: 'admin',
312
+ },
313
+ ]);
314
+
315
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
316
+ expect(values.username_user).toBe('admin');
317
+
318
+ values.username_user = undefined;
319
+ (model as any).handleFilterFormValuesChange({ username_user: undefined }, { username_user: undefined });
320
+
321
+ await waitFor(() => {
322
+ expect(model.dispatchEvent).toHaveBeenCalledWith(
323
+ 'formValuesChange',
324
+ {
325
+ changedValues: {
326
+ username_user: undefined,
327
+ },
328
+ allValues: {
329
+ username_user: undefined,
330
+ },
331
+ },
332
+ { debounce: true },
333
+ );
334
+ });
335
+ expect(values.username_user).toBeUndefined();
336
+ });
337
+
299
338
  it('applies fixed values even when the target filter field already has a value', async () => {
300
339
  const { model, values } = createFilterFormDefaultValuesModel(
301
340
  [
@@ -315,6 +354,78 @@ describe('filter-form defaultValues wiring', () => {
315
354
  expect(values.username_user).toBe('Bob');
316
355
  });
317
356
 
357
+ it('applies override values until the target filter field is changed by user', async () => {
358
+ const { model, values } = createFilterFormDefaultValuesModel(
359
+ [
360
+ {
361
+ key: 'username-override',
362
+ enable: true,
363
+ targetPath: 'username',
364
+ mode: 'override',
365
+ value: '{{ ctx.formValues.nickname_nick }}',
366
+ },
367
+ ],
368
+ { nickname_nick: 'Bob', username_user: 'Manual' },
369
+ );
370
+
371
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
372
+ expect(values.username_user).toBe('Bob');
373
+
374
+ values.nickname_nick = 'Carol';
375
+ model.defaultValuesRefreshSeq += 1;
376
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any, {
377
+ refreshSeq: model.defaultValuesRefreshSeq,
378
+ });
379
+ expect(values.username_user).toBe('Carol');
380
+
381
+ values.username_user = 'User value';
382
+ (model as any).handleFilterFormValuesChange(
383
+ { username_user: 'User value' },
384
+ { nickname_nick: 'Carol', username_user: 'User value' },
385
+ );
386
+
387
+ await waitFor(() => {
388
+ expect(values.username_user).toBe('User value');
389
+ });
390
+
391
+ values.nickname_nick = 'Dora';
392
+ model.defaultValuesRefreshSeq += 1;
393
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any, {
394
+ refreshSeq: model.defaultValuesRefreshSeq,
395
+ });
396
+ expect(values.username_user).toBe('User value');
397
+ });
398
+
399
+ it('clears filter form user-edited override state on forced apply', async () => {
400
+ const { model, values } = createFilterFormDefaultValuesModel(
401
+ [
402
+ {
403
+ key: 'username-override',
404
+ enable: true,
405
+ targetPath: 'username',
406
+ mode: 'override',
407
+ condition: {
408
+ logic: '$and',
409
+ items: [{ path: '{{ ctx.formValues.nickname_nick }}', operator: '$eq', value: 'allow' }],
410
+ },
411
+ value: '{{ ctx.formValues.nickname_nick }}',
412
+ },
413
+ ],
414
+ { nickname_nick: 'deny', username_user: 'User value' },
415
+ );
416
+
417
+ model.userEditedFieldNames.add('username_user');
418
+
419
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any, { force: true });
420
+ expect(values.username_user).toBe('User value');
421
+ expect(model.userEditedFieldNames.has('username_user')).toBe(false);
422
+
423
+ values.nickname_nick = 'allow';
424
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
425
+
426
+ expect(values.username_user).toBe('allow');
427
+ });
428
+
318
429
  it('skips filter form field values when the rule condition does not match', async () => {
319
430
  const { model, values } = createFilterFormDefaultValuesModel(
320
431
  [