@nocobase/client-v2 2.1.0-beta.25 → 2.1.0-beta.26
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/es/flow/actions/linkageRulesFormValueRefresh.d.ts +10 -0
- package/es/flow/index.d.ts +1 -0
- package/es/flow/models/actions/AssociateActionModel.d.ts +19 -0
- package/es/flow/models/actions/AssociationActionUtils.d.ts +17 -0
- package/es/flow/models/actions/DisassociateActionModel.d.ts +16 -0
- package/es/flow/models/actions/index.d.ts +3 -0
- package/es/flow/models/base/GridModel.d.ts +3 -1
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +1 -0
- package/es/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.d.ts +9 -0
- package/es/index.d.ts +1 -0
- package/es/index.mjs +80 -80
- package/lib/index.js +87 -87
- package/package.json +5 -5
- package/src/__tests__/globalDeps.test.ts +5 -0
- package/src/flow/actions/__tests__/linkageRules.formValueDrivenRefresh.test.ts +438 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +42 -0
- package/src/flow/actions/linkageRules.tsx +8 -1
- package/src/flow/actions/linkageRulesFormValueRefresh.ts +492 -0
- package/src/flow/actions/linkageRulesRefresh.tsx +4 -2
- package/src/flow/index.ts +1 -0
- package/src/flow/models/actions/AssociateActionModel.tsx +196 -0
- package/src/flow/models/actions/AssociationActionUtils.ts +90 -0
- package/src/flow/models/actions/DisassociateActionModel.tsx +57 -0
- package/src/flow/models/actions/__tests__/AssociationActionModel.test.ts +250 -0
- package/src/flow/models/actions/index.ts +3 -0
- package/src/flow/models/base/GridModel.tsx +21 -1
- package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +98 -0
- package/src/flow/models/blocks/details/DetailsItemModel.tsx +3 -0
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +5 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +21 -5
- package/src/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.ts +20 -0
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +11 -3
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +235 -0
- package/src/index.ts +1 -0
- package/src/utils/globalDeps.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "2.1.0-beta.
|
|
3
|
+
"version": "2.1.0-beta.26",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.mjs",
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"@formily/antd-v5": "1.2.3",
|
|
25
25
|
"@formily/react": "^2.2.27",
|
|
26
26
|
"@formily/shared": "^2.2.27",
|
|
27
|
-
"@nocobase/flow-engine": "2.1.0-beta.
|
|
28
|
-
"@nocobase/sdk": "2.1.0-beta.
|
|
29
|
-
"@nocobase/shared": "2.1.0-beta.
|
|
27
|
+
"@nocobase/flow-engine": "2.1.0-beta.26",
|
|
28
|
+
"@nocobase/sdk": "2.1.0-beta.26",
|
|
29
|
+
"@nocobase/shared": "2.1.0-beta.26",
|
|
30
30
|
"ahooks": "^3.7.2",
|
|
31
31
|
"antd": "5.24.2",
|
|
32
32
|
"classnames": "^2.3.1",
|
|
@@ -36,5 +36,5 @@
|
|
|
36
36
|
"react-i18next": "^11.15.1",
|
|
37
37
|
"react-router-dom": "^6.30.1"
|
|
38
38
|
},
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "b17e1a72057813fa27d8435bf0f2af67ea4b059f"
|
|
40
40
|
}
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import { defineGlobalDeps } from '../utils/globalDeps';
|
|
11
11
|
|
|
12
|
+
vi.mock('../index', () => ({}));
|
|
13
|
+
|
|
12
14
|
describe('client-v2 defineGlobalDeps', () => {
|
|
13
15
|
it('should register shared AMD dependencies for remote plugins', () => {
|
|
14
16
|
const define = vi.fn();
|
|
@@ -24,5 +26,8 @@ describe('client-v2 defineGlobalDeps', () => {
|
|
|
24
26
|
expect(define).toHaveBeenCalledWith('@nocobase/client-v2', expect.any(Function));
|
|
25
27
|
expect(define).toHaveBeenCalledWith('@nocobase/flow-engine', expect.any(Function));
|
|
26
28
|
expect(define).toHaveBeenCalledWith('ahooks', expect.any(Function));
|
|
29
|
+
expect(define).toHaveBeenCalledWith('dayjs', expect.any(Function));
|
|
30
|
+
expect(define).toHaveBeenCalledWith('lodash', expect.any(Function));
|
|
31
|
+
expect(define).toHaveBeenCalledWith('@emotion/css', expect.any(Function));
|
|
27
32
|
});
|
|
28
33
|
});
|
|
@@ -0,0 +1,438 @@
|
|
|
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 { FlowContext, FlowRuntimeContext } from '@nocobase/flow-engine';
|
|
11
|
+
import { waitFor } from '@testing-library/react';
|
|
12
|
+
import { EventEmitter } from 'events';
|
|
13
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
14
|
+
import { actionLinkageRules, blockLinkageRules } from '../linkageRules';
|
|
15
|
+
|
|
16
|
+
function createRule(overrides: any = {}) {
|
|
17
|
+
return {
|
|
18
|
+
key: 'r1',
|
|
19
|
+
title: 'r1',
|
|
20
|
+
enable: true,
|
|
21
|
+
condition: { logic: '$and', items: [] },
|
|
22
|
+
actions: [],
|
|
23
|
+
...overrides,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createRuntime(
|
|
28
|
+
params: any,
|
|
29
|
+
options: { fieldIndex?: string[]; fieldIndexRef?: { current: string[] }; actionHandler?: any; engineEmitter?: any } = {},
|
|
30
|
+
) {
|
|
31
|
+
const formEmitter = new EventEmitter();
|
|
32
|
+
const formBlock: any = {
|
|
33
|
+
uid: 'form-block',
|
|
34
|
+
emitter: formEmitter,
|
|
35
|
+
formValueRuntime: {},
|
|
36
|
+
};
|
|
37
|
+
const actionHandler = options.actionHandler || vi.fn(async () => {});
|
|
38
|
+
const linkageRunjsHandler = vi.fn(async () => {});
|
|
39
|
+
const modelContext: any = new FlowContext();
|
|
40
|
+
modelContext.defineProperty('blockModel', { value: formBlock });
|
|
41
|
+
modelContext.defineProperty('app', {
|
|
42
|
+
value: {
|
|
43
|
+
jsonLogic: {
|
|
44
|
+
apply: () => true,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
modelContext.defineProperty('fieldIndex', {
|
|
49
|
+
get: () => options.fieldIndexRef?.current || options.fieldIndex || [],
|
|
50
|
+
cache: false,
|
|
51
|
+
});
|
|
52
|
+
modelContext.defineMethod('resolveJsonTemplate', async (_template: any) => _template);
|
|
53
|
+
modelContext.defineMethod('getAction', (name: string) => {
|
|
54
|
+
if (name === 'actionLinkageRules') {
|
|
55
|
+
return { useRawParams: true, handler: actionHandler };
|
|
56
|
+
}
|
|
57
|
+
if (name === 'blockLinkageRules') {
|
|
58
|
+
return { useRawParams: true, handler: actionHandler };
|
|
59
|
+
}
|
|
60
|
+
if (name === 'linkageRunjs') {
|
|
61
|
+
return { handler: linkageRunjsHandler };
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
});
|
|
65
|
+
modelContext.defineMethod('getActions', () => new Map());
|
|
66
|
+
modelContext.defineMethod('t', (s: string) => s);
|
|
67
|
+
|
|
68
|
+
const model: any = {
|
|
69
|
+
uid: 'action-model',
|
|
70
|
+
context: modelContext,
|
|
71
|
+
flowEngine: options.engineEmitter ? { emitter: options.engineEmitter } : undefined,
|
|
72
|
+
isFork: false,
|
|
73
|
+
forks: new Set(),
|
|
74
|
+
getFlow: vi.fn(() => ({})),
|
|
75
|
+
getStepParams: vi.fn(() => params),
|
|
76
|
+
getAction: (name: string) => modelContext.getAction(name),
|
|
77
|
+
getActions: () => new Map(),
|
|
78
|
+
translate: (s: string) => s,
|
|
79
|
+
};
|
|
80
|
+
const ctx: any = new FlowRuntimeContext(model, 'buttonSettings');
|
|
81
|
+
ctx.defineMethod('resolveJsonTemplate', async (_template: any) => _template);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
ctx,
|
|
85
|
+
model,
|
|
86
|
+
formEmitter,
|
|
87
|
+
actionHandler,
|
|
88
|
+
linkageRunjsHandler,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe('linkageRules: form value driven refresh', () => {
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
vi.useRealTimers();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('refreshes action linkage rules when a ctx.formValues dependency changes', async () => {
|
|
98
|
+
const params = {
|
|
99
|
+
value: [
|
|
100
|
+
createRule({
|
|
101
|
+
condition: {
|
|
102
|
+
logic: '$and',
|
|
103
|
+
items: [{ path: '{{ ctx.formValues.status }}', operator: '$eq', value: 'active' }],
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
const { ctx, formEmitter, actionHandler } = createRuntime(params);
|
|
109
|
+
|
|
110
|
+
await actionLinkageRules.handler(ctx, params);
|
|
111
|
+
formEmitter.emit('formValuesChange', {
|
|
112
|
+
source: 'user',
|
|
113
|
+
txId: 'tx-1',
|
|
114
|
+
changedPaths: [['other']],
|
|
115
|
+
});
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
117
|
+
expect(actionHandler).not.toHaveBeenCalled();
|
|
118
|
+
|
|
119
|
+
formEmitter.emit('formValuesChange', {
|
|
120
|
+
source: 'user',
|
|
121
|
+
txId: 'tx-2',
|
|
122
|
+
changedPaths: [['status']],
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await waitFor(() => expect(actionHandler).toHaveBeenCalledTimes(1));
|
|
126
|
+
expect(actionHandler.mock.calls[0][0].inputArgs).toMatchObject({
|
|
127
|
+
source: 'user',
|
|
128
|
+
txId: 'tx-2',
|
|
129
|
+
linkageTxId: 'tx-2',
|
|
130
|
+
changedPaths: [['status']],
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('refreshes block linkage rules when a ctx.formValues dependency changes', async () => {
|
|
135
|
+
const params = {
|
|
136
|
+
value: [
|
|
137
|
+
createRule({
|
|
138
|
+
condition: {
|
|
139
|
+
logic: '$and',
|
|
140
|
+
items: [{ path: '{{ ctx.formValues.status }}', operator: '$eq', value: 'active' }],
|
|
141
|
+
},
|
|
142
|
+
}),
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
const { ctx, formEmitter, actionHandler } = createRuntime(params);
|
|
146
|
+
|
|
147
|
+
await blockLinkageRules.handler(ctx, params);
|
|
148
|
+
formEmitter.emit('formValuesChange', {
|
|
149
|
+
source: 'user',
|
|
150
|
+
txId: 'tx-1',
|
|
151
|
+
changedPaths: [['status']],
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await waitFor(() => expect(actionHandler).toHaveBeenCalledTimes(1));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('dedupes subscriptions for repeated handler runs', async () => {
|
|
158
|
+
const params = {
|
|
159
|
+
value: [
|
|
160
|
+
createRule({
|
|
161
|
+
condition: {
|
|
162
|
+
logic: '$and',
|
|
163
|
+
items: [{ path: '{{ ctx.formValues.status }}', operator: '$eq', value: 'active' }],
|
|
164
|
+
},
|
|
165
|
+
}),
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
const { ctx, formEmitter, actionHandler } = createRuntime(params);
|
|
169
|
+
|
|
170
|
+
await actionLinkageRules.handler(ctx, params);
|
|
171
|
+
await actionLinkageRules.handler(ctx, params);
|
|
172
|
+
|
|
173
|
+
expect(formEmitter.listenerCount('formValuesChange')).toBe(1);
|
|
174
|
+
|
|
175
|
+
formEmitter.emit('formValuesChange', {
|
|
176
|
+
source: 'user',
|
|
177
|
+
txId: 'tx-1',
|
|
178
|
+
changedPaths: [['status']],
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await waitFor(() => expect(actionHandler).toHaveBeenCalledTimes(1));
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('keeps action refresh subscription after the action is hidden and unmounted', async () => {
|
|
185
|
+
const engineEmitter = new EventEmitter();
|
|
186
|
+
const params = {
|
|
187
|
+
value: [
|
|
188
|
+
createRule({
|
|
189
|
+
condition: {
|
|
190
|
+
logic: '$and',
|
|
191
|
+
items: [{ path: '{{ ctx.formValues.status }}', operator: '$eq', value: 'active' }],
|
|
192
|
+
},
|
|
193
|
+
}),
|
|
194
|
+
],
|
|
195
|
+
};
|
|
196
|
+
const { ctx, model, formEmitter, actionHandler } = createRuntime(params, { engineEmitter });
|
|
197
|
+
|
|
198
|
+
await actionLinkageRules.handler(ctx, params);
|
|
199
|
+
expect(formEmitter.listenerCount('formValuesChange')).toBe(1);
|
|
200
|
+
|
|
201
|
+
engineEmitter.emit('model:unmounted', { model });
|
|
202
|
+
expect(formEmitter.listenerCount('formValuesChange')).toBe(1);
|
|
203
|
+
|
|
204
|
+
formEmitter.emit('formValuesChange', {
|
|
205
|
+
source: 'user',
|
|
206
|
+
txId: 'tx-1',
|
|
207
|
+
changedPaths: [['status']],
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await waitFor(() => expect(actionHandler).toHaveBeenCalledTimes(1));
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('maps ctx.item.value dependencies to the current row path', async () => {
|
|
214
|
+
const params = {
|
|
215
|
+
value: [
|
|
216
|
+
createRule({
|
|
217
|
+
condition: {
|
|
218
|
+
logic: '$and',
|
|
219
|
+
items: [{ path: '{{ ctx.item.value.nickname }}', operator: '$eq', value: 'A' }],
|
|
220
|
+
},
|
|
221
|
+
}),
|
|
222
|
+
],
|
|
223
|
+
};
|
|
224
|
+
const { ctx, formEmitter, actionHandler } = createRuntime(params, { fieldIndex: ['users:1'] });
|
|
225
|
+
|
|
226
|
+
await actionLinkageRules.handler(ctx, params);
|
|
227
|
+
formEmitter.emit('formValuesChange', {
|
|
228
|
+
source: 'user',
|
|
229
|
+
txId: 'tx-1',
|
|
230
|
+
changedPaths: [['users', 0, 'nickname']],
|
|
231
|
+
});
|
|
232
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
233
|
+
expect(actionHandler).not.toHaveBeenCalled();
|
|
234
|
+
|
|
235
|
+
formEmitter.emit('formValuesChange', {
|
|
236
|
+
source: 'user',
|
|
237
|
+
txId: 'tx-2',
|
|
238
|
+
changedPaths: [['users', 1, 'nickname']],
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
await waitFor(() => expect(actionHandler).toHaveBeenCalledTimes(1));
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('parses object-patch changedPaths before matching ctx.item.value dependencies', async () => {
|
|
245
|
+
const params = {
|
|
246
|
+
value: [
|
|
247
|
+
createRule({
|
|
248
|
+
condition: {
|
|
249
|
+
logic: '$and',
|
|
250
|
+
items: [{ path: '{{ ctx.item.value.nickname }}', operator: '$eq', value: 'A' }],
|
|
251
|
+
},
|
|
252
|
+
}),
|
|
253
|
+
],
|
|
254
|
+
};
|
|
255
|
+
const { ctx, formEmitter, actionHandler } = createRuntime(params, { fieldIndex: ['users:1'] });
|
|
256
|
+
|
|
257
|
+
await actionLinkageRules.handler(ctx, params);
|
|
258
|
+
formEmitter.emit('formValuesChange', {
|
|
259
|
+
source: 'user',
|
|
260
|
+
txId: 'tx-1',
|
|
261
|
+
changedPaths: [['users[1].nickname']],
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
await waitFor(() => expect(actionHandler).toHaveBeenCalledTimes(1));
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('recomputes ctx.item.value dependencies from the latest fieldIndex', async () => {
|
|
268
|
+
const fieldIndexRef = { current: ['users:1'] };
|
|
269
|
+
const params = {
|
|
270
|
+
value: [
|
|
271
|
+
createRule({
|
|
272
|
+
condition: {
|
|
273
|
+
logic: '$and',
|
|
274
|
+
items: [{ path: '{{ ctx.item.value.nickname }}', operator: '$eq', value: 'A' }],
|
|
275
|
+
},
|
|
276
|
+
}),
|
|
277
|
+
],
|
|
278
|
+
};
|
|
279
|
+
const { ctx, formEmitter, actionHandler } = createRuntime(params, { fieldIndexRef });
|
|
280
|
+
|
|
281
|
+
await actionLinkageRules.handler(ctx, params);
|
|
282
|
+
fieldIndexRef.current = ['users:0'];
|
|
283
|
+
formEmitter.emit('formValuesChange', {
|
|
284
|
+
source: 'user',
|
|
285
|
+
txId: 'tx-1',
|
|
286
|
+
changedPaths: [['users', 0, 'nickname']],
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
await waitFor(() => expect(actionHandler).toHaveBeenCalledTimes(1));
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('maps ctx.item.index dependencies to the current list path', async () => {
|
|
293
|
+
const params = {
|
|
294
|
+
value: [
|
|
295
|
+
createRule({
|
|
296
|
+
condition: {
|
|
297
|
+
logic: '$and',
|
|
298
|
+
items: [{ path: '{{ ctx.item.index }}', operator: '$eq', value: 1 }],
|
|
299
|
+
},
|
|
300
|
+
}),
|
|
301
|
+
],
|
|
302
|
+
};
|
|
303
|
+
const { ctx, formEmitter, actionHandler } = createRuntime(params, { fieldIndex: ['users:1'] });
|
|
304
|
+
|
|
305
|
+
await actionLinkageRules.handler(ctx, params);
|
|
306
|
+
formEmitter.emit('formValuesChange', {
|
|
307
|
+
source: 'user',
|
|
308
|
+
txId: 'tx-1',
|
|
309
|
+
changedPaths: [['users', 1, 'nickname']],
|
|
310
|
+
});
|
|
311
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
312
|
+
expect(actionHandler).not.toHaveBeenCalled();
|
|
313
|
+
|
|
314
|
+
formEmitter.emit('formValuesChange', {
|
|
315
|
+
source: 'user',
|
|
316
|
+
txId: 'tx-2',
|
|
317
|
+
changedPaths: [['users']],
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
await waitFor(() => expect(actionHandler).toHaveBeenCalledTimes(1));
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('collects ctx.formValues dependencies from linkageRunjs scripts without resolving the script early', async () => {
|
|
324
|
+
const params = {
|
|
325
|
+
value: [
|
|
326
|
+
createRule({
|
|
327
|
+
actions: [
|
|
328
|
+
{
|
|
329
|
+
key: 'a1',
|
|
330
|
+
name: 'linkageRunjs',
|
|
331
|
+
params: {
|
|
332
|
+
value: {
|
|
333
|
+
script: 'return ctx.formValues.amount',
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
}),
|
|
339
|
+
],
|
|
340
|
+
};
|
|
341
|
+
const { ctx, formEmitter, actionHandler, linkageRunjsHandler } = createRuntime(params);
|
|
342
|
+
|
|
343
|
+
await actionLinkageRules.handler(ctx, params);
|
|
344
|
+
expect(linkageRunjsHandler).toHaveBeenCalledTimes(1);
|
|
345
|
+
|
|
346
|
+
formEmitter.emit('formValuesChange', {
|
|
347
|
+
source: 'user',
|
|
348
|
+
txId: 'tx-1',
|
|
349
|
+
changedPaths: [['amount']],
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
await waitFor(() => expect(actionHandler).toHaveBeenCalledTimes(1));
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('reruns once with the latest relevant change after a refresh is already running', async () => {
|
|
356
|
+
let resolveFirstRefresh: () => void = () => undefined;
|
|
357
|
+
const firstRefresh = new Promise<void>((resolve) => {
|
|
358
|
+
resolveFirstRefresh = resolve;
|
|
359
|
+
});
|
|
360
|
+
const actionHandler = vi
|
|
361
|
+
.fn()
|
|
362
|
+
.mockImplementationOnce(() => firstRefresh)
|
|
363
|
+
.mockImplementation(async () => undefined);
|
|
364
|
+
const params = {
|
|
365
|
+
value: [
|
|
366
|
+
createRule({
|
|
367
|
+
condition: {
|
|
368
|
+
logic: '$and',
|
|
369
|
+
items: [{ path: '{{ ctx.formValues.status }}', operator: '$eq', value: 'active' }],
|
|
370
|
+
},
|
|
371
|
+
}),
|
|
372
|
+
],
|
|
373
|
+
};
|
|
374
|
+
const { ctx, formEmitter } = createRuntime(params, { actionHandler });
|
|
375
|
+
|
|
376
|
+
await actionLinkageRules.handler(ctx, params);
|
|
377
|
+
formEmitter.emit('formValuesChange', {
|
|
378
|
+
source: 'user',
|
|
379
|
+
txId: 'tx-1',
|
|
380
|
+
changedPaths: [['status']],
|
|
381
|
+
});
|
|
382
|
+
await waitFor(() => expect(actionHandler).toHaveBeenCalledTimes(1));
|
|
383
|
+
|
|
384
|
+
formEmitter.emit('formValuesChange', {
|
|
385
|
+
source: 'user',
|
|
386
|
+
txId: 'tx-2',
|
|
387
|
+
changedPaths: [['status']],
|
|
388
|
+
});
|
|
389
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
390
|
+
expect(actionHandler).toHaveBeenCalledTimes(1);
|
|
391
|
+
|
|
392
|
+
resolveFirstRefresh();
|
|
393
|
+
await waitFor(() => expect(actionHandler).toHaveBeenCalledTimes(2));
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('does not recursively refresh while handling its own linkage write event', async () => {
|
|
397
|
+
const params = {
|
|
398
|
+
value: [
|
|
399
|
+
createRule({
|
|
400
|
+
condition: {
|
|
401
|
+
logic: '$and',
|
|
402
|
+
items: [{ path: '{{ ctx.formValues.status }}', operator: '$eq', value: 'active' }],
|
|
403
|
+
},
|
|
404
|
+
}),
|
|
405
|
+
],
|
|
406
|
+
};
|
|
407
|
+
const formEmitter = new EventEmitter();
|
|
408
|
+
const actionHandler = vi.fn(async () => {
|
|
409
|
+
formEmitter.emit('formValuesChange', {
|
|
410
|
+
source: 'linkage',
|
|
411
|
+
txId: 'tx-linkage',
|
|
412
|
+
linkageTxId: 'tx-1',
|
|
413
|
+
changedPaths: [['status']],
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
const runtime = createRuntime(params, { actionHandler });
|
|
417
|
+
runtime.formEmitter.removeAllListeners();
|
|
418
|
+
formEmitter.on('formValuesChange', (...args) => runtime.formEmitter.emit('formValuesChange', ...args));
|
|
419
|
+
|
|
420
|
+
await actionLinkageRules.handler(runtime.ctx, params);
|
|
421
|
+
formEmitter.emit('formValuesChange', {
|
|
422
|
+
source: 'user',
|
|
423
|
+
txId: 'tx-1',
|
|
424
|
+
changedPaths: [['status']],
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
await waitFor(() => expect(actionHandler).toHaveBeenCalledTimes(1));
|
|
428
|
+
|
|
429
|
+
runtime.formEmitter.emit('formValuesChange', {
|
|
430
|
+
source: 'linkage',
|
|
431
|
+
txId: 'tx-linkage-late',
|
|
432
|
+
linkageTxId: 'tx-1',
|
|
433
|
+
changedPaths: [['status']],
|
|
434
|
+
});
|
|
435
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
436
|
+
expect(actionHandler).toHaveBeenCalledTimes(1);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
@@ -161,4 +161,46 @@ describe('linkageRulesRefresh action', () => {
|
|
|
161
161
|
expect(ctx.resolveJsonTemplate).toHaveBeenCalled();
|
|
162
162
|
expect(handler).toHaveBeenCalledWith(ctx, { value: ['x'] });
|
|
163
163
|
});
|
|
164
|
+
|
|
165
|
+
it('passes raw params to useRawParams linkage actions', async () => {
|
|
166
|
+
const handler = vi.fn(async () => {});
|
|
167
|
+
const rawParams = {
|
|
168
|
+
value: [
|
|
169
|
+
{
|
|
170
|
+
key: 'r1',
|
|
171
|
+
enable: true,
|
|
172
|
+
condition: { logic: '$and', items: [] },
|
|
173
|
+
actions: [
|
|
174
|
+
{
|
|
175
|
+
name: 'linkageRunjs',
|
|
176
|
+
params: {
|
|
177
|
+
value: {
|
|
178
|
+
script: 'return ctx.formValues.amount',
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
const model: any = {
|
|
187
|
+
isFork: false,
|
|
188
|
+
forks: new Set(),
|
|
189
|
+
getFlow: vi.fn(() => ({})),
|
|
190
|
+
getStepParams: vi.fn(() => rawParams),
|
|
191
|
+
};
|
|
192
|
+
const ctx: any = {
|
|
193
|
+
model,
|
|
194
|
+
resolveJsonTemplate: vi.fn(async () => ({ value: ['resolved'] })),
|
|
195
|
+
getAction: vi.fn(() => ({ useRawParams: true, handler })),
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
await linkageRulesRefresh.handler(ctx, {
|
|
199
|
+
actionName: 'actionLinkageRules',
|
|
200
|
+
flowKey: 'buttonSettings',
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(ctx.resolveJsonTemplate).not.toHaveBeenCalled();
|
|
204
|
+
expect(handler).toHaveBeenCalledWith(ctx, rawParams);
|
|
205
|
+
});
|
|
164
206
|
});
|
|
@@ -51,7 +51,12 @@ import {
|
|
|
51
51
|
getCollectionFromModel,
|
|
52
52
|
isToManyAssociationField,
|
|
53
53
|
} from '../internal/utils/modelUtils';
|
|
54
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
namePathToPathKey,
|
|
56
|
+
parsePathString,
|
|
57
|
+
resolveDynamicNamePath,
|
|
58
|
+
} from '../models/blocks/form/value-runtime/path';
|
|
59
|
+
import { ensureFormValueDrivenLinkageRefresh } from './linkageRulesFormValueRefresh';
|
|
55
60
|
|
|
56
61
|
interface LinkageRule {
|
|
57
62
|
/** 随机生成的字符串 */
|
|
@@ -2081,6 +2086,7 @@ export const blockLinkageRules = defineAction({
|
|
|
2081
2086
|
},
|
|
2082
2087
|
useRawParams: true,
|
|
2083
2088
|
handler: async (ctx, params) => {
|
|
2089
|
+
ensureFormValueDrivenLinkageRefresh(ctx, params, 'blockLinkageRules');
|
|
2084
2090
|
const resolved = await resolveLinkageRulesParamsPreservingRunJsScripts(ctx, params);
|
|
2085
2091
|
return commonLinkageRulesHandler(ctx, resolved);
|
|
2086
2092
|
},
|
|
@@ -2107,6 +2113,7 @@ export const actionLinkageRules = defineAction({
|
|
|
2107
2113
|
},
|
|
2108
2114
|
useRawParams: true,
|
|
2109
2115
|
handler: async (ctx, params) => {
|
|
2116
|
+
ensureFormValueDrivenLinkageRefresh(ctx, params, 'actionLinkageRules');
|
|
2110
2117
|
const resolved = await resolveLinkageRulesParamsPreservingRunJsScripts(ctx, params);
|
|
2111
2118
|
return commonLinkageRulesHandler(ctx, resolved);
|
|
2112
2119
|
},
|