@nocobase/client-v2 2.2.0-alpha.1 → 2.2.0-alpha.3

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 (220) hide show
  1. package/es/PluginSettingsManager.d.ts +33 -0
  2. package/es/RouteRepository.d.ts +21 -6
  3. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  4. package/es/components/category-tabs/index.d.ts +9 -0
  5. package/es/components/form/JsonTextArea.d.ts +2 -1
  6. package/es/components/form/TypedVariableInput.d.ts +22 -4
  7. package/es/components/form/VariableJsonTextArea.d.ts +19 -0
  8. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  9. package/es/components/form/filter/index.d.ts +2 -0
  10. package/es/components/form/index.d.ts +1 -0
  11. package/es/components/index.d.ts +2 -0
  12. package/es/flow/actions/afterSuccess.d.ts +8 -1
  13. package/es/flow/actions/index.d.ts +1 -1
  14. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  15. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  16. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
  17. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  18. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -0
  19. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  20. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  21. package/es/flow/components/FieldAssignValueInput.d.ts +2 -0
  22. package/es/flow/index.d.ts +1 -0
  23. package/es/flow/models/base/GridModel.d.ts +1 -1
  24. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
  25. package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
  26. package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
  27. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
  28. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +30 -0
  29. package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
  30. package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
  31. package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
  32. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  33. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
  34. package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
  35. package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
  36. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
  37. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
  38. package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
  39. package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
  40. package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
  41. package/es/flow/utils/formValueDeps.d.ts +32 -0
  42. package/es/flow-compat/FieldValidation.d.ts +2 -1
  43. package/es/index.d.ts +2 -1
  44. package/es/index.mjs +233 -137
  45. package/lib/index.js +234 -138
  46. package/package.json +7 -7
  47. package/src/PluginSettingsManager.ts +53 -0
  48. package/src/RouteRepository.ts +126 -24
  49. package/src/__tests__/PluginSettingsManager.test.ts +13 -0
  50. package/src/__tests__/RouteRepository.test.ts +216 -0
  51. package/src/__tests__/browserChecker.test.ts +61 -0
  52. package/src/__tests__/exports.test.ts +16 -0
  53. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +199 -0
  54. package/src/__tests__/plugin-manager.test.tsx +44 -2
  55. package/src/__tests__/settings-center.test.tsx +40 -1
  56. package/src/collection-manager/field-configure.ts +1 -1
  57. package/src/collection-manager/interfaces/id.ts +1 -1
  58. package/src/collection-manager/interfaces/m2m.tsx +2 -2
  59. package/src/collection-manager/interfaces/m2o.tsx +2 -2
  60. package/src/collection-manager/interfaces/nanoid.ts +1 -1
  61. package/src/collection-manager/interfaces/o2m.tsx +2 -2
  62. package/src/collection-manager/interfaces/obo.tsx +2 -2
  63. package/src/collection-manager/interfaces/oho.tsx +2 -2
  64. package/src/collection-manager/interfaces/properties/index.ts +2 -2
  65. package/src/collection-manager/interfaces/uuid.ts +1 -1
  66. package/src/components/AppComponents.tsx +19 -3
  67. package/src/components/README.md +7 -1
  68. package/src/components/README.zh-CN.md +6 -1
  69. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  70. package/src/components/category-tabs/index.ts +10 -0
  71. package/src/components/form/JsonTextArea.tsx +21 -11
  72. package/src/components/form/TypedVariableInput.tsx +416 -93
  73. package/src/components/form/VariableJsonTextArea.tsx +175 -0
  74. package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
  75. package/src/components/form/__tests__/TypedVariableInput.test.tsx +268 -9
  76. package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
  77. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  78. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  79. package/src/components/form/filter/index.ts +2 -0
  80. package/src/components/form/index.tsx +1 -0
  81. package/src/components/index.ts +2 -0
  82. package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
  83. package/src/flow/__tests__/getKey.test.ts +7 -0
  84. package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
  85. package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
  86. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +58 -0
  87. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1022 -0
  88. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
  89. package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
  90. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
  91. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
  92. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +104 -0
  93. package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
  94. package/src/flow/actions/afterSuccess.tsx +142 -3
  95. package/src/flow/actions/customVariable.tsx +1 -2
  96. package/src/flow/actions/dataScopeFilter.ts +10 -0
  97. package/src/flow/actions/dateTimeFormat.tsx +42 -28
  98. package/src/flow/actions/index.ts +1 -1
  99. package/src/flow/actions/linkageRules.tsx +123 -35
  100. package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
  101. package/src/flow/actions/openView.tsx +38 -4
  102. package/src/flow/actions/runjs.tsx +2 -14
  103. package/src/flow/actions/validation.tsx +62 -30
  104. package/src/flow/admin-shell/BaseLayoutModel.tsx +25 -3
  105. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +5 -2
  106. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +13 -0
  107. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +34 -10
  108. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
  109. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
  110. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  111. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  112. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  113. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  114. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
  115. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
  116. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
  117. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  118. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +82 -0
  119. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
  120. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  121. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  122. package/src/flow/components/DefaultValue.tsx +1 -2
  123. package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
  124. package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
  125. package/src/flow/components/FieldAssignValueInput.tsx +10 -5
  126. package/src/flow/components/FlowRoute.tsx +56 -11
  127. package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
  128. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
  129. package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
  130. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  131. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  132. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  133. package/src/flow/components/fieldAssignOptions.ts +155 -27
  134. package/src/flow/flows/editMarkdownFlow.tsx +1 -1
  135. package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
  136. package/src/flow/index.ts +2 -1
  137. package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
  138. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  139. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  140. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  141. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  142. package/src/flow/models/base/ActionModel.tsx +12 -1
  143. package/src/flow/models/base/ActionModelCore.tsx +6 -4
  144. package/src/flow/models/base/GridModel.tsx +38 -7
  145. package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
  146. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
  147. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  148. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
  149. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  150. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  151. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +31 -3
  152. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  153. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
  154. package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
  155. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
  156. package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
  157. package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
  158. package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
  159. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  160. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  161. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
  162. package/src/flow/models/blocks/form/submitHandler.ts +17 -3
  163. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +940 -75
  164. package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
  165. package/src/flow/models/blocks/form/value-runtime/runtime.ts +348 -13
  166. package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
  167. package/src/flow/models/blocks/js-block/JSBlock.tsx +225 -9
  168. package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
  169. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  170. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +36 -13
  171. package/src/flow/models/blocks/table/TableBlockModel.tsx +38 -17
  172. package/src/flow/models/blocks/table/TableColumnModel.tsx +36 -3
  173. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +111 -1
  174. package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
  175. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  176. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
  177. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +159 -1
  178. package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
  179. package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
  180. package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
  181. package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
  182. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +29 -6
  183. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +16 -4
  184. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
  185. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
  186. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -0
  187. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
  188. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
  189. package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
  190. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
  191. package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
  192. package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
  193. package/src/flow/models/fields/InputFieldModel.tsx +40 -2
  194. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  195. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  196. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  197. package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
  198. package/src/flow/models/fields/TextareaFieldModel.tsx +49 -3
  199. package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
  200. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +117 -1
  201. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
  202. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +100 -0
  203. package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
  204. package/src/flow/resolveViewParamsToViewList.tsx +11 -3
  205. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +258 -0
  206. package/src/flow/utils/dataScopeFormValueClear.ts +218 -81
  207. package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
  208. package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
  209. package/src/flow/utils/formValueDeps.ts +170 -0
  210. package/src/flow-compat/FieldValidation.tsx +122 -60
  211. package/src/flow-compat/Popover.tsx +43 -4
  212. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  213. package/src/index.ts +8 -1
  214. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  215. package/src/nocobase-buildin-plugin/index.tsx +44 -14
  216. package/src/settings-center/AdminSettingsLayout.tsx +10 -2
  217. package/src/settings-center/SystemSettingsPage.tsx +1 -2
  218. package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
  219. package/src/settings-center/plugin-manager/index.tsx +3 -0
  220. package/src/settings-center/utils.tsx +0 -6
@@ -14,6 +14,7 @@ import dayjs from 'dayjs';
14
14
  import { observable } from '@formily/reactive';
15
15
  import { get as lodashGet, merge as lodashMerge, set as lodashSet } from 'lodash';
16
16
  import { FlowContext, JSRunner } from '@nocobase/flow-engine';
17
+ import { getValuesByPath } from '@nocobase/shared';
17
18
  import { FormValueRuntime } from '..';
18
19
  import type { FormInstance } from 'antd';
19
20
 
@@ -95,6 +96,10 @@ function createFieldContext(runtime: FormValueRuntime) {
95
96
  if (match) {
96
97
  const pathString = match[1];
97
98
  if (!pathString) return this;
99
+ if (pathString === 'formValues') return this.formValues;
100
+ if (pathString.startsWith('formValues.')) {
101
+ return getValuesByPath(runtime.getFormValuesSnapshot(), pathString.slice('formValues.'.length));
102
+ }
98
103
  const segs = pathString.split('.').filter(Boolean);
99
104
  return lodashGet(this, segs);
100
105
  }
@@ -122,6 +127,93 @@ function createFieldContext(runtime: FormValueRuntime) {
122
127
  }
123
128
 
124
129
  describe('FormValueRuntime (default rules)', () => {
130
+ it('builds draft snapshots from current user-edited values only', async () => {
131
+ const engineEmitter = new EventEmitter();
132
+ const blockEmitter = new EventEmitter();
133
+ const formStub = createFormStub({});
134
+ const dispatchEvent = vi.fn();
135
+
136
+ const blockModel = {
137
+ uid: 'form-user-edited-draft-snapshot',
138
+ flowEngine: { emitter: engineEmitter },
139
+ emitter: blockEmitter,
140
+ dispatchEvent,
141
+ getAclActionName: () => 'create',
142
+ } as ConstructorParameters<typeof FormValueRuntime>[0]['model'];
143
+
144
+ const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as unknown as FormInstance });
145
+ runtime.mount({ sync: true });
146
+
147
+ const blockCtx = createFieldContext(runtime);
148
+ await runtime.setFormValues(blockCtx, [{ path: ['fixed'], value: 'Fixed auto value' }], { source: 'system' });
149
+ await runtime.setFormValues(blockCtx, [{ path: ['defaultTitle'], value: 'Default auto value' }], {
150
+ source: 'default',
151
+ });
152
+ await runtime.setFormValues(blockCtx, [{ path: ['overrideTitle'], value: 'Override auto value' }], {
153
+ source: 'override',
154
+ });
155
+
156
+ expect(runtime.getUserEditedValuePatches()).toEqual([]);
157
+ expect(runtime.getUserEditedValuesSnapshot()).toEqual({});
158
+
159
+ await runtime.setFormValues(
160
+ blockCtx,
161
+ [
162
+ { path: ['title'], value: 'Manual title' },
163
+ { path: ['roles', 0, 'roleName'], value: 'Manual role' },
164
+ ],
165
+ { source: 'user' },
166
+ );
167
+
168
+ expect(runtime.getUserEditedValuePatches()).toEqual([
169
+ { path: ['title'], value: 'Manual title' },
170
+ { path: ['roles', 0, 'roleName'], value: 'Manual role' },
171
+ ]);
172
+ expect(runtime.getUserEditedValuesSnapshot()).toEqual({
173
+ title: 'Manual title',
174
+ roles: [{ roleName: 'Manual role' }],
175
+ });
176
+
177
+ await runtime.setFormValues(blockCtx, [{ path: ['title'], value: 'Fixed title' }], { source: 'system' });
178
+
179
+ expect(runtime.getUserEditedValuesSnapshot()).toEqual({
180
+ roles: [{ roleName: 'Manual role' }],
181
+ });
182
+ });
183
+
184
+ it('omits non-user descendant values from parent user-edited draft patches', async () => {
185
+ const engineEmitter = new EventEmitter();
186
+ const blockEmitter = new EventEmitter();
187
+ const formStub = createFormStub({});
188
+ const dispatchEvent = vi.fn();
189
+
190
+ const blockModel = {
191
+ uid: 'form-user-edited-parent-draft-patch',
192
+ flowEngine: { emitter: engineEmitter },
193
+ emitter: blockEmitter,
194
+ dispatchEvent,
195
+ getAclActionName: () => 'create',
196
+ } as ConstructorParameters<typeof FormValueRuntime>[0]['model'];
197
+
198
+ const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as unknown as FormInstance });
199
+ runtime.mount({ sync: true });
200
+
201
+ const blockCtx = createFieldContext(runtime);
202
+ await runtime.setFormValues(
203
+ blockCtx,
204
+ [{ path: ['roles'], value: [{ roleName: 'Manual role', fixedRoleName: 'Initial fixed role' }] }],
205
+ { source: 'user' },
206
+ );
207
+ await runtime.setFormValues(blockCtx, [{ path: ['roles', 0, 'fixedRoleName'], value: 'System fixed role' }], {
208
+ source: 'system',
209
+ });
210
+
211
+ expect(runtime.getUserEditedValuePatches()).toEqual([{ path: ['roles'], value: [{ roleName: 'Manual role' }] }]);
212
+ expect(runtime.getUserEditedValuesSnapshot()).toEqual({
213
+ roles: [{ roleName: 'Manual role' }],
214
+ });
215
+ });
216
+
125
217
  it('skips object patches when values are unchanged', async () => {
126
218
  const engineEmitter = new EventEmitter();
127
219
  const blockEmitter = new EventEmitter();
@@ -747,13 +839,13 @@ describe('FormValueRuntime (form assign rules)', () => {
747
839
  await waitFor(() => expect(formStub.getFieldValue(['a'])).toBe('Y'));
748
840
  });
749
841
 
750
- it('mode=default follows explicit semantics and stops after user change', async () => {
842
+ it('mode=assign follows to-many association title changes without clearing on transient empty arrays', async () => {
751
843
  const engineEmitter = new EventEmitter();
752
844
  const blockEmitter = new EventEmitter();
753
- const formStub = createFormStub({ b: 'X' });
845
+ const formStub = createFormStub({ roles: [] });
754
846
 
755
847
  const blockModel: any = {
756
- uid: 'form-assign-2',
848
+ uid: 'form-assign-to-many-source-title',
757
849
  flowEngine: { emitter: engineEmitter },
758
850
  emitter: blockEmitter,
759
851
  dispatchEvent: vi.fn(),
@@ -764,46 +856,66 @@ describe('FormValueRuntime (form assign rules)', () => {
764
856
  runtime.mount({ sync: true });
765
857
 
766
858
  const blockCtx = createFieldContext(runtime);
767
- const fieldModel: any = {
768
- uid: 'field-a2',
769
- context: { fieldPathArray: ['a'] },
859
+ const usernameField: any = { isAssociationField: () => false, interface: 'input' };
860
+ const rolesCollection: any = { getField: () => null, filterTargetKey: 'name' };
861
+ const rolesField: any = {
862
+ isAssociationField: () => true,
863
+ type: 'belongsToMany',
864
+ targetCollection: rolesCollection,
770
865
  };
771
- blockCtx.defineProperty('engine', {
772
- value: {
773
- getModel: (id: string) => (id === 'field-a2' ? fieldModel : null),
866
+ const collection: any = {
867
+ getField: (name: string) => {
868
+ if (name === 'username') return usernameField;
869
+ if (name === 'roles') return rolesField;
870
+ return null;
774
871
  },
775
- });
872
+ };
873
+ blockCtx.defineProperty('collection', { value: collection });
776
874
  blockModel.context = blockCtx;
777
875
 
778
876
  runtime.syncAssignRules([
779
877
  {
780
- key: 'r1',
878
+ key: 'username-from-role-title',
781
879
  enable: true,
782
- targetPath: 'a',
783
- mode: 'default',
880
+ targetPath: 'username',
881
+ mode: 'assign',
784
882
  condition: { logic: '$and', items: [] },
785
- value: '__B__',
883
+ value: '{{ ctx.formValues.roles.title }}',
786
884
  },
787
885
  ]);
788
886
 
789
- await waitFor(() => expect(formStub.getFieldValue(['a'])).toBe('X'));
887
+ await new Promise((resolve) => setTimeout(resolve, 20));
888
+ expect(formStub.getFieldValue(['username'])).toBeUndefined();
790
889
 
791
- // user writes to target; default mode MUST stop permanently
792
- await runtime.setFormValues(blockCtx, [{ path: ['a'], value: 'user' }], { source: 'user' });
793
- expect(formStub.getFieldValue(['a'])).toBe('user');
890
+ await runtime.setFormValues(blockCtx, [{ path: ['roles'], value: [{ title: '经理' }] }], { source: 'user' });
891
+ await waitFor(() => expect(formStub.getFieldValue(['username'])).toBe('经理'));
794
892
 
795
- await runtime.setFormValues(blockCtx, [{ path: ['b'], value: 'Y' }], { source: 'user' });
796
- await new Promise((resolve) => setTimeout(resolve, 20));
797
- expect(formStub.getFieldValue(['a'])).toBe('user');
893
+ await runtime.setFormValues(blockCtx, [{ path: ['roles'], value: [{ title: '经理' }, { title: 'Admin' }] }], {
894
+ source: 'user',
895
+ });
896
+ await waitFor(() => expect(formStub.getFieldValue(['username'])).toBe('经理, Admin'));
897
+
898
+ await runtime.setFormValues(blockCtx, [{ path: ['roles'], value: [{ title: null }] }], { source: 'system' });
899
+ await new Promise((resolve) => setTimeout(resolve, 100));
900
+ expect(formStub.getFieldValue(['username'])).toBe('经理, Admin');
901
+
902
+ await runtime.setFormValues(blockCtx, [{ path: ['roles'], value: [{ __index__: 'row-1', __is_new__: true }] }], {
903
+ source: 'system',
904
+ });
905
+ await new Promise((resolve) => setTimeout(resolve, 100));
906
+ expect(formStub.getFieldValue(['username'])).toBe('经理, Admin');
907
+
908
+ await runtime.setFormValues(blockCtx, [{ path: ['roles'], value: [{ title: '销售' }] }], { source: 'user' });
909
+ await waitFor(() => expect(formStub.getFieldValue(['username'])).toBe('销售'));
798
910
  });
799
911
 
800
- it('reapplies mode=default value after form reset starts a new create session', async () => {
912
+ it('mode=assign clears scalar target when to-many association source is really cleared by user', async () => {
801
913
  const engineEmitter = new EventEmitter();
802
914
  const blockEmitter = new EventEmitter();
803
- const formStub = createFormStub({});
915
+ const formStub = createFormStub({ roles: [] });
804
916
 
805
917
  const blockModel: any = {
806
- uid: 'form-assign-reset-reapply-default',
918
+ uid: 'form-assign-to-many-source-user-clear',
807
919
  flowEngine: { emitter: engineEmitter },
808
920
  emitter: blockEmitter,
809
921
  dispatchEvent: vi.fn(),
@@ -814,47 +926,103 @@ describe('FormValueRuntime (form assign rules)', () => {
814
926
  runtime.mount({ sync: true });
815
927
 
816
928
  const blockCtx = createFieldContext(runtime);
817
- const fieldModel: any = {
818
- uid: 'field-a-reset-reapply-default',
819
- context: { fieldPathArray: ['a'] },
929
+ const usernameField: any = { isAssociationField: () => false, interface: 'input' };
930
+ const rolesCollection: any = { getField: () => null, filterTargetKey: 'name' };
931
+ const rolesField: any = {
932
+ isAssociationField: () => true,
933
+ type: 'belongsToMany',
934
+ targetCollection: rolesCollection,
820
935
  };
821
- blockCtx.defineProperty('engine', {
822
- value: {
823
- getModel: (id: string) => (id === 'field-a-reset-reapply-default' ? fieldModel : null),
936
+ const collection: any = {
937
+ getField: (name: string) => {
938
+ if (name === 'username') return usernameField;
939
+ if (name === 'roles') return rolesField;
940
+ return null;
824
941
  },
825
- });
942
+ };
943
+ blockCtx.defineProperty('collection', { value: collection });
826
944
  blockModel.context = blockCtx;
827
945
 
828
946
  runtime.syncAssignRules([
829
947
  {
830
- key: 'r1',
948
+ key: 'username-from-role-title-user-clear',
831
949
  enable: true,
832
- targetPath: 'a',
833
- mode: 'default',
950
+ targetPath: 'username',
951
+ mode: 'assign',
834
952
  condition: { logic: '$and', items: [] },
835
- value: 'AAA',
953
+ value: '{{ ctx.formValues.roles.title }}',
836
954
  },
837
955
  ]);
838
956
 
839
- await waitFor(() => expect(formStub.getFieldValue(['a'])).toBe('AAA'));
957
+ await runtime.setFormValues(blockCtx, [{ path: ['roles'], value: [{ title: '经理' }] }], { source: 'user' });
958
+ await waitFor(() => expect(formStub.getFieldValue(['username'])).toBe('经理'));
840
959
 
841
- await runtime.setFormValues(blockCtx, [{ path: ['a'], value: 'AAA1' }], { source: 'user' });
842
- expect(formStub.getFieldValue(['a'])).toBe('AAA1');
960
+ await runtime.setFormValues(blockCtx, [{ path: ['roles'], value: [] }], { source: 'user' });
961
+ await waitFor(() => expect(formStub.getFieldValue(['username'])).toBe(''));
962
+ });
843
963
 
844
- formStub.__store.a = undefined;
845
- delete formStub.__store.a;
846
- runtime.resetAfterFormReset();
964
+ it('mode=assign does not stringify multi-value aggregate for non-text scalar targets', async () => {
965
+ const engineEmitter = new EventEmitter();
966
+ const blockEmitter = new EventEmitter();
967
+ const formStub = createFormStub({ roles: [] });
847
968
 
848
- await waitFor(() => expect(formStub.getFieldValue(['a'])).toBe('AAA'));
969
+ const blockModel: any = {
970
+ uid: 'form-assign-to-many-number-target',
971
+ flowEngine: { emitter: engineEmitter },
972
+ emitter: blockEmitter,
973
+ dispatchEvent: vi.fn(),
974
+ getAclActionName: () => 'create',
975
+ };
976
+
977
+ const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
978
+ runtime.mount({ sync: true });
979
+
980
+ const blockCtx = createFieldContext(runtime);
981
+ const scoreField: any = { isAssociationField: () => false, interface: 'number' };
982
+ const rolesCollection: any = { getField: () => null, filterTargetKey: 'name' };
983
+ const rolesField: any = {
984
+ isAssociationField: () => true,
985
+ type: 'belongsToMany',
986
+ targetCollection: rolesCollection,
987
+ };
988
+ const collection: any = {
989
+ getField: (name: string) => {
990
+ if (name === 'score') return scoreField;
991
+ if (name === 'roles') return rolesField;
992
+ return null;
993
+ },
994
+ };
995
+ blockCtx.defineProperty('collection', { value: collection });
996
+ blockModel.context = blockCtx;
997
+
998
+ runtime.syncAssignRules([
999
+ {
1000
+ key: 'score-from-role-level',
1001
+ enable: true,
1002
+ targetPath: 'score',
1003
+ mode: 'assign',
1004
+ condition: { logic: '$and', items: [] },
1005
+ value: '{{ ctx.formValues.roles.level }}',
1006
+ },
1007
+ ]);
1008
+
1009
+ await runtime.setFormValues(blockCtx, [{ path: ['roles'], value: [{ level: 3 }] }], { source: 'user' });
1010
+ await waitFor(() => expect(formStub.getFieldValue(['score'])).toBe(3));
1011
+
1012
+ await runtime.setFormValues(blockCtx, [{ path: ['roles'], value: [{ level: 1 }, { level: 2 }] }], {
1013
+ source: 'user',
1014
+ });
1015
+ await new Promise((resolve) => setTimeout(resolve, 50));
1016
+ expect(formStub.getFieldValue(['score'])).toBe(3);
849
1017
  });
850
1018
 
851
- it('resets valuesMirror before rescheduling default rules after form reset', async () => {
1019
+ it('tracks dynamic ctx.resolveJsonTemplate formValues dependencies from RunJS values', async () => {
852
1020
  const engineEmitter = new EventEmitter();
853
1021
  const blockEmitter = new EventEmitter();
854
1022
  const formStub = createFormStub({ selector: 'x', x: 'AAA', y: 'BBB' });
855
1023
 
856
1024
  const blockModel: any = {
857
- uid: 'form-assign-reset-values-mirror',
1025
+ uid: 'form-assign-runjs-dynamic-template-deps',
858
1026
  flowEngine: { emitter: engineEmitter },
859
1027
  emitter: blockEmitter,
860
1028
  dispatchEvent: vi.fn(),
@@ -866,56 +1034,104 @@ describe('FormValueRuntime (form assign rules)', () => {
866
1034
 
867
1035
  const blockCtx = createFieldContext(runtime);
868
1036
  const fieldModel: any = {
869
- uid: 'field-a-reset-values-mirror',
1037
+ uid: 'field-a-runjs-dynamic-template-deps',
870
1038
  context: { fieldPathArray: ['a'] },
871
1039
  };
872
1040
  blockCtx.defineProperty('engine', {
873
1041
  value: {
874
- getModel: (id: string) => (id === 'field-a-reset-values-mirror' ? fieldModel : null),
1042
+ getModel: (id: string) => (id === 'field-a-runjs-dynamic-template-deps' ? fieldModel : null),
875
1043
  },
876
1044
  });
877
1045
  blockModel.context = blockCtx;
878
1046
 
879
1047
  runtime.syncAssignRules([
880
1048
  {
881
- key: 'r1',
1049
+ key: 'runjs-dynamic-template-deps',
882
1050
  enable: true,
883
1051
  targetPath: 'a',
884
- mode: 'default',
1052
+ mode: 'assign',
885
1053
  condition: { logic: '$and', items: [] },
886
- value: '__DYNAMIC__',
1054
+ value: {
1055
+ code: "const key = ctx.formValues.selector; return await ctx.resolveJsonTemplate('{{ ctx.formValues.' + key + ' }}');",
1056
+ version: 'v1',
1057
+ },
887
1058
  },
888
1059
  ]);
889
1060
 
890
1061
  await waitFor(() => expect(formStub.getFieldValue(['a'])).toBe('AAA'));
891
1062
 
892
- await runtime.setFormValues(blockCtx, [{ path: ['a'], value: 'AAA1' }], { source: 'user' });
893
- expect(formStub.getFieldValue(['a'])).toBe('AAA1');
1063
+ await runtime.setFormValues(blockCtx, [{ path: ['x'], value: 'AAA2' }], { source: 'user' });
1064
+ await waitFor(() => expect(formStub.getFieldValue(['a'])).toBe('AAA2'));
894
1065
 
895
- for (const key of Object.keys(formStub.__store)) {
896
- delete formStub.__store[key];
897
- }
898
- runtime.resetAfterFormReset();
1066
+ await runtime.setFormValues(blockCtx, [{ path: ['selector'], value: 'y' }], { source: 'user' });
1067
+ await waitFor(() => expect(formStub.getFieldValue(['a'])).toBe('BBB'));
1068
+ });
899
1069
 
900
- await new Promise((resolve) => setTimeout(resolve, 20));
901
- expect(formStub.getFieldValue(['a'])).toBeUndefined();
1070
+ it('mode=assign keeps local formValues replacement text literal', async () => {
1071
+ const engineEmitter = new EventEmitter();
1072
+ const blockEmitter = new EventEmitter();
1073
+ const formStub = createFormStub({ roles: [] });
902
1074
 
903
- await runtime.setFormValues(blockCtx, [{ path: ['selector'], value: 'y' }], { source: 'user' });
904
- await runtime.setFormValues(blockCtx, [{ path: ['y'], value: 'BBB2' }], { source: 'user' });
905
- await waitFor(() => expect(formStub.getFieldValue(['a'])).toBe('BBB2'));
1075
+ const blockModel: any = {
1076
+ uid: 'form-assign-local-template-literal',
1077
+ flowEngine: { emitter: engineEmitter },
1078
+ emitter: blockEmitter,
1079
+ dispatchEvent: vi.fn(),
1080
+ getAclActionName: () => 'create',
1081
+ };
1082
+
1083
+ const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
1084
+ runtime.mount({ sync: true });
1085
+
1086
+ const blockCtx = createFieldContext(runtime);
1087
+ blockCtx.defineProperty('locale', { value: 'zh-CN' });
1088
+
1089
+ const usernameField: any = { isAssociationField: () => false, interface: 'input' };
1090
+ const rolesCollection: any = { getField: () => null, filterTargetKey: 'name' };
1091
+ const rolesField: any = {
1092
+ isAssociationField: () => true,
1093
+ type: 'belongsToMany',
1094
+ targetCollection: rolesCollection,
1095
+ };
1096
+ const collection: any = {
1097
+ getField: (name: string) => {
1098
+ if (name === 'username') return usernameField;
1099
+ if (name === 'roles') return rolesField;
1100
+ return null;
1101
+ },
1102
+ };
1103
+ blockCtx.defineProperty('collection', { value: collection });
1104
+ blockModel.context = blockCtx;
1105
+
1106
+ runtime.syncAssignRules([
1107
+ {
1108
+ key: 'username-from-role-template-literal',
1109
+ enable: true,
1110
+ targetPath: 'username',
1111
+ mode: 'assign',
1112
+ condition: { logic: '$and', items: [] },
1113
+ value: '{{ ctx.formValues.roles.title }}',
1114
+ },
1115
+ ]);
1116
+
1117
+ await runtime.setFormValues(blockCtx, [{ path: ['roles'], value: [{ title: '{{ ctx.locale }}' }] }], {
1118
+ source: 'user',
1119
+ });
1120
+
1121
+ await waitFor(() => expect(formStub.getFieldValue(['username'])).toBe('{{ ctx.locale }}'));
906
1122
  });
907
1123
 
908
- it('skips mode=default form assignment in update form', async () => {
1124
+ it('mode=default follows explicit semantics and stops after user change', async () => {
909
1125
  const engineEmitter = new EventEmitter();
910
1126
  const blockEmitter = new EventEmitter();
911
1127
  const formStub = createFormStub({ b: 'X' });
912
1128
 
913
1129
  const blockModel: any = {
914
- uid: 'form-assign-update-skip-default',
1130
+ uid: 'form-assign-2',
915
1131
  flowEngine: { emitter: engineEmitter },
916
1132
  emitter: blockEmitter,
917
1133
  dispatchEvent: vi.fn(),
918
- getAclActionName: () => 'update',
1134
+ getAclActionName: () => 'create',
919
1135
  };
920
1136
 
921
1137
  const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
@@ -923,12 +1139,12 @@ describe('FormValueRuntime (form assign rules)', () => {
923
1139
 
924
1140
  const blockCtx = createFieldContext(runtime);
925
1141
  const fieldModel: any = {
926
- uid: 'field-a-update-skip-default',
1142
+ uid: 'field-a2',
927
1143
  context: { fieldPathArray: ['a'] },
928
1144
  };
929
1145
  blockCtx.defineProperty('engine', {
930
1146
  value: {
931
- getModel: (id: string) => (id === 'field-a-update-skip-default' ? fieldModel : null),
1147
+ getModel: (id: string) => (id === 'field-a2' ? fieldModel : null),
932
1148
  },
933
1149
  });
934
1150
  blockModel.context = blockCtx;
@@ -944,21 +1160,24 @@ describe('FormValueRuntime (form assign rules)', () => {
944
1160
  },
945
1161
  ]);
946
1162
 
947
- await new Promise((resolve) => setTimeout(resolve, 20));
948
- expect(formStub.getFieldValue(['a'])).toBeUndefined();
1163
+ await waitFor(() => expect(formStub.getFieldValue(['a'])).toBe('X'));
1164
+
1165
+ // user writes to target; default mode MUST stop permanently
1166
+ await runtime.setFormValues(blockCtx, [{ path: ['a'], value: 'user' }], { source: 'user' });
1167
+ expect(formStub.getFieldValue(['a'])).toBe('user');
949
1168
 
950
1169
  await runtime.setFormValues(blockCtx, [{ path: ['b'], value: 'Y' }], { source: 'user' });
951
1170
  await new Promise((resolve) => setTimeout(resolve, 20));
952
- expect(formStub.getFieldValue(['a'])).toBeUndefined();
1171
+ expect(formStub.getFieldValue(['a'])).toBe('user');
953
1172
  });
954
1173
 
955
- it('syncs UI props for mode=default form assignment on configured fields', async () => {
1174
+ it('mode=default follows to-many association subpath changes without clearing on transient empty arrays', async () => {
956
1175
  const engineEmitter = new EventEmitter();
957
1176
  const blockEmitter = new EventEmitter();
958
- const formStub = createFormStub();
1177
+ const formStub = createFormStub({ roles: [] });
959
1178
 
960
1179
  const blockModel: any = {
961
- uid: 'form-assign-default-ui-sync',
1180
+ uid: 'form-assign-default-to-many-source',
962
1181
  flowEngine: { emitter: engineEmitter },
963
1182
  emitter: blockEmitter,
964
1183
  dispatchEvent: vi.fn(),
@@ -969,10 +1188,542 @@ describe('FormValueRuntime (form assign rules)', () => {
969
1188
  runtime.mount({ sync: true });
970
1189
 
971
1190
  const blockCtx = createFieldContext(runtime);
972
- blockModel.context = blockCtx;
973
-
974
- const fieldCtx = createFieldContext(runtime);
975
- fieldCtx.defineProperty('blockModel', { value: blockModel });
1191
+ const usernameField: any = { isAssociationField: () => false, interface: 'input' };
1192
+ const rolesCollection: any = { getField: () => null, filterTargetKey: 'name' };
1193
+ const rolesField: any = {
1194
+ isAssociationField: () => true,
1195
+ type: 'belongsToMany',
1196
+ targetCollection: rolesCollection,
1197
+ };
1198
+ const collection: any = {
1199
+ getField: (name: string) => {
1200
+ if (name === 'username') return usernameField;
1201
+ if (name === 'roles') return rolesField;
1202
+ return null;
1203
+ },
1204
+ };
1205
+ blockCtx.defineProperty('collection', { value: collection });
1206
+ blockModel.context = blockCtx;
1207
+
1208
+ runtime.syncAssignRules([
1209
+ {
1210
+ key: 'username-from-role-name',
1211
+ enable: true,
1212
+ targetPath: 'username',
1213
+ mode: 'default',
1214
+ condition: { logic: '$and', items: [] },
1215
+ value: '{{ ctx.formValues.roles.name }}',
1216
+ },
1217
+ ]);
1218
+
1219
+ await new Promise((resolve) => setTimeout(resolve, 20));
1220
+ expect(formStub.getFieldValue(['username'])).toBeUndefined();
1221
+
1222
+ await runtime.setFormValues(blockCtx, [{ path: ['roles'], value: [{ name: 'manager' }] }], { source: 'user' });
1223
+ await waitFor(() => expect(formStub.getFieldValue(['username'])).toBe('manager'));
1224
+
1225
+ // Association selects can briefly carry an incomplete display record while option data is refreshing.
1226
+ // For scalar targets, default mode should not treat that empty aggregate as a real default value.
1227
+ await runtime.setFormValues(blockCtx, [{ path: ['roles'], value: [{ name: null }] }], { source: 'system' });
1228
+ await new Promise((resolve) => setTimeout(resolve, 100));
1229
+ expect(formStub.getFieldValue(['username'])).toBe('manager');
1230
+
1231
+ await runtime.setFormValues(blockCtx, [{ path: ['roles'], value: [{ name: 'member' }] }], { source: 'user' });
1232
+ await waitFor(() => expect(formStub.getFieldValue(['username'])).toBe('member'));
1233
+
1234
+ await runtime.setFormValues(blockCtx, [{ path: ['username'], value: 'manual' }], { source: 'user' });
1235
+ await runtime.setFormValues(blockCtx, [{ path: ['roles'], value: [{ name: 'admin' }] }], { source: 'user' });
1236
+ await new Promise((resolve) => setTimeout(resolve, 20));
1237
+ expect(formStub.getFieldValue(['username'])).toBe('manual');
1238
+ });
1239
+
1240
+ it('mode=override overwrites existing update values until the user changes the target', async () => {
1241
+ const engineEmitter = new EventEmitter();
1242
+ const blockEmitter = new EventEmitter();
1243
+ const formStub = createFormStub({ a: 'Existing', b: 'X' });
1244
+
1245
+ const blockModel: any = {
1246
+ uid: 'form-assign-override-update',
1247
+ flowEngine: { emitter: engineEmitter },
1248
+ emitter: blockEmitter,
1249
+ dispatchEvent: vi.fn(),
1250
+ getAclActionName: () => 'update',
1251
+ };
1252
+
1253
+ const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
1254
+ runtime.mount({ sync: true });
1255
+
1256
+ const blockCtx = createFieldContext(runtime);
1257
+ const fieldModel: any = {
1258
+ uid: 'field-a-override-update',
1259
+ context: { fieldPathArray: ['a'] },
1260
+ };
1261
+ blockCtx.defineProperty('engine', {
1262
+ value: {
1263
+ getModel: (id: string) => (id === 'field-a-override-update' ? fieldModel : null),
1264
+ },
1265
+ });
1266
+ blockModel.context = blockCtx;
1267
+
1268
+ runtime.syncAssignRules([
1269
+ {
1270
+ key: 'r1',
1271
+ enable: true,
1272
+ targetPath: 'a',
1273
+ mode: 'override',
1274
+ condition: { logic: '$and', items: [] },
1275
+ value: '__B__',
1276
+ },
1277
+ ]);
1278
+
1279
+ await waitFor(() => expect(formStub.getFieldValue(['a'])).toBe('X'));
1280
+ expect(runtime.canApplyOverrideValuePatch(['a'])).toBe(true);
1281
+
1282
+ await runtime.setFormValues(blockCtx, [{ path: ['b'], value: 'Y' }], { source: 'user' });
1283
+ await waitFor(() => expect(formStub.getFieldValue(['a'])).toBe('Y'));
1284
+
1285
+ await runtime.setFormValues(blockCtx, [{ path: ['a'], value: 'Manual' }], { source: 'user' });
1286
+ expect(formStub.getFieldValue(['a'])).toBe('Manual');
1287
+ expect(runtime.canApplyOverrideValuePatch(['a'])).toBe(false);
1288
+
1289
+ await runtime.setFormValues(blockCtx, [{ path: ['b'], value: 'Z' }], { source: 'user' });
1290
+ await new Promise((resolve) => setTimeout(resolve, 20));
1291
+ expect(formStub.getFieldValue(['a'])).toBe('Manual');
1292
+ });
1293
+
1294
+ it('stops mode=override for an association root when the user edits descendant fields', async () => {
1295
+ const engineEmitter = new EventEmitter();
1296
+ const blockEmitter = new EventEmitter();
1297
+ const formStub = createFormStub({
1298
+ b: { id: 2, name: 'Auto' },
1299
+ user: { id: 1, name: 'Existing' },
1300
+ });
1301
+
1302
+ const blockModel: any = {
1303
+ uid: 'form-assign-override-association-root',
1304
+ flowEngine: { emitter: engineEmitter },
1305
+ emitter: blockEmitter,
1306
+ dispatchEvent: vi.fn(),
1307
+ getAclActionName: () => 'update',
1308
+ };
1309
+
1310
+ const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
1311
+ runtime.mount({ sync: true });
1312
+
1313
+ const blockCtx = createFieldContext(runtime);
1314
+ blockModel.context = blockCtx;
1315
+
1316
+ runtime.syncAssignRules([
1317
+ {
1318
+ key: 'r1',
1319
+ enable: true,
1320
+ targetPath: 'user',
1321
+ mode: 'override',
1322
+ condition: { logic: '$and', items: [] },
1323
+ value: '__B__',
1324
+ },
1325
+ ]);
1326
+
1327
+ await waitFor(() => expect(formStub.getFieldValue(['user'])).toEqual({ id: 2, name: 'Auto' }));
1328
+ expect(runtime.canApplyOverrideValuePatch(['user'])).toBe(true);
1329
+
1330
+ lodashSet((formStub as any).__store, ['user'], { id: 3, name: 'Manual' });
1331
+ runtime.handleFormValuesChange({ user: formStub.getFieldValue(['user']) }, formStub.getFieldsValue());
1332
+ expect(runtime.canApplyOverrideValuePatch(['user'])).toBe(false);
1333
+
1334
+ await runtime.setFormValues(blockCtx, [{ path: ['b'], value: { id: 4, name: 'Next' } }], { source: 'user' });
1335
+ await new Promise((resolve) => setTimeout(resolve, 20));
1336
+
1337
+ expect(formStub.getFieldValue(['user'])).toEqual({ id: 3, name: 'Manual' });
1338
+ });
1339
+
1340
+ it('stops mode=override for a subtable relation array after a user edits a row cell', async () => {
1341
+ const engineEmitter = new EventEmitter();
1342
+ const blockEmitter = new EventEmitter();
1343
+ const formStub = createFormStub({
1344
+ roles: [
1345
+ { id: 1, name: 'Existing A' },
1346
+ { id: 2, name: 'Existing B' },
1347
+ ],
1348
+ });
1349
+
1350
+ const blockModel: any = {
1351
+ uid: 'form-assign-override-subtable-array',
1352
+ flowEngine: { emitter: engineEmitter },
1353
+ emitter: blockEmitter,
1354
+ dispatchEvent: vi.fn(),
1355
+ getAclActionName: () => 'update',
1356
+ };
1357
+
1358
+ const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
1359
+ runtime.mount({ sync: true });
1360
+
1361
+ const blockCtx = createFieldContext(runtime);
1362
+ blockModel.context = blockCtx;
1363
+
1364
+ runtime.syncAssignRules([
1365
+ {
1366
+ key: 'r1',
1367
+ enable: true,
1368
+ targetPath: 'roles',
1369
+ mode: 'override',
1370
+ condition: { logic: '$and', items: [] },
1371
+ value: [
1372
+ { id: 1, name: 'Auto A' },
1373
+ { id: 2, name: 'Auto B' },
1374
+ ],
1375
+ },
1376
+ ]);
1377
+
1378
+ await waitFor(() =>
1379
+ expect(formStub.getFieldValue(['roles'])).toEqual([
1380
+ { id: 1, name: 'Auto A' },
1381
+ { id: 2, name: 'Auto B' },
1382
+ ]),
1383
+ );
1384
+
1385
+ lodashSet((formStub as any).__store, ['roles', 0, 'name'], 'Manual A');
1386
+ runtime.handleFormFieldsChange([{ name: ['roles', 0, 'name'], touched: true } as any]);
1387
+ runtime.handleFormValuesChange({ roles: formStub.getFieldValue(['roles']) }, formStub.getFieldsValue());
1388
+ expect(runtime.canApplyOverrideValuePatch(['roles'])).toBe(false);
1389
+
1390
+ runtime.syncAssignRules([
1391
+ {
1392
+ key: 'r1',
1393
+ enable: true,
1394
+ targetPath: 'roles',
1395
+ mode: 'override',
1396
+ condition: { logic: '$and', items: [] },
1397
+ value: [
1398
+ { id: 1, name: 'Next A' },
1399
+ { id: 2, name: 'Next B' },
1400
+ { id: 3, name: 'Unexpected extra row' },
1401
+ ],
1402
+ },
1403
+ ]);
1404
+ await new Promise((resolve) => setTimeout(resolve, 20));
1405
+
1406
+ expect(formStub.getFieldValue(['roles'])).toEqual([
1407
+ { id: 1, name: 'Manual A' },
1408
+ { id: 2, name: 'Auto B' },
1409
+ ]);
1410
+ });
1411
+
1412
+ it('stops mode=override after a user clears a multiple value array', async () => {
1413
+ const engineEmitter = new EventEmitter();
1414
+ const blockEmitter = new EventEmitter();
1415
+ const formStub = createFormStub({ b: 'ready', tags: ['Existing'] });
1416
+
1417
+ const blockModel: any = {
1418
+ uid: 'form-assign-override-multiple-clear',
1419
+ flowEngine: { emitter: engineEmitter },
1420
+ emitter: blockEmitter,
1421
+ dispatchEvent: vi.fn(),
1422
+ getAclActionName: () => 'update',
1423
+ };
1424
+
1425
+ const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
1426
+ runtime.mount({ sync: true });
1427
+
1428
+ const blockCtx = createFieldContext(runtime);
1429
+ blockModel.context = blockCtx;
1430
+
1431
+ runtime.syncAssignRules([
1432
+ {
1433
+ key: 'r1',
1434
+ enable: true,
1435
+ targetPath: 'tags',
1436
+ mode: 'override',
1437
+ condition: { logic: '$and', items: [{ path: '{{ ctx.formValues.b }}', operator: '$notEmpty', value: '' }] },
1438
+ value: ['A', 'B'],
1439
+ },
1440
+ ]);
1441
+
1442
+ await waitFor(() => expect(formStub.getFieldValue(['tags'])).toEqual(['A', 'B']));
1443
+ expect(runtime.canApplyOverrideValuePatch(['tags'])).toBe(true);
1444
+
1445
+ lodashSet((formStub as any).__store, ['tags'], []);
1446
+ runtime.handleFormValuesChange({ tags: [] }, formStub.getFieldsValue());
1447
+ expect(runtime.canApplyOverrideValuePatch(['tags'])).toBe(false);
1448
+
1449
+ await runtime.setFormValues(blockCtx, [{ path: ['b'], value: 'changed' }], { source: 'user' });
1450
+ await new Promise((resolve) => setTimeout(resolve, 20));
1451
+
1452
+ expect(formStub.getFieldValue(['tags'])).toEqual([]);
1453
+ });
1454
+
1455
+ it('stops mode=override after a user removes an item from a multiple value array', async () => {
1456
+ const engineEmitter = new EventEmitter();
1457
+ const blockEmitter = new EventEmitter();
1458
+ const formStub = createFormStub({ b: 'ready', tags: ['Existing'] });
1459
+
1460
+ const blockModel: any = {
1461
+ uid: 'form-assign-override-multiple-remove-item',
1462
+ flowEngine: { emitter: engineEmitter },
1463
+ emitter: blockEmitter,
1464
+ dispatchEvent: vi.fn(),
1465
+ getAclActionName: () => 'update',
1466
+ };
1467
+
1468
+ const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
1469
+ runtime.mount({ sync: true });
1470
+
1471
+ const blockCtx = createFieldContext(runtime);
1472
+ blockModel.context = blockCtx;
1473
+
1474
+ runtime.syncAssignRules([
1475
+ {
1476
+ key: 'r1',
1477
+ enable: true,
1478
+ targetPath: 'tags',
1479
+ mode: 'override',
1480
+ condition: { logic: '$and', items: [{ path: '{{ ctx.formValues.b }}', operator: '$notEmpty', value: '' }] },
1481
+ value: ['A', 'B'],
1482
+ },
1483
+ ]);
1484
+
1485
+ await waitFor(() => expect(formStub.getFieldValue(['tags'])).toEqual(['A', 'B']));
1486
+ expect(runtime.canApplyOverrideValuePatch(['tags'])).toBe(true);
1487
+
1488
+ lodashSet((formStub as any).__store, ['tags'], ['A']);
1489
+ runtime.handleFormValuesChange({ tags: ['A'] }, formStub.getFieldsValue());
1490
+ expect(runtime.canApplyOverrideValuePatch(['tags'])).toBe(false);
1491
+
1492
+ await runtime.setFormValues(blockCtx, [{ path: ['b'], value: 'changed' }], { source: 'user' });
1493
+ await new Promise((resolve) => setTimeout(resolve, 20));
1494
+
1495
+ expect(formStub.getFieldValue(['tags'])).toEqual(['A']);
1496
+ });
1497
+
1498
+ it('reapplies mode=override after user edited state is reset', async () => {
1499
+ const engineEmitter = new EventEmitter();
1500
+ const blockEmitter = new EventEmitter();
1501
+ const formStub = createFormStub({ a: 'Existing', b: 'X' });
1502
+
1503
+ const blockModel: any = {
1504
+ uid: 'form-assign-override-reset-user-edited',
1505
+ flowEngine: { emitter: engineEmitter },
1506
+ emitter: blockEmitter,
1507
+ dispatchEvent: vi.fn(),
1508
+ getAclActionName: () => 'update',
1509
+ };
1510
+
1511
+ const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
1512
+ runtime.mount({ sync: true });
1513
+
1514
+ const blockCtx = createFieldContext(runtime);
1515
+ const fieldModel: any = {
1516
+ uid: 'field-a-override-reset-user-edited',
1517
+ context: { fieldPathArray: ['a'] },
1518
+ };
1519
+ blockCtx.defineProperty('engine', {
1520
+ value: {
1521
+ getModel: (id: string) => (id === 'field-a-override-reset-user-edited' ? fieldModel : null),
1522
+ },
1523
+ });
1524
+ blockModel.context = blockCtx;
1525
+
1526
+ runtime.syncAssignRules([
1527
+ {
1528
+ key: 'r1',
1529
+ enable: true,
1530
+ targetPath: 'a',
1531
+ mode: 'override',
1532
+ condition: { logic: '$and', items: [] },
1533
+ value: '__B__',
1534
+ },
1535
+ ]);
1536
+
1537
+ await waitFor(() => expect(formStub.getFieldValue(['a'])).toBe('X'));
1538
+
1539
+ await runtime.setFormValues(blockCtx, [{ path: ['a'], value: 'Manual' }], { source: 'user' });
1540
+ expect(formStub.getFieldValue(['a'])).toBe('Manual');
1541
+
1542
+ formStub.__store.a = 'Next record';
1543
+ formStub.__store.b = 'Y';
1544
+ runtime.handleFormValuesChange({ a: 'Next record', b: 'Y' }, formStub.getFieldsValue());
1545
+ runtime.resetUserEditedState();
1546
+ expect(runtime.canApplyOverrideValuePatch(['a'])).toBe(true);
1547
+
1548
+ await waitFor(() => expect(formStub.getFieldValue(['a'])).toBe('Y'));
1549
+ });
1550
+
1551
+ it('reapplies mode=default value after form reset starts a new create session', async () => {
1552
+ const engineEmitter = new EventEmitter();
1553
+ const blockEmitter = new EventEmitter();
1554
+ const formStub = createFormStub({});
1555
+
1556
+ const blockModel: any = {
1557
+ uid: 'form-assign-reset-reapply-default',
1558
+ flowEngine: { emitter: engineEmitter },
1559
+ emitter: blockEmitter,
1560
+ dispatchEvent: vi.fn(),
1561
+ getAclActionName: () => 'create',
1562
+ };
1563
+
1564
+ const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
1565
+ runtime.mount({ sync: true });
1566
+
1567
+ const blockCtx = createFieldContext(runtime);
1568
+ const fieldModel: any = {
1569
+ uid: 'field-a-reset-reapply-default',
1570
+ context: { fieldPathArray: ['a'] },
1571
+ };
1572
+ blockCtx.defineProperty('engine', {
1573
+ value: {
1574
+ getModel: (id: string) => (id === 'field-a-reset-reapply-default' ? fieldModel : null),
1575
+ },
1576
+ });
1577
+ blockModel.context = blockCtx;
1578
+
1579
+ runtime.syncAssignRules([
1580
+ {
1581
+ key: 'r1',
1582
+ enable: true,
1583
+ targetPath: 'a',
1584
+ mode: 'default',
1585
+ condition: { logic: '$and', items: [] },
1586
+ value: 'AAA',
1587
+ },
1588
+ ]);
1589
+
1590
+ await waitFor(() => expect(formStub.getFieldValue(['a'])).toBe('AAA'));
1591
+
1592
+ await runtime.setFormValues(blockCtx, [{ path: ['a'], value: 'AAA1' }], { source: 'user' });
1593
+ expect(formStub.getFieldValue(['a'])).toBe('AAA1');
1594
+
1595
+ formStub.__store.a = undefined;
1596
+ delete formStub.__store.a;
1597
+ runtime.resetAfterFormReset();
1598
+
1599
+ await waitFor(() => expect(formStub.getFieldValue(['a'])).toBe('AAA'));
1600
+ });
1601
+
1602
+ it('resets valuesMirror before rescheduling default rules after form reset', async () => {
1603
+ const engineEmitter = new EventEmitter();
1604
+ const blockEmitter = new EventEmitter();
1605
+ const formStub = createFormStub({ selector: 'x', x: 'AAA', y: 'BBB' });
1606
+
1607
+ const blockModel: any = {
1608
+ uid: 'form-assign-reset-values-mirror',
1609
+ flowEngine: { emitter: engineEmitter },
1610
+ emitter: blockEmitter,
1611
+ dispatchEvent: vi.fn(),
1612
+ getAclActionName: () => 'create',
1613
+ };
1614
+
1615
+ const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
1616
+ runtime.mount({ sync: true });
1617
+
1618
+ const blockCtx = createFieldContext(runtime);
1619
+ const fieldModel: any = {
1620
+ uid: 'field-a-reset-values-mirror',
1621
+ context: { fieldPathArray: ['a'] },
1622
+ };
1623
+ blockCtx.defineProperty('engine', {
1624
+ value: {
1625
+ getModel: (id: string) => (id === 'field-a-reset-values-mirror' ? fieldModel : null),
1626
+ },
1627
+ });
1628
+ blockModel.context = blockCtx;
1629
+
1630
+ runtime.syncAssignRules([
1631
+ {
1632
+ key: 'r1',
1633
+ enable: true,
1634
+ targetPath: 'a',
1635
+ mode: 'default',
1636
+ condition: { logic: '$and', items: [] },
1637
+ value: '__DYNAMIC__',
1638
+ },
1639
+ ]);
1640
+
1641
+ await waitFor(() => expect(formStub.getFieldValue(['a'])).toBe('AAA'));
1642
+
1643
+ await runtime.setFormValues(blockCtx, [{ path: ['a'], value: 'AAA1' }], { source: 'user' });
1644
+ expect(formStub.getFieldValue(['a'])).toBe('AAA1');
1645
+
1646
+ for (const key of Object.keys(formStub.__store)) {
1647
+ delete formStub.__store[key];
1648
+ }
1649
+ runtime.resetAfterFormReset();
1650
+
1651
+ await new Promise((resolve) => setTimeout(resolve, 20));
1652
+ expect(formStub.getFieldValue(['a'])).toBeUndefined();
1653
+
1654
+ await runtime.setFormValues(blockCtx, [{ path: ['selector'], value: 'y' }], { source: 'user' });
1655
+ await runtime.setFormValues(blockCtx, [{ path: ['y'], value: 'BBB2' }], { source: 'user' });
1656
+ await waitFor(() => expect(formStub.getFieldValue(['a'])).toBe('BBB2'));
1657
+ });
1658
+
1659
+ it('skips mode=default form assignment in update form', async () => {
1660
+ const engineEmitter = new EventEmitter();
1661
+ const blockEmitter = new EventEmitter();
1662
+ const formStub = createFormStub({ b: 'X' });
1663
+
1664
+ const blockModel: any = {
1665
+ uid: 'form-assign-update-skip-default',
1666
+ flowEngine: { emitter: engineEmitter },
1667
+ emitter: blockEmitter,
1668
+ dispatchEvent: vi.fn(),
1669
+ getAclActionName: () => 'update',
1670
+ };
1671
+
1672
+ const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
1673
+ runtime.mount({ sync: true });
1674
+
1675
+ const blockCtx = createFieldContext(runtime);
1676
+ const fieldModel: any = {
1677
+ uid: 'field-a-update-skip-default',
1678
+ context: { fieldPathArray: ['a'] },
1679
+ };
1680
+ blockCtx.defineProperty('engine', {
1681
+ value: {
1682
+ getModel: (id: string) => (id === 'field-a-update-skip-default' ? fieldModel : null),
1683
+ },
1684
+ });
1685
+ blockModel.context = blockCtx;
1686
+
1687
+ runtime.syncAssignRules([
1688
+ {
1689
+ key: 'r1',
1690
+ enable: true,
1691
+ targetPath: 'a',
1692
+ mode: 'default',
1693
+ condition: { logic: '$and', items: [] },
1694
+ value: '__B__',
1695
+ },
1696
+ ]);
1697
+
1698
+ await new Promise((resolve) => setTimeout(resolve, 20));
1699
+ expect(formStub.getFieldValue(['a'])).toBeUndefined();
1700
+
1701
+ await runtime.setFormValues(blockCtx, [{ path: ['b'], value: 'Y' }], { source: 'user' });
1702
+ await new Promise((resolve) => setTimeout(resolve, 20));
1703
+ expect(formStub.getFieldValue(['a'])).toBeUndefined();
1704
+ });
1705
+
1706
+ it('syncs UI props for mode=default form assignment on configured fields', async () => {
1707
+ const engineEmitter = new EventEmitter();
1708
+ const blockEmitter = new EventEmitter();
1709
+ const formStub = createFormStub();
1710
+
1711
+ const blockModel: any = {
1712
+ uid: 'form-assign-default-ui-sync',
1713
+ flowEngine: { emitter: engineEmitter },
1714
+ emitter: blockEmitter,
1715
+ dispatchEvent: vi.fn(),
1716
+ getAclActionName: () => 'create',
1717
+ };
1718
+
1719
+ const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
1720
+ runtime.mount({ sync: true });
1721
+
1722
+ const blockCtx = createFieldContext(runtime);
1723
+ blockModel.context = blockCtx;
1724
+
1725
+ const fieldCtx = createFieldContext(runtime);
1726
+ fieldCtx.defineProperty('blockModel', { value: blockModel });
976
1727
  fieldCtx.defineProperty('fieldPathArray', { value: ['color'] });
977
1728
 
978
1729
  const colorFieldModel: any = {
@@ -1014,6 +1765,116 @@ describe('FormValueRuntime (form assign rules)', () => {
1014
1765
  expect(colorFieldModel.setProps).toHaveBeenCalledWith({ value: '#1677FF' });
1015
1766
  });
1016
1767
 
1768
+ it('syncs mounted field UI props after programmatic form writes', async () => {
1769
+ const engineEmitter = new EventEmitter();
1770
+ const blockEmitter = new EventEmitter();
1771
+ const formStub = createFormStub();
1772
+
1773
+ const blockModel: any = {
1774
+ uid: 'form-runtime-mounted-field-ui-sync',
1775
+ flowEngine: { emitter: engineEmitter },
1776
+ emitter: blockEmitter,
1777
+ dispatchEvent: vi.fn(),
1778
+ getAclActionName: () => 'create',
1779
+ };
1780
+
1781
+ const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
1782
+
1783
+ const blockCtx = createFieldContext(runtime);
1784
+ blockModel.context = blockCtx;
1785
+
1786
+ const fieldCtx = createFieldContext(runtime);
1787
+ fieldCtx.defineProperty('blockModel', { value: blockModel });
1788
+ fieldCtx.defineProperty('fieldPathArray', { value: ['username'] });
1789
+
1790
+ const inputFieldModel: any = {
1791
+ setProps: vi.fn(),
1792
+ };
1793
+ const formItemModel: any = {
1794
+ uid: 'form-item-username-runtime-ui-sync',
1795
+ context: fieldCtx,
1796
+ subModels: { field: inputFieldModel },
1797
+ getStepParams: (flowKey: string, stepKey: string) => {
1798
+ if (flowKey === 'fieldSettings' && stepKey === 'init') {
1799
+ return { fieldPath: 'username' };
1800
+ }
1801
+ return undefined;
1802
+ },
1803
+ setProps: vi.fn(),
1804
+ };
1805
+
1806
+ blockCtx.defineProperty('engine', {
1807
+ value: {
1808
+ forEachModel: (visit: (model: any) => void) => visit(formItemModel),
1809
+ },
1810
+ });
1811
+
1812
+ runtime.mount({ sync: true });
1813
+
1814
+ await runtime.setFormValues(blockCtx, [{ path: ['username'], value: 'Admin' }], { source: 'system' });
1815
+
1816
+ expect(formStub.getFieldValue(['username'])).toBe('Admin');
1817
+ expect(formItemModel.setProps).toHaveBeenCalledWith({ value: 'Admin' });
1818
+ expect(inputFieldModel.setProps).toHaveBeenCalledWith({ value: 'Admin' });
1819
+ });
1820
+
1821
+ it('syncs mounted field UI props from model index without scanning all models on every write', async () => {
1822
+ const engineEmitter = new EventEmitter();
1823
+ const blockEmitter = new EventEmitter();
1824
+ const formStub = createFormStub();
1825
+
1826
+ const blockModel: any = {
1827
+ uid: 'form-runtime-mounted-field-ui-index-sync',
1828
+ flowEngine: { emitter: engineEmitter },
1829
+ emitter: blockEmitter,
1830
+ dispatchEvent: vi.fn(),
1831
+ getAclActionName: () => 'create',
1832
+ };
1833
+
1834
+ const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
1835
+
1836
+ const blockCtx = createFieldContext(runtime);
1837
+ blockModel.context = blockCtx;
1838
+
1839
+ const fieldCtx = createFieldContext(runtime);
1840
+ fieldCtx.defineProperty('blockModel', { value: blockModel });
1841
+ fieldCtx.defineProperty('fieldPathArray', { value: ['username'] });
1842
+
1843
+ const inputFieldModel: any = {
1844
+ setProps: vi.fn(),
1845
+ };
1846
+ const formItemModel: any = {
1847
+ uid: 'form-item-username-runtime-ui-index-sync',
1848
+ context: fieldCtx,
1849
+ subModels: { field: inputFieldModel },
1850
+ getStepParams: (flowKey: string, stepKey: string) => {
1851
+ if (flowKey === 'fieldSettings' && stepKey === 'init') {
1852
+ return { fieldPath: 'username' };
1853
+ }
1854
+ return undefined;
1855
+ },
1856
+ setProps: vi.fn(),
1857
+ };
1858
+
1859
+ const forEachModel = vi.fn((visit: (model: any) => void) => visit(formItemModel));
1860
+ blockCtx.defineProperty('engine', {
1861
+ value: {
1862
+ forEachModel,
1863
+ },
1864
+ });
1865
+
1866
+ runtime.mount({ sync: true });
1867
+ engineEmitter.emit('model:mounted', { model: formItemModel });
1868
+ forEachModel.mockClear();
1869
+
1870
+ await runtime.setFormValues(blockCtx, [{ path: ['username'], value: 'Admin' }], { source: 'system' });
1871
+ await runtime.setFormValues(blockCtx, [{ path: ['username'], value: 'Owner' }], { source: 'system' });
1872
+
1873
+ expect(forEachModel).not.toHaveBeenCalled();
1874
+ expect(formItemModel.setProps).toHaveBeenLastCalledWith({ value: 'Owner' });
1875
+ expect(inputFieldModel.setProps).toHaveBeenLastCalledWith({ value: 'Owner' });
1876
+ });
1877
+
1017
1878
  it('linkage assignment takes precedence over mode=assign form assignment', async () => {
1018
1879
  const engineEmitter = new EventEmitter();
1019
1880
  const blockEmitter = new EventEmitter();
@@ -2396,6 +3257,10 @@ describe('FormValueRuntime (form assign rules)', () => {
2396
3257
 
2397
3258
  lodashSet((formStub as any).__store, ['users'], [{ __is_new__: true, name: '' }]);
2398
3259
  runtime.handleFormValuesChange({ users: formStub.getFieldValue(['users']) }, formStub.getFieldsValue());
3260
+ expect((runtime as any).findExplicitHit('users')).toBe('users');
3261
+ expect((runtime as any).findExplicitHit('users[0].name')).toBeNull();
3262
+ expect((runtime as any).findUserEditedHit('users')).toBe('users');
3263
+ expect((runtime as any).findUserEditedHit('users[0].name')).toBeNull();
2399
3264
 
2400
3265
  const row0: any = {
2401
3266
  uid: 'users.name',