@nocobase/client-v2 2.3.0-alpha.1 → 3.0.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 (123) hide show
  1. package/es/acl/ACLProvider.d.ts +10 -0
  2. package/es/authRedirect.d.ts +5 -0
  3. package/es/components/AttachmentUpload.d.ts +51 -0
  4. package/es/components/form/ScanInput/useCodeScanner.d.ts +2 -2
  5. package/es/components/index.d.ts +1 -0
  6. package/es/flow/actions/linkageRules.d.ts +1 -0
  7. package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
  8. package/es/flow/components/FlowRoute.d.ts +2 -2
  9. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  10. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  11. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  12. package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
  13. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  14. package/es/index.d.ts +4 -1
  15. package/es/index.mjs +139 -125
  16. package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
  17. package/es/nocobase-buildin-plugin/index.d.ts +9 -0
  18. package/es/settings-app/SettingsApplication.d.ts +18 -0
  19. package/es/settings-app/SettingsBrand.d.ts +16 -0
  20. package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
  21. package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
  22. package/es/settings-app/SettingsGroupNav.d.ts +17 -0
  23. package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
  24. package/es/settings-app/SettingsRouterManager.d.ts +15 -0
  25. package/es/settings-app/SettingsSearch.d.ts +17 -0
  26. package/es/settings-app/SettingsShell.d.ts +10 -0
  27. package/es/settings-app/isSettingsApp.d.ts +18 -0
  28. package/es/settings-app/runtimePaths.d.ts +18 -0
  29. package/es/settings-app/settingsDocumentPath.d.ts +14 -0
  30. package/es/settings-app/settingsTheme.d.ts +87 -0
  31. package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
  32. package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
  33. package/es/settings-center/groups.d.ts +66 -0
  34. package/es/settings-center/useSettingsGroups.d.ts +36 -0
  35. package/es/settings-center/useSettingsSearch.d.ts +35 -0
  36. package/es/settings-center/utils.d.ts +24 -0
  37. package/es/theme/index.d.ts +1 -0
  38. package/es/theme/itemActive.d.ts +24 -0
  39. package/es/theme/type.d.ts +9 -0
  40. package/lib/index.js +141 -127
  41. package/lib/locale/languageCodes.js +1 -0
  42. package/package.json +8 -7
  43. package/src/__tests__/authRedirect.test.ts +137 -0
  44. package/src/__tests__/browserChecker.test.ts +124 -6
  45. package/src/__tests__/mockSettingsApplication.ts +29 -0
  46. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +51 -10
  47. package/src/__tests__/plugin-manager.test.tsx +7 -6
  48. package/src/__tests__/settings-application.test.ts +164 -0
  49. package/src/__tests__/settings-auth-routes.test.ts +71 -0
  50. package/src/__tests__/settings-center.test.tsx +38 -37
  51. package/src/__tests__/settings-layout-root.test.tsx +260 -0
  52. package/src/__tests__/settings-runtime-paths.test.ts +99 -0
  53. package/src/__tests__/settings-shell.test.tsx +200 -0
  54. package/src/acl/ACLProvider.tsx +21 -0
  55. package/src/authRedirect.ts +72 -3
  56. package/src/components/AttachmentUpload.tsx +275 -0
  57. package/src/components/form/ScanInput/CodeScanner.tsx +61 -34
  58. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  59. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +114 -18
  60. package/src/components/form/ScanInput/useCodeScanner.ts +166 -23
  61. package/src/components/index.ts +1 -0
  62. package/src/flow/__tests__/FlowRoute.test.tsx +126 -8
  63. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  64. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  65. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  66. package/src/flow/actions/linkageRules.tsx +26 -6
  67. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -56
  68. package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
  69. package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
  70. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +77 -13
  71. package/src/flow/common/Markdown/style.ts +4 -0
  72. package/src/flow/components/BlockItemCard.tsx +2 -2
  73. package/src/flow/components/FlowRoute.tsx +71 -29
  74. package/src/flow/index.ts +2 -0
  75. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  76. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  77. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  78. package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
  79. package/src/flow/models/base/PageModel/PageTabModel.tsx +67 -17
  80. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +509 -12
  81. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  82. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  83. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  84. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
  85. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
  86. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  87. package/src/flow/models/blocks/table/TableBlockModel.tsx +3 -1
  88. package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
  89. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  90. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  91. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  92. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  93. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  94. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  95. package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
  96. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  97. package/src/index.ts +10 -1
  98. package/src/locale/languageCodes.ts +1 -0
  99. package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
  100. package/src/nocobase-buildin-plugin/index.tsx +99 -71
  101. package/src/settings-app/SettingsApplication.ts +31 -0
  102. package/src/settings-app/SettingsBrand.tsx +108 -0
  103. package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
  104. package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
  105. package/src/settings-app/SettingsGroupNav.tsx +124 -0
  106. package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
  107. package/src/settings-app/SettingsRouterManager.ts +92 -0
  108. package/src/settings-app/SettingsSearch.tsx +238 -0
  109. package/src/settings-app/SettingsShell.tsx +167 -0
  110. package/src/settings-app/isSettingsApp.ts +21 -0
  111. package/src/settings-app/runtimePaths.ts +104 -0
  112. package/src/settings-app/settingsDocumentPath.ts +66 -0
  113. package/src/settings-app/settingsTheme.ts +301 -0
  114. package/src/settings-app/useSettingsThemeConfig.ts +100 -0
  115. package/src/settings-center/AdminSettingsLayout.tsx +122 -141
  116. package/src/settings-center/SystemSettingsPage.tsx +4 -170
  117. package/src/settings-center/groups.ts +108 -0
  118. package/src/settings-center/useSettingsGroups.ts +105 -0
  119. package/src/settings-center/useSettingsSearch.ts +173 -0
  120. package/src/settings-center/utils.tsx +50 -1
  121. package/src/theme/index.tsx +1 -0
  122. package/src/theme/itemActive.ts +31 -0
  123. package/src/theme/type.ts +10 -0
@@ -0,0 +1,58 @@
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 { createMockClient, PluginFlowEngine } from '@nocobase/client-v2';
11
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
12
+
13
+ const { detectedDeviceType } = vi.hoisted(() => ({
14
+ detectedDeviceType: { value: 'mobile' },
15
+ }));
16
+
17
+ vi.mock('react-device-detect', () => ({
18
+ get deviceType() {
19
+ return detectedDeviceType.value;
20
+ },
21
+ }));
22
+
23
+ describe('PluginFlowEngine', () => {
24
+ beforeEach(() => {
25
+ detectedDeviceType.value = 'mobile';
26
+ });
27
+
28
+ it('should register the current device type before shared flow components', async () => {
29
+ const app = createMockClient({
30
+ router: {
31
+ type: 'memory',
32
+ initialEntries: ['/'],
33
+ },
34
+ });
35
+ const plugin = new PluginFlowEngine({}, app);
36
+ const addComponents = app.addComponents.bind(app);
37
+ const deviceTypesBeforeComponentRegistration: string[] = [];
38
+ vi.spyOn(app, 'addComponents').mockImplementation((components) => {
39
+ deviceTypesBeforeComponentRegistration.push(app.flowEngine.context.deviceType);
40
+ return addComponents(components);
41
+ });
42
+
43
+ await plugin.load();
44
+
45
+ expect(app.flowEngine.context.deviceType).toBe('mobile');
46
+ expect(deviceTypesBeforeComponentRegistration).toEqual(['mobile']);
47
+ });
48
+
49
+ it('should normalize the browser device type to computer', async () => {
50
+ detectedDeviceType.value = 'browser';
51
+ const app = createMockClient();
52
+ const plugin = new PluginFlowEngine({}, app);
53
+
54
+ await plugin.load();
55
+
56
+ expect(app.flowEngine.context.deviceType).toBe('computer');
57
+ });
58
+ });
@@ -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 = {
@@ -68,6 +68,12 @@ interface LinkageRule {
68
68
  }[];
69
69
  }
70
70
 
71
+ export function updateLinkageRules<T>(rules: T[], updater: (nextRules: T[]) => void): T[] {
72
+ const nextRules = _.cloneDeep(rules);
73
+ updater(nextRules);
74
+ return nextRules;
75
+ }
76
+
71
77
  const previewValueForLog = (value: any) => {
72
78
  if (value == null) return value;
73
79
  const t = typeof value;
@@ -1500,13 +1506,26 @@ async function resolveLinkageRulesParamsPreservingRunJsScripts(ctx: FlowContext,
1500
1506
  }
1501
1507
 
1502
1508
  const LinkageRulesUI = observer(
1503
- (props: { readonly value: LinkageRule[]; supportedActions: string[]; title?: string }) => {
1504
- const { value: rules, supportedActions } = props;
1509
+ (props: {
1510
+ readonly value: LinkageRule[];
1511
+ onChange?: (value: LinkageRule[]) => void;
1512
+ supportedActions: string[];
1513
+ title?: string;
1514
+ }) => {
1515
+ const { value: rules = [], onChange, supportedActions } = props;
1505
1516
  const ctx = useFlowContext();
1506
1517
  const flowEngine = useFlowEngine();
1507
1518
  const t = ctx.model.translate.bind(ctx.model);
1508
1519
  const assignPriorityTip = t('Assignment takes precedence over form field assignment');
1509
1520
 
1521
+ const replaceRules = (updater: (nextRules: LinkageRule[]) => void) => {
1522
+ if (onChange) {
1523
+ onChange(updateLinkageRules(rules, updater));
1524
+ } else {
1525
+ updater(rules);
1526
+ }
1527
+ };
1528
+
1510
1529
  // 创建新规则的默认值
1511
1530
  const createNewRule = (): LinkageRule => ({
1512
1531
  key: uid(),
@@ -1523,7 +1542,7 @@ const LinkageRulesUI = observer(
1523
1542
 
1524
1543
  // 删除规则
1525
1544
  const handleDeleteRule = (index: number) => {
1526
- rules.splice(index, 1);
1545
+ replaceRules((nextRules) => nextRules.splice(index, 1));
1527
1546
  };
1528
1547
 
1529
1548
  // 上移规则
@@ -1562,7 +1581,9 @@ const LinkageRulesUI = observer(
1562
1581
 
1563
1582
  // 切换规则启用状态
1564
1583
  const handleToggleEnable = (index: number, enable: boolean) => {
1565
- rules[index].enable = enable;
1584
+ replaceRules((nextRules) => {
1585
+ nextRules[index].enable = enable;
1586
+ });
1566
1587
  };
1567
1588
 
1568
1589
  // 获取可用的动作类型
@@ -2222,7 +2243,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
2222
2243
  }
2223
2244
 
2224
2245
  rememberOriginalProp(key, model.props?.[key]);
2225
- if (key === 'hiddenText') {
2246
+ if (key === 'hiddenText' && normalizedProps[key]) {
2226
2247
  rememberOriginalProp('title', model.props?.title);
2227
2248
  }
2228
2249
  if (key === 'required') {
@@ -2286,7 +2307,6 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
2286
2307
  const newProps = { ...model.__originalProps, ...patchProps };
2287
2308
  const prevHidden = !!model.hidden;
2288
2309
  const nextHidden = !!newProps.hiddenModel;
2289
-
2290
2310
  model.setProps(_.omit(newProps, ['hiddenModel', 'value', 'hiddenText', LINKAGE_ASSIGN_MODE_PROP]));
2291
2311
  syncFieldOptionsToForks(model, patchProps);
2292
2312
  if (typeof model.setHidden === 'function') {
@@ -18,7 +18,6 @@ import { theme as antdTheme, ConfigProvider, Grid, Popover } from 'antd';
18
18
  import { createStyles, createGlobalStyle } from 'antd-style';
19
19
  import React, { FC, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
20
20
  import ReactDOM from 'react-dom';
21
- import { useTranslation } from 'react-i18next';
22
21
  import { useLocation } from 'react-router-dom';
23
22
  import {
24
23
  AdminLayoutMenuModelRenderer,
@@ -35,7 +34,6 @@ import { ResetThemeTokenAndKeepAlgorithm } from './ResetThemeTokenAndKeepAlgorit
35
34
  import { PinnedPluginListLite } from './PinnedPluginListLite';
36
35
  import { useApplications } from './useApplications';
37
36
  import { useGlobalTheme, type CustomToken } from '../../../theme';
38
- import { useSystemSettings } from '../../system-settings';
39
37
  import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../../flow-compat';
40
38
  import {
41
39
  FLOW_SETTINGS_PREFERENCE_CHANGE_EVENT,
@@ -44,60 +42,7 @@ import {
44
42
  } from './flowSettingsPreference';
45
43
  import { joinAdminLayoutRoutePath, type AdminLayoutRoutePathLike } from './resolveAdminRouteRuntimeTarget';
46
44
  import { useAppListRender } from './AppListRender';
47
-
48
- const className1 = css`
49
- height: var(--nb-header-height);
50
- margin-right: 4px;
51
- display: inline-flex;
52
- flex-shrink: 0;
53
- color: #fff;
54
- padding: 0;
55
- align-items: center;
56
- `;
57
- const className1WithFixedWidth = css`
58
- ${className1}
59
- width: 168px;
60
- `;
61
- const className1WithAutoWidth = css`
62
- ${className1}
63
- width: auto;
64
- min-width: 168px;
65
- `;
66
- const className2 = css`
67
- object-fit: contain;
68
- width: 100%;
69
- height: 100%;
70
- `;
71
- const className3 = css`
72
- width: 100%;
73
- height: 100%;
74
- font-weight: 500;
75
- text-align: center;
76
- overflow: hidden;
77
- text-overflow: ellipsis;
78
- white-space: nowrap;
79
- `;
80
-
81
- const NocoBaseLogo = observer(() => {
82
- const { token } = antdTheme.useToken();
83
- const customToken = token as CustomToken;
84
- const result = useSystemSettings();
85
- const { t } = useTranslation('lm-collections');
86
- const fontSizeStyle = useMemo(() => ({ fontSize: customToken.fontSizeHeading3 }), [customToken.fontSizeHeading3]);
87
-
88
- const hasLogo = result?.data?.data?.logo?.url;
89
- const logo = hasLogo ? (
90
- <img className={className2} src={result?.data?.data?.logo?.url} />
91
- ) : (
92
- <span style={fontSizeStyle} className={className3}>
93
- {t(result?.data?.data?.title)}
94
- </span>
95
- );
96
-
97
- return (
98
- <div className={hasLogo ? className1WithFixedWidth : className1WithAutoWidth}>{result?.loading ? null : logo}</div>
99
- );
100
- });
45
+ import { NocoBaseLogo } from './NocoBaseLogo';
101
46
 
102
47
  const resetStyle = css`
103
48
  .ant-layout-sider-children {