@nocobase/client-v2 2.2.0-beta.9 → 3.0.0-alpha.1

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 (208) hide show
  1. package/es/APIClient.d.ts +1 -3
  2. package/es/BaseApplication.d.ts +1 -0
  3. package/es/acl/ACLProvider.d.ts +10 -0
  4. package/es/authRedirect.d.ts +5 -0
  5. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  6. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  7. package/es/components/AttachmentUpload.d.ts +51 -0
  8. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  9. package/es/components/category-tabs/index.d.ts +9 -0
  10. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  11. package/es/components/form/TypedVariableInput.d.ts +31 -5
  12. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  13. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  14. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  15. package/es/components/form/filter/index.d.ts +2 -0
  16. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  17. package/es/components/index.d.ts +2 -0
  18. package/es/flow/actions/index.d.ts +1 -1
  19. package/es/flow/actions/linkageRules.d.ts +3 -0
  20. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  21. package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
  22. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  23. package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
  24. package/es/flow/components/FlowRoute.d.ts +2 -2
  25. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  26. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  27. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  28. package/es/flow/index.d.ts +1 -0
  29. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  30. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  31. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  32. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  33. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  34. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  35. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  36. package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
  37. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  38. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  39. package/es/flow-compat/routeTypes.d.ts +1 -0
  40. package/es/index.d.ts +8 -1
  41. package/es/index.mjs +190 -141
  42. package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
  43. package/es/nocobase-buildin-plugin/index.d.ts +9 -0
  44. package/es/settings-app/SettingsApplication.d.ts +18 -0
  45. package/es/settings-app/SettingsBrand.d.ts +16 -0
  46. package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
  47. package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
  48. package/es/settings-app/SettingsGroupNav.d.ts +17 -0
  49. package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
  50. package/es/settings-app/SettingsRouterManager.d.ts +15 -0
  51. package/es/settings-app/SettingsSearch.d.ts +17 -0
  52. package/es/settings-app/SettingsShell.d.ts +10 -0
  53. package/es/settings-app/isSettingsApp.d.ts +18 -0
  54. package/es/settings-app/runtimePaths.d.ts +18 -0
  55. package/es/settings-app/settingsDocumentPath.d.ts +14 -0
  56. package/es/settings-app/settingsTheme.d.ts +87 -0
  57. package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
  58. package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
  59. package/es/settings-center/groups.d.ts +66 -0
  60. package/es/settings-center/useSettingsGroups.d.ts +36 -0
  61. package/es/settings-center/useSettingsSearch.d.ts +35 -0
  62. package/es/settings-center/utils.d.ts +24 -0
  63. package/es/theme/index.d.ts +1 -0
  64. package/es/theme/itemActive.d.ts +24 -0
  65. package/es/theme/type.d.ts +9 -0
  66. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  67. package/es/utils/index.d.ts +1 -0
  68. package/lib/index.js +195 -146
  69. package/lib/locale/languageCodes.js +3 -1
  70. package/package.json +9 -7
  71. package/src/APIClient.ts +1 -10
  72. package/src/Application.tsx +4 -0
  73. package/src/BaseApplication.tsx +11 -6
  74. package/src/__tests__/app.test.tsx +73 -0
  75. package/src/__tests__/authRedirect.test.ts +137 -0
  76. package/src/__tests__/browserChecker.test.ts +185 -6
  77. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  78. package/src/__tests__/mockSettingsApplication.ts +29 -0
  79. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +79 -10
  80. package/src/__tests__/plugin-manager.test.tsx +7 -6
  81. package/src/__tests__/settings-application.test.ts +164 -0
  82. package/src/__tests__/settings-auth-routes.test.ts +71 -0
  83. package/src/__tests__/settings-center.test.tsx +38 -37
  84. package/src/__tests__/settings-layout-root.test.tsx +260 -0
  85. package/src/__tests__/settings-runtime-paths.test.ts +99 -0
  86. package/src/__tests__/settings-shell.test.tsx +200 -0
  87. package/src/acl/ACLProvider.tsx +21 -0
  88. package/src/authRedirect.ts +72 -3
  89. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  90. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  91. package/src/collection-manager/field-validation.ts +1 -1
  92. package/src/components/AttachmentUpload.tsx +275 -0
  93. package/src/components/README.md +7 -1
  94. package/src/components/README.zh-CN.md +6 -1
  95. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  96. package/src/components/category-tabs/index.ts +10 -0
  97. package/src/components/form/JsonTextArea.tsx +4 -0
  98. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  99. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  100. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  101. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  102. package/src/components/form/TypedVariableInput.tsx +452 -101
  103. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  104. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  105. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  106. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  107. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  108. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  109. package/src/components/form/filter/index.ts +2 -0
  110. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  111. package/src/components/index.ts +2 -0
  112. package/src/flow/FlowPage.tsx +9 -1
  113. package/src/flow/__tests__/FlowPage.test.tsx +50 -3
  114. package/src/flow/__tests__/FlowRoute.test.tsx +128 -10
  115. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  116. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  117. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  118. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  119. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  120. package/src/flow/actions/index.ts +2 -0
  121. package/src/flow/actions/linkageRules.tsx +111 -16
  122. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  123. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -57
  124. package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
  125. package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
  126. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  127. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +77 -13
  128. package/src/flow/common/Markdown/style.ts +4 -0
  129. package/src/flow/components/BlockItemCard.tsx +2 -2
  130. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  131. package/src/flow/components/FieldAssignValueInput.tsx +91 -20
  132. package/src/flow/components/FlowRoute.tsx +78 -32
  133. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  134. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  135. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  136. package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
  137. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  138. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  139. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
  140. package/src/flow/index.ts +3 -0
  141. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  142. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  143. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  144. package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
  145. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  146. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  147. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  148. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  149. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  150. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  151. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  152. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  153. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  154. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  155. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
  156. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
  157. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
  158. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
  159. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  160. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  161. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  162. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  163. package/src/flow/models/blocks/table/TableBlockModel.tsx +4 -2
  164. package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
  165. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  166. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  167. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +2 -1
  168. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  169. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  170. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  171. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  172. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  173. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  174. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
  175. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
  176. package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
  177. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  178. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  179. package/src/flow-compat/routeTypes.ts +1 -0
  180. package/src/index.ts +14 -1
  181. package/src/locale/languageCodes.ts +3 -1
  182. package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
  183. package/src/nocobase-buildin-plugin/index.tsx +110 -70
  184. package/src/settings-app/SettingsApplication.ts +31 -0
  185. package/src/settings-app/SettingsBrand.tsx +108 -0
  186. package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
  187. package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
  188. package/src/settings-app/SettingsGroupNav.tsx +124 -0
  189. package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
  190. package/src/settings-app/SettingsRouterManager.ts +92 -0
  191. package/src/settings-app/SettingsSearch.tsx +238 -0
  192. package/src/settings-app/SettingsShell.tsx +167 -0
  193. package/src/settings-app/isSettingsApp.ts +21 -0
  194. package/src/settings-app/runtimePaths.ts +104 -0
  195. package/src/settings-app/settingsDocumentPath.ts +66 -0
  196. package/src/settings-app/settingsTheme.ts +301 -0
  197. package/src/settings-app/useSettingsThemeConfig.ts +100 -0
  198. package/src/settings-center/AdminSettingsLayout.tsx +122 -141
  199. package/src/settings-center/SystemSettingsPage.tsx +4 -170
  200. package/src/settings-center/groups.ts +108 -0
  201. package/src/settings-center/useSettingsGroups.ts +105 -0
  202. package/src/settings-center/useSettingsSearch.ts +173 -0
  203. package/src/settings-center/utils.tsx +50 -1
  204. package/src/theme/index.tsx +1 -0
  205. package/src/theme/itemActive.ts +31 -0
  206. package/src/theme/type.ts +10 -0
  207. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  208. package/src/utils/index.tsx +1 -0
@@ -0,0 +1,314 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { FlowEngine } from '@nocobase/flow-engine';
11
+ import { describe, expect, it, vi } from 'vitest';
12
+ import { ActionModel as ConfiguredActionModel } from '../../models/base/ActionModel';
13
+ import { ActionModel } from '../../models/base/ActionModelCore';
14
+ import { actionLinkageRules, linkageSetActionProps, updateLinkageRules } from '../linkageRules';
15
+
16
+ class TestActionModel extends ActionModel {}
17
+
18
+ describe('action linkage rules on action forks', () => {
19
+ it('does not snapshot unrelated master props when disabling an action', async () => {
20
+ const engine = new FlowEngine();
21
+ engine.registerModels({ TestActionModel });
22
+ const master = engine.createModel<TestActionModel>({
23
+ use: 'TestActionModel',
24
+ props: { title: 'Edit' },
25
+ });
26
+ const fork = master.createFork({ className: 'row-action' });
27
+
28
+ const ctx = {
29
+ flowKey: 'buttonSettings',
30
+ model: fork,
31
+ app: {
32
+ jsonLogic: {
33
+ apply: vi.fn(() => true),
34
+ },
35
+ },
36
+ t: (value: string) => value,
37
+ resolveJsonTemplate: vi.fn(async (value: unknown) => value),
38
+ getAction: (name: string) => {
39
+ if (name !== 'linkageSetActionProps') return undefined;
40
+ return {
41
+ handler: async (_ctx: unknown, params: { setProps: Function }) => {
42
+ params.setProps(fork, { disabled: true });
43
+ },
44
+ };
45
+ },
46
+ } as never;
47
+
48
+ await actionLinkageRules.handler(ctx, {
49
+ value: [
50
+ {
51
+ key: 'disable-edit',
52
+ enable: true,
53
+ condition: { logic: '$and', items: [] },
54
+ actions: [{ name: 'linkageSetActionProps', params: { value: 'disabled' } }],
55
+ },
56
+ ],
57
+ });
58
+
59
+ expect(fork.localProps.title).toBeUndefined();
60
+ expect(fork.__originalProps.title).toBeUndefined();
61
+ expect(fork.localProps.disabled).toBe(true);
62
+
63
+ master.setProps('title', 'Updated');
64
+ await actionLinkageRules.handler(ctx, {
65
+ value: [
66
+ {
67
+ key: 'disable-edit',
68
+ enable: true,
69
+ condition: { logic: '$and', items: [] },
70
+ actions: [{ name: 'linkageSetActionProps', params: { value: 'disabled' } }],
71
+ },
72
+ ],
73
+ });
74
+
75
+ expect(fork.getProps().title).toBe('Updated');
76
+ expect(fork.serialize().props).toMatchObject({ title: 'Updated' });
77
+ expect(fork.serialize().props).not.toHaveProperty('disabled');
78
+
79
+ const refreshedFork = master.createFork({ className: 'row-action' });
80
+ expect(refreshedFork.getProps().title).toBe('Updated');
81
+ });
82
+
83
+ it.each(['disable', 'delete'] as const)(
84
+ 'restores a row action when linkage rules are %sd without persisting disabled',
85
+ async (change) => {
86
+ const engine = new FlowEngine();
87
+ engine.registerModels({ ConfiguredActionModel });
88
+ engine.registerActions({ actionLinkageRules, linkageSetActionProps });
89
+
90
+ let savedData: ReturnType<ConfiguredActionModel['serialize']> | undefined;
91
+ engine.setModelRepository({
92
+ save: async (model) => {
93
+ savedData = model.serialize();
94
+ return savedData;
95
+ },
96
+ } as Parameters<FlowEngine['setModelRepository']>[0]);
97
+
98
+ const enabledRules = [
99
+ {
100
+ key: 'disable-edit',
101
+ enable: true,
102
+ condition: { logic: '$and', items: [] },
103
+ actions: [{ name: 'linkageSetActionProps', params: { value: 'disabled' } }],
104
+ },
105
+ ];
106
+ const master = engine.createModel<ConfiguredActionModel>({
107
+ use: 'ConfiguredActionModel',
108
+ props: { title: 'Edit' },
109
+ stepParams: {
110
+ buttonSettings: {
111
+ linkageRules: { value: enabledRules },
112
+ },
113
+ },
114
+ });
115
+ const fork = master.createFork({ className: 'row-action' });
116
+
117
+ await fork.dispatchEvent('beforeRender', undefined, { useCache: false });
118
+ expect(fork.getProps().disabled).toBe(true);
119
+
120
+ const nextRules =
121
+ change === 'disable'
122
+ ? updateLinkageRules(enabledRules, (rules) => {
123
+ rules[0].enable = false;
124
+ })
125
+ : [];
126
+ fork.setStepParams('buttonSettings', 'linkageRules', { value: nextRules });
127
+ await fork.saveStepParams();
128
+ await fork.rerender();
129
+
130
+ expect(savedData).toBeDefined();
131
+ expect(savedData?.props).not.toHaveProperty('disabled');
132
+ expect(savedData?.stepParams.buttonSettings.linkageRules.value).toEqual(nextRules);
133
+ expect(fork.getProps().disabled).toBeUndefined();
134
+
135
+ const reloadedEngine = new FlowEngine();
136
+ reloadedEngine.registerModels({ ConfiguredActionModel });
137
+ reloadedEngine.registerActions({ actionLinkageRules, linkageSetActionProps });
138
+ const reloadedMaster = reloadedEngine.createModel<ConfiguredActionModel>({
139
+ use: 'ConfiguredActionModel',
140
+ props: savedData?.props,
141
+ stepParams: savedData?.stepParams,
142
+ });
143
+ const reloadedFork = reloadedMaster.createFork({ className: 'row-action' });
144
+
145
+ await reloadedFork.dispatchEvent('beforeRender', undefined, { useCache: false });
146
+ expect(reloadedFork.getProps().disabled).toBeUndefined();
147
+ },
148
+ );
149
+
150
+ it.each(['disabled', 'deleted'] as const)(
151
+ 'ignores a historical disabled prop after the linkage rule is %s',
152
+ async (state) => {
153
+ const engine = new FlowEngine();
154
+ engine.registerModels({ ConfiguredActionModel });
155
+ engine.registerActions({ actionLinkageRules, linkageSetActionProps });
156
+ const rules =
157
+ state === 'disabled'
158
+ ? [
159
+ {
160
+ key: 'disable-edit',
161
+ enable: false,
162
+ condition: { logic: '$and', items: [] },
163
+ actions: [{ name: 'linkageSetActionProps', params: { value: 'disabled' } }],
164
+ },
165
+ ]
166
+ : [];
167
+ const master = engine.createModel<ConfiguredActionModel>({
168
+ use: 'ConfiguredActionModel',
169
+ props: { title: 'Edit', disabled: true },
170
+ stepParams: {
171
+ buttonSettings: {
172
+ linkageRules: { value: rules },
173
+ },
174
+ },
175
+ });
176
+ const fork = master.createFork({ className: 'row-action' });
177
+
178
+ expect(master.getProps()).not.toHaveProperty('disabled');
179
+ await fork.dispatchEvent('beforeRender', undefined, { useCache: false });
180
+
181
+ expect(fork.getProps().disabled).toBeUndefined();
182
+ },
183
+ );
184
+
185
+ it('still applies an enabled linkage rule after dropping a historical disabled prop', async () => {
186
+ const engine = new FlowEngine();
187
+ engine.registerModels({ ConfiguredActionModel });
188
+ engine.registerActions({ actionLinkageRules, linkageSetActionProps });
189
+ const master = engine.createModel<ConfiguredActionModel>({
190
+ use: 'ConfiguredActionModel',
191
+ props: { title: 'Edit', disabled: true },
192
+ stepParams: {
193
+ buttonSettings: {
194
+ linkageRules: {
195
+ value: [
196
+ {
197
+ key: 'disable-edit',
198
+ enable: true,
199
+ condition: { logic: '$and', items: [] },
200
+ actions: [{ name: 'linkageSetActionProps', params: { value: 'disabled' } }],
201
+ },
202
+ ],
203
+ },
204
+ },
205
+ },
206
+ });
207
+ const fork = master.createFork({ className: 'row-action' });
208
+
209
+ expect(master.getProps()).not.toHaveProperty('disabled');
210
+ await fork.dispatchEvent('beforeRender', undefined, { useCache: false });
211
+
212
+ expect(fork.getProps().disabled).toBe(true);
213
+ });
214
+
215
+ it('preserves a default disabled state as runtime-only state', async () => {
216
+ class DefaultDisabledActionModel extends ConfiguredActionModel {
217
+ defaultProps = { ...this.defaultProps, disabled: true };
218
+ }
219
+ const engine = new FlowEngine();
220
+ engine.registerModels({ DefaultDisabledActionModel });
221
+ engine.registerActions({ actionLinkageRules, linkageSetActionProps });
222
+ const master = engine.createModel<DefaultDisabledActionModel>({
223
+ use: 'DefaultDisabledActionModel',
224
+ props: { title: 'Edit' },
225
+ stepParams: {
226
+ buttonSettings: {
227
+ linkageRules: { value: [] },
228
+ },
229
+ },
230
+ });
231
+ const fork = master.createFork({ className: 'row-action' });
232
+
233
+ await fork.dispatchEvent('beforeRender', undefined, { useCache: false });
234
+
235
+ expect(fork.getProps().disabled).toBe(true);
236
+ expect(fork.serialize().props).not.toHaveProperty('disabled');
237
+ });
238
+
239
+ it('updates a disabled row action title through the real beforeRender flow', async () => {
240
+ const engine = new FlowEngine();
241
+ engine.registerModels({ ConfiguredActionModel });
242
+ engine.registerActions({ actionLinkageRules, linkageSetActionProps });
243
+ let savedData: ReturnType<ConfiguredActionModel['serialize']> | undefined;
244
+ engine.setModelRepository({
245
+ save: async (model) => {
246
+ savedData = model.serialize();
247
+ return savedData;
248
+ },
249
+ } as Parameters<FlowEngine['setModelRepository']>[0]);
250
+ const master = engine.createModel<ConfiguredActionModel>({
251
+ use: 'ConfiguredActionModel',
252
+ props: { title: 'Edit' },
253
+ stepParams: {
254
+ buttonSettings: {
255
+ general: { title: 'Edit' },
256
+ linkageRules: {
257
+ value: [
258
+ {
259
+ key: 'disable-edit',
260
+ enable: true,
261
+ condition: { logic: '$and', items: [] },
262
+ actions: [{ name: 'linkageSetActionProps', params: { value: 'disabled' } }],
263
+ },
264
+ ],
265
+ },
266
+ },
267
+ },
268
+ });
269
+ const fork = master.createFork({ className: 'row-action' });
270
+
271
+ await fork.dispatchEvent('beforeRender', undefined, { useCache: false });
272
+ expect(fork.getProps()).toMatchObject({ title: 'Edit', disabled: true });
273
+
274
+ fork.setStepParams('buttonSettings', 'general', { title: 'Updated' });
275
+ await fork.saveStepParams();
276
+ await fork.rerender();
277
+
278
+ expect(savedData?.props).not.toHaveProperty('disabled');
279
+ expect(savedData?.stepParams.buttonSettings.general.title).toBe('Updated');
280
+ expect(fork.getProps()).toMatchObject({ title: 'Updated', disabled: true });
281
+
282
+ const reloadedEngine = new FlowEngine();
283
+ reloadedEngine.registerModels({ ConfiguredActionModel });
284
+ reloadedEngine.registerActions({ actionLinkageRules, linkageSetActionProps });
285
+ const reloadedMaster = reloadedEngine.createModel<ConfiguredActionModel>({
286
+ use: 'ConfiguredActionModel',
287
+ props: savedData?.props,
288
+ stepParams: savedData?.stepParams,
289
+ });
290
+ const reloadedFork = reloadedMaster.createFork({ className: 'row-action' });
291
+
292
+ await reloadedFork.dispatchEvent('beforeRender', undefined, { useCache: false });
293
+ expect(reloadedFork.getProps()).toMatchObject({ title: 'Updated', disabled: true });
294
+ });
295
+
296
+ it('creates a new rules value when disabling a linkage rule', () => {
297
+ const rules = [
298
+ {
299
+ key: 'disable-edit',
300
+ title: 'Linkage rule',
301
+ enable: true,
302
+ condition: { logic: '$and', items: [] },
303
+ actions: [],
304
+ },
305
+ ];
306
+ const nextRules = updateLinkageRules(rules, (next) => {
307
+ next[0].enable = false;
308
+ });
309
+
310
+ expect(nextRules).toEqual([expect.objectContaining({ key: 'disable-edit', enable: false })]);
311
+ expect(nextRules).not.toBe(rules);
312
+ expect(rules[0].enable).toBe(true);
313
+ });
314
+ });
@@ -225,6 +225,108 @@ describe('ensureFormValueDrivenDataScopeClear', () => {
225
225
  expect(onChange).toHaveBeenCalledWith(null);
226
226
  });
227
227
 
228
+ it('does not clear a popup form field when data scope depends on the external parent item', () => {
229
+ const emitter = new EventEmitter();
230
+ const formBlock = {
231
+ uid: 'form-1',
232
+ disposed: false,
233
+ emitter,
234
+ context: {
235
+ form: {},
236
+ formValues: { staff_m2o: null },
237
+ },
238
+ };
239
+
240
+ const onChange = vi.fn();
241
+ const model: any = {
242
+ disposed: false,
243
+ props: {
244
+ value: { id: 10 },
245
+ onChange,
246
+ },
247
+ context: {
248
+ blockModel: formBlock,
249
+ },
250
+ };
251
+
252
+ const ctx: any = {
253
+ model,
254
+ flowKey: 'selectSettings',
255
+ };
256
+
257
+ const filter = {
258
+ logic: '$and',
259
+ items: [{ path: 'orgId', operator: '$eq', value: '{{ ctx.item.parentItem.value.org_m2o.id }}' }],
260
+ };
261
+
262
+ ensureFormValueDrivenDataScopeClear(ctx, filter);
263
+
264
+ emitter.emit('formValuesChange', {
265
+ changedPaths: [['staff_m2o']],
266
+ allValues: { staff_m2o: { id: 10 } },
267
+ });
268
+
269
+ expect(onChange).not.toHaveBeenCalled();
270
+ });
271
+
272
+ it('maps popup current item dependencies to the popup form root', () => {
273
+ const emitter = new EventEmitter();
274
+ const formBlock = {
275
+ uid: 'form-1',
276
+ disposed: false,
277
+ emitter,
278
+ context: {
279
+ form: {},
280
+ formValues: {
281
+ org_m2o: { id: 1 },
282
+ staff_m2o: { id: 10 },
283
+ },
284
+ },
285
+ };
286
+
287
+ const onChange = vi.fn();
288
+ const model: any = {
289
+ disposed: false,
290
+ props: {
291
+ value: { id: 10 },
292
+ onChange,
293
+ },
294
+ context: {
295
+ blockModel: formBlock,
296
+ },
297
+ };
298
+
299
+ const ctx: any = {
300
+ model,
301
+ flowKey: 'selectSettings',
302
+ };
303
+
304
+ const filter = {
305
+ logic: '$and',
306
+ items: [{ path: 'orgId', operator: '$eq', value: '{{ ctx.item.value.org_m2o.id }}' }],
307
+ };
308
+
309
+ ensureFormValueDrivenDataScopeClear(ctx, filter);
310
+
311
+ emitter.emit('formValuesChange', {
312
+ changedPaths: [['staff_m2o']],
313
+ allValues: {
314
+ org_m2o: { id: 1 },
315
+ staff_m2o: { id: 10 },
316
+ },
317
+ });
318
+ expect(onChange).not.toHaveBeenCalled();
319
+
320
+ emitter.emit('formValuesChange', {
321
+ changedPaths: [['org_m2o']],
322
+ allValues: {
323
+ org_m2o: { id: 2 },
324
+ staff_m2o: { id: 10 },
325
+ },
326
+ });
327
+ expect(onChange).toHaveBeenCalledWith(null);
328
+ });
329
+
228
330
  it('does not clear a row field when a sibling field changes but the item dependency value is unchanged', () => {
229
331
  const emitter = new EventEmitter();
230
332
  const formBlock = {
@@ -0,0 +1,171 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { ActionScene } from '@nocobase/flow-engine';
11
+ import { describe, expect, it, vi } from 'vitest';
12
+
13
+ import * as registeredActions from '../index';
14
+ import * as linkageActions from '../linkageRules';
15
+
16
+ const tabLinkageRules = (linkageActions as Record<string, unknown>).tabLinkageRules as
17
+ | typeof linkageActions.menuLinkageRules
18
+ | undefined;
19
+ const linkageSetTabProps = (linkageActions as Record<string, unknown>).linkageSetTabProps as
20
+ | typeof linkageActions.linkageSetMenuItemProps
21
+ | undefined;
22
+
23
+ function createRuntime() {
24
+ let matched = true;
25
+ const setProps = vi.fn(function (this: { props: Record<string, unknown> }, props: Record<string, unknown>) {
26
+ this.props = {
27
+ ...this.props,
28
+ ...props,
29
+ };
30
+ });
31
+ const setHidden = vi.fn(function (this: { hidden: boolean }, value: boolean) {
32
+ this.hidden = value;
33
+ });
34
+ const model = {
35
+ uid: 'tab-1',
36
+ props: {} as Record<string, unknown>,
37
+ hidden: false,
38
+ __allModels: [],
39
+ setProps,
40
+ setHidden,
41
+ };
42
+ const ctx = {
43
+ app: { jsonLogic: { apply: () => matched } },
44
+ model,
45
+ t: (text: string) => text,
46
+ getAction: (name: string) => (name === 'linkageSetTabProps' ? linkageSetTabProps : undefined),
47
+ resolveJsonTemplate: vi.fn(async (value: unknown) => value),
48
+ };
49
+
50
+ return {
51
+ ctx,
52
+ model,
53
+ setMatched(value: boolean) {
54
+ matched = value;
55
+ },
56
+ };
57
+ }
58
+
59
+ const hideRule = {
60
+ value: [
61
+ {
62
+ key: 'rule-1',
63
+ title: 'Hide tab',
64
+ enable: true,
65
+ condition: {
66
+ logic: '$and',
67
+ items: [{ path: 'status', operator: '$eq', value: 'visible' }],
68
+ },
69
+ actions: [
70
+ {
71
+ key: 'action-1',
72
+ name: 'linkageSetTabProps',
73
+ params: { value: 'hidden' },
74
+ },
75
+ ],
76
+ },
77
+ ],
78
+ };
79
+
80
+ describe('tab linkage rules', () => {
81
+ it('should append the tab linkage scene without changing existing scene values', () => {
82
+ const scenes = ActionScene as unknown as Record<string, number>;
83
+
84
+ expect(scenes.MENU_LINKAGE_RULES).toBe(7);
85
+ expect(scenes.TAB_LINKAGE_RULES).toBe(8);
86
+ expect(scenes.TAB_LINKAGE_RULES).toBe(Math.max(...Object.values(scenes).filter(Number.isInteger)));
87
+ });
88
+
89
+ it('should register tab linkage actions from the actions entry', () => {
90
+ expect((registeredActions as Record<string, unknown>).tabLinkageRules).toBe(tabLinkageRules);
91
+ expect((registeredActions as Record<string, unknown>).linkageSetTabProps).toBe(linkageSetTabProps);
92
+ expect(tabLinkageRules).toBeDefined();
93
+ expect(linkageSetTabProps).toBeDefined();
94
+ });
95
+
96
+ it('should expose tab state and runjs actions in ui schema', () => {
97
+ expect(tabLinkageRules).toBeDefined();
98
+ expect(linkageSetTabProps).toBeDefined();
99
+
100
+ const schema = tabLinkageRules?.uiSchema?.({
101
+ getActions: () =>
102
+ new Map([
103
+ ['linkageSetTabProps', linkageSetTabProps],
104
+ ['linkageRunjs', linkageActions.linkageRunjs],
105
+ ['linkageSetMenuItemProps', linkageActions.linkageSetMenuItemProps],
106
+ ]),
107
+ } as never) as Record<string, Record<string, Record<string, unknown>>> | undefined;
108
+
109
+ expect(schema?.value?.['x-component-props']?.supportedActions).toEqual(['linkageSetTabProps', 'linkageRunjs']);
110
+ expect(linkageSetTabProps?.scene).toBe((ActionScene as unknown as Record<string, number>).TAB_LINKAGE_RULES);
111
+ expect(linkageActions.linkageRunjs.scene).toContain(
112
+ (ActionScene as unknown as Record<string, number>).TAB_LINKAGE_RULES,
113
+ );
114
+ });
115
+
116
+ it('should hide the tab when the condition matches and restore it when the condition misses', async () => {
117
+ expect(tabLinkageRules).toBeDefined();
118
+ const runtime = createRuntime();
119
+
120
+ await tabLinkageRules?.handler(runtime.ctx as never, hideRule);
121
+ expect(runtime.model.hidden).toBe(true);
122
+
123
+ runtime.setMatched(false);
124
+ await tabLinkageRules?.handler(runtime.ctx as never, hideRule);
125
+ expect(runtime.model.hidden).toBe(false);
126
+ });
127
+
128
+ it('should restore the tab when linkage rules are cleared', async () => {
129
+ expect(tabLinkageRules).toBeDefined();
130
+ const runtime = createRuntime();
131
+
132
+ await tabLinkageRules?.handler(runtime.ctx as never, hideRule);
133
+ expect(runtime.model.hidden).toBe(true);
134
+
135
+ await tabLinkageRules?.handler(runtime.ctx as never, { value: [] });
136
+ expect(runtime.model.hidden).toBe(false);
137
+ });
138
+
139
+ it('should not restore stale tab props while applying or clearing visibility rules', async () => {
140
+ expect(tabLinkageRules).toBeDefined();
141
+ const runtime = createRuntime();
142
+ runtime.model.props = {
143
+ title: 'Original title',
144
+ icon: 'original-icon',
145
+ route: { options: { badge: 'original' } },
146
+ };
147
+
148
+ await tabLinkageRules?.handler(runtime.ctx as never, hideRule);
149
+ expect(runtime.model.hidden).toBe(true);
150
+
151
+ const updatedProps = {
152
+ title: 'Updated title',
153
+ icon: 'updated-icon',
154
+ route: { options: { badge: 'updated' } },
155
+ };
156
+ runtime.model.props = updatedProps;
157
+
158
+ await tabLinkageRules?.handler(runtime.ctx as never, hideRule);
159
+ expect(runtime.model.props).toMatchObject(updatedProps);
160
+ expect(runtime.model.hidden).toBe(true);
161
+
162
+ runtime.setMatched(false);
163
+ await tabLinkageRules?.handler(runtime.ctx as never, hideRule);
164
+ expect(runtime.model.props).toMatchObject(updatedProps);
165
+ expect(runtime.model.hidden).toBe(false);
166
+
167
+ await tabLinkageRules?.handler(runtime.ctx as never, { value: [] });
168
+ expect(runtime.model.props).toMatchObject(updatedProps);
169
+ expect(runtime.model.hidden).toBe(false);
170
+ });
171
+ });
@@ -135,11 +135,15 @@ describe('linkageRulesRefresh action', () => {
135
135
  expect(handler).toHaveBeenCalledWith(ctx, { value: ['master-mounted'] });
136
136
  });
137
137
 
138
- it('runs linkage action on master model when forks exist in design mode', async () => {
138
+ it('skips master model when forks can handle flow in design mode', async () => {
139
139
  const handler = vi.fn(async () => {});
140
140
  const model: any = {
141
141
  isFork: false,
142
- forks: new Set([{}]),
142
+ forks: new Set([
143
+ {
144
+ getFlow: vi.fn(() => ({})),
145
+ },
146
+ ]),
143
147
  getFlow: vi.fn(() => ({})),
144
148
  getStepParams: vi.fn(() => ({ value: ['master'] })),
145
149
  context: {
@@ -157,7 +161,7 @@ describe('linkageRulesRefresh action', () => {
157
161
  flowKey: 'buttonSettings',
158
162
  });
159
163
 
160
- expect(handler).toHaveBeenCalledWith(ctx, { value: ['master'] });
164
+ expect(handler).not.toHaveBeenCalled();
161
165
  });
162
166
 
163
167
  it('runs linkage action on fork model and resolves params', async () => {
@@ -43,10 +43,12 @@ export {
43
43
  linkageSetDetailsFieldProps,
44
44
  actionLinkageRules,
45
45
  menuLinkageRules,
46
+ tabLinkageRules,
46
47
  blockLinkageRules,
47
48
  linkageSetBlockProps,
48
49
  linkageSetActionProps,
49
50
  linkageSetMenuItemProps,
51
+ linkageSetTabProps,
50
52
  linkageSetFieldProps,
51
53
  subFormLinkageSetFieldProps,
52
54
  linkageAssignField,