@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
@@ -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() {
@@ -436,6 +436,28 @@ export class FilterFormBlockModel extends FilterBlockModel<{
436
436
  return isEqual(current, this.lastDefaultValueByFieldName.get(name));
437
437
  }
438
438
 
439
+ private canApplyFormOverrideValue(name: string, force?: boolean) {
440
+ if (force) return true;
441
+ return !this.userEditedFieldNames.has(name);
442
+ }
443
+
444
+ private normalizeFieldValueMode(mode: unknown): 'default' | 'assign' | 'override' {
445
+ if (mode === 'assign') return 'assign';
446
+ if (mode === 'override') return 'override';
447
+ return 'default';
448
+ }
449
+
450
+ private markFilterFormUserEditedFields(changedValues: any) {
451
+ if (!changedValues || typeof changedValues !== 'object' || Array.isArray(changedValues)) return;
452
+ for (const name of Object.keys(changedValues)) {
453
+ this.userEditedFieldNames.add(String(name));
454
+ }
455
+ }
456
+
457
+ private resetFilterFormUserEditedFields() {
458
+ this.userEditedFieldNames.clear();
459
+ }
460
+
439
461
  private async matchDefaultValueCondition(condition: any) {
440
462
  if (!condition) return true;
441
463
 
@@ -458,6 +480,10 @@ export class FilterFormBlockModel extends FilterBlockModel<{
458
480
  if (!form) return appliedValues;
459
481
 
460
482
  const force = options?.force === true;
483
+ if (force) {
484
+ this.resetFilterFormUserEditedFields();
485
+ }
486
+
461
487
  const params = this.getStepParams?.('formFilterBlockModelSettings', 'defaultValues');
462
488
  const rules = (params?.value || []) as any[];
463
489
  if (!Array.isArray(rules) || rules.length === 0) return appliedValues;
@@ -466,7 +492,7 @@ export class FilterFormBlockModel extends FilterBlockModel<{
466
492
  // RunJS support
467
493
  if (isRunJSValue(raw)) {
468
494
  const { code, version } = normalizeRunJSValue(raw);
469
- const ret = await runjsWithSafeGlobals(this.context, code, { version });
495
+ const ret = await this.context.runjs(code, undefined, { version });
470
496
  return ret?.success ? ret.value : undefined;
471
497
  }
472
498
 
@@ -499,8 +525,9 @@ export class FilterFormBlockModel extends FilterBlockModel<{
499
525
 
500
526
  const operator = getDefaultOperator(itemModel as any);
501
527
  const normalized = normalizeFilterValueByOperator(operator, resolved);
502
- const mode = String(rule.mode || 'default') === 'assign' ? 'assign' : 'default';
528
+ const mode = this.normalizeFieldValueMode(rule.mode);
503
529
  if (mode === 'default' && !this.canApplyFormDefaultValue(String(name), current, force)) continue;
530
+ if (mode === 'override' && !this.canApplyFormOverrideValue(String(name), force)) continue;
504
531
  if (isEqual(current, normalized)) {
505
532
  if (mode === 'default') {
506
533
  this.lastDefaultValueByFieldName.set(String(name), normalized);
@@ -527,6 +554,7 @@ export class FilterFormBlockModel extends FilterBlockModel<{
527
554
  }
528
555
 
529
556
  private handleFilterFormValuesChange(changedValues: any, allValues: any) {
557
+ this.markFilterFormUserEditedFields(changedValues);
530
558
  const refreshSeq = ++this.defaultValuesRefreshSeq;
531
559
  void (async () => {
532
560
  const appliedValues = await this.applyFormDefaultValues({ refreshSeq });
@@ -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
  },
@@ -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,
@@ -315,6 +320,78 @@ describe('filter-form defaultValues wiring', () => {
315
320
  expect(values.username_user).toBe('Bob');
316
321
  });
317
322
 
323
+ it('applies override values until the target filter field is changed by user', async () => {
324
+ const { model, values } = createFilterFormDefaultValuesModel(
325
+ [
326
+ {
327
+ key: 'username-override',
328
+ enable: true,
329
+ targetPath: 'username',
330
+ mode: 'override',
331
+ value: '{{ ctx.formValues.nickname_nick }}',
332
+ },
333
+ ],
334
+ { nickname_nick: 'Bob', username_user: 'Manual' },
335
+ );
336
+
337
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
338
+ expect(values.username_user).toBe('Bob');
339
+
340
+ values.nickname_nick = 'Carol';
341
+ model.defaultValuesRefreshSeq += 1;
342
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any, {
343
+ refreshSeq: model.defaultValuesRefreshSeq,
344
+ });
345
+ expect(values.username_user).toBe('Carol');
346
+
347
+ values.username_user = 'User value';
348
+ (model as any).handleFilterFormValuesChange(
349
+ { username_user: 'User value' },
350
+ { nickname_nick: 'Carol', username_user: 'User value' },
351
+ );
352
+
353
+ await waitFor(() => {
354
+ expect(values.username_user).toBe('User value');
355
+ });
356
+
357
+ values.nickname_nick = 'Dora';
358
+ model.defaultValuesRefreshSeq += 1;
359
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any, {
360
+ refreshSeq: model.defaultValuesRefreshSeq,
361
+ });
362
+ expect(values.username_user).toBe('User value');
363
+ });
364
+
365
+ it('clears filter form user-edited override state on forced apply', async () => {
366
+ const { model, values } = createFilterFormDefaultValuesModel(
367
+ [
368
+ {
369
+ key: 'username-override',
370
+ enable: true,
371
+ targetPath: 'username',
372
+ mode: 'override',
373
+ condition: {
374
+ logic: '$and',
375
+ items: [{ path: '{{ ctx.formValues.nickname_nick }}', operator: '$eq', value: 'allow' }],
376
+ },
377
+ value: '{{ ctx.formValues.nickname_nick }}',
378
+ },
379
+ ],
380
+ { nickname_nick: 'deny', username_user: 'User value' },
381
+ );
382
+
383
+ model.userEditedFieldNames.add('username_user');
384
+
385
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any, { force: true });
386
+ expect(values.username_user).toBe('User value');
387
+ expect(model.userEditedFieldNames.has('username_user')).toBe(false);
388
+
389
+ values.nickname_nick = 'allow';
390
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
391
+
392
+ expect(values.username_user).toBe('allow');
393
+ });
394
+
318
395
  it('skips filter form field values when the rule condition does not match', async () => {
319
396
  const { model, values } = createFilterFormDefaultValuesModel(
320
397
  [
@@ -210,7 +210,7 @@ FilterFormCustomFieldModel.registerFlow({
210
210
  'x-decorator': 'FormItem',
211
211
  required: true,
212
212
  description:
213
- '{{t("Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.")}}',
213
+ '{{t("Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.")}}',
214
214
  },
215
215
  source: {
216
216
  type: 'array',
@@ -18,12 +18,23 @@ type TestFieldModel = {
18
18
  setupReactiveRender: ReturnType<typeof vi.fn>;
19
19
  _reactiveWrapperCache?: unknown;
20
20
  __originalRender?: () => React.ReactNode;
21
+ translate?: (text: string) => string;
21
22
  };
22
23
 
23
24
  const MultipleKeywordsInput = (props: Record<string, unknown>) => {
24
25
  return <div data-testid="multiple-keywords-input" {...props} />;
25
26
  };
26
27
 
28
+ const Select = (props: Record<string, unknown>) => {
29
+ return <div data-testid="select" {...props} />;
30
+ };
31
+
32
+ function mockT(text: string) {
33
+ if (text === '{{t("Yes")}}') return '是';
34
+ if (text === '{{t("No")}}') return '否';
35
+ return text;
36
+ }
37
+
27
38
  function createFieldModel() {
28
39
  return {
29
40
  props: {
@@ -52,7 +63,10 @@ function createFilterItemModel({
52
63
  collectionField,
53
64
  context: {
54
65
  app: {
55
- getComponent: (name: string) => (name === 'MultipleKeywordsInput' ? MultipleKeywordsInput : undefined),
66
+ getComponent: (name: string) => {
67
+ if (name === 'MultipleKeywordsInput') return MultipleKeywordsInput;
68
+ if (name === 'Select') return Select;
69
+ },
56
70
  },
57
71
  collectionField,
58
72
  },
@@ -112,6 +126,47 @@ describe('customizeFilterRender action', () => {
112
126
  expect((rerenderedElement as React.ReactElement).props.placeholder).toBe('updated runtime placeholder');
113
127
  });
114
128
 
129
+ it('translates operator schema select options', () => {
130
+ const fieldModel = {
131
+ ...createFieldModel(),
132
+ translate: mockT,
133
+ };
134
+ const model = createFilterItemModel({
135
+ fieldModel,
136
+ operator: '$isTruly',
137
+ collectionField: {
138
+ interface: 'checkbox',
139
+ type: 'boolean',
140
+ filterable: {
141
+ operators: [
142
+ {
143
+ label: '{{t("Yes")}}',
144
+ value: '$isTruly',
145
+ schema: {
146
+ 'x-component': 'Select',
147
+ 'x-component-props': {
148
+ options: [
149
+ { label: '{{t("Yes")}}', value: true },
150
+ { label: '{{t("No")}}', value: false },
151
+ ],
152
+ },
153
+ },
154
+ },
155
+ ],
156
+ },
157
+ },
158
+ });
159
+
160
+ customizeFilterRender.handler?.({ model } as any);
161
+
162
+ const element = fieldModel.render();
163
+ expect((element as React.ReactElement).type).toBe(Select);
164
+ expect((element as React.ReactElement).props.options).toEqual([
165
+ { label: '是', value: true },
166
+ { label: '否', value: false },
167
+ ]);
168
+ });
169
+
115
170
  it('restores original render when switching to an operator without a schema component', () => {
116
171
  const fieldModel = createFieldModel();
117
172
  const originalRender = fieldModel.render;
@@ -239,6 +239,7 @@ EditFormModel.registerFlow({
239
239
  // 切换记录(翻页等)时,清理“用户改动标记”,避免把上一条记录的编辑状态带到下一条
240
240
  if (recordChanged) {
241
241
  ctx.model.resetUserModifiedFields?.();
242
+ ctx.model.resetRuntimeUserEditedState?.();
242
243
  }
243
244
 
244
245
  ctx.form.setFieldsValue(currentRecord);
@@ -117,7 +117,7 @@ FormSubmitActionModel.registerFlow({
117
117
  try {
118
118
  ctx.model.setProps('loading', true);
119
119
  const { submitHandler } = await import('./submitHandler');
120
- await submitHandler(ctx, params);
120
+ return await submitHandler(ctx, params);
121
121
  } catch (error) {
122
122
  ctx.model.setProps('loading', false);
123
123
  if (error instanceof FlowExitAllException) {
@@ -166,6 +166,13 @@ export class FormBlockModel<
166
166
  this.userModifiedTopLevelFields.clear();
167
167
  }
168
168
 
169
+ /**
170
+ * @internal
171
+ */
172
+ resetRuntimeUserEditedState(): void {
173
+ this.formValueRuntime?.resetUserEditedState?.();
174
+ }
175
+
169
176
  public async onDispatchEventStart(eventName: string, options?: any, inputArgs?: Record<string, any>): Promise<void> {
170
177
  if (eventName === 'beforeRender') {
171
178
  const grid = this.subModels.grid;
@@ -10,7 +10,7 @@
10
10
  /**
11
11
  * JS Form Action:表单工具栏按钮点击执行 JS。
12
12
  */
13
- import { tExpr, createSafeWindow, createSafeDocument, createSafeNavigator } from '@nocobase/flow-engine';
13
+ import { tExpr } from '@nocobase/flow-engine';
14
14
  import { CodeEditor } from '../../../components/code-editor';
15
15
  import { FormActionModel } from './FormActionModel';
16
16
  import { resolveRunJsParams } from '../../utils/resolveRunJsParams';
@@ -73,12 +73,7 @@ ctx.message.success('Current form values: ' + JSON.stringify(values));
73
73
  await ctx.resource.refresh();
74
74
  }
75
75
  });
76
- const navigator = createSafeNavigator();
77
- await ctx.runjs(
78
- code,
79
- { window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
80
- { version },
81
- );
76
+ await ctx.runjs(code, undefined, { version });
82
77
  },
83
78
  },
84
79
  },
@@ -420,6 +420,23 @@ describe('FormBlockModel (form/formValues injection & server resolve anchors)',
420
420
  expect(savedUids).toContain('grid-1');
421
421
  });
422
422
 
423
+ it('keeps dirty reset separate from runtime user-edited reset', async () => {
424
+ const model = await setupFormModel();
425
+ const resetUserEditedState = vi.fn();
426
+ model.formValueRuntime = { resetUserEditedState } as any;
427
+
428
+ model.markUserModifiedFields({ status: 'draft' });
429
+ expect(model.getUserModifiedFields().has('status')).toBe(true);
430
+
431
+ model.resetUserModifiedFields();
432
+
433
+ expect(model.getUserModifiedFields().size).toBe(0);
434
+ expect(resetUserEditedState).not.toHaveBeenCalled();
435
+
436
+ model.resetRuntimeUserEditedState();
437
+ expect(resetUserEditedState).toHaveBeenCalledTimes(1);
438
+ });
439
+
423
440
  it('re-syncs delegated assignRules when grid submodel is added after block init', async () => {
424
441
  const model = await setupFormModel();
425
442
  const syncAssignRules = vi.fn();
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { SingleRecordResource } from '@nocobase/flow-engine';
11
11
  import { describe, expect, it, vi } from 'vitest';
12
+ import { EditFormModel } from '../EditFormModel';
12
13
  import { submitHandler } from '../submitHandler';
13
14
 
14
15
  describe('submitHandler', () => {
@@ -52,13 +53,14 @@ describe('submitHandler', () => {
52
53
  t: (value: string) => value,
53
54
  };
54
55
 
55
- await submitHandler(ctx, {
56
+ const responseRecord = await submitHandler(ctx, {
56
57
  assignedValues: {
57
58
  status: 'published',
58
59
  reviewer: 'Alice',
59
60
  },
60
61
  });
61
62
 
63
+ expect(responseRecord).toEqual({ id: 1 });
62
64
  expect(resource.save).toHaveBeenCalledWith(
63
65
  {
64
66
  title: 'Draft title',
@@ -68,4 +70,55 @@ describe('submitHandler', () => {
68
70
  undefined,
69
71
  );
70
72
  });
73
+
74
+ it('returns the refreshed record for edit forms when the save response is empty', async () => {
75
+ const refreshedRecord = { id: 2, title: 'Updated title' };
76
+ const resource = Object.create(SingleRecordResource.prototype);
77
+ resource.getMeta = vi.fn((key: string) => (key === 'currentFilterByTk' ? 2 : undefined));
78
+ resource.setFilterByTk = vi.fn(() => resource);
79
+ resource.save = vi.fn(async () => undefined);
80
+ resource.refresh = vi.fn(async () => undefined);
81
+ resource.getData = vi.fn(() => refreshedRecord);
82
+
83
+ const blockModel = Object.create(EditFormModel.prototype);
84
+ const form = {
85
+ validateFields: vi.fn(async () => undefined),
86
+ getFieldsValue: vi.fn(() => ({
87
+ title: 'Updated title',
88
+ })),
89
+ };
90
+ Object.defineProperty(blockModel, 'context', { value: { form } });
91
+ Object.defineProperty(blockModel, 'collection', {
92
+ value: {
93
+ name: 'posts',
94
+ getFilterByTK: (record: { id?: number }) => record.id,
95
+ },
96
+ });
97
+ blockModel.resetUserModifiedFields = vi.fn();
98
+
99
+ const responseRecord = await submitHandler(
100
+ {
101
+ resource,
102
+ blockModel,
103
+ model: {
104
+ getStepParams: vi.fn(),
105
+ },
106
+ message: {
107
+ error: vi.fn(),
108
+ },
109
+ t: (value: string) => value,
110
+ },
111
+ {},
112
+ );
113
+
114
+ expect(responseRecord).toBe(refreshedRecord);
115
+ expect(resource.setFilterByTk).toHaveBeenCalledWith(2);
116
+ expect(resource.save).toHaveBeenCalledWith(
117
+ {
118
+ title: 'Updated title',
119
+ },
120
+ undefined,
121
+ );
122
+ expect(resource.refresh).toHaveBeenCalled();
123
+ });
71
124
  });
@@ -12,6 +12,16 @@ import { mergeAssignFieldValues, resolveAssignFieldValues } from '../assign-form
12
12
  import type { FormBlockModel } from './FormBlockModel';
13
13
  import { omitHiddenModelValuesFromSubmit, shouldSkipSubmitValidation, validateSubmitForm } from './submitValues';
14
14
 
15
+ function getResponseRecord(response: unknown) {
16
+ if (!response || typeof response !== 'object') {
17
+ return response;
18
+ }
19
+ if ('data' in response) {
20
+ return (response as { data?: unknown }).data;
21
+ }
22
+ return response;
23
+ }
24
+
15
25
  export async function submitHandler(ctx, params, cb?: (values?: any, filterByTk?: any) => void) {
16
26
  const resource = ctx.resource;
17
27
  const blockModel = ctx.blockModel as FormBlockModel;
@@ -41,30 +51,34 @@ export async function submitHandler(ctx, params, cb?: (values?: any, filterByTk?
41
51
  resource.setFilterByTk(currentFilterByTk);
42
52
  }
43
53
  }
44
- const data: any = cb ? await cb(values) : await resource.save(values, params.requestConfig);
54
+ const data: unknown = cb ? await cb(values) : await resource.save(values, params.requestConfig);
55
+ let responseRecord = getResponseRecord(data);
45
56
  if (isEditFormModel) {
46
57
  resource.isNewRecord = false;
47
58
  // 编辑表单保存成功后,表单应回到“已同步”状态:下一次刷新应允许覆盖为服务端值
48
59
  blockModel.resetUserModifiedFields?.();
49
60
  await resource.refresh();
61
+ responseRecord = responseRecord ?? resource.getData?.();
50
62
  } else {
51
63
  blockModel.form.resetFields();
52
64
  blockModel.emitter.emit('onFieldReset');
53
65
  blockModel.resetUserModifiedFields?.();
54
66
  blockModel.formValueRuntime?.resetAfterFormReset?.();
55
67
  if (ctx.view.inputArgs.collectionName === blockModel.collection.name && ctx.view.inputArgs.onChange) {
56
- ctx.view.inputArgs.onChange(data?.data);
68
+ ctx.view.inputArgs.onChange(responseRecord);
57
69
  }
58
70
  }
71
+ return responseRecord;
59
72
  } else if (resource instanceof MultiRecordResource) {
60
73
  const currentFilterByTk = resource.getMeta('currentFilterByTk');
61
74
  if (!currentFilterByTk) {
62
75
  ctx.message.error(ctx.t('No filterByTk found for multi-record resource.'));
63
76
  return;
64
77
  }
65
- (await cb)
78
+ const data = cb
66
79
  ? await cb(values, currentFilterByTk)
67
80
  : await resource.update(currentFilterByTk, values, params.requestConfig);
68
81
  blockModel.resetUserModifiedFields?.();
82
+ return getResponseRecord(data) ?? blockModel.getCurrentRecord?.();
69
83
  }
70
84
  }