@nocobase/flow-engine 2.4.0-alpha.1 → 2.4.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/FlowDefinition.d.ts +1 -0
- package/lib/FlowDefinition.js +3 -0
- package/lib/data-source/jioToJoiSchema.js +9 -1
- package/lib/flow-registry/BaseFlowRegistry.d.ts +2 -0
- package/lib/flow-registry/InstanceFlowRegistry.d.ts +1 -1
- package/lib/flowContext.d.ts +1 -0
- package/lib/flowContext.js +6 -1
- package/lib/models/CollectionFieldModel.d.ts +1 -1
- package/lib/models/CollectionFieldModel.js +6 -3
- package/lib/views/createViewMeta.js +13 -7
- package/package.json +4 -4
- package/src/FlowDefinition.ts +4 -0
- package/src/__tests__/createViewMeta.popup.test.ts +83 -1
- package/src/__tests__/objectVariable.test.ts +37 -1
- package/src/data-source/__tests__/index.test.ts +28 -0
- package/src/data-source/__tests__/jioToJoiSchema.test.ts +51 -0
- package/src/data-source/jioToJoiSchema.ts +10 -1
- package/src/flow-registry/BaseFlowRegistry.ts +2 -0
- package/src/flow-registry/InstanceFlowRegistry.ts +1 -1
- package/src/flowContext.ts +7 -1
- package/src/models/CollectionFieldModel.tsx +7 -4
- package/src/models/__tests__/CollectionFieldModel.test.ts +5 -0
- package/src/views/createViewMeta.ts +17 -7
package/lib/FlowDefinition.d.ts
CHANGED
|
@@ -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);
|
package/lib/FlowDefinition.js
CHANGED
|
@@ -94,7 +94,15 @@ function jioToJoiSchema(jioConfig) {
|
|
|
94
94
|
if (name === "required") hasRequired = true;
|
|
95
95
|
if (typeof schema[name] === "function") {
|
|
96
96
|
try {
|
|
97
|
-
|
|
97
|
+
if (jioConfig.type === "number" && name === "precision") {
|
|
98
|
+
const precisionSchema = import_joi.default.number().precision(getArgs(name, args)[0]).strict();
|
|
99
|
+
schema = schema.custom((value, helpers) => {
|
|
100
|
+
const { error } = precisionSchema.validate(value);
|
|
101
|
+
return error ? helpers.error("number.precision", error.details[0].context) : value;
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
schema = schema[name](...getArgs(name, args));
|
|
105
|
+
}
|
|
98
106
|
} catch (err) {
|
|
99
107
|
console.warn(`\u8C03\u7528 Joi \u65B9\u6CD5 ${name} \u5931\u8D25:`, err);
|
|
100
108
|
}
|
|
@@ -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
|
-
|
|
14
|
+
readonly model: FlowModel;
|
|
15
15
|
static readonly _type: "instance";
|
|
16
16
|
constructor(model: FlowModel);
|
|
17
17
|
save(): Promise<void>;
|
package/lib/flowContext.d.ts
CHANGED
|
@@ -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>;
|
package/lib/flowContext.js
CHANGED
|
@@ -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(">", ">");
|
|
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) {
|
|
@@ -360,7 +360,9 @@ function createPopupMeta(ctx, anchorView) {
|
|
|
360
360
|
const view = getPopupView(ctx, anchorView);
|
|
361
361
|
const inputArgs = view == null ? void 0 : view.inputArgs;
|
|
362
362
|
const srcId = inputArgs == null ? void 0 : inputArgs.sourceId;
|
|
363
|
-
|
|
363
|
+
const hasSourceId = srcId != null && srcId !== "";
|
|
364
|
+
const pendingSourceAssoc = !hasSourceId && typeof (inputArgs == null ? void 0 : inputArgs.sourceAssociationName) === "string" ? inputArgs.sourceAssociationName : void 0;
|
|
365
|
+
let assoc = (inputArgs == null ? void 0 : inputArgs.associationName) || pendingSourceAssoc;
|
|
364
366
|
let dsKey = (inputArgs == null ? void 0 : inputArgs.dataSourceKey) || "main";
|
|
365
367
|
if (!assoc || typeof assoc !== "string" || !assoc.includes(".")) {
|
|
366
368
|
const stack = getViewStack(view);
|
|
@@ -376,7 +378,7 @@ function createPopupMeta(ctx, anchorView) {
|
|
|
376
378
|
dsKey = (p == null ? void 0 : p.dataSourceKey) || dsKey;
|
|
377
379
|
}
|
|
378
380
|
}
|
|
379
|
-
if (
|
|
381
|
+
if ((hasSourceId || pendingSourceAssoc) && assoc && typeof assoc === "string") {
|
|
380
382
|
const parentCollectionName = String(assoc).includes(".") ? String(assoc).split(".")[0] : void 0;
|
|
381
383
|
if (parentCollectionName) {
|
|
382
384
|
const parentCollectionAccessor = /* @__PURE__ */ __name(() => {
|
|
@@ -388,11 +390,15 @@ function createPopupMeta(ctx, anchorView) {
|
|
|
388
390
|
return null;
|
|
389
391
|
}
|
|
390
392
|
}, "parentCollectionAccessor");
|
|
391
|
-
const srcMeta = await (0, import_variablesParams.buildRecordMeta)(
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
393
|
+
const srcMeta = await (0, import_variablesParams.buildRecordMeta)(
|
|
394
|
+
parentCollectionAccessor,
|
|
395
|
+
t("Current popup parent record"),
|
|
396
|
+
() => hasSourceId ? {
|
|
397
|
+
collection: parentCollectionName,
|
|
398
|
+
dataSourceKey: dsKey,
|
|
399
|
+
filterByTk: srcId
|
|
400
|
+
} : void 0
|
|
401
|
+
);
|
|
396
402
|
if (srcMeta) {
|
|
397
403
|
props.sourceRecord = srcMeta;
|
|
398
404
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.4.0-alpha.
|
|
3
|
+
"version": "2.4.0-alpha.10",
|
|
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.4.0-alpha.
|
|
12
|
-
"@nocobase/shared": "2.4.0-alpha.
|
|
11
|
+
"@nocobase/sdk": "2.4.0-alpha.10",
|
|
12
|
+
"@nocobase/shared": "2.4.0-alpha.10",
|
|
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": "
|
|
40
|
+
"gitHead": "c869865c0413fbe4ef85b26be1f2b8d7078c38df"
|
|
41
41
|
}
|
package/src/FlowDefinition.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { describe, it, expect, vi } from 'vitest';
|
|
11
|
-
import { FlowContext } from '../flowContext';
|
|
11
|
+
import { FlowContext, type PropertyMeta } from '../flowContext';
|
|
12
12
|
import { FlowEngine } from '../flowEngine';
|
|
13
13
|
import type { FlowView } from '../views/FlowView';
|
|
14
14
|
import { buildPopupRuntime, createPopupMeta, registerPopupVariable } from '../views/createViewMeta';
|
|
@@ -397,4 +397,86 @@ describe('createPopupMeta - popup variables', () => {
|
|
|
397
397
|
|
|
398
398
|
expect(ctx.getPropertyOptions('popup')?.resolveOnServer?.('sourceRecord.department.title')).toBe(true);
|
|
399
399
|
});
|
|
400
|
+
|
|
401
|
+
describe('popup sourceRecord meta for quick create popups', () => {
|
|
402
|
+
function makeCtxWithUsers() {
|
|
403
|
+
const engine = new FlowEngine();
|
|
404
|
+
const ds = engine.context.dataSourceManager.getDataSource('main');
|
|
405
|
+
ds.addCollection({
|
|
406
|
+
name: 'orgs',
|
|
407
|
+
filterTargetKey: 'id',
|
|
408
|
+
fields: [{ name: 'id', type: 'integer', interface: 'number' }],
|
|
409
|
+
});
|
|
410
|
+
ds.addCollection({
|
|
411
|
+
name: 'users',
|
|
412
|
+
filterTargetKey: 'id',
|
|
413
|
+
fields: [
|
|
414
|
+
{ name: 'id', type: 'integer', interface: 'number' },
|
|
415
|
+
{ name: 'nickname', type: 'string', interface: 'input' },
|
|
416
|
+
{ name: 'orgs', type: 'hasMany', target: 'orgs', interface: 'o2m' },
|
|
417
|
+
],
|
|
418
|
+
});
|
|
419
|
+
return engine.context;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function makeQuickCreatePopupView(inputArgs: Record<string, unknown>): FlowView {
|
|
423
|
+
return {
|
|
424
|
+
type: 'drawer',
|
|
425
|
+
inputArgs: {
|
|
426
|
+
openerUids: ['approval-form-uid'],
|
|
427
|
+
scene: 'create',
|
|
428
|
+
dataSourceKey: 'main',
|
|
429
|
+
collectionName: 'orgs',
|
|
430
|
+
...inputArgs,
|
|
431
|
+
},
|
|
432
|
+
Header: null,
|
|
433
|
+
Footer: null,
|
|
434
|
+
close: () => void 0,
|
|
435
|
+
update: () => void 0,
|
|
436
|
+
} as unknown as FlowView;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function getPopupProps(ctx: FlowContext, view: FlowView) {
|
|
440
|
+
const meta = await createPopupMeta(ctx, view)();
|
|
441
|
+
const props = typeof meta?.properties === 'function' ? await meta.properties() : meta?.properties;
|
|
442
|
+
return { meta, props: props || {} };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
it('exposes the parent record meta from sourceAssociationName before the parent record has a primary key', async () => {
|
|
446
|
+
const ctx = makeCtxWithUsers();
|
|
447
|
+
const view = makeQuickCreatePopupView({ sourceAssociationName: 'users.orgs' });
|
|
448
|
+
|
|
449
|
+
const { meta, props } = await getPopupProps(ctx, view);
|
|
450
|
+
const sourceRecordMeta = props.sourceRecord as PropertyMeta;
|
|
451
|
+
expect(sourceRecordMeta?.title).toBe('Current popup parent record');
|
|
452
|
+
const fields =
|
|
453
|
+
typeof sourceRecordMeta.properties === 'function'
|
|
454
|
+
? await sourceRecordMeta.properties()
|
|
455
|
+
: sourceRecordMeta.properties;
|
|
456
|
+
expect(fields).toHaveProperty('nickname');
|
|
457
|
+
|
|
458
|
+
// 父记录尚无主键时仅用于配置变量,不应生成服务端取值参数
|
|
459
|
+
const vars = (await meta?.buildVariablesParams?.(ctx)) as { sourceRecord?: unknown } | undefined;
|
|
460
|
+
expect(vars?.sourceRecord).toBeUndefined();
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('does not expose the parent record meta without sourceId or sourceAssociationName', async () => {
|
|
464
|
+
const ctx = makeCtxWithUsers();
|
|
465
|
+
const view = makeQuickCreatePopupView({});
|
|
466
|
+
|
|
467
|
+
const { props } = await getPopupProps(ctx, view);
|
|
468
|
+
expect(props.sourceRecord).toBeUndefined();
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('keeps resolving the parent record by sourceId once the parent record is persisted', async () => {
|
|
472
|
+
const ctx = makeCtxWithUsers();
|
|
473
|
+
const view = makeQuickCreatePopupView({ associationName: 'users.orgs', sourceId: 7 });
|
|
474
|
+
|
|
475
|
+
const { meta, props } = await getPopupProps(ctx, view);
|
|
476
|
+
expect((props.sourceRecord as PropertyMeta)?.title).toBe('Current popup parent record');
|
|
477
|
+
|
|
478
|
+
const vars = (await meta?.buildVariablesParams?.(ctx)) as { sourceRecord?: unknown } | undefined;
|
|
479
|
+
expect(vars?.sourceRecord).toEqual({ collection: 'users', dataSourceKey: 'main', filterByTk: 7 });
|
|
480
|
+
});
|
|
481
|
+
});
|
|
400
482
|
});
|
|
@@ -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
|
});
|
|
@@ -114,6 +114,34 @@ describe('DataSource & Collection APIs', () => {
|
|
|
114
114
|
await expect(rules[0].validator({}, '123')).rejects.toBe('单行文本 长度必须为 18 个字符');
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
it('enforces collection field precision in component validation rules', async () => {
|
|
118
|
+
const { m, engine } = makeManager();
|
|
119
|
+
const translate = vi.spyOn(engine, 'translate').mockImplementation((key) => key);
|
|
120
|
+
const ds = new DataSource({ key: 'main' });
|
|
121
|
+
m.addDataSource(ds);
|
|
122
|
+
ds.addCollection({
|
|
123
|
+
name: 'employees',
|
|
124
|
+
fields: [
|
|
125
|
+
{
|
|
126
|
+
name: 'years',
|
|
127
|
+
type: 'double',
|
|
128
|
+
interface: 'number',
|
|
129
|
+
title: 'Years',
|
|
130
|
+
validation: { type: 'number', rules: [{ name: 'precision', args: { limit: 2 } }] },
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
const rules = ds.getCollection('employees').getField('years').getComponentProps().rules;
|
|
135
|
+
|
|
136
|
+
await expect(rules[0].validator({}, 39.2234)).rejects.toContain('2 decimal places');
|
|
137
|
+
expect(translate).toHaveBeenCalledWith(
|
|
138
|
+
'number.precision',
|
|
139
|
+
expect.objectContaining({ ns: 'data-source-main', label: 'Years', limit: 2 }),
|
|
140
|
+
);
|
|
141
|
+
await expect(rules[0].validator({}, 39.22)).resolves.toBeUndefined();
|
|
142
|
+
translate.mockRestore();
|
|
143
|
+
});
|
|
144
|
+
|
|
117
145
|
it('ensureLoaded, reload and data source events work for main loader', async () => {
|
|
118
146
|
const { m, engine } = makeManager();
|
|
119
147
|
const loadedListener = vi.fn();
|
|
@@ -49,6 +49,57 @@ describe('jioToJoiSchema', () => {
|
|
|
49
49
|
expect(s2.validate('zz').error).toBeTruthy();
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
+
describe('number precision', () => {
|
|
53
|
+
const schema = jioToJoiSchema({ type: 'number', rules: [{ name: 'precision', args: { limit: 2 } }] });
|
|
54
|
+
|
|
55
|
+
it.each([39.2234, -39.2234, 0.001, 1e-7])('rejects %s without rounding', (value) => {
|
|
56
|
+
const result = schema.validate(value);
|
|
57
|
+
expect(result.error?.details[0]).toMatchObject({ type: 'number.precision', context: { limit: 2 } });
|
|
58
|
+
expect(result.value).toBe(value);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it.each([39.22, -39.22, 39.2, 39, 0, 0.01, '', null, undefined])('accepts %s', (value) => {
|
|
62
|
+
expect(schema.validate(value).error).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('supports zero precision', () => {
|
|
66
|
+
const integerSchema = jioToJoiSchema({ type: 'number', rules: [{ name: 'precision', args: { limit: 0 } }] });
|
|
67
|
+
expect(integerSchema.validate(39).error).toBeUndefined();
|
|
68
|
+
expect(integerSchema.validate(39.2).error?.details[0].type).toBe('number.precision');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('validates numeric strings without rounding them', () => {
|
|
72
|
+
expect(schema.validate('39.22')).toEqual({ value: 39.22 });
|
|
73
|
+
expect(schema.validate('39.2200')).toEqual({ value: 39.22 });
|
|
74
|
+
expect(schema.validate('39.2234').error?.details[0].type).toBe('number.precision');
|
|
75
|
+
expect(schema.validate('1e-7').error?.details[0].type).toBe('number.precision');
|
|
76
|
+
expect(schema.validate('not a number').error?.details[0].type).toBe('number.base');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('preserves required and min/max validation', () => {
|
|
80
|
+
const requiredSchema = jioToJoiSchema({
|
|
81
|
+
type: 'number',
|
|
82
|
+
rules: [
|
|
83
|
+
{ name: 'precision', args: { limit: 2 } },
|
|
84
|
+
{ name: 'min', args: { limit: 1 } },
|
|
85
|
+
{ name: 'max', args: { limit: 10 } },
|
|
86
|
+
{ name: 'required' },
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
expect(requiredSchema.validate(undefined).error?.details[0].type).toBe('any.required');
|
|
90
|
+
expect(requiredSchema.validate('').error).toBeDefined();
|
|
91
|
+
expect(requiredSchema.validate(null).error).toBeDefined();
|
|
92
|
+
expect(requiredSchema.validate(0.99).error?.details[0].type).toBe('number.min');
|
|
93
|
+
expect(requiredSchema.validate(10.01).error?.details[0].type).toBe('number.max');
|
|
94
|
+
expect(requiredSchema.validate(1.23).error).toBeUndefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('preserves numeric string conversion when precision is not configured', () => {
|
|
98
|
+
const numberSchema = jioToJoiSchema({ type: 'number', rules: [{ name: 'min', args: { limit: 1 } }] });
|
|
99
|
+
expect(numberSchema.validate('39.2234')).toEqual({ value: 39.2234 });
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
52
103
|
it('optional when no required and allows empty string', () => {
|
|
53
104
|
const schema = jioToJoiSchema({ type: 'string' });
|
|
54
105
|
expect(schema.validate('').error).toBeUndefined();
|
|
@@ -85,7 +85,16 @@ export function jioToJoiSchema<T extends JioType>(jioConfig: {
|
|
|
85
85
|
if (name === 'required') hasRequired = true;
|
|
86
86
|
if (typeof schema[name] === 'function') {
|
|
87
87
|
try {
|
|
88
|
-
|
|
88
|
+
if (jioConfig.type === 'number' && name === 'precision') {
|
|
89
|
+
// Validate precision without rounding, while preserving the outer schema's numeric string conversion.
|
|
90
|
+
const precisionSchema = Joi.number().precision(getArgs(name, args)[0]).strict();
|
|
91
|
+
schema = schema.custom((value: number, helpers: Joi.CustomHelpers) => {
|
|
92
|
+
const { error } = precisionSchema.validate(value);
|
|
93
|
+
return error ? helpers.error('number.precision', error.details[0].context) : value;
|
|
94
|
+
});
|
|
95
|
+
} else {
|
|
96
|
+
schema = schema[name](...getArgs(name, args));
|
|
97
|
+
}
|
|
89
98
|
} catch (err) {
|
|
90
99
|
console.warn(`调用 Joi 方法 ${name} 失败:`, err);
|
|
91
100
|
}
|
|
@@ -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;
|
package/src/flowContext.ts
CHANGED
|
@@ -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('>', '>');
|
|
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 {}
|
|
@@ -390,7 +390,13 @@ export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): Proper
|
|
|
390
390
|
const view = getPopupView(ctx, anchorView);
|
|
391
391
|
const inputArgs = view?.inputArgs;
|
|
392
392
|
const srcId = inputArgs?.sourceId;
|
|
393
|
-
|
|
393
|
+
const hasSourceId = srcId != null && srcId !== '';
|
|
394
|
+
// 上级记录暂无主键时(如审批配置态表单里的快速创建弹窗),只会传 sourceAssociationName:仍展示变量结构,取值依赖运行时的 sourceId
|
|
395
|
+
const pendingSourceAssoc =
|
|
396
|
+
!hasSourceId && typeof inputArgs?.sourceAssociationName === 'string'
|
|
397
|
+
? inputArgs.sourceAssociationName
|
|
398
|
+
: undefined;
|
|
399
|
+
let assoc: string | undefined = inputArgs?.associationName || pendingSourceAssoc;
|
|
394
400
|
let dsKey: string = inputArgs?.dataSourceKey || 'main';
|
|
395
401
|
|
|
396
402
|
// 兜底:若 associationName 缺失或不含“.”,尝试从当前视图模型的 openView 参数推断
|
|
@@ -409,7 +415,7 @@ export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): Proper
|
|
|
409
415
|
}
|
|
410
416
|
}
|
|
411
417
|
|
|
412
|
-
if (
|
|
418
|
+
if ((hasSourceId || pendingSourceAssoc) && assoc && typeof assoc === 'string') {
|
|
413
419
|
const parentCollectionName = String(assoc).includes('.') ? String(assoc).split('.')[0] : undefined;
|
|
414
420
|
if (parentCollectionName) {
|
|
415
421
|
const parentCollectionAccessor = () => {
|
|
@@ -420,11 +426,15 @@ export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): Proper
|
|
|
420
426
|
return null;
|
|
421
427
|
}
|
|
422
428
|
};
|
|
423
|
-
const srcMeta = await buildRecordMeta(parentCollectionAccessor, t('Current popup parent record'), () =>
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
429
|
+
const srcMeta = await buildRecordMeta(parentCollectionAccessor, t('Current popup parent record'), () =>
|
|
430
|
+
hasSourceId
|
|
431
|
+
? {
|
|
432
|
+
collection: parentCollectionName,
|
|
433
|
+
dataSourceKey: dsKey,
|
|
434
|
+
filterByTk: srcId,
|
|
435
|
+
}
|
|
436
|
+
: undefined,
|
|
437
|
+
);
|
|
428
438
|
if (srcMeta) {
|
|
429
439
|
props.sourceRecord = srcMeta;
|
|
430
440
|
}
|