@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.
Files changed (35) hide show
  1. package/es/flow/actions/linkageRulesFormValueRefresh.d.ts +10 -0
  2. package/es/flow/index.d.ts +1 -0
  3. package/es/flow/models/actions/AssociateActionModel.d.ts +19 -0
  4. package/es/flow/models/actions/AssociationActionUtils.d.ts +17 -0
  5. package/es/flow/models/actions/DisassociateActionModel.d.ts +16 -0
  6. package/es/flow/models/actions/index.d.ts +3 -0
  7. package/es/flow/models/base/GridModel.d.ts +3 -1
  8. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +1 -0
  9. package/es/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.d.ts +9 -0
  10. package/es/index.d.ts +1 -0
  11. package/es/index.mjs +80 -80
  12. package/lib/index.js +87 -87
  13. package/package.json +5 -5
  14. package/src/__tests__/globalDeps.test.ts +5 -0
  15. package/src/flow/actions/__tests__/linkageRules.formValueDrivenRefresh.test.ts +438 -0
  16. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +42 -0
  17. package/src/flow/actions/linkageRules.tsx +8 -1
  18. package/src/flow/actions/linkageRulesFormValueRefresh.ts +492 -0
  19. package/src/flow/actions/linkageRulesRefresh.tsx +4 -2
  20. package/src/flow/index.ts +1 -0
  21. package/src/flow/models/actions/AssociateActionModel.tsx +196 -0
  22. package/src/flow/models/actions/AssociationActionUtils.ts +90 -0
  23. package/src/flow/models/actions/DisassociateActionModel.tsx +57 -0
  24. package/src/flow/models/actions/__tests__/AssociationActionModel.test.ts +250 -0
  25. package/src/flow/models/actions/index.ts +3 -0
  26. package/src/flow/models/base/GridModel.tsx +21 -1
  27. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +98 -0
  28. package/src/flow/models/blocks/details/DetailsItemModel.tsx +3 -0
  29. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +5 -1
  30. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +21 -5
  31. package/src/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.ts +20 -0
  32. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +11 -3
  33. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +235 -0
  34. package/src/index.ts +1 -0
  35. 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.25",
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.25",
28
- "@nocobase/sdk": "2.1.0-beta.25",
29
- "@nocobase/shared": "2.1.0-beta.25",
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": "824f8b8200e9fe086135768934d3ef427b212446"
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 { namePathToPathKey, parsePathString, resolveDynamicNamePath } from '../models/blocks/form/value-runtime/path';
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
  },