@nocobase/client-v2 2.1.31 → 2.1.32

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/client-v2",
3
- "version": "2.1.31",
3
+ "version": "2.1.32",
4
4
  "license": "Apache-2.0",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.mjs",
@@ -27,11 +27,11 @@
27
27
  "@formily/antd-v5": "1.2.3",
28
28
  "@formily/react": "^2.2.27",
29
29
  "@formily/shared": "^2.2.27",
30
- "@nocobase/evaluators": "2.1.31",
31
- "@nocobase/flow-engine": "2.1.31",
32
- "@nocobase/sdk": "2.1.31",
33
- "@nocobase/shared": "2.1.31",
34
- "@nocobase/utils": "2.1.31",
30
+ "@nocobase/evaluators": "2.1.32",
31
+ "@nocobase/flow-engine": "2.1.32",
32
+ "@nocobase/sdk": "2.1.32",
33
+ "@nocobase/shared": "2.1.32",
34
+ "@nocobase/utils": "2.1.32",
35
35
  "ahooks": "^3.7.2",
36
36
  "antd": "5.24.2",
37
37
  "antd-style": "3.7.1",
@@ -48,5 +48,5 @@
48
48
  "react-i18next": "^11.15.1",
49
49
  "react-router-dom": "^6.30.1"
50
50
  },
51
- "gitHead": "b5d46b76adb72b4fa736dd0e6c89d8a0bc83bfe8"
51
+ "gitHead": "737a1324f650d7bc66594203b68eb0180553a551"
52
52
  }
@@ -11,7 +11,7 @@ import { FlowEngine } from '@nocobase/flow-engine';
11
11
  import { describe, expect, it, vi } from 'vitest';
12
12
  import { ActionModel as ConfiguredActionModel } from '../../models/base/ActionModel';
13
13
  import { ActionModel } from '../../models/base/ActionModelCore';
14
- import { actionLinkageRules, linkageSetActionProps } from '../linkageRules';
14
+ import { actionLinkageRules, linkageSetActionProps, updateLinkageRules } from '../linkageRules';
15
15
 
16
16
  class TestActionModel extends ActionModel {}
17
17
 
@@ -73,16 +73,180 @@ describe('action linkage rules on action forks', () => {
73
73
  });
74
74
 
75
75
  expect(fork.getProps().title).toBe('Updated');
76
- expect(fork.serialize().props.title).toBe('Updated');
76
+ expect(fork.serialize().props).toMatchObject({ title: 'Updated' });
77
+ expect(fork.serialize().props).not.toHaveProperty('disabled');
77
78
 
78
79
  const refreshedFork = master.createFork({ className: 'row-action' });
79
80
  expect(refreshedFork.getProps().title).toBe('Updated');
80
81
  });
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
+
82
239
  it('updates a disabled row action title through the real beforeRender flow', async () => {
83
240
  const engine = new FlowEngine();
84
241
  engine.registerModels({ ConfiguredActionModel });
85
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]);
86
250
  const master = engine.createModel<ConfiguredActionModel>({
87
251
  use: 'ConfiguredActionModel',
88
252
  props: { title: 'Edit' },
@@ -107,9 +271,44 @@ describe('action linkage rules on action forks', () => {
107
271
  await fork.dispatchEvent('beforeRender', undefined, { useCache: false });
108
272
  expect(fork.getProps()).toMatchObject({ title: 'Edit', disabled: true });
109
273
 
110
- master.setStepParams('buttonSettings', 'general', { title: 'Updated' });
111
- await new Promise((resolve) => setTimeout(resolve, 0));
274
+ fork.setStepParams('buttonSettings', 'general', { title: 'Updated' });
275
+ await fork.saveStepParams();
276
+ await fork.rerender();
112
277
 
278
+ expect(savedData?.props).not.toHaveProperty('disabled');
279
+ expect(savedData?.stepParams.buttonSettings.general.title).toBe('Updated');
113
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);
114
313
  });
115
314
  });
@@ -72,6 +72,12 @@ interface LinkageRule {
72
72
  }[];
73
73
  }
74
74
 
75
+ export function updateLinkageRules<T>(rules: T[], updater: (nextRules: T[]) => void): T[] {
76
+ const nextRules = _.cloneDeep(rules);
77
+ updater(nextRules);
78
+ return nextRules;
79
+ }
80
+
75
81
  const previewValueForLog = (value: any) => {
76
82
  if (value == null) return value;
77
83
  const t = typeof value;
@@ -1469,13 +1475,26 @@ async function resolveLinkageRulesParamsPreservingRunJsScripts(ctx: FlowContext,
1469
1475
  }
1470
1476
 
1471
1477
  const LinkageRulesUI = observer(
1472
- (props: { readonly value: LinkageRule[]; supportedActions: string[]; title?: string }) => {
1473
- const { value: rules, supportedActions } = props;
1478
+ (props: {
1479
+ readonly value: LinkageRule[];
1480
+ onChange?: (value: LinkageRule[]) => void;
1481
+ supportedActions: string[];
1482
+ title?: string;
1483
+ }) => {
1484
+ const { value: rules = [], onChange, supportedActions } = props;
1474
1485
  const ctx = useFlowContext();
1475
1486
  const flowEngine = useFlowEngine();
1476
1487
  const t = ctx.model.translate.bind(ctx.model);
1477
1488
  const assignPriorityTip = t('Assignment takes precedence over form field assignment');
1478
1489
 
1490
+ const replaceRules = (updater: (nextRules: LinkageRule[]) => void) => {
1491
+ if (onChange) {
1492
+ onChange(updateLinkageRules(rules, updater));
1493
+ } else {
1494
+ updater(rules);
1495
+ }
1496
+ };
1497
+
1479
1498
  // 创建新规则的默认值
1480
1499
  const createNewRule = (): LinkageRule => ({
1481
1500
  key: uid(),
@@ -1492,7 +1511,7 @@ const LinkageRulesUI = observer(
1492
1511
 
1493
1512
  // 删除规则
1494
1513
  const handleDeleteRule = (index: number) => {
1495
- rules.splice(index, 1);
1514
+ replaceRules((nextRules) => nextRules.splice(index, 1));
1496
1515
  };
1497
1516
 
1498
1517
  // 上移规则
@@ -1531,7 +1550,9 @@ const LinkageRulesUI = observer(
1531
1550
 
1532
1551
  // 切换规则启用状态
1533
1552
  const handleToggleEnable = (index: number, enable: boolean) => {
1534
- rules[index].enable = enable;
1553
+ replaceRules((nextRules) => {
1554
+ nextRules[index].enable = enable;
1555
+ });
1535
1556
  };
1536
1557
 
1537
1558
  // 获取可用的动作类型
@@ -7,7 +7,8 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { DefaultStructure, FlowModel, tExpr, useFlowModel } from '@nocobase/flow-engine';
10
+ import { FlowModel, tExpr, useFlowModel } from '@nocobase/flow-engine';
11
+ import type { DefaultStructure, FlowModelOptions } from '@nocobase/flow-engine';
11
12
  import { Icon } from '../../../components';
12
13
  import { Button, Tooltip } from 'antd';
13
14
  import type { ButtonProps } from 'antd/es/button';
@@ -86,8 +87,10 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
86
87
  return null;
87
88
  }
88
89
 
89
- onInit(options: any): void {
90
+ onInit(options: FlowModelOptions<T>): void {
90
91
  super.onInit(options);
92
+ // Action disabled state is computed at runtime and must never be restored from persisted props.
93
+ delete this.props.disabled;
91
94
  this.context.defineProperty('actionName', {
92
95
  get: () => {
93
96
  return this.getAclActionName();
@@ -96,6 +99,12 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
96
99
  });
97
100
  }
98
101
 
102
+ serialize() {
103
+ const data = super.serialize();
104
+ delete data.props.disabled;
105
+ return data;
106
+ }
107
+
99
108
  getInputArgs() {
100
109
  const inputArgs: Record<string, any> = {};
101
110
  const defaultKeys: string[] = [];
@@ -1,3 +1,12 @@
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
+
1
10
  import { render, screen } from '@nocobase/test/client';
2
11
  import { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
3
12
  import { App, ConfigProvider } from 'antd';
@@ -35,3 +44,43 @@ describe('ActionModel rendering', () => {
35
44
  expect(screen.getByRole('button', { name: 'Open details' })).toBeInTheDocument();
36
45
  });
37
46
  });
47
+
48
+ describe('ActionModel disabled state persistence', () => {
49
+ it('drops disabled from persisted props during initialization', () => {
50
+ const engine = new FlowEngine();
51
+ engine.registerModels({ NoIconActionModel });
52
+ const model = engine.createModel<NoIconActionModel>({
53
+ use: 'NoIconActionModel',
54
+ props: { title: 'Open details', disabled: true },
55
+ });
56
+
57
+ expect(model.getProps()).not.toHaveProperty('disabled');
58
+ });
59
+
60
+ it('keeps runtime disabled state without serializing it', () => {
61
+ const engine = new FlowEngine();
62
+ engine.registerModels({ NoIconActionModel });
63
+ const model = engine.createModel<NoIconActionModel>({
64
+ use: 'NoIconActionModel',
65
+ props: { title: 'Open details' },
66
+ });
67
+
68
+ model.setProps('disabled', true);
69
+
70
+ expect(model.getProps().disabled).toBe(true);
71
+ expect(model.serialize().props).toEqual({ title: 'Open details' });
72
+ });
73
+
74
+ it('does not serialize fork-local disabled state', () => {
75
+ const engine = new FlowEngine();
76
+ engine.registerModels({ NoIconActionModel });
77
+ const model = engine.createModel<NoIconActionModel>({
78
+ use: 'NoIconActionModel',
79
+ props: { title: 'Open details' },
80
+ });
81
+ const fork = model.createFork({ disabled: true });
82
+
83
+ expect(fork.getProps().disabled).toBe(true);
84
+ expect(fork.serialize().props).toEqual({ title: 'Open details' });
85
+ });
86
+ });