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

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 (216) 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 +21 -0
  29. package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
  30. package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
  31. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  32. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
  33. package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
  34. package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
  35. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
  36. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
  37. package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
  38. package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
  39. package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
  40. package/es/flow/utils/formValueDeps.d.ts +32 -0
  41. package/es/flow-compat/FieldValidation.d.ts +2 -1
  42. package/es/index.d.ts +2 -1
  43. package/es/index.mjs +233 -137
  44. package/lib/index.js +234 -138
  45. package/package.json +7 -7
  46. package/src/PluginSettingsManager.ts +53 -0
  47. package/src/RouteRepository.ts +126 -24
  48. package/src/__tests__/PluginSettingsManager.test.ts +13 -0
  49. package/src/__tests__/RouteRepository.test.ts +216 -0
  50. package/src/__tests__/browserChecker.test.ts +61 -0
  51. package/src/__tests__/exports.test.ts +16 -0
  52. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +199 -0
  53. package/src/__tests__/plugin-manager.test.tsx +44 -2
  54. package/src/__tests__/settings-center.test.tsx +40 -1
  55. package/src/collection-manager/field-configure.ts +1 -1
  56. package/src/collection-manager/interfaces/id.ts +1 -1
  57. package/src/collection-manager/interfaces/m2m.tsx +2 -2
  58. package/src/collection-manager/interfaces/m2o.tsx +2 -2
  59. package/src/collection-manager/interfaces/nanoid.ts +1 -1
  60. package/src/collection-manager/interfaces/o2m.tsx +2 -2
  61. package/src/collection-manager/interfaces/obo.tsx +2 -2
  62. package/src/collection-manager/interfaces/oho.tsx +2 -2
  63. package/src/collection-manager/interfaces/properties/index.ts +2 -2
  64. package/src/collection-manager/interfaces/uuid.ts +1 -1
  65. package/src/components/AppComponents.tsx +19 -3
  66. package/src/components/README.md +7 -1
  67. package/src/components/README.zh-CN.md +6 -1
  68. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  69. package/src/components/category-tabs/index.ts +10 -0
  70. package/src/components/form/JsonTextArea.tsx +21 -11
  71. package/src/components/form/TypedVariableInput.tsx +416 -93
  72. package/src/components/form/VariableJsonTextArea.tsx +175 -0
  73. package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
  74. package/src/components/form/__tests__/TypedVariableInput.test.tsx +266 -9
  75. package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
  76. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  77. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  78. package/src/components/form/filter/index.ts +2 -0
  79. package/src/components/form/index.tsx +1 -0
  80. package/src/components/index.ts +2 -0
  81. package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
  82. package/src/flow/__tests__/getKey.test.ts +7 -0
  83. package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
  84. package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
  85. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +58 -0
  86. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1022 -0
  87. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
  88. package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
  89. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
  90. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
  91. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +104 -0
  92. package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
  93. package/src/flow/actions/afterSuccess.tsx +142 -3
  94. package/src/flow/actions/customVariable.tsx +1 -2
  95. package/src/flow/actions/dataScopeFilter.ts +10 -0
  96. package/src/flow/actions/dateTimeFormat.tsx +42 -28
  97. package/src/flow/actions/index.ts +1 -1
  98. package/src/flow/actions/linkageRules.tsx +123 -35
  99. package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
  100. package/src/flow/actions/openView.tsx +38 -4
  101. package/src/flow/actions/runjs.tsx +2 -14
  102. package/src/flow/actions/validation.tsx +62 -30
  103. package/src/flow/admin-shell/BaseLayoutModel.tsx +25 -3
  104. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +5 -2
  105. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +13 -0
  106. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +34 -10
  107. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
  108. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
  109. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  110. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  111. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  112. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  113. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
  114. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
  115. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
  116. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  117. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +82 -0
  118. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
  119. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  120. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  121. package/src/flow/components/DefaultValue.tsx +1 -2
  122. package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
  123. package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
  124. package/src/flow/components/FieldAssignValueInput.tsx +10 -5
  125. package/src/flow/components/FlowRoute.tsx +56 -11
  126. package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
  127. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
  128. package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
  129. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  130. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  131. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  132. package/src/flow/components/fieldAssignOptions.ts +155 -27
  133. package/src/flow/flows/editMarkdownFlow.tsx +1 -1
  134. package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
  135. package/src/flow/index.ts +2 -1
  136. package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
  137. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  138. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  139. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  140. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  141. package/src/flow/models/base/ActionModel.tsx +12 -1
  142. package/src/flow/models/base/GridModel.tsx +38 -7
  143. package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
  144. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
  145. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  146. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  147. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  148. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +31 -3
  149. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  150. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
  151. package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
  152. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
  153. package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
  154. package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
  155. package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
  156. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  157. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  158. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
  159. package/src/flow/models/blocks/form/submitHandler.ts +17 -3
  160. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +880 -102
  161. package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
  162. package/src/flow/models/blocks/form/value-runtime/runtime.ts +257 -13
  163. package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
  164. package/src/flow/models/blocks/js-block/JSBlock.tsx +2 -7
  165. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  166. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +36 -13
  167. package/src/flow/models/blocks/table/TableBlockModel.tsx +38 -17
  168. package/src/flow/models/blocks/table/TableColumnModel.tsx +36 -3
  169. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +111 -1
  170. package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
  171. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  172. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
  173. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +159 -1
  174. package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
  175. package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
  176. package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
  177. package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
  178. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +29 -6
  179. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +16 -4
  180. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
  181. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
  182. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -0
  183. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
  184. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
  185. package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
  186. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
  187. package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
  188. package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
  189. package/src/flow/models/fields/InputFieldModel.tsx +40 -2
  190. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  191. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  192. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  193. package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
  194. package/src/flow/models/fields/TextareaFieldModel.tsx +49 -3
  195. package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
  196. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +117 -1
  197. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
  198. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +100 -0
  199. package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
  200. package/src/flow/resolveViewParamsToViewList.tsx +11 -3
  201. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +258 -0
  202. package/src/flow/utils/dataScopeFormValueClear.ts +218 -81
  203. package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
  204. package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
  205. package/src/flow/utils/formValueDeps.ts +170 -0
  206. package/src/flow-compat/FieldValidation.tsx +122 -60
  207. package/src/flow-compat/Popover.tsx +43 -4
  208. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  209. package/src/index.ts +8 -1
  210. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  211. package/src/nocobase-buildin-plugin/index.tsx +44 -14
  212. package/src/settings-center/AdminSettingsLayout.tsx +10 -2
  213. package/src/settings-center/SystemSettingsPage.tsx +1 -2
  214. package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
  215. package/src/settings-center/plugin-manager/index.tsx +3 -0
  216. package/src/settings-center/utils.tsx +0 -6
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { describe, expect, it, vi } from 'vitest';
11
11
  import { FlowEngine, FlowModel } from '@nocobase/flow-engine';
12
- import { fieldLinkageRules } from '../linkageRules';
12
+ import { fieldLinkageRules, linkageAssignField } from '../linkageRules';
13
13
 
14
14
  describe('fieldLinkageRules action - linkage scope metadata', () => {
15
15
  const targetCollection: any = {
@@ -868,6 +868,155 @@ describe('fieldLinkageRules action - linkage scope metadata', () => {
868
868
  expect(setFormValues).toHaveBeenCalledTimes(2);
869
869
  });
870
870
 
871
+ it('keeps row-scoped override patches editable after the target is user edited', async () => {
872
+ const store = {
873
+ roles: [{ uid: 'role-uid-1', name: 'existing' }],
874
+ };
875
+ const getAt = (path: Array<string | number>) => path.reduce((acc: any, seg) => acc?.[seg], store as any);
876
+ const setAt = (path: Array<string | number>, value: any) => {
877
+ let cur: any = store;
878
+ for (const seg of path.slice(0, -1)) {
879
+ cur = cur[seg];
880
+ }
881
+ cur[path[path.length - 1]] = value;
882
+ };
883
+ const editedKeys = new Set<string>();
884
+ const toKey = (path: Array<string | number>) => JSON.stringify(path);
885
+ const form = {
886
+ getFieldValue: (path: Array<string | number>) => getAt(path),
887
+ };
888
+ const formValueRuntime = {
889
+ canApplyOverrideValuePatch: vi.fn((path: Array<string | number>) => !editedKeys.has(toKey(path))),
890
+ };
891
+ const setFormValues = vi.fn(async (patches: Array<{ path: Array<string | number>; value: any }>) => {
892
+ for (const patch of patches) {
893
+ setAt(patch.path, patch.value);
894
+ }
895
+ });
896
+
897
+ const engine = new FlowEngine();
898
+ const rowFork = new FlowModel({ uid: 'role-override-row-fork', flowEngine: engine }) as any;
899
+ rowFork.context.defineProperty('fieldIndex', {
900
+ value: ['roles:0'],
901
+ });
902
+ rowFork.context.defineProperty('form', {
903
+ value: form,
904
+ });
905
+ rowFork.context.defineProperty('setFormValues', {
906
+ value: setFormValues,
907
+ });
908
+ rowFork.context.defineProperty('app', {
909
+ value: {
910
+ jsonLogic: {
911
+ apply: () => true,
912
+ },
913
+ },
914
+ });
915
+ rowFork.subModels = { items: [] };
916
+ rowFork.getAction = vi.fn((name: string) => {
917
+ if (name === 'linkageAssignField') {
918
+ return linkageAssignField;
919
+ }
920
+ });
921
+
922
+ const masterModel: any = {
923
+ uid: 'master-role-override-grid',
924
+ forks: new Set([rowFork]),
925
+ };
926
+ const rolesField: any = {
927
+ type: 'hasMany',
928
+ isAssociationField: () => true,
929
+ targetCollection: {
930
+ getField: (name: string) => ({ name, isAssociationField: () => false }),
931
+ },
932
+ };
933
+ const blockModel: any = {
934
+ collection: {
935
+ getField: (name: string) => (name === 'roles' ? rolesField : null),
936
+ },
937
+ formValueRuntime,
938
+ };
939
+ rowFork.context.defineProperty('blockModel', {
940
+ value: blockModel,
941
+ });
942
+ const gridModel: any = {
943
+ uid: 'grid-model-role-override-row',
944
+ context: {
945
+ blockModel,
946
+ },
947
+ getAction: vi.fn((name: string) => {
948
+ if (name === 'linkageAssignField') {
949
+ return linkageAssignField;
950
+ }
951
+ }),
952
+ __allModels: [],
953
+ };
954
+ const ctx: any = {
955
+ model: gridModel,
956
+ engine: {
957
+ forEachModel: (cb: (m: any) => void) => {
958
+ cb(masterModel);
959
+ },
960
+ },
961
+ flowKey: 'eventSettings',
962
+ inputArgs: {
963
+ source: 'user',
964
+ txId: 'tx-role-override-row',
965
+ changedPaths: [['roles', 0, 'uid']],
966
+ },
967
+ app: {
968
+ jsonLogic: {
969
+ apply: () => true,
970
+ },
971
+ },
972
+ resolveJsonTemplate: async (v: any) => v,
973
+ };
974
+ const makeParams = (value: string) => ({
975
+ value: [
976
+ {
977
+ key: `rule-${value}`,
978
+ title: `rule-${value}`,
979
+ enable: true,
980
+ condition: { logic: '$and', items: [] },
981
+ actions: [
982
+ {
983
+ name: 'linkageAssignField',
984
+ params: {
985
+ value: [
986
+ {
987
+ key: `r-${value}`,
988
+ enable: true,
989
+ targetPath: 'roles.name',
990
+ mode: 'override',
991
+ value,
992
+ condition: { logic: '$and', items: [] },
993
+ },
994
+ ],
995
+ },
996
+ },
997
+ ],
998
+ },
999
+ ],
1000
+ });
1001
+
1002
+ await fieldLinkageRules.handler(ctx, makeParams('role-uid-1'));
1003
+ expect(store.roles[0].name).toBe('role-uid-1');
1004
+
1005
+ store.roles[0].uid = 'role-uid-2';
1006
+ await fieldLinkageRules.handler(ctx, makeParams('role-uid-2'));
1007
+ expect(store.roles[0].name).toBe('role-uid-2');
1008
+ expect(setFormValues).toHaveBeenCalledTimes(2);
1009
+
1010
+ store.roles[0].name = 'manual';
1011
+ store.roles[0].uid = 'role-uid-3';
1012
+ editedKeys.add(toKey(['roles', 0, 'name']));
1013
+ await fieldLinkageRules.handler(ctx, makeParams('role-uid-3'));
1014
+
1015
+ expect(store.roles[0].name).toBe('manual');
1016
+ expect(setFormValues).toHaveBeenCalledTimes(2);
1017
+ expect(formValueRuntime.canApplyOverrideValuePatch).toHaveBeenCalledWith(['roles', 0, 'name']);
1018
+ });
1019
+
871
1020
  it('skips stale subtable row forks after the row has been removed', async () => {
872
1021
  const setFormValues = vi.fn(async () => undefined);
873
1022
  const defaultHandler = vi.fn((actionCtx: any, { addFormValuePatch }: any) => {
@@ -69,6 +69,72 @@ describe('linkage assign actions - legacy params', () => {
69
69
  expect(setProps).not.toHaveBeenCalledWith(fieldModel, { value: 'default-in-update' });
70
70
  });
71
71
 
72
+ it('linkageAssignField should mark override mode for field targets', async () => {
73
+ const fieldModel: any = { uid: 'f-override', fieldPath: 'a.b' };
74
+ const ctx: any = {
75
+ model: {
76
+ subModels: { grid: { subModels: { items: [fieldModel] } } },
77
+ },
78
+ engine: { getModel: vi.fn(() => ({ fieldPath: 'a.b' })) },
79
+ app: { jsonLogic: { apply: vi.fn() } },
80
+ };
81
+ const setProps = vi.fn();
82
+
83
+ await linkageAssignField.handler(ctx, {
84
+ value: [
85
+ {
86
+ key: 'r-override',
87
+ enable: true,
88
+ targetPath: 'a.b',
89
+ mode: 'override',
90
+ condition: { logic: '$and', items: [] },
91
+ value: 'override-value',
92
+ },
93
+ ],
94
+ setProps,
95
+ });
96
+
97
+ expect(setProps).toHaveBeenCalledTimes(1);
98
+ const [, props] = setProps.mock.calls[0];
99
+ expect(props).toMatchObject({
100
+ value: 'override-value',
101
+ __linkageAssignMode: 'override',
102
+ });
103
+ });
104
+
105
+ it('linkageAssignField should emit override patches for targets without form item models', async () => {
106
+ const ctx: any = {
107
+ model: { subModels: { grid: { subModels: { items: [] } } } },
108
+ engine: { getModel: vi.fn(() => null) },
109
+ app: { jsonLogic: { apply: vi.fn() } },
110
+ };
111
+ const setProps = vi.fn();
112
+ const addFormValuePatch = vi.fn();
113
+
114
+ await linkageAssignField.handler(ctx, {
115
+ value: [
116
+ {
117
+ key: 'r-override-direct',
118
+ enable: true,
119
+ targetPath: 'user.name',
120
+ mode: 'override',
121
+ condition: { logic: '$and', items: [] },
122
+ value: 'override-direct',
123
+ },
124
+ ],
125
+ setProps,
126
+ addFormValuePatch,
127
+ });
128
+
129
+ expect(setProps).not.toHaveBeenCalled();
130
+ expect(addFormValuePatch).toHaveBeenCalledWith({
131
+ path: 'user.name',
132
+ value: 'override-direct',
133
+ whenEmpty: false,
134
+ mode: 'override',
135
+ });
136
+ });
137
+
72
138
  it('subFormLinkageAssignField should apply legacy object params at runtime', () => {
73
139
  const forkModel: any = {
74
140
  uid: 'f1',
@@ -381,6 +447,75 @@ describe('linkage assign actions - legacy params', () => {
381
447
  expect(addFormValuePatch).toHaveBeenCalledTimes(1);
382
448
  });
383
449
 
450
+ it('subFormLinkageAssignField should skip override patch after user edits the target path', () => {
451
+ const ctx: any = {
452
+ model: {
453
+ uid: 'sub-form-override-path',
454
+ getAclActionName: vi.fn(() => 'create'),
455
+ context: {},
456
+ parent: {
457
+ getStepParams: (flowKey: string, stepKey: string) => {
458
+ if (flowKey === 'fieldSettings' && stepKey === 'init') {
459
+ return { fieldPath: 'M2M.M2M' };
460
+ }
461
+ return {};
462
+ },
463
+ },
464
+ subModels: { grid: { subModels: { items: [] } } },
465
+ },
466
+ engine: {
467
+ getModel: vi.fn(() => null),
468
+ },
469
+ app: { jsonLogic: { apply: vi.fn() } },
470
+ };
471
+ const addFormValuePatch = vi.fn();
472
+
473
+ subFormLinkageAssignField.handler(ctx, {
474
+ value: [
475
+ {
476
+ key: 'r-override-path-1',
477
+ enable: true,
478
+ targetPath: 'Name',
479
+ mode: 'override',
480
+ condition: { logic: '$and', items: [] },
481
+ value: 'override-1',
482
+ },
483
+ ],
484
+ addFormValuePatch,
485
+ setProps: vi.fn(),
486
+ });
487
+
488
+ expect(addFormValuePatch).toHaveBeenCalledTimes(1);
489
+ expect(addFormValuePatch).toHaveBeenNthCalledWith(1, {
490
+ path: 'M2M.M2M.Name',
491
+ value: 'override-1',
492
+ whenEmpty: false,
493
+ mode: 'override',
494
+ });
495
+
496
+ ctx.inputArgs = {
497
+ source: 'user',
498
+ changedPaths: [['M2M', 'M2M', 'Name']],
499
+ };
500
+
501
+ subFormLinkageAssignField.handler(ctx, {
502
+ value: [
503
+ {
504
+ key: 'r-override-path-2',
505
+ enable: true,
506
+ targetPath: 'Name',
507
+ mode: 'override',
508
+ condition: { logic: '$and', items: [] },
509
+ value: 'override-2',
510
+ },
511
+ ],
512
+ addFormValuePatch,
513
+ setProps: vi.fn(),
514
+ });
515
+
516
+ expect(addFormValuePatch).toHaveBeenCalledTimes(1);
517
+ });
518
+
384
519
  it('linkageAssignField should evaluate RunJS value before assign', async () => {
385
520
  const fieldModel: any = { uid: 'f-runjs-assign', fieldPath: 'a.b' };
386
521
  const runjs = vi.fn(async () => ({ success: true, value: 'from-runjs' }));
@@ -27,12 +27,12 @@ describe('linkageRules hidden state propagation', () => {
27
27
  const hostModel: any = {
28
28
  uid: 'host',
29
29
  hidden: false,
30
- context: { blockModel },
30
+ context: { blockModel, fieldPathArray: ['children'] },
31
31
  __allModels: [],
32
32
  setProps: vi.fn(),
33
33
  getFlow: vi.fn(() => ({})),
34
34
  getStepParams: vi.fn((_flowKey: string, stepKey: string) => {
35
- if (stepKey === 'init') return { fieldPath: 'org_m2o' };
35
+ if (stepKey === 'init') return { fieldPath: 'children' };
36
36
  return undefined;
37
37
  }),
38
38
  translate: (s: string) => s,
@@ -41,10 +41,22 @@ describe('linkageRules hidden state propagation', () => {
41
41
  const sameTargetModel: any = {
42
42
  uid: 'same-target',
43
43
  hidden: false,
44
- context: { blockModel },
44
+ context: { blockModel, fieldPathArray: ['children'] },
45
45
  setProps: vi.fn(),
46
46
  getStepParams: vi.fn((_flowKey: string, stepKey: string) => {
47
- if (stepKey === 'init') return { fieldPath: 'org_m2o' };
47
+ if (stepKey === 'init') return { fieldPath: 'children' };
48
+ return undefined;
49
+ }),
50
+ translate: (s: string) => s,
51
+ };
52
+
53
+ const nestedColumnModel: any = {
54
+ uid: 'nested-column',
55
+ hidden: true,
56
+ context: { blockModel, fieldPathArray: ['children'] },
57
+ setProps: vi.fn(),
58
+ getStepParams: vi.fn((_flowKey: string, stepKey: string) => {
59
+ if (stepKey === 'init') return { fieldPath: 'children.children' };
48
60
  return undefined;
49
61
  }),
50
62
  translate: (s: string) => s,
@@ -52,7 +64,7 @@ describe('linkageRules hidden state propagation', () => {
52
64
 
53
65
  const engine = {
54
66
  forEachModel: (visitor: (m: any) => void) => {
55
- [hostModel, sameTargetModel].forEach(visitor);
67
+ [hostModel, sameTargetModel, nestedColumnModel].forEach(visitor);
56
68
  },
57
69
  };
58
70
 
@@ -92,9 +104,11 @@ describe('linkageRules hidden state propagation', () => {
92
104
  });
93
105
  expect(hostModel.hidden).toBe(true);
94
106
  expect(sameTargetModel.hidden).toBe(true);
107
+ expect(nestedColumnModel.hidden).toBe(true);
95
108
 
96
109
  await actionLinkageRules.handler(ctx, { value: [] });
97
110
  expect(hostModel.hidden).toBe(false);
98
111
  expect(sameTargetModel.hidden).toBe(false);
112
+ expect(nestedColumnModel.hidden).toBe(true);
99
113
  });
100
114
  });
@@ -728,6 +728,98 @@ describe('linkageSetFieldProps action', () => {
728
728
  expect(form.setFieldValue).not.toHaveBeenCalled();
729
729
  });
730
730
 
731
+ it('should not clear the parent field when a nested same-name field inherits the parent path', async () => {
732
+ const setFormValues = vi.fn(async () => undefined);
733
+ const form = {
734
+ getFieldValue: vi.fn((path: Array<string | number>) => {
735
+ if (JSON.stringify(path) === JSON.stringify(['children', 'children'])) {
736
+ return [{ title: 'nested' }];
737
+ }
738
+ if (JSON.stringify(path) === JSON.stringify(['children'])) {
739
+ return [{ title: 'parent' }];
740
+ }
741
+ return undefined;
742
+ }),
743
+ setFieldValue: vi.fn(),
744
+ };
745
+ const fieldModel: any = {
746
+ uid: 'nested-children-field',
747
+ hidden: false,
748
+ context: {
749
+ form,
750
+ fieldPathArray: ['children'],
751
+ },
752
+ props: {
753
+ label: 'Children',
754
+ },
755
+ getStepParams: vi.fn((flowKey: string, stepKey: string) => {
756
+ if (flowKey === 'fieldSettings' && stepKey === 'init') {
757
+ return { fieldPath: 'children.children' };
758
+ }
759
+ }),
760
+ setProps(key: any, value?: any) {
761
+ if (typeof key === 'string') {
762
+ this.props[key] = value;
763
+ } else {
764
+ this.props = { ...this.props, ...key };
765
+ }
766
+ },
767
+ };
768
+ const ctx: any = {
769
+ app: {
770
+ jsonLogic: {
771
+ apply: vi.fn(() => true),
772
+ },
773
+ },
774
+ model: {
775
+ context: { form },
776
+ subModels: {
777
+ grid: {
778
+ subModels: {
779
+ items: [fieldModel],
780
+ },
781
+ },
782
+ },
783
+ },
784
+ setFormValues,
785
+ getAction: (name: string) => (name === 'linkageSetFieldProps' ? linkageSetFieldProps : null),
786
+ resolveJsonTemplate: vi.fn(async (value) => value),
787
+ };
788
+
789
+ await fieldLinkageRules.handler(ctx, {
790
+ value: [
791
+ {
792
+ key: 'rule-1',
793
+ enable: true,
794
+ condition: { logic: '$and', items: [] },
795
+ actions: [
796
+ {
797
+ key: 'action-1',
798
+ name: 'linkageSetFieldProps',
799
+ params: {
800
+ value: {
801
+ fields: ['nested-children-field'],
802
+ state: 'hidden',
803
+ },
804
+ },
805
+ },
806
+ ],
807
+ },
808
+ ],
809
+ });
810
+
811
+ expect(fieldModel.hidden).toBe(true);
812
+ expect(setFormValues).toHaveBeenCalledWith(
813
+ [{ path: ['children', 'children'], value: undefined }],
814
+ expect.objectContaining({ source: 'linkage' }),
815
+ );
816
+ expect(setFormValues).not.toHaveBeenCalledWith(
817
+ [{ path: ['children'], value: undefined }],
818
+ expect.objectContaining({ source: 'linkage' }),
819
+ );
820
+ expect(form.setFieldValue).not.toHaveBeenCalled();
821
+ });
822
+
731
823
  it('should keep hidden clear after same-round assignment for the same field', async () => {
732
824
  const formValues: { name?: string } = {};
733
825
  const setFormValues = vi.fn(async (patches: Array<{ path: Array<string | number>; value: string | undefined }>) => {
@@ -9,6 +9,7 @@
9
9
 
10
10
  import React from 'react';
11
11
  import { describe, it, expect, vi } from 'vitest';
12
+ import { RUNJS_OPEN_VIEW_ROUTE_STATE } from '@nocobase/flow-engine';
12
13
  import { openView } from '../openView';
13
14
  import { FlowPage } from '../../FlowPage';
14
15
 
@@ -50,6 +51,27 @@ describe('openView action - route mode defineProperties/defineMethods', () => {
50
51
  return { ctx, engine };
51
52
  };
52
53
 
54
+ const createFirstStageCtx = (inputArgs: Record<PropertyKey, unknown>) => {
55
+ const navigateTo = vi.fn();
56
+ const ctx: any = {
57
+ inputArgs,
58
+ engine: { context: { themeToken: { colorBgLayout: '#fff' } } },
59
+ model: {
60
+ uid: 'popup-uid',
61
+ context: {
62
+ inputArgs: {},
63
+ defineProperty: vi.fn(),
64
+ },
65
+ flowEngine: { context: { themeToken: { colorBgLayout: '#fff' } } },
66
+ },
67
+ view: {
68
+ navigation: { navigateTo },
69
+ },
70
+ };
71
+
72
+ return { ctx, navigateTo };
73
+ };
74
+
53
75
  it('injects defineProperties/defineMethods on onModelLoaded even when not present in current inputArgs', async () => {
54
76
  const { ctx } = createRouteManagedCtx();
55
77
 
@@ -149,4 +171,86 @@ describe('openView action - route mode defineProperties/defineMethods', () => {
149
171
  const defineMethodCalls = pageModelContext.defineMethod.mock.calls.map((c: any[]) => c[0]);
150
172
  expect(defineMethodCalls).toContain('pong');
151
173
  });
174
+
175
+ it('adds route state to first-stage navigation only for RunJS ctx.openView calls', async () => {
176
+ const { ctx, navigateTo } = createFirstStageCtx({
177
+ mode: 'dialog',
178
+ size: 'large',
179
+ [RUNJS_OPEN_VIEW_ROUTE_STATE]: { mode: 'dialog', size: 'large' },
180
+ });
181
+
182
+ await openView.handler(ctx, { mode: 'drawer', size: 'medium', navigation: true });
183
+
184
+ expect(navigateTo).toHaveBeenCalledWith({
185
+ viewUid: 'popup-uid',
186
+ filterByTk: undefined,
187
+ sourceId: undefined,
188
+ tabUid: undefined,
189
+ openViewRouteState: { mode: 'dialog', size: 'large' },
190
+ });
191
+ });
192
+
193
+ it('normalizes first-stage RunJS route state mode on mobile layout', async () => {
194
+ const { ctx, navigateTo } = createFirstStageCtx({
195
+ isMobileLayout: true,
196
+ mode: 'dialog',
197
+ size: 'large',
198
+ [RUNJS_OPEN_VIEW_ROUTE_STATE]: { mode: 'dialog', size: 'large' },
199
+ });
200
+
201
+ await openView.handler(ctx, { mode: 'drawer', size: 'medium', navigation: true });
202
+
203
+ expect(navigateTo).toHaveBeenCalledWith({
204
+ viewUid: 'popup-uid',
205
+ filterByTk: undefined,
206
+ sourceId: undefined,
207
+ tabUid: undefined,
208
+ openViewRouteState: { mode: 'embed', size: 'large' },
209
+ });
210
+ });
211
+
212
+ it('uses route replay mode and size before persisted openView defaults', async () => {
213
+ const { ctx } = createRouteManagedCtx();
214
+ ctx.inputArgs.mode = 'dialog';
215
+ ctx.inputArgs.size = 'large';
216
+
217
+ await openView.handler(ctx, { mode: 'drawer', size: 'medium', navigation: true });
218
+
219
+ expect(ctx.viewer.open).toHaveBeenCalledWith(
220
+ expect.objectContaining({
221
+ type: 'dialog',
222
+ width: '80%',
223
+ }),
224
+ );
225
+ });
226
+
227
+ it('uses decoded openView route state during route replay', async () => {
228
+ const { ctx } = createRouteManagedCtx();
229
+ ctx.inputArgs.openViewRouteState = { mode: 'dialog', size: 'large' };
230
+
231
+ await openView.handler(ctx, { mode: 'drawer', size: 'medium', navigation: true });
232
+
233
+ expect(ctx.viewer.open).toHaveBeenCalledWith(
234
+ expect.objectContaining({
235
+ type: 'dialog',
236
+ width: '80%',
237
+ }),
238
+ );
239
+ });
240
+
241
+ it('keeps first-stage navigation unchanged for non-RunJS openView calls', async () => {
242
+ const { ctx, navigateTo } = createFirstStageCtx({
243
+ mode: 'dialog',
244
+ size: 'large',
245
+ });
246
+
247
+ await openView.handler(ctx, { mode: 'drawer', size: 'medium', navigation: true });
248
+
249
+ expect(navigateTo).toHaveBeenCalledWith({
250
+ viewUid: 'popup-uid',
251
+ filterByTk: undefined,
252
+ sourceId: undefined,
253
+ tabUid: undefined,
254
+ });
255
+ });
152
256
  });
@@ -13,7 +13,7 @@ import { subFormFieldLinkageRules } from '../linkageRules';
13
13
  import { SubFormFieldModel, SubFormListFieldModel } from '../../models/fields/AssociationFieldModel/SubFormFieldModel';
14
14
 
15
15
  describe('subFormFieldLinkageRules action', () => {
16
- it('passes inputArgs to fork runtime context', async () => {
16
+ it('passes inputArgs to fork runtime context when grid forks are stored in a Set', async () => {
17
17
  const linkageAssignHandler = vi.fn();
18
18
  const engine = new FlowEngine();
19
19
  const forkModel = new FlowModel({ uid: 'fork-model', flowEngine: engine }) as any;
@@ -38,7 +38,7 @@ describe('subFormFieldLinkageRules action', () => {
38
38
  hidden: false,
39
39
  subModels: {
40
40
  grid: {
41
- forks: [forkModel],
41
+ forks: new Set([forkModel]),
42
42
  },
43
43
  },
44
44
  },