@nocobase/client-v2 2.1.0-beta.24 → 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 (57) hide show
  1. package/es/BaseApplication.d.ts +1 -0
  2. package/es/flow/actions/dataScopeFilter.d.ts +9 -0
  3. package/es/flow/actions/linkageRulesFormValueRefresh.d.ts +10 -0
  4. package/es/flow/index.d.ts +1 -0
  5. package/es/flow/internal/utils/rebuildFieldSubModel.d.ts +2 -1
  6. package/es/flow/models/actions/AssociateActionModel.d.ts +19 -0
  7. package/es/flow/models/actions/AssociationActionUtils.d.ts +17 -0
  8. package/es/flow/models/actions/DisassociateActionModel.d.ts +16 -0
  9. package/es/flow/models/actions/index.d.ts +3 -0
  10. package/es/flow/models/base/GridModel.d.ts +3 -1
  11. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +1 -0
  12. package/es/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.d.ts +9 -0
  13. package/es/flow/models/fields/JSFieldModel.d.ts +5 -0
  14. package/es/index.d.ts +1 -0
  15. package/es/index.mjs +101 -101
  16. package/lib/index.js +99 -99
  17. package/package.json +6 -5
  18. package/src/BaseApplication.tsx +4 -0
  19. package/src/__tests__/globalDeps.test.ts +6 -0
  20. package/src/__tests__/remotePlugins.test.ts +27 -0
  21. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +158 -0
  22. package/src/flow/actions/__tests__/linkageRules.formValueDrivenRefresh.test.ts +438 -0
  23. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +42 -0
  24. package/src/flow/actions/dataScope.tsx +6 -4
  25. package/src/flow/actions/dataScopeFilter.ts +70 -0
  26. package/src/flow/actions/linkageRules.tsx +8 -1
  27. package/src/flow/actions/linkageRulesFormValueRefresh.ts +492 -0
  28. package/src/flow/actions/linkageRulesRefresh.tsx +4 -2
  29. package/src/flow/actions/setTargetDataScope.tsx +6 -5
  30. package/src/flow/index.ts +1 -0
  31. package/src/flow/internal/utils/__tests__/rebuildFieldSubModel.test.ts +77 -2
  32. package/src/flow/internal/utils/rebuildFieldSubModel.ts +21 -5
  33. package/src/flow/models/actions/AssociateActionModel.tsx +196 -0
  34. package/src/flow/models/actions/AssociationActionUtils.ts +90 -0
  35. package/src/flow/models/actions/DisassociateActionModel.tsx +57 -0
  36. package/src/flow/models/actions/__tests__/AssociationActionModel.test.ts +250 -0
  37. package/src/flow/models/actions/index.ts +3 -0
  38. package/src/flow/models/base/GridModel.tsx +21 -1
  39. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +98 -0
  40. package/src/flow/models/blocks/details/DetailsItemModel.tsx +3 -0
  41. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +9 -5
  42. package/src/flow/models/blocks/filter-form/__tests__/FilterFormBlockModel.cleanup.test.ts +138 -0
  43. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +22 -0
  44. package/src/flow/models/blocks/table/JSColumnModel.tsx +30 -2
  45. package/src/flow/models/blocks/table/TableBlockModel.tsx +8 -1
  46. package/src/flow/models/blocks/table/TableColumnModel.tsx +1 -0
  47. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +51 -0
  48. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +49 -0
  49. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +5 -1
  50. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +21 -5
  51. package/src/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.ts +20 -0
  52. package/src/flow/models/fields/JSFieldModel.tsx +54 -14
  53. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +11 -3
  54. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +235 -0
  55. package/src/index.ts +1 -0
  56. package/src/utils/globalDeps.ts +10 -0
  57. package/src/utils/requirejs.ts +1 -1
@@ -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
  });
@@ -7,12 +7,12 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { defineAction, MultiRecordResource, pruneFilter, tExpr, useFlowSettingsContext } from '@nocobase/flow-engine';
11
- import { isEmptyFilter, transformFilter } from '@nocobase/utils/client';
12
- import _ from 'lodash';
10
+ import { defineAction, MultiRecordResource, tExpr, useFlowSettingsContext } from '@nocobase/flow-engine';
11
+ import { isEmptyFilter } from '@nocobase/utils/client';
13
12
  import React from 'react';
14
13
  import { FilterGroup, VariableFilterItem } from '../components/filter';
15
14
  import { FieldModel } from '../models/base/FieldModel';
15
+ import { normalizeDataScopeFilter } from './dataScopeFilter';
16
16
 
17
17
  export const dataScope = defineAction({
18
18
  name: 'dataScope',
@@ -43,6 +43,7 @@ export const dataScope = defineAction({
43
43
  filter: { logic: '$and', items: [] },
44
44
  };
45
45
  },
46
+ useRawParams: true,
46
47
  async handler(ctx, params) {
47
48
  // @ts-ignore
48
49
  const resource = ctx.model?.resource as MultiRecordResource;
@@ -50,7 +51,8 @@ export const dataScope = defineAction({
50
51
  return;
51
52
  }
52
53
 
53
- const filter = pruneFilter(transformFilter(params.filter));
54
+ const resolvedFilter = await ctx.resolveJsonTemplate(params.filter);
55
+ const filter = normalizeDataScopeFilter(params.filter, resolvedFilter);
54
56
 
55
57
  if (isEmptyFilter(filter)) {
56
58
  resource.removeFilterGroup(ctx.model.uid);
@@ -0,0 +1,70 @@
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 { isVariableExpression, pruneFilter } from '@nocobase/flow-engine';
11
+ import { transformFilter } from '@nocobase/utils/client';
12
+ import _ from 'lodash';
13
+
14
+ const PRESERVE_NULL = { __nocobaseDataScopeNull__: true };
15
+
16
+ function isPreserveNull(value: any) {
17
+ return (
18
+ value &&
19
+ typeof value === 'object' &&
20
+ !Array.isArray(value) &&
21
+ value.__nocobaseDataScopeNull__ === true &&
22
+ Object.keys(value).length === 1
23
+ );
24
+ }
25
+
26
+ function restorePreservedNull(value: any): any {
27
+ if (isPreserveNull(value)) {
28
+ return null;
29
+ }
30
+ if (Array.isArray(value)) {
31
+ return value.map((item) => restorePreservedNull(item));
32
+ }
33
+ if (value && typeof value === 'object') {
34
+ return Object.keys(value).reduce<Record<string, any>>((result, key) => {
35
+ result[key] = restorePreservedNull(value[key]);
36
+ return result;
37
+ }, {});
38
+ }
39
+ return value;
40
+ }
41
+
42
+ function markEmptyVariableValues(rawNode: any, resolvedNode: any) {
43
+ if (!rawNode || !resolvedNode || typeof rawNode !== 'object' || typeof resolvedNode !== 'object') {
44
+ return;
45
+ }
46
+
47
+ if ('path' in rawNode && 'operator' in rawNode) {
48
+ if (
49
+ isVariableExpression(rawNode.value) &&
50
+ (resolvedNode.value === undefined || resolvedNode.value === null || resolvedNode.value === '')
51
+ ) {
52
+ resolvedNode.value = PRESERVE_NULL;
53
+ }
54
+ return;
55
+ }
56
+
57
+ if (Array.isArray(rawNode.items) && Array.isArray(resolvedNode.items)) {
58
+ rawNode.items.forEach((rawItem, index) => {
59
+ markEmptyVariableValues(rawItem, resolvedNode.items[index]);
60
+ });
61
+ }
62
+ }
63
+
64
+ export function normalizeDataScopeFilter(rawFilter: any, resolvedFilter: any) {
65
+ const filterForTransform = _.cloneDeep(resolvedFilter);
66
+ markEmptyVariableValues(rawFilter, filterForTransform);
67
+
68
+ const filter = pruneFilter(transformFilter(filterForTransform));
69
+ return restorePreservedNull(filter);
70
+ }
@@ -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
  },
@@ -0,0 +1,492 @@
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 {
11
+ extractUsedVariablePathsFromRunJS,
12
+ FlowContext,
13
+ FlowModel,
14
+ FlowRuntimeContext,
15
+ isRunJSValue,
16
+ } from '@nocobase/flow-engine';
17
+ import _ from 'lodash';
18
+ import {
19
+ namePathToPathKey,
20
+ parsePathString,
21
+ pathKeyToNamePath,
22
+ } from '../models/blocks/form/value-runtime/path';
23
+ import {
24
+ collectStaticDepsFromRunJSValue,
25
+ collectStaticDepsFromTemplateValue,
26
+ recordDep,
27
+ type DepCollector,
28
+ } from '../models/blocks/form/value-runtime/deps';
29
+ import { linkageRulesRefresh } from './linkageRulesRefresh';
30
+
31
+ type NamePath = Array<string | number>;
32
+
33
+ type LinkageRefreshDeps = {
34
+ wildcard: boolean;
35
+ valuePaths: NamePath[];
36
+ structuralPaths: NamePath[];
37
+ };
38
+
39
+ type LinkageRefreshBinding = {
40
+ signature: string;
41
+ running: boolean;
42
+ linkageTxIds: Set<string>;
43
+ pendingPayload: any;
44
+ dispose: () => void;
45
+ };
46
+
47
+ type FieldIndexEntry = {
48
+ name: string;
49
+ index: number;
50
+ };
51
+
52
+ const FORM_VALUES_CHANGE_EVENT = 'formValuesChange';
53
+ const LINKAGE_REFRESH_BINDINGS_KEY = '__formValueDrivenLinkageRefreshBindings';
54
+
55
+ function isSameNamePath(a: NamePath, b: NamePath) {
56
+ return a.length === b.length && a.every((seg, index) => seg === b[index]);
57
+ }
58
+
59
+ function isNamePathPrefix(prefix: NamePath, path: NamePath) {
60
+ if (prefix.length > path.length) return false;
61
+ return prefix.every((seg, index) => seg === path[index]);
62
+ }
63
+
64
+ function dedupeNamePaths(paths: NamePath[]) {
65
+ const byKey = new Map<string, NamePath>();
66
+ for (const path of paths) {
67
+ if (!path?.length) continue;
68
+ byKey.set(namePathToPathKey(path), path);
69
+ }
70
+ return Array.from(byKey.values());
71
+ }
72
+
73
+ function minimizeValueNamePaths(paths: NamePath[]) {
74
+ const deduped = dedupeNamePaths(paths);
75
+ return deduped.filter((path, index) => {
76
+ return !deduped.some((other, otherIndex) => otherIndex !== index && isNamePathPrefix(path, other));
77
+ });
78
+ }
79
+
80
+ function parseFieldIndexEntries(fieldIndex: unknown): FieldIndexEntry[] {
81
+ const arr = Array.isArray(fieldIndex) ? fieldIndex : [];
82
+ const entries: FieldIndexEntry[] = [];
83
+ for (const it of arr) {
84
+ if (typeof it !== 'string') continue;
85
+ const [name, indexStr] = it.split(':');
86
+ const index = Number(indexStr);
87
+ if (!name || Number.isNaN(index)) continue;
88
+ entries.push({ name, index });
89
+ }
90
+ return entries;
91
+ }
92
+
93
+ function getFieldIndexEntriesFromContext(ctx: any): FieldIndexEntry[] {
94
+ return parseFieldIndexEntries(ctx?.model?.context?.fieldIndex ?? ctx?.fieldIndex);
95
+ }
96
+
97
+ function buildItemRowPath(entries: FieldIndexEntry[], parentDepth: number): NamePath | null {
98
+ const targetIndex = entries.length - 1 - parentDepth;
99
+ if (targetIndex < 0) return null;
100
+
101
+ const out: NamePath = [];
102
+ for (let i = 0; i <= targetIndex; i++) {
103
+ out.push(entries[i].name, entries[i].index);
104
+ }
105
+ return out;
106
+ }
107
+
108
+ function buildItemListRootPath(entries: FieldIndexEntry[], parentDepth: number): NamePath | null {
109
+ const rowPath = buildItemRowPath(entries, parentDepth);
110
+ if (!rowPath?.length) return null;
111
+ return rowPath.slice(0, -1);
112
+ }
113
+
114
+ function resolveItemDependencyPath(ctx: FlowContext, depPath: NamePath): LinkageRefreshDeps {
115
+ const entries = getFieldIndexEntriesFromContext(ctx as any);
116
+ if (!entries.length) {
117
+ return { wildcard: true, valuePaths: [], structuralPaths: [] };
118
+ }
119
+
120
+ let parentDepth = 0;
121
+ let cursor = [...depPath];
122
+ while (cursor[0] === 'parentItem') {
123
+ parentDepth += 1;
124
+ cursor = cursor.slice(1);
125
+ }
126
+
127
+ const head = cursor[0];
128
+ if (!head) {
129
+ return { wildcard: true, valuePaths: [], structuralPaths: [] };
130
+ }
131
+
132
+ if (head === 'value') {
133
+ const rowPath = buildItemRowPath(entries, parentDepth);
134
+ if (!rowPath) return { wildcard: true, valuePaths: [], structuralPaths: [] };
135
+ const listRootPath = buildItemListRootPath(entries, parentDepth);
136
+ return {
137
+ wildcard: false,
138
+ valuePaths: [[...rowPath, ...cursor.slice(1)]],
139
+ structuralPaths: listRootPath ? [listRootPath] : [],
140
+ };
141
+ }
142
+
143
+ if (head === 'index' || head === 'length') {
144
+ const listRootPath = buildItemListRootPath(entries, parentDepth);
145
+ if (!listRootPath) return { wildcard: true, valuePaths: [], structuralPaths: [] };
146
+ return {
147
+ wildcard: false,
148
+ valuePaths: [],
149
+ structuralPaths: [listRootPath],
150
+ };
151
+ }
152
+
153
+ if (head === '__is_new__' || head === '__is_stored__') {
154
+ const rowPath = buildItemRowPath(entries, parentDepth);
155
+ if (!rowPath) return { wildcard: true, valuePaths: [], structuralPaths: [] };
156
+ return {
157
+ wildcard: false,
158
+ valuePaths: [[...rowPath, head]],
159
+ structuralPaths: [],
160
+ };
161
+ }
162
+
163
+ return { wildcard: true, valuePaths: [], structuralPaths: [] };
164
+ }
165
+
166
+ function addRunjsUsageToCollector(script: string, collector: DepCollector) {
167
+ if (typeof script !== 'string' || !script.trim()) return;
168
+ const usage = extractUsedVariablePathsFromRunJS(script) || {};
169
+ for (const [varName, rawPaths] of Object.entries(usage)) {
170
+ const paths = Array.isArray(rawPaths) ? rawPaths : [];
171
+ const normalized = paths.length ? paths : [''];
172
+ for (const subPath of normalized) {
173
+ if (varName === 'formValues') {
174
+ if (!subPath) {
175
+ collector.wildcard = true;
176
+ continue;
177
+ }
178
+ const segs = parsePathString(String(subPath)).filter((seg) => typeof seg !== 'object') as NamePath;
179
+ recordDep(segs, collector);
180
+ continue;
181
+ }
182
+ if (varName === 'item') {
183
+ const key = subPath ? `ctx:item:${String(subPath)}` : 'ctx:item';
184
+ collector.deps.add(key);
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ function collectRunjsDepsFromLinkageRules(params: any, collector: DepCollector) {
191
+ const seen = new WeakSet<object>();
192
+ const visit = (value: any) => {
193
+ if (!value || typeof value !== 'object') return;
194
+ if (seen.has(value)) return;
195
+ seen.add(value);
196
+
197
+ if (isRunJSValue(value)) {
198
+ collectStaticDepsFromRunJSValue(value, collector);
199
+ }
200
+
201
+ if (Array.isArray(value)) {
202
+ value.forEach(visit);
203
+ return;
204
+ }
205
+
206
+ const actionName = (value as any)?.name;
207
+ if (actionName === 'linkageRunjs' || actionName === 'runjs') {
208
+ addRunjsUsageToCollector(_.get(value, ['params', 'value', 'script']), collector);
209
+ addRunjsUsageToCollector(_.get(value, ['params', 'code']), collector);
210
+ }
211
+
212
+ Object.values(value).forEach(visit);
213
+ };
214
+
215
+ visit(params);
216
+ }
217
+
218
+ function collectLinkageRefreshDeps(ctx: FlowContext, params: any): LinkageRefreshDeps {
219
+ const collector: DepCollector = { deps: new Set(), wildcard: false };
220
+
221
+ collectStaticDepsFromTemplateValue(params, collector);
222
+ collectRunjsDepsFromLinkageRules(params, collector);
223
+
224
+ const valuePaths: NamePath[] = [];
225
+ const structuralPaths: NamePath[] = [];
226
+ let wildcard = collector.wildcard;
227
+
228
+ for (const depKey of collector.deps) {
229
+ if (depKey === 'fv:*') {
230
+ wildcard = true;
231
+ continue;
232
+ }
233
+
234
+ if (depKey.startsWith('fv:')) {
235
+ const inner = depKey.slice('fv:'.length);
236
+ if (!inner) {
237
+ wildcard = true;
238
+ continue;
239
+ }
240
+ valuePaths.push(pathKeyToNamePath(inner));
241
+ continue;
242
+ }
243
+
244
+ if (depKey === 'ctx:item' || depKey.startsWith('ctx:item:')) {
245
+ const subPath = depKey === 'ctx:item' ? '' : depKey.slice('ctx:item:'.length);
246
+ const depPath = subPath
247
+ ? (parsePathString(subPath).filter((seg) => typeof seg !== 'object') as NamePath)
248
+ : [];
249
+ const resolved = resolveItemDependencyPath(ctx, depPath);
250
+ wildcard ||= resolved.wildcard;
251
+ valuePaths.push(...resolved.valuePaths);
252
+ structuralPaths.push(...resolved.structuralPaths);
253
+ }
254
+ }
255
+
256
+ return {
257
+ wildcard,
258
+ valuePaths: minimizeValueNamePaths(valuePaths),
259
+ structuralPaths: dedupeNamePaths(structuralPaths),
260
+ };
261
+ }
262
+
263
+ function hasLinkageRefreshDeps(deps: LinkageRefreshDeps) {
264
+ return deps.wildcard || deps.valuePaths.length > 0 || deps.structuralPaths.length > 0;
265
+ }
266
+
267
+ function getChangedPathsFromPayload(payload: any): NamePath[] {
268
+ const rawChangedPaths = Array.isArray(payload?.changedPaths) ? payload.changedPaths : [];
269
+ const out: NamePath[] = [];
270
+
271
+ for (const path of rawChangedPaths) {
272
+ if (Array.isArray(path)) {
273
+ if (path.length === 1 && typeof path[0] === 'string') {
274
+ const namePath = pathKeyToNamePath(path[0]);
275
+ if (namePath.length) out.push(namePath);
276
+ continue;
277
+ }
278
+ const segs = path.filter((seg) => typeof seg === 'string' || typeof seg === 'number') as NamePath;
279
+ if (segs.length) out.push(segs);
280
+ continue;
281
+ }
282
+ if (typeof path === 'string' && path) {
283
+ out.push(pathKeyToNamePath(path));
284
+ }
285
+ }
286
+
287
+ if (out.length) {
288
+ return out;
289
+ }
290
+
291
+ const changedValues = payload?.changedValues;
292
+ if (changedValues && typeof changedValues === 'object') {
293
+ for (const key of Object.keys(changedValues)) {
294
+ const namePath = pathKeyToNamePath(key);
295
+ if (namePath.length) out.push(namePath);
296
+ }
297
+ }
298
+
299
+ return out;
300
+ }
301
+
302
+ function linkageRefreshDepsMatchPayload(deps: LinkageRefreshDeps, payload: any) {
303
+ if (!hasLinkageRefreshDeps(deps)) return false;
304
+ const changedPaths = getChangedPathsFromPayload(payload);
305
+ if (deps.wildcard) return true;
306
+ if (!changedPaths.length) return true;
307
+
308
+ for (const changedPath of changedPaths) {
309
+ for (const depPath of deps.valuePaths) {
310
+ if (isNamePathPrefix(depPath, changedPath) || isNamePathPrefix(changedPath, depPath)) {
311
+ return true;
312
+ }
313
+ }
314
+
315
+ for (const depPath of deps.structuralPaths) {
316
+ if (isSameNamePath(depPath, changedPath) || isNamePathPrefix(changedPath, depPath)) {
317
+ return true;
318
+ }
319
+ }
320
+ }
321
+
322
+ return false;
323
+ }
324
+
325
+ function getDepsSignature(deps: LinkageRefreshDeps, formBlock: any) {
326
+ const toKeys = (paths: NamePath[]) => paths.map((path) => namePathToPathKey(path)).sort();
327
+ return JSON.stringify({
328
+ formBlockUid: formBlock?.uid,
329
+ wildcard: deps.wildcard,
330
+ valuePaths: toKeys(deps.valuePaths),
331
+ structuralPaths: toKeys(deps.structuralPaths),
332
+ });
333
+ }
334
+
335
+ function getLinkageRefreshBindings(model: any): Map<string, LinkageRefreshBinding> {
336
+ return (model[LINKAGE_REFRESH_BINDINGS_KEY] ||= new Map<string, LinkageRefreshBinding>());
337
+ }
338
+
339
+ function isFormBlockForLinkageRefresh(model: any) {
340
+ if (!model || typeof model !== 'object') return false;
341
+ if (!model.emitter || typeof model.emitter.on !== 'function' || typeof model.emitter.off !== 'function') return false;
342
+ return !!model.formValueRuntime || !!model.context?.form || typeof model.context?.setFormValues === 'function';
343
+ }
344
+
345
+ function findFormBlockForLinkageRefresh(ctx: FlowContext): any | null {
346
+ const candidates: any[] = [];
347
+ const push = (model: any) => {
348
+ if (model && !candidates.includes(model)) candidates.push(model);
349
+ };
350
+
351
+ push((ctx.model as any)?.context?.blockModel);
352
+ push(ctx.model);
353
+
354
+ let cursor: any = (ctx.model as any)?.parent;
355
+ while (cursor) {
356
+ push(cursor);
357
+ cursor = cursor?.parent;
358
+ }
359
+
360
+ return candidates.find(isFormBlockForLinkageRefresh) || null;
361
+ }
362
+
363
+ function disposeLinkageRefreshBinding(model: any, key: string) {
364
+ const bindings = getLinkageRefreshBindings(model);
365
+ const existing = bindings.get(key);
366
+ if (existing) {
367
+ existing.dispose();
368
+ }
369
+ }
370
+
371
+ export function ensureFormValueDrivenLinkageRefresh(ctx: FlowContext, params: any, actionName: string) {
372
+ const model: any = ctx.model;
373
+ const flowKey = (ctx as any)?.flowKey;
374
+ if (!model || !flowKey) return;
375
+
376
+ const stepKey = 'linkageRules';
377
+ const bindingKey = `${flowKey}:${stepKey}:${actionName}`;
378
+ const bindings = getLinkageRefreshBindings(model);
379
+ const deps = collectLinkageRefreshDeps(ctx, params);
380
+ if (!hasLinkageRefreshDeps(deps)) {
381
+ disposeLinkageRefreshBinding(model, bindingKey);
382
+ return;
383
+ }
384
+
385
+ const formBlock = findFormBlockForLinkageRefresh(ctx);
386
+ if (!formBlock) {
387
+ disposeLinkageRefreshBinding(model, bindingKey);
388
+ return;
389
+ }
390
+
391
+ const signature = getDepsSignature(deps, formBlock);
392
+ const existing = bindings.get(bindingKey);
393
+ if (existing?.signature === signature) {
394
+ return;
395
+ }
396
+ if (existing) {
397
+ existing.dispose();
398
+ }
399
+
400
+ const binding: LinkageRefreshBinding = {
401
+ signature,
402
+ running: false,
403
+ linkageTxIds: new Set(),
404
+ pendingPayload: null,
405
+ dispose: () => {},
406
+ };
407
+
408
+ const engineEmitter = model?.flowEngine?.emitter || (ctx as any)?.engine?.emitter || model?.context?.engine?.emitter;
409
+
410
+ const dispose = () => {
411
+ formBlock.emitter?.off?.(FORM_VALUES_CHANGE_EVENT, listener);
412
+ engineEmitter?.off?.('model:unmounted', cleanupOnUnmount);
413
+ engineEmitter?.off?.('model:destroyed', cleanupOnDestroyed);
414
+ if (bindings.get(bindingKey) === binding) {
415
+ bindings.delete(bindingKey);
416
+ }
417
+ };
418
+
419
+ const rememberLinkageTxId = (linkageTxId: unknown) => {
420
+ if (typeof linkageTxId !== 'string' || !linkageTxId) return;
421
+ binding.linkageTxIds.add(linkageTxId);
422
+ if (binding.linkageTxIds.size <= 20) return;
423
+ const oldest = binding.linkageTxIds.values().next().value;
424
+ if (oldest) binding.linkageTxIds.delete(oldest);
425
+ };
426
+
427
+ const listener = (payload: any) => {
428
+ const payloadLinkageTxId = typeof payload?.linkageTxId === 'string' ? payload.linkageTxId : undefined;
429
+ if (payload?.source === 'linkage' && payloadLinkageTxId && binding.linkageTxIds.has(payloadLinkageTxId)) {
430
+ return;
431
+ }
432
+ if (model.disposed || formBlock.disposed) {
433
+ dispose();
434
+ return;
435
+ }
436
+ const latestDeps = collectLinkageRefreshDeps(ctx, params);
437
+ if (!linkageRefreshDepsMatchPayload(latestDeps, payload)) return;
438
+ if (binding.running) {
439
+ binding.pendingPayload = payload;
440
+ return;
441
+ }
442
+
443
+ const refreshLinkageTxId = payloadLinkageTxId || (typeof payload?.txId === 'string' ? payload.txId : undefined);
444
+ rememberLinkageTxId(refreshLinkageTxId);
445
+ binding.running = true;
446
+ const refreshCtx = new FlowRuntimeContext(model, flowKey);
447
+ refreshCtx.defineProperty('inputArgs', {
448
+ value: {
449
+ ...(payload || {}),
450
+ linkageTxId: refreshLinkageTxId,
451
+ },
452
+ });
453
+
454
+ void linkageRulesRefresh
455
+ .handler(refreshCtx, {
456
+ actionName,
457
+ flowKey,
458
+ stepKey,
459
+ })
460
+ .catch((error) => {
461
+ console.warn('[linkageRules] Failed to refresh form value driven linkage rules', error);
462
+ })
463
+ .finally(() => {
464
+ binding.running = false;
465
+ const pendingPayload = binding.pendingPayload;
466
+ binding.pendingPayload = null;
467
+ if (pendingPayload) {
468
+ listener(pendingPayload);
469
+ }
470
+ });
471
+ };
472
+
473
+ const cleanupOnUnmount = ({ model: unmountedModel }: { model: FlowModel }) => {
474
+ // Action linkage may hide the action itself, which unmounts its renderer.
475
+ // Keep the watcher alive so later form changes can restore the action state.
476
+ if (unmountedModel === formBlock || (unmountedModel === model && model.disposed)) {
477
+ dispose();
478
+ }
479
+ };
480
+
481
+ const cleanupOnDestroyed = ({ model: destroyedModel }: { model: FlowModel }) => {
482
+ if (destroyedModel === model || destroyedModel === formBlock) {
483
+ dispose();
484
+ }
485
+ };
486
+
487
+ binding.dispose = dispose;
488
+ bindings.set(bindingKey, binding);
489
+ formBlock.emitter.on(FORM_VALUES_CHANGE_EVENT, listener);
490
+ engineEmitter?.on?.('model:unmounted', cleanupOnUnmount);
491
+ engineEmitter?.on?.('model:destroyed', cleanupOnDestroyed);
492
+ }
@@ -65,10 +65,12 @@ export const linkageRulesRefresh = defineAction({
65
65
 
66
66
  const run = (async () => {
67
67
  const raw = model?.getStepParams?.(flowKey, stepKey);
68
- const resolved = await ctx.resolveJsonTemplate({ value: [], ...(raw || {}) });
69
68
  const action = ctx.getAction?.(actionName);
69
+ const paramsForAction = action?.useRawParams
70
+ ? { value: [], ...(raw || {}) }
71
+ : await ctx.resolveJsonTemplate({ value: [], ...(raw || {}) });
70
72
  if (action?.handler) {
71
- await action.handler(ctx, resolved);
73
+ await action.handler(ctx, paramsForAction);
72
74
  }
73
75
  })();
74
76