@nocobase/flow-engine 3.0.0-alpha.5 → 3.0.0-alpha.7

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.
@@ -2384,7 +2384,7 @@ const _FlowEngineContext = class _FlowEngineContext extends BaseFlowEngineContex
2384
2384
  return this.resolveJsonTemplate(template);
2385
2385
  });
2386
2386
  this.defineMethod("resolveJsonTemplate", async function(template) {
2387
- var _a, _b;
2387
+ var _a, _b, _c;
2388
2388
  const used = (0, import_utils.extractUsedVariablePaths)(template);
2389
2389
  const usedVarNames = Object.keys(used || {});
2390
2390
  if (!usedVarNames.length) {
@@ -2412,13 +2412,13 @@ const _FlowEngineContext = class _FlowEngineContext extends BaseFlowEngineContex
2412
2412
  let serverResolved = template;
2413
2413
  if (needServer) {
2414
2414
  const inferRecordRefWithMeta = /* @__PURE__ */ __name((ctx) => {
2415
- var _a2, _b2, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
2415
+ var _a2, _b2, _c2, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
2416
2416
  const ref = (0, import_variablesParams.inferRecordRef)(ctx);
2417
2417
  if (ref) return ref;
2418
2418
  try {
2419
2419
  const tk = (_b2 = (_a2 = ctx == null ? void 0 : ctx.resource) == null ? void 0 : _a2.getMeta) == null ? void 0 : _b2.call(_a2, "currentFilterByTk");
2420
2420
  if (typeof tk === "undefined" || tk === null) return void 0;
2421
- const collection = ((_c = ctx == null ? void 0 : ctx.collection) == null ? void 0 : _c.name) || ((_j = (_i = (_h = (_g = (_f = (_e = (_d = ctx == null ? void 0 : ctx.resource) == null ? void 0 : _d.getResourceName) == null ? void 0 : _e.call(_d)) == null ? void 0 : _f.split) == null ? void 0 : _g.call(_f, ".")) == null ? void 0 : _h.slice) == null ? void 0 : _i.call(_h, -1)) == null ? void 0 : _j[0]);
2421
+ const collection = ((_c2 = ctx == null ? void 0 : ctx.collection) == null ? void 0 : _c2.name) || ((_j = (_i = (_h = (_g = (_f = (_e = (_d = ctx == null ? void 0 : ctx.resource) == null ? void 0 : _d.getResourceName) == null ? void 0 : _e.call(_d)) == null ? void 0 : _f.split) == null ? void 0 : _g.call(_f, ".")) == null ? void 0 : _h.slice) == null ? void 0 : _i.call(_h, -1)) == null ? void 0 : _j[0]);
2422
2422
  if (!collection) return void 0;
2423
2423
  const dataSourceKey = ((_k = ctx == null ? void 0 : ctx.collection) == null ? void 0 : _k.dataSourceKey) || ((_m = (_l = ctx == null ? void 0 : ctx.resource) == null ? void 0 : _l.getDataSourceKey) == null ? void 0 : _m.call(_l));
2424
2424
  return { collection, dataSourceKey, filterByTk: tk };
@@ -2461,6 +2461,11 @@ const _FlowEngineContext = class _FlowEngineContext extends BaseFlowEngineContex
2461
2461
  }, "collectFromMeta");
2462
2462
  const inputFromMeta = await collectFromMeta();
2463
2463
  const autoInput = { ...inputFromMeta };
2464
+ const viewPaths = serverVarPaths.view || [];
2465
+ if (!autoInput.view && viewPaths.some((path) => path === "record" || path.startsWith("record.") || path.startsWith("record["))) {
2466
+ const recordRef = (0, import_variablesParams.inferViewRecordRef)(this);
2467
+ if (recordRef) autoInput.view = { record: recordRef };
2468
+ }
2464
2469
  try {
2465
2470
  const varName = "formValues";
2466
2471
  const neededPaths = serverVarPaths[varName] || [];
@@ -2512,11 +2517,12 @@ const _FlowEngineContext = class _FlowEngineContext extends BaseFlowEngineContex
2512
2517
  if (this.api) {
2513
2518
  try {
2514
2519
  serverResolved = await (0, import_params_resolvers.enqueueVariablesResolve)(this, {
2520
+ rd: (0, import_params_resolvers.buildFlowModelResolveDescriptor)(this, (_a = this.model) == null ? void 0 : _a.uid),
2515
2521
  template,
2516
2522
  contextParams: autoContextParams || {}
2517
2523
  });
2518
2524
  } catch (e) {
2519
- (_b = (_a = this.logger) == null ? void 0 : _a.warn) == null ? void 0 : _b.call(_a, { err: e }, "variables:resolve failed, fallback to client-only");
2525
+ (_c = (_b = this.logger) == null ? void 0 : _b.warn) == null ? void 0 : _c.call(_b, { err: e }, "variables:resolve failed, fallback to client-only");
2520
2526
  serverResolved = template;
2521
2527
  }
2522
2528
  }
@@ -8,6 +8,16 @@
8
8
  */
9
9
  import type { Collection } from '../data-source';
10
10
  import type { FlowContext, PropertyMetaFactory } from '../flowContext';
11
+ /**
12
+ * 从值中提取主键:
13
+ * - 支持主键原始值(string/number)
14
+ * - 支持对象(按主键名取值)
15
+ *
16
+ * @param value 字段当前值
17
+ * @param primaryKey 主键字段名
18
+ * @returns 解析出的主键值,无法解析时返回 undefined
19
+ */
20
+ export declare function getAssociationFilterByTk(value: unknown, primaryKey: string | string[]): unknown;
11
21
  /**
12
22
  * 创建一个用于“对象类变量”(如 formValues / item)的 `resolveOnServer` 判定函数。
13
23
  * 仅当访问路径以“关联字段名”开头(且继续访问其子属性)时,返回 true 交由服务端解析;
@@ -38,7 +38,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
38
38
  var associationObjectVariable_exports = {};
39
39
  __export(associationObjectVariable_exports, {
40
40
  createAssociationAwareObjectMetaFactory: () => createAssociationAwareObjectMetaFactory,
41
- createAssociationSubpathResolver: () => createAssociationSubpathResolver
41
+ createAssociationSubpathResolver: () => createAssociationSubpathResolver,
42
+ getAssociationFilterByTk: () => getAssociationFilterByTk
42
43
  });
43
44
  module.exports = __toCommonJS(associationObjectVariable_exports);
44
45
  var import_lodash = __toESM(require("lodash"));
@@ -58,13 +59,14 @@ function findFieldByName(collection, name) {
58
59
  return fields.find((f) => f.name === name);
59
60
  }
60
61
  __name(findFieldByName, "findFieldByName");
61
- function toFilterByTk(value, primaryKey) {
62
+ function getAssociationFilterByTk(value, primaryKey) {
62
63
  if (value == null) return void 0;
63
64
  if (Array.isArray(primaryKey)) {
64
65
  if (typeof value !== "object" || !value) return void 0;
66
+ const record = value;
65
67
  const out = {};
66
68
  for (const k of primaryKey) {
67
- const v = value[k];
69
+ const v = record[k];
68
70
  if (typeof v === "undefined" || v === null) return void 0;
69
71
  out[k] = v;
70
72
  }
@@ -76,7 +78,7 @@ function toFilterByTk(value, primaryKey) {
76
78
  }
77
79
  return void 0;
78
80
  }
79
- __name(toFilterByTk, "toFilterByTk");
81
+ __name(getAssociationFilterByTk, "getAssociationFilterByTk");
80
82
  function createAssociationSubpathResolver(collectionAccessor, valueAccessor) {
81
83
  return (p) => {
82
84
  if (!p || !p.includes(".")) return false;
@@ -122,7 +124,7 @@ function createAssociationAwareObjectMetaFactory(collectionAccessor, title, valu
122
124
  const associationValue = obj[name];
123
125
  if (associationValue == null) continue;
124
126
  if (Array.isArray(associationValue)) {
125
- const ids = associationValue.map((item) => toFilterByTk(item, primaryKey)).filter((v) => v != null);
127
+ const ids = associationValue.map((item) => getAssociationFilterByTk(item, primaryKey)).filter((v) => v != null);
126
128
  if (ids.length) {
127
129
  params[name] = {
128
130
  collection: target,
@@ -131,7 +133,7 @@ function createAssociationAwareObjectMetaFactory(collectionAccessor, title, valu
131
133
  };
132
134
  }
133
135
  } else {
134
- const id = toFilterByTk(associationValue, primaryKey);
136
+ const id = getAssociationFilterByTk(associationValue, primaryKey);
135
137
  if (id != null) {
136
138
  params[name] = {
137
139
  collection: target,
@@ -153,5 +155,6 @@ __name(createAssociationAwareObjectMetaFactory, "createAssociationAwareObjectMet
153
155
  // Annotate the CommonJS export names for ESM import in node:
154
156
  0 && (module.exports = {
155
157
  createAssociationAwareObjectMetaFactory,
156
- createAssociationSubpathResolver
158
+ createAssociationSubpathResolver,
159
+ getAssociationFilterByTk
157
160
  });
@@ -11,11 +11,11 @@ export { escapeT, getT, tExpr } from './translation';
11
11
  export { FlowCancelSaveException, FlowExitAllException, FlowExitException } from './exceptions';
12
12
  export { defineAction } from './flow-definitions';
13
13
  export { isInheritedFrom } from './inheritance';
14
- export { resolveCreateModelOptions, resolveDefaultParams, resolveExpressions } from './params-resolvers';
14
+ export { buildFlowModelResolveDescriptor, resolveCreateModelOptions, resolveDefaultParams, resolveExpressions, } from './params-resolvers';
15
15
  export { compileUiSchema, resolveStepUiSchema, resolveStepDisabledInSettings, resolveUiMode, shouldHideEventInSettings, shouldHideStepInSettings, } from './schema-utils';
16
16
  export { setupRuntimeContextSteps } from './setupRuntimeContextSteps';
17
17
  export { createCollectionContextMeta } from './createCollectionContextMeta';
18
- export { createAssociationAwareObjectMetaFactory, createAssociationSubpathResolver } from './associationObjectVariable';
18
+ export { createAssociationAwareObjectMetaFactory, createAssociationSubpathResolver, getAssociationFilterByTk, } from './associationObjectVariable';
19
19
  export { buildRecordMeta, collectContextParamsForTemplate, createCurrentRecordMetaFactory, createRecordResolveOnServerWithLocal, createRecordMetaFactory, extractUsedVariableNames, extractUsedVariablePaths, inferRecordRef, type RecordParamsBuilder, } from './variablesParams';
20
20
  export { extractPropertyPath, formatPathToVariable, isVariableExpression } from './context';
21
21
  export { clearAutoFlowError, getAutoFlowError, setAutoFlowError, type AutoFlowError } from './autoFlowError';
@@ -34,6 +34,7 @@ __export(utils_exports, {
34
34
  FlowExitException: () => import_exceptions.FlowExitException,
35
35
  MENU_KEYS: () => import_constants.MENU_KEYS,
36
36
  RUNJS_OPEN_VIEW_ROUTE_STATE: () => import_openViewRouteState.RUNJS_OPEN_VIEW_ROUTE_STATE,
37
+ buildFlowModelResolveDescriptor: () => import_params_resolvers.buildFlowModelResolveDescriptor,
37
38
  buildRecordMeta: () => import_variablesParams.buildRecordMeta,
38
39
  clearAutoFlowError: () => import_autoFlowError.clearAutoFlowError,
39
40
  collectContextParamsForTemplate: () => import_variablesParams.collectContextParamsForTemplate,
@@ -57,6 +58,7 @@ __export(utils_exports, {
57
58
  extractUsedVariablePaths: () => import_variablesParams.extractUsedVariablePaths,
58
59
  extractUsedVariablePathsFromRunJS: () => import_runjsValue.extractUsedVariablePathsFromRunJS,
59
60
  formatPathToVariable: () => import_context.formatPathToVariable,
61
+ getAssociationFilterByTk: () => import_associationObjectVariable.getAssociationFilterByTk,
60
62
  getAutoFlowError: () => import_autoFlowError.getAutoFlowError,
61
63
  getT: () => import_translation.getT,
62
64
  inferRecordRef: () => import_variablesParams.inferRecordRef,
@@ -127,6 +129,7 @@ var import_randomId = require("./randomId");
127
129
  FlowExitException,
128
130
  MENU_KEYS,
129
131
  RUNJS_OPEN_VIEW_ROUTE_STATE,
132
+ buildFlowModelResolveDescriptor,
130
133
  buildRecordMeta,
131
134
  clearAutoFlowError,
132
135
  collectContextParamsForTemplate,
@@ -150,6 +153,7 @@ var import_randomId = require("./randomId");
150
153
  extractUsedVariablePaths,
151
154
  extractUsedVariablePathsFromRunJS,
152
155
  formatPathToVariable,
156
+ getAssociationFilterByTk,
153
157
  getAutoFlowError,
154
158
  getT,
155
159
  inferRecordRef,
@@ -27,10 +27,12 @@ export type JSONValue = string | {
27
27
  [key: string]: JSONValue;
28
28
  } | JSONValue[];
29
29
  type BatchPayload = {
30
+ rd?: string;
30
31
  template: JSONValue;
31
32
  contextParams?: ServerContextParams | undefined;
32
33
  };
33
34
  export declare function enqueueVariablesResolve(ctx: FlowRuntimeContext, payload: BatchPayload): Promise<unknown>;
35
+ export declare function buildFlowModelResolveDescriptor(ctx: Pick<FlowModelContext, 'api'>, flowModelUid?: string | number | null): any;
34
36
  /**
35
37
  * 解析参数中的 {{xxx}} 表达式,自动处理异步属性访问
36
38
  */
@@ -37,6 +37,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
37
37
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
38
38
  var params_resolvers_exports = {};
39
39
  __export(params_resolvers_exports, {
40
+ buildFlowModelResolveDescriptor: () => buildFlowModelResolveDescriptor,
40
41
  enqueueVariablesResolve: () => enqueueVariablesResolve,
41
42
  preprocessExpression: () => preprocessExpression,
42
43
  resolveCreateModelOptions: () => resolveCreateModelOptions,
@@ -45,6 +46,7 @@ __export(params_resolvers_exports, {
45
46
  });
46
47
  module.exports = __toCommonJS(params_resolvers_exports);
47
48
  var import_shared = require("@nocobase/shared");
49
+ var import_client = require("@nocobase/utils/client");
48
50
  var import_lodash = __toESM(require("lodash"));
49
51
  async function resolveDefaultParams(defaultParams, ctx) {
50
52
  if (!defaultParams) {
@@ -144,6 +146,7 @@ function enqueueVariablesResolve(ctx, payload) {
144
146
  try {
145
147
  const batch = items.map((it) => ({
146
148
  id: it.id,
149
+ rd: it.payload.rd,
147
150
  template: it.payload.template,
148
151
  contextParams: it.payload.contextParams || {}
149
152
  }));
@@ -187,6 +190,11 @@ function enqueueVariablesResolve(ctx, payload) {
187
190
  return p;
188
191
  }
189
192
  __name(enqueueVariablesResolve, "enqueueVariablesResolve");
193
+ function buildFlowModelResolveDescriptor(ctx, flowModelUid) {
194
+ var _a, _b;
195
+ return (0, import_client.generateFlowModelRdFromToken)(flowModelUid, (_b = (_a = ctx == null ? void 0 : ctx.api) == null ? void 0 : _a.auth) == null ? void 0 : _b.token);
196
+ }
197
+ __name(buildFlowModelResolveDescriptor, "buildFlowModelResolveDescriptor");
190
198
  async function resolveExpressions(params, ctx) {
191
199
  const compile = /* @__PURE__ */ __name(async (source) => {
192
200
  if (typeof source === "string" && /\{\{.*?\}\}/.test(source)) {
@@ -308,6 +316,7 @@ async function compileExpression(expression, ctx) {
308
316
  __name(compileExpression, "compileExpression");
309
317
  // Annotate the CommonJS export names for ESM import in node:
310
318
  0 && (module.exports = {
319
+ buildFlowModelResolveDescriptor,
311
320
  enqueueVariablesResolve,
312
321
  preprocessExpression,
313
322
  resolveCreateModelOptions,
@@ -210,6 +210,11 @@ async function collectContextParamsForTemplate(ctx, template) {
210
210
  input[key] = built;
211
211
  }
212
212
  }
213
+ const viewPaths = usage.view || [];
214
+ if (!input.view && viewPaths.some((path) => path === "record" || path.startsWith("record.") || path.startsWith("record["))) {
215
+ const recordRef = inferViewRecordRef(ctx);
216
+ if (recordRef) input.view = { record: recordRef };
217
+ }
213
218
  return (0, import_serverContextParams.buildServerContextParams)(ctx, input);
214
219
  }
215
220
  __name(collectContextParamsForTemplate, "collectContextParamsForTemplate");
@@ -33,6 +33,13 @@ __export(createViewMeta_exports, {
33
33
  });
34
34
  module.exports = __toCommonJS(createViewMeta_exports);
35
35
  var import_variablesParams = require("../utils/variablesParams");
36
+ function buildPopupSourceRecordRef(ref) {
37
+ if ((ref == null ? void 0 : ref.sourceId) == null || ref.sourceId === "" || typeof ref.associationName !== "string") return void 0;
38
+ const collection = ref.associationName.split(".")[0];
39
+ if (!collection) return void 0;
40
+ return { collection, dataSourceKey: ref.dataSourceKey || "main", filterByTk: ref.sourceId };
41
+ }
42
+ __name(buildPopupSourceRecordRef, "buildPopupSourceRecordRef");
36
43
  function isDefined(value) {
37
44
  return value !== void 0 && value !== null;
38
45
  }
@@ -306,9 +313,11 @@ function createPopupMeta(ctx, anchorView) {
306
313
  let level = 1;
307
314
  let parentRef = await getParentRecordRef(level, c);
308
315
  while (parentRef) {
309
- if (!cur.parent) cur.parent = {};
310
- cur.parent.record = parentRef;
311
- cur = cur.parent;
316
+ const parent = { record: parentRef };
317
+ const sourceRecord = buildPopupSourceRecordRef(parentRef);
318
+ if (sourceRecord) parent.sourceRecord = sourceRecord;
319
+ cur.parent = parent;
320
+ cur = parent;
312
321
  level += 1;
313
322
  parentRef = await getParentRecordRef(level, c);
314
323
  }
@@ -317,19 +326,12 @@ function createPopupMeta(ctx, anchorView) {
317
326
  (_b = (_a = c.logger) == null ? void 0 : _a.debug) == null ? void 0 : _b.call(_a, { err }, "[FlowEngine] buildVariablesParams: build parent-chain failed");
318
327
  }
319
328
  try {
320
- const srcId = inputArgs == null ? void 0 : inputArgs.sourceId;
321
- const assoc = inputArgs == null ? void 0 : inputArgs.associationName;
322
- const dsKey = (inputArgs == null ? void 0 : inputArgs.dataSourceKey) || "main";
323
- if (srcId != null && srcId !== "" && assoc && typeof assoc === "string") {
324
- const parentCollectionName = String(assoc).split(".")[0];
325
- if (parentCollectionName) {
326
- params.sourceRecord = {
327
- collection: parentCollectionName,
328
- dataSourceKey: dsKey,
329
- filterByTk: srcId
330
- };
331
- }
332
- }
329
+ const sourceRecord = buildPopupSourceRecordRef({
330
+ sourceId: inputArgs == null ? void 0 : inputArgs.sourceId,
331
+ associationName: inputArgs == null ? void 0 : inputArgs.associationName,
332
+ dataSourceKey: inputArgs == null ? void 0 : inputArgs.dataSourceKey
333
+ });
334
+ if (sourceRecord) params.sourceRecord = sourceRecord;
333
335
  } catch (err) {
334
336
  (_d = (_c = c.logger) == null ? void 0 : _c.debug) == null ? void 0 : _d.call(_c, { err }, "[FlowEngine] buildVariablesParams: infer sourceRecord failed");
335
337
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/flow-engine",
3
- "version": "3.0.0-alpha.5",
3
+ "version": "3.0.0-alpha.7",
4
4
  "private": false,
5
5
  "description": "A standalone flow engine for NocoBase, managing workflows, models, and actions.",
6
6
  "main": "lib/index.js",
@@ -8,8 +8,8 @@
8
8
  "dependencies": {
9
9
  "@formily/antd-v5": "1.x",
10
10
  "@formily/reactive": "2.x",
11
- "@nocobase/sdk": "3.0.0-alpha.5",
12
- "@nocobase/shared": "3.0.0-alpha.5",
11
+ "@nocobase/sdk": "3.0.0-alpha.7",
12
+ "@nocobase/shared": "3.0.0-alpha.7",
13
13
  "ahooks": "^3.7.2",
14
14
  "axios": "^1.7.0",
15
15
  "dayjs": "^1.11.9",
@@ -37,5 +37,5 @@
37
37
  ],
38
38
  "author": "NocoBase Team",
39
39
  "license": "Apache-2.0",
40
- "gitHead": "82c7ed1cdb2f247c190582e34ee37f154cac0968"
40
+ "gitHead": "19cd28c710f3b3c614ebe2fd7d5a415d527ee559"
41
41
  }
@@ -50,11 +50,11 @@ import {
50
50
  resolveModuleUrl,
51
51
  } from './utils';
52
52
  import { FlowExitAllException } from './utils/exceptions';
53
- import { enqueueVariablesResolve, JSONValue } from './utils/params-resolvers';
53
+ import { buildFlowModelResolveDescriptor, enqueueVariablesResolve, JSONValue } from './utils/params-resolvers';
54
54
  import type { RecordRef } from './utils/serverContextParams';
55
55
  import { buildServerContextParams as _buildServerContextParams } from './utils/serverContextParams';
56
56
  import { getDirtyAwareApiClient } from './utils/dirtyAwareApiClient';
57
- import { inferRecordRef } from './utils/variablesParams';
57
+ import { inferRecordRef, inferViewRecordRef } from './utils/variablesParams';
58
58
  import { FlowView, FlowViewer } from './views/FlowView';
59
59
  import { RunJSContextRegistry, getModelClassName, type RunJSVersion } from './runjs-context/registry';
60
60
  import { createEphemeralContext } from './utils/createEphemeralContext';
@@ -3316,6 +3316,15 @@ export class FlowEngineContext extends BaseFlowEngineContext {
3316
3316
  const inputFromMeta = await collectFromMeta();
3317
3317
  const autoInput = { ...inputFromMeta } as Record<string, any>;
3318
3318
 
3319
+ const viewPaths = serverVarPaths.view || [];
3320
+ if (
3321
+ !autoInput.view &&
3322
+ viewPaths.some((path) => path === 'record' || path.startsWith('record.') || path.startsWith('record['))
3323
+ ) {
3324
+ const recordRef = inferViewRecordRef(this);
3325
+ if (recordRef) autoInput.view = { record: recordRef };
3326
+ }
3327
+
3319
3328
  // Special-case: formValues
3320
3329
  // If server needs to resolve some formValues paths but meta params only cover association anchors
3321
3330
  // (e.g. formValues.customer) and some top-level paths are missing (e.g. formValues.status),
@@ -3388,6 +3397,7 @@ export class FlowEngineContext extends BaseFlowEngineContext {
3388
3397
  if (this.api) {
3389
3398
  try {
3390
3399
  serverResolved = await enqueueVariablesResolve(this as FlowRuntimeContext<FlowModel>, {
3400
+ rd: buildFlowModelResolveDescriptor(this as FlowRuntimeContext<FlowModel>, this.model?.uid),
3391
3401
  template,
3392
3402
  contextParams: autoContextParams || {},
3393
3403
  });
@@ -34,7 +34,8 @@ describe('variablesParams helpers', () => {
34
34
 
35
35
  it('inferRecordRef fallback to collection.getFilterByTK when resource has no filterByTk', () => {
36
36
  const engine = new FlowEngine();
37
- const ds = engine.context.dataSourceManager.getDataSource('main')!;
37
+ const ds = engine.context.dataSourceManager.getDataSource('main');
38
+ if (!ds) throw new Error('main data source is required');
38
39
  ds.addCollection({
39
40
  name: 'users',
40
41
  filterTargetKey: 'id',
@@ -108,6 +109,32 @@ describe('variablesParams helpers', () => {
108
109
  });
109
110
  });
110
111
 
112
+ it('collectContextParamsForTemplate infers view.record when its meta has no descriptor', async () => {
113
+ const ctx: any = {
114
+ getPropertyOptions: () => undefined,
115
+ view: {
116
+ inputArgs: {
117
+ collectionName: 'posts',
118
+ dataSourceKey: 'main',
119
+ filterByTk: 3,
120
+ },
121
+ },
122
+ };
123
+
124
+ const res = await collectContextParamsForTemplate(ctx, {
125
+ recordId: '{{ ctx.view.record.id }}',
126
+ viewType: '{{ ctx.view.type }}',
127
+ });
128
+
129
+ expect(res).toEqual({
130
+ 'view.record': {
131
+ collection: 'posts',
132
+ dataSourceKey: 'main',
133
+ filterByTk: 3,
134
+ },
135
+ });
136
+ });
137
+
111
138
  it('createRecordResolveOnServerWithLocal: no local record => always use server', () => {
112
139
  const resolver = createRecordResolveOnServerWithLocal(
113
140
  () => ({ name: 'posts', dataSourceKey: 'main' }) as any,
@@ -51,13 +51,14 @@ function findFieldByName(collection: Collection | null | undefined, name?: strin
51
51
  * @param primaryKey 主键字段名
52
52
  * @returns 解析出的主键值,无法解析时返回 undefined
53
53
  */
54
- function toFilterByTk(value: unknown, primaryKey: string | string[]) {
54
+ export function getAssociationFilterByTk(value: unknown, primaryKey: string | string[]) {
55
55
  if (value == null) return undefined;
56
56
  if (Array.isArray(primaryKey)) {
57
57
  if (typeof value !== 'object' || !value) return undefined;
58
- const out: Record<string, any> = {};
58
+ const record = value as Record<string, unknown>;
59
+ const out: Record<string, unknown> = {};
59
60
  for (const k of primaryKey) {
60
- const v = (value as any)[k];
61
+ const v = record[k];
61
62
  if (typeof v === 'undefined' || v === null) return undefined;
62
63
  out[k] = v;
63
64
  }
@@ -65,7 +66,7 @@ function toFilterByTk(value: unknown, primaryKey: string | string[]) {
65
66
  }
66
67
  if (typeof value === 'string' || typeof value === 'number') return value;
67
68
  if (typeof value === 'object') {
68
- return (value as any)[primaryKey];
69
+ return (value as Record<string, unknown>)[primaryKey];
69
70
  }
70
71
  return undefined;
71
72
  }
@@ -149,7 +150,9 @@ export function createAssociationAwareObjectMetaFactory(
149
150
  if (associationValue == null) continue;
150
151
 
151
152
  if (Array.isArray(associationValue)) {
152
- const ids = associationValue.map((item) => toFilterByTk(item, primaryKey)).filter((v) => v != null);
153
+ const ids = associationValue
154
+ .map((item) => getAssociationFilterByTk(item, primaryKey))
155
+ .filter((v) => v != null);
153
156
  if (ids.length) {
154
157
  params[name] = {
155
158
  collection: target,
@@ -158,7 +161,7 @@ export function createAssociationAwareObjectMetaFactory(
158
161
  };
159
162
  }
160
163
  } else {
161
- const id = toFilterByTk(associationValue, primaryKey);
164
+ const id = getAssociationFilterByTk(associationValue, primaryKey);
162
165
  if (id != null) {
163
166
  params[name] = {
164
167
  collection: target,
@@ -31,7 +31,12 @@ export { defineAction } from './flow-definitions';
31
31
  export { isInheritedFrom } from './inheritance';
32
32
 
33
33
  // 参数解析器
34
- export { resolveCreateModelOptions, resolveDefaultParams, resolveExpressions } from './params-resolvers';
34
+ export {
35
+ buildFlowModelResolveDescriptor,
36
+ resolveCreateModelOptions,
37
+ resolveDefaultParams,
38
+ resolveExpressions,
39
+ } from './params-resolvers';
35
40
 
36
41
  // Schema 工具
37
42
  export {
@@ -48,7 +53,11 @@ export { setupRuntimeContextSteps } from './setupRuntimeContextSteps';
48
53
 
49
54
  // Record Proxy 工具
50
55
  export { createCollectionContextMeta } from './createCollectionContextMeta';
51
- export { createAssociationAwareObjectMetaFactory, createAssociationSubpathResolver } from './associationObjectVariable';
56
+ export {
57
+ createAssociationAwareObjectMetaFactory,
58
+ createAssociationSubpathResolver,
59
+ getAssociationFilterByTk,
60
+ } from './associationObjectVariable';
52
61
  export {
53
62
  buildRecordMeta,
54
63
  collectContextParamsForTemplate,
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { getValuesByPath } from '@nocobase/shared';
11
+ import { generateFlowModelRdFromToken } from '@nocobase/utils/client';
11
12
  import _ from 'lodash';
12
13
  import { FlowContext, FlowModelContext, FlowRuntimeContext } from '../flowContext';
13
14
  import type { FlowModel } from '../models';
@@ -78,6 +79,7 @@ export type JSONValue = string | { [key: string]: JSONValue } | JSONValue[];
78
79
  // =========================
79
80
 
80
81
  type BatchPayload = {
82
+ rd?: string;
81
83
  template: JSONValue;
82
84
  contextParams?: ServerContextParams | undefined;
83
85
  };
@@ -170,6 +172,7 @@ export function enqueueVariablesResolve(ctx: FlowRuntimeContext, payload: BatchP
170
172
  try {
171
173
  const batch = items.map((it) => ({
172
174
  id: it.id,
175
+ rd: it.payload.rd,
173
176
  template: it.payload.template,
174
177
  contextParams: it.payload.contextParams || {},
175
178
  }));
@@ -218,6 +221,13 @@ export function enqueueVariablesResolve(ctx: FlowRuntimeContext, payload: BatchP
218
221
  return p;
219
222
  }
220
223
 
224
+ export function buildFlowModelResolveDescriptor(
225
+ ctx: Pick<FlowModelContext, 'api'>,
226
+ flowModelUid?: string | number | null,
227
+ ) {
228
+ return generateFlowModelRdFromToken(flowModelUid, ctx?.api?.auth?.token);
229
+ }
230
+
221
231
  /**
222
232
  * 解析参数中的 {{xxx}} 表达式,自动处理异步属性访问
223
233
  */
@@ -263,5 +263,15 @@ export async function collectContextParamsForTemplate(
263
263
  input[key] = built;
264
264
  }
265
265
  }
266
+
267
+ const viewPaths = usage.view || [];
268
+ if (
269
+ !input.view &&
270
+ viewPaths.some((path) => path === 'record' || path.startsWith('record.') || path.startsWith('record['))
271
+ ) {
272
+ const recordRef = inferViewRecordRef(ctx);
273
+ if (recordRef) input.view = { record: recordRef };
274
+ }
275
+
266
276
  return buildServerContextParams(ctx, input);
267
277
  }
@@ -15,6 +15,13 @@ import type { FlowView } from './FlowView';
15
15
 
16
16
  type PopupModelLike = { getStepParams?: (a: string, b: string) => any } | undefined;
17
17
 
18
+ function buildPopupSourceRecordRef(ref?: Pick<RecordRef, 'associationName' | 'dataSourceKey' | 'sourceId'>) {
19
+ if (ref?.sourceId == null || ref.sourceId === '' || typeof ref.associationName !== 'string') return undefined;
20
+ const collection = ref.associationName.split('.')[0];
21
+ if (!collection) return undefined;
22
+ return { collection, dataSourceKey: ref.dataSourceKey || 'main', filterByTk: ref.sourceId };
23
+ }
24
+
18
25
  function isDefined(value: any) {
19
26
  return value !== undefined && value !== null;
20
27
  }
@@ -327,13 +334,15 @@ export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): Proper
327
334
  const stack = getViewStack(view);
328
335
  const currentIndex = getAnchoredViewStackIndex(view, stack);
329
336
  if (currentIndex >= 2) {
330
- let cur: Record<string, any> = params;
337
+ let cur = params;
331
338
  let level = 1;
332
339
  let parentRef = await getParentRecordRef(level, c);
333
340
  while (parentRef) {
334
- if (!cur.parent) cur.parent = {};
335
- cur.parent.record = parentRef;
336
- cur = cur.parent;
341
+ const parent: PopupVariableParams = { record: parentRef };
342
+ const sourceRecord = buildPopupSourceRecordRef(parentRef);
343
+ if (sourceRecord) parent.sourceRecord = sourceRecord;
344
+ cur.parent = parent;
345
+ cur = parent;
337
346
  level += 1;
338
347
  parentRef = await getParentRecordRef(level, c);
339
348
  }
@@ -343,20 +352,12 @@ export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): Proper
343
352
  }
344
353
 
345
354
  try {
346
- const srcId = inputArgs?.sourceId;
347
- const assoc: string | undefined = inputArgs?.associationName;
348
- const dsKey: string = inputArgs?.dataSourceKey || 'main';
349
- if (srcId != null && srcId !== '' && assoc && typeof assoc === 'string') {
350
- // associationName 形如 `posts.comments`,父级集合为 `posts`
351
- const parentCollectionName = String(assoc).split('.')[0];
352
- if (parentCollectionName) {
353
- params.sourceRecord = {
354
- collection: parentCollectionName,
355
- dataSourceKey: dsKey,
356
- filterByTk: srcId,
357
- };
358
- }
359
- }
355
+ const sourceRecord = buildPopupSourceRecordRef({
356
+ sourceId: inputArgs?.sourceId,
357
+ associationName: inputArgs?.associationName,
358
+ dataSourceKey: inputArgs?.dataSourceKey,
359
+ });
360
+ if (sourceRecord) params.sourceRecord = sourceRecord;
360
361
  } catch (err) {
361
362
  c.logger?.debug?.({ err }, '[FlowEngine] buildVariablesParams: infer sourceRecord failed');
362
363
  }