@nocobase/flow-engine 2.3.0-beta.7 → 2.3.0-beta.9

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.
@@ -13,6 +13,7 @@ export declare class FlowDefinition {
13
13
  _steps: Map<string, FlowStep>;
14
14
  protected options: Omit<FlowDefinitionOptions, 'steps'>;
15
15
  constructor(options: FlowDefinitionOptions, flowRegistry: IFlowRepository);
16
+ get model(): import(".").FlowModel<import("./types").DefaultStructure>;
16
17
  get key(): string;
17
18
  get title(): string;
18
19
  set title(title: string);
@@ -53,6 +53,9 @@ const _FlowDefinition = class _FlowDefinition {
53
53
  }
54
54
  _steps = import_reactive.observable.shallow(/* @__PURE__ */ new Map());
55
55
  options;
56
+ get model() {
57
+ return this.flowRegistry.model;
58
+ }
56
59
  get key() {
57
60
  return this.options.key;
58
61
  }
@@ -8,8 +8,10 @@
8
8
  */
9
9
  import { FlowDefinitionOptions } from '../types';
10
10
  import { FlowDefinition } from '../FlowDefinition';
11
+ import type { FlowModel } from '../models';
11
12
  type FlowKey = string;
12
13
  export interface IFlowRepository {
14
+ readonly model?: FlowModel;
13
15
  addFlows(flowDefs: Record<string, Omit<FlowDefinitionOptions, 'key'>>): void;
14
16
  addFlow(flowKey: string, flowOptions: Omit<FlowDefinitionOptions, 'key'>): FlowDefinition | void;
15
17
  removeFlow(flowKey: string): void;
@@ -11,7 +11,7 @@ import { FlowModel } from '../models';
11
11
  import { BaseFlowRegistry } from './BaseFlowRegistry';
12
12
  type FlowKey = string;
13
13
  export declare class InstanceFlowRegistry extends BaseFlowRegistry {
14
- protected model: FlowModel;
14
+ readonly model: FlowModel;
15
15
  static readonly _type: "instance";
16
16
  constructor(model: FlowModel);
17
17
  save(): Promise<void>;
@@ -349,6 +349,7 @@ declare class BaseFlowEngineContext extends FlowContext {
349
349
  */
350
350
  renderJson: (template: JSONValue) => Promise<any>;
351
351
  resolveJsonTemplate: (template: JSONValue, options?: ResolveJsonTemplateOptions) => Promise<any>;
352
+ variableContractModelUid?: string;
352
353
  getVar: (path: string) => Promise<any>;
353
354
  request: (options: RequestOptions) => Promise<any>;
354
355
  runjs: (code: string, variables?: Record<string, any>, options?: JSRunnerOptions) => Promise<any>;
@@ -2518,7 +2518,7 @@ const _FlowEngineContext = class _FlowEngineContext extends BaseFlowEngineContex
2518
2518
  try {
2519
2519
  const contractRd = (0, import_params_resolvers.buildFlowModelResolveDescriptor)(
2520
2520
  this,
2521
- options == null ? void 0 : options.contractModelUid
2521
+ (options == null ? void 0 : options.contractModelUid) ?? this.variableContractModelUid
2522
2522
  );
2523
2523
  serverResolved = await (0, import_params_resolvers.enqueueVariablesResolve)(this, {
2524
2524
  ...contractRd ? { contractRd } : {},
@@ -2986,11 +2986,16 @@ __name(_FlowForkModelContext, "FlowForkModelContext");
2986
2986
  let FlowForkModelContext = _FlowForkModelContext;
2987
2987
  const _FlowRuntimeContext = class _FlowRuntimeContext extends BaseFlowModelContext {
2988
2988
  constructor(model, flowKey, _mode = "runtime") {
2989
+ var _a, _b;
2989
2990
  super();
2990
2991
  this.model = model;
2991
2992
  this.flowKey = flowKey;
2992
2993
  this._mode = _mode;
2993
2994
  this.addDelegate(this.model.context);
2995
+ const owner = (_b = (_a = model.getFlow) == null ? void 0 : _a.call(model, flowKey)) == null ? void 0 : _b.model;
2996
+ if (owner && owner.uid !== model.uid) {
2997
+ this.defineProperty("variableContractModelUid", { value: owner.uid });
2998
+ }
2994
2999
  this.defineMethod("getStepParams", (stepKey) => {
2995
3000
  return model.getStepParams(flowKey, stepKey) || {};
2996
3001
  });
@@ -38,7 +38,7 @@ export declare class CollectionFieldModel<T extends DefaultStructure = DefaultSt
38
38
  get collectionField(): CollectionField;
39
39
  afterAddAsSubModel(): Promise<void>;
40
40
  static getBindingsByField(ctx: FlowEngineContext, collectionField: CollectionField): BindingOptions[];
41
- static getDefaultBindingByField(ctx: FlowEngineContext, collectionField: CollectionField, options?: {
41
+ static getDefaultBindingByField(ctx: FlowEngineContext, collectionField: CollectionField | null | undefined, options?: {
42
42
  useStrict?: boolean;
43
43
  fallbackToTargetTitleField?: boolean;
44
44
  targetCollectionTitleField?: CollectionField;
@@ -83,7 +83,7 @@ function FieldDeletePlaceholder() {
83
83
  const dataSourcePrefix = dataSource ? `${t(dataSource.displayName || dataSource.key)} > ` : "";
84
84
  const collectionPrefix = collection ? `${t(collection.title) || collection.name || collection.tableName} > ` : "";
85
85
  return `${dataSourcePrefix}${collectionPrefix}${name}`;
86
- }, []);
86
+ }, [collection, dataSource, name, t]);
87
87
  return /* @__PURE__ */ import_react.default.createElement(import_antd.Form.Item, null, /* @__PURE__ */ import_react.default.createElement(
88
88
  "div",
89
89
  {
@@ -111,7 +111,7 @@ function FieldWithoutPermissionPlaceholder() {
111
111
  const dataSourcePrefix = `${t(dataSource.displayName || dataSource.key)} > `;
112
112
  const collectionPrefix = collection ? `${t(collection.title) || collection.name || collection.tableName} > ` : "";
113
113
  return `${dataSourcePrefix}${collectionPrefix}${name}`;
114
- }, []);
114
+ }, [collection, dataSource.displayName, dataSource.key, name, t]);
115
115
  const { actionName } = model.forbidden;
116
116
  const messageValue = (0, import_react.useMemo)(() => {
117
117
  return t(
@@ -121,7 +121,7 @@ function FieldWithoutPermissionPlaceholder() {
121
121
  actionName: t(import_lodash.default.capitalize(actionName))
122
122
  }
123
123
  ).replaceAll("&gt;", ">");
124
- }, [nameValue, t]);
124
+ }, [actionName, nameValue, t]);
125
125
  return /* @__PURE__ */ import_react.default.createElement(import_antd.Tooltip, { title: messageValue }, /* @__PURE__ */ import_react.default.createElement(import_FormItem.FormItem, { showLabel: true, label: model.context.collectionField.title || name, style: { opacity: "0.4" } }, /* @__PURE__ */ import_react.default.createElement(import_icons.LockOutlined, null)));
126
126
  }
127
127
  __name(FieldWithoutPermissionPlaceholder, "FieldWithoutPermissionPlaceholder");
@@ -188,6 +188,9 @@ const _CollectionFieldModel = class _CollectionFieldModel extends import_flowMod
188
188
  );
189
189
  }
190
190
  static getDefaultBindingByField(ctx, collectionField, options = {}) {
191
+ if (!collectionField) {
192
+ return null;
193
+ }
191
194
  if (options.fallbackToTargetTitleField) {
192
195
  const binding = this.getDefaultBindingByField(ctx, collectionField, { useStrict: true });
193
196
  if (!binding) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/flow-engine",
3
- "version": "2.3.0-beta.7",
3
+ "version": "2.3.0-beta.9",
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": "2.3.0-beta.7",
12
- "@nocobase/shared": "2.3.0-beta.7",
11
+ "@nocobase/sdk": "2.3.0-beta.9",
12
+ "@nocobase/shared": "2.3.0-beta.9",
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": "3485d95ce53290d6e74d68222a0568ea60420e23"
40
+ "gitHead": "c215e60ed704afe6c086bd96304f4b0d3b46d02e"
41
41
  }
@@ -27,6 +27,10 @@ export class FlowDefinition {
27
27
  }
28
28
  }
29
29
 
30
+ get model() {
31
+ return this.flowRegistry.model;
32
+ }
33
+
30
34
  get key() {
31
35
  return this.options.key;
32
36
  }
@@ -9,7 +9,8 @@
9
9
 
10
10
  import { describe, expect, it, vi } from 'vitest';
11
11
  import { generateFlowModelRdFromToken } from '@nocobase/utils/client';
12
- import { FlowContext } from '../flowContext';
12
+ import { FlowContext, FlowRuntimeContext } from '../flowContext';
13
+ import { FlowModel } from '../models';
13
14
  import { FlowEngine } from '../flowEngine';
14
15
  import {
15
16
  createAssociationAwareObjectMetaFactory,
@@ -490,4 +491,39 @@ describe('objectVariable utilities', () => {
490
491
  expect(resolved).toEqual({ x: 'T1' });
491
492
  expect((ctx as any).api.request).not.toHaveBeenCalled();
492
493
  });
494
+
495
+ it('keeps runtime and configuration descriptors separate for a forwarded instance flow', async () => {
496
+ const engine = new FlowEngine();
497
+ const owner = new FlowModel({ uid: 'reference-owner', flowEngine: engine });
498
+ const target = new FlowModel({ uid: 'reference-target', flowEngine: engine });
499
+ owner.registerFlow({ key: 'forwarded', steps: {} });
500
+ const getFlow = vi.spyOn(target, 'getFlow').mockReturnValue(owner.getFlow('forwarded'));
501
+ const context = new FlowRuntimeContext(target, 'forwarded');
502
+ getFlow.mockRestore(); // Delayed getVar calls must retain the owner after the event bridge is restored.
503
+
504
+ type ResolveItem = { id: string; template: unknown; rd?: string; contractRd?: string };
505
+ const calls: ResolveItem[] = [];
506
+ const payload = Buffer.from(JSON.stringify({ userId: 1, signInTime: 'forwarded-flow' })).toString('base64url');
507
+ const token = `test.${payload}.sig`;
508
+ engine.context.defineProperty('api', {
509
+ value: {
510
+ auth: { token },
511
+ request: vi.fn(async ({ data }: { data: { values: { batch: ResolveItem[] } } }) => {
512
+ const batch = data.values.batch;
513
+ calls.push(...batch);
514
+ return { data: { data: { results: batch.map((item) => ({ id: item.id, data: 'resolved' })) } } };
515
+ }),
516
+ },
517
+ });
518
+ target.context.defineProperty('remote', { value: {}, resolveOnServer: true });
519
+
520
+ expect(await context.getVar('ctx.remote.name')).toBe('resolved');
521
+ expect(calls[0].rd).toBe(generateFlowModelRdFromToken(target.uid, token));
522
+ expect(calls[0].contractRd).toBe(generateFlowModelRdFromToken(owner.uid, token));
523
+ await context.resolveJsonTemplate('{{ ctx.remote.name }}', { contractModelUid: 'explicit-owner' });
524
+ expect(calls[1].contractRd).toBe(generateFlowModelRdFromToken('explicit-owner', token));
525
+ await new FlowRuntimeContext(target, 'local').resolveJsonTemplate('{{ ctx.remote.name }}');
526
+ expect(calls[2].contractRd).toBeUndefined();
527
+ expect(owner.getFlow('forwarded')?.serialize()).not.toHaveProperty('model');
528
+ });
493
529
  });
@@ -10,10 +10,12 @@
10
10
  import { FlowDefinitionOptions } from '../types';
11
11
  import { FlowDefinition } from '../FlowDefinition';
12
12
  import { observable } from '@formily/reactive';
13
+ import type { FlowModel } from '../models';
13
14
 
14
15
  type FlowKey = string;
15
16
 
16
17
  export interface IFlowRepository {
18
+ readonly model?: FlowModel;
17
19
  addFlows(flowDefs: Record<string, Omit<FlowDefinitionOptions, 'key'>>): void;
18
20
  addFlow(flowKey: string, flowOptions: Omit<FlowDefinitionOptions, 'key'>): FlowDefinition | void;
19
21
  removeFlow(flowKey: string): void;
@@ -15,7 +15,7 @@ type FlowKey = string;
15
15
 
16
16
  export class InstanceFlowRegistry extends BaseFlowRegistry {
17
17
  static readonly _type = 'instance' as const;
18
- constructor(protected model: FlowModel) {
18
+ constructor(public readonly model: FlowModel) {
19
19
  super();
20
20
  }
21
21
 
@@ -3051,6 +3051,7 @@ class BaseFlowEngineContext extends FlowContext {
3051
3051
  */
3052
3052
  declare renderJson: (template: JSONValue) => Promise<any>;
3053
3053
  declare resolveJsonTemplate: (template: JSONValue, options?: ResolveJsonTemplateOptions) => Promise<any>;
3054
+ declare variableContractModelUid?: string;
3054
3055
  declare getVar: (path: string) => Promise<any>;
3055
3056
  declare request: (options: RequestOptions) => Promise<any>;
3056
3057
  declare runjs: (code: string, variables?: Record<string, any>, options?: JSRunnerOptions) => Promise<any>;
@@ -3408,7 +3409,7 @@ export class FlowEngineContext extends BaseFlowEngineContext {
3408
3409
  try {
3409
3410
  const contractRd = buildFlowModelResolveDescriptor(
3410
3411
  this as FlowRuntimeContext<FlowModel>,
3411
- options?.contractModelUid,
3412
+ options?.contractModelUid ?? this.variableContractModelUid,
3412
3413
  );
3413
3414
  serverResolved = await enqueueVariablesResolve(this as FlowRuntimeContext<FlowModel>, {
3414
3415
  ...(contractRd ? { contractRd } : {}),
@@ -3943,6 +3944,11 @@ export class FlowRuntimeContext<
3943
3944
  ) {
3944
3945
  super();
3945
3946
  this.addDelegate(this.model.context);
3947
+ const owner = model.getFlow?.(flowKey)?.model;
3948
+ if (owner && owner.uid !== model.uid) {
3949
+ // A forwarded instance flow keeps its configuration owner, without changing the runtime model.
3950
+ this.defineProperty('variableContractModelUid', { value: owner.uid });
3951
+ }
3946
3952
  this.defineMethod('getStepParams', (stepKey: string) => {
3947
3953
  return model.getStepParams(flowKey, stepKey) || {};
3948
3954
  });
@@ -51,7 +51,7 @@ export function FieldDeletePlaceholder() {
51
51
  const dataSourcePrefix = dataSource ? `${t(dataSource.displayName || dataSource.key)} > ` : '';
52
52
  const collectionPrefix = collection ? `${t(collection.title) || collection.name || collection.tableName} > ` : '';
53
53
  return `${dataSourcePrefix}${collectionPrefix}${name}`;
54
- }, []);
54
+ }, [collection, dataSource, name, t]);
55
55
  return (
56
56
  <Form.Item>
57
57
  <div
@@ -80,7 +80,7 @@ function FieldWithoutPermissionPlaceholder() {
80
80
  const dataSourcePrefix = `${t(dataSource.displayName || dataSource.key)} > `;
81
81
  const collectionPrefix = collection ? `${t(collection.title) || collection.name || collection.tableName} > ` : '';
82
82
  return `${dataSourcePrefix}${collectionPrefix}${name}`;
83
- }, []);
83
+ }, [collection, dataSource.displayName, dataSource.key, name, t]);
84
84
  const { actionName } = model.forbidden;
85
85
  const messageValue = useMemo(() => {
86
86
  return t(
@@ -90,7 +90,7 @@ function FieldWithoutPermissionPlaceholder() {
90
90
  actionName: t(_.capitalize(actionName)),
91
91
  },
92
92
  ).replaceAll('&gt;', '>');
93
- }, [nameValue, t]);
93
+ }, [actionName, nameValue, t]);
94
94
 
95
95
  return (
96
96
  <Tooltip title={messageValue}>
@@ -198,13 +198,16 @@ export class CollectionFieldModel<T extends DefaultStructure = DefaultStructure>
198
198
 
199
199
  static getDefaultBindingByField(
200
200
  ctx: FlowEngineContext,
201
- collectionField: CollectionField,
201
+ collectionField: CollectionField | null | undefined,
202
202
  options: {
203
203
  useStrict?: boolean;
204
204
  fallbackToTargetTitleField?: boolean;
205
205
  targetCollectionTitleField?: CollectionField;
206
206
  } = {},
207
207
  ): BindingOptions | null {
208
+ if (!collectionField) {
209
+ return null;
210
+ }
208
211
  if (options.fallbackToTargetTitleField) {
209
212
  const binding = this.getDefaultBindingByField(ctx, collectionField, { useStrict: true });
210
213
  if (!binding) {
@@ -108,6 +108,11 @@ describe('CollectionFieldModel', () => {
108
108
  expect(defaultBinding).toBeNull();
109
109
  });
110
110
 
111
+ it('should return null when the collection field has been deleted', () => {
112
+ const defaultBinding = TestModel.getDefaultBindingByField(mockContext, undefined);
113
+ expect(defaultBinding).toBeNull();
114
+ });
115
+
111
116
  it('should return null if no bindings exist for the interface', () => {
112
117
  class Test1Model extends CollectionFieldModel {}
113
118
  class Test2Model extends Test1Model {}