@nocobase/flow-engine 2.0.0-alpha.50 → 2.0.0-alpha.51
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/components/FlowModelRenderer.d.ts +1 -0
- package/lib/components/FlowModelRenderer.js +13 -2
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +2 -14
- package/lib/data-source/jioToJoiSchema.js +1 -0
- package/lib/flowEngine.d.ts +6 -0
- package/lib/flowEngine.js +33 -0
- package/lib/hooks/useApplyAutoFlows.d.ts +1 -0
- package/lib/hooks/useApplyAutoFlows.js +2 -2
- package/lib/utils/associationObjectVariable.js +1 -1
- package/lib/utils/createCollectionContextMeta.d.ts +1 -1
- package/lib/utils/createCollectionContextMeta.js +7 -5
- package/lib/utils/variablesParams.js +1 -1
- package/package.json +4 -4
- package/src/__tests__/flowEngine.removeModel.test.ts +72 -0
- package/src/acl/__tests__/Acl.test.tsx +4 -5
- package/src/components/FlowModelRenderer.tsx +16 -2
- package/src/components/__tests__/FlowModelRenderer.test.tsx +89 -0
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +0 -13
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +0 -55
- package/src/data-source/jioToJoiSchema.ts +1 -0
- package/src/flowEngine.ts +38 -0
- package/src/hooks/useApplyAutoFlows.ts +3 -3
- package/src/utils/__tests__/createCollectionContextMeta.test.ts +51 -0
- package/src/utils/associationObjectVariable.ts +1 -1
- package/src/utils/createCollectionContextMeta.ts +7 -4
- package/src/utils/variablesParams.ts +1 -1
|
@@ -63,7 +63,10 @@ const FlowModelRendererWithAutoFlows = (0, import_reactive_react.observer)(
|
|
|
63
63
|
extraToolbarItems,
|
|
64
64
|
fallback
|
|
65
65
|
}) => {
|
|
66
|
-
const { loading: pending, error: autoFlowsError } = (0, import_hooks.useApplyAutoFlows)(model, inputArgs, {
|
|
66
|
+
const { loading: pending, error: autoFlowsError } = (0, import_hooks.useApplyAutoFlows)(model, inputArgs, {
|
|
67
|
+
throwOnError: false,
|
|
68
|
+
useCache: model.context.useCache
|
|
69
|
+
});
|
|
67
70
|
(0, import_utils.setAutoFlowError)(model, autoFlowsError || null);
|
|
68
71
|
if (pending) {
|
|
69
72
|
return /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, fallback);
|
|
@@ -189,8 +192,16 @@ const FlowModelRenderer = (0, import_reactive_react.observer)(
|
|
|
189
192
|
inputArgs,
|
|
190
193
|
showErrorFallback = true,
|
|
191
194
|
settingsMenuLevel,
|
|
192
|
-
extraToolbarItems
|
|
195
|
+
extraToolbarItems,
|
|
196
|
+
useCache
|
|
193
197
|
}) => {
|
|
198
|
+
(0, import_react.useEffect)(() => {
|
|
199
|
+
if (model == null ? void 0 : model.context) {
|
|
200
|
+
model.context.defineProperty("useCache", {
|
|
201
|
+
value: typeof useCache === "boolean" ? useCache : model.context.useCache
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}, [model == null ? void 0 : model.context, useCache]);
|
|
194
205
|
if (!model || typeof model.render !== "function") {
|
|
195
206
|
console.warn("FlowModelRenderer: Invalid model or render method not found.", model);
|
|
196
207
|
return null;
|
|
@@ -227,27 +227,15 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
|
|
|
227
227
|
);
|
|
228
228
|
const getModelConfigurableFlowsAndSteps = (0, import_react.useCallback)(
|
|
229
229
|
async (targetModel, modelKey) => {
|
|
230
|
-
var _a;
|
|
231
230
|
try {
|
|
232
231
|
const flowsMap = new Map(targetModel.constructor.globalFlowRegistry.getFlows());
|
|
233
|
-
const originUse = targetModel == null ? void 0 : targetModel.use;
|
|
234
|
-
if (typeof originUse === "string" && originUse !== targetModel.constructor.name) {
|
|
235
|
-
const originCls = (_a = targetModel.flowEngine) == null ? void 0 : _a.getModelClass(originUse);
|
|
236
|
-
if (originCls == null ? void 0 : originCls.globalFlowRegistry) {
|
|
237
|
-
for (const [k, v] of originCls.globalFlowRegistry.getFlows()) {
|
|
238
|
-
if (!flowsMap.has(k)) {
|
|
239
|
-
flowsMap.set(k, v);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
232
|
const flows = flowsMap;
|
|
245
233
|
const flowsArray = Array.from(flows.values());
|
|
246
234
|
const flowsWithSteps = await Promise.all(
|
|
247
235
|
flowsArray.map(async (flow) => {
|
|
248
236
|
const configurableSteps = await Promise.all(
|
|
249
237
|
Object.entries(flow.steps).map(async ([stepKey, stepDefinition]) => {
|
|
250
|
-
var
|
|
238
|
+
var _a;
|
|
251
239
|
const actionStep = stepDefinition;
|
|
252
240
|
if (actionStep.hideInSettings) {
|
|
253
241
|
return null;
|
|
@@ -257,7 +245,7 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
|
|
|
257
245
|
let stepTitle = actionStep.title;
|
|
258
246
|
if (actionStep.use) {
|
|
259
247
|
try {
|
|
260
|
-
const action = (
|
|
248
|
+
const action = (_a = targetModel.getAction) == null ? void 0 : _a.call(targetModel, actionStep.use);
|
|
261
249
|
hasActionUiSchema = action && action.uiSchema != null;
|
|
262
250
|
stepTitle = stepTitle || (action == null ? void 0 : action.title);
|
|
263
251
|
} catch (error) {
|
package/lib/flowEngine.d.ts
CHANGED
|
@@ -263,6 +263,12 @@ export declare class FlowEngine {
|
|
|
263
263
|
* @returns {boolean} Returns true if successfully destroyed, false otherwise (e.g. instance does not exist)
|
|
264
264
|
*/
|
|
265
265
|
removeModel(uid: string): boolean;
|
|
266
|
+
/**
|
|
267
|
+
* Remove a local model instance and all its sub-models recursively.
|
|
268
|
+
* @param {string} uid UID of the model instance to destroy
|
|
269
|
+
* @returns {boolean} Returns true if successfully destroyed, false otherwise
|
|
270
|
+
*/
|
|
271
|
+
removeModelWithSubModels(uid: string): boolean;
|
|
266
272
|
/**
|
|
267
273
|
* Check if the model repository is set.
|
|
268
274
|
* @returns {boolean} Returns true if set, false otherwise.
|
package/lib/flowEngine.js
CHANGED
|
@@ -586,6 +586,39 @@ const _FlowEngine = class _FlowEngine {
|
|
|
586
586
|
});
|
|
587
587
|
return true;
|
|
588
588
|
}
|
|
589
|
+
/**
|
|
590
|
+
* Remove a local model instance and all its sub-models recursively.
|
|
591
|
+
* @param {string} uid UID of the model instance to destroy
|
|
592
|
+
* @returns {boolean} Returns true if successfully destroyed, false otherwise
|
|
593
|
+
*/
|
|
594
|
+
removeModelWithSubModels(uid) {
|
|
595
|
+
const model = this.getModel(uid);
|
|
596
|
+
if (!model) {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
const collectDescendants = /* @__PURE__ */ __name((m, acc) => {
|
|
600
|
+
if (m.subModels) {
|
|
601
|
+
for (const key in m.subModels) {
|
|
602
|
+
const sub = m.subModels[key];
|
|
603
|
+
if (Array.isArray(sub)) {
|
|
604
|
+
[...sub].forEach((s) => collectDescendants(s, acc));
|
|
605
|
+
} else if (sub) {
|
|
606
|
+
collectDescendants(sub, acc);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
acc.push(m);
|
|
611
|
+
}, "collectDescendants");
|
|
612
|
+
const allModels = [];
|
|
613
|
+
collectDescendants(model, allModels);
|
|
614
|
+
let success = true;
|
|
615
|
+
for (const m of allModels) {
|
|
616
|
+
if (!this.removeModel(m.uid)) {
|
|
617
|
+
success = false;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return success;
|
|
621
|
+
}
|
|
589
622
|
/**
|
|
590
623
|
* Check if the model repository is set.
|
|
591
624
|
* @returns {boolean} Returns true if set, false otherwise.
|
|
@@ -15,6 +15,7 @@ import { FlowModel } from '../models';
|
|
|
15
15
|
*/
|
|
16
16
|
export declare function useApplyAutoFlows(modelOrUid: FlowModel | string, inputArgs?: Record<string, any>, options?: {
|
|
17
17
|
throwOnError?: boolean;
|
|
18
|
+
useCache?: boolean;
|
|
18
19
|
}): {
|
|
19
20
|
readonly loading: boolean;
|
|
20
21
|
readonly error: Error;
|
|
@@ -45,10 +45,10 @@ function useApplyAutoFlows(modelOrUid, inputArgs, options) {
|
|
|
45
45
|
const { loading, error } = (0, import_ahooks.useRequest)(
|
|
46
46
|
async () => {
|
|
47
47
|
if (!model) return;
|
|
48
|
-
await model.dispatchEvent("beforeRender", inputArgs);
|
|
48
|
+
await model.dispatchEvent("beforeRender", inputArgs, { useCache: options == null ? void 0 : options.useCache });
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
|
-
refreshDeps: [model, inputArgs]
|
|
51
|
+
refreshDeps: [model, inputArgs, options == null ? void 0 : options.useCache]
|
|
52
52
|
}
|
|
53
53
|
);
|
|
54
54
|
if ((options == null ? void 0 : options.throwOnError) !== false && error) {
|
|
@@ -98,7 +98,7 @@ function createAssociationSubpathResolver(collectionAccessor, valueAccessor) {
|
|
|
98
98
|
}
|
|
99
99
|
__name(createAssociationSubpathResolver, "createAssociationSubpathResolver");
|
|
100
100
|
function createAssociationAwareObjectMetaFactory(collectionAccessor, title, valueAccessor) {
|
|
101
|
-
const baseFactory = (0, import_createCollectionContextMeta.createCollectionContextMeta)(collectionAccessor, title);
|
|
101
|
+
const baseFactory = (0, import_createCollectionContextMeta.createCollectionContextMeta)(collectionAccessor, title, true);
|
|
102
102
|
const factory = /* @__PURE__ */ __name(async () => {
|
|
103
103
|
const base = await baseFactory();
|
|
104
104
|
if (!base) return null;
|
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { Collection } from '../data-source';
|
|
10
10
|
import type { PropertyMetaFactory } from '../flowContext';
|
|
11
|
-
export declare function createCollectionContextMeta(collectionOrFactory: Collection | (() => Collection | null), title?: string): PropertyMetaFactory;
|
|
11
|
+
export declare function createCollectionContextMeta(collectionOrFactory: Collection | (() => Collection | null), title?: string, includeNonFilterable?: boolean): PropertyMetaFactory;
|
|
@@ -32,7 +32,7 @@ __export(createCollectionContextMeta_exports, {
|
|
|
32
32
|
module.exports = __toCommonJS(createCollectionContextMeta_exports);
|
|
33
33
|
const RELATION_FIELD_TYPES = ["belongsTo", "hasOne", "hasMany", "belongsToMany", "belongsToArray"];
|
|
34
34
|
const NUMERIC_FIELD_TYPES = ["integer", "float", "double", "decimal"];
|
|
35
|
-
function createFieldMetadata(field) {
|
|
35
|
+
function createFieldMetadata(field, includeNonFilterable) {
|
|
36
36
|
const baseProperties = createMetaBaseProperties(field);
|
|
37
37
|
if (field.isAssociationField()) {
|
|
38
38
|
const targetCollection = field.targetCollection;
|
|
@@ -49,7 +49,9 @@ function createFieldMetadata(field) {
|
|
|
49
49
|
properties: /* @__PURE__ */ __name(async () => {
|
|
50
50
|
const subProperties = {};
|
|
51
51
|
targetCollection.fields.forEach((subField) => {
|
|
52
|
-
|
|
52
|
+
if (includeNonFilterable || subField.filterable) {
|
|
53
|
+
subProperties[subField.name] = createFieldMetadata(subField, includeNonFilterable);
|
|
54
|
+
}
|
|
53
55
|
});
|
|
54
56
|
return subProperties;
|
|
55
57
|
}, "properties")
|
|
@@ -89,7 +91,7 @@ function createMetaBaseProperties(field) {
|
|
|
89
91
|
};
|
|
90
92
|
}
|
|
91
93
|
__name(createMetaBaseProperties, "createMetaBaseProperties");
|
|
92
|
-
function createCollectionContextMeta(collectionOrFactory, title) {
|
|
94
|
+
function createCollectionContextMeta(collectionOrFactory, title, includeNonFilterable) {
|
|
93
95
|
const metaFn = /* @__PURE__ */ __name(async () => {
|
|
94
96
|
const collection = typeof collectionOrFactory === "function" ? collectionOrFactory() : collectionOrFactory;
|
|
95
97
|
if (!collection) {
|
|
@@ -101,8 +103,8 @@ function createCollectionContextMeta(collectionOrFactory, title) {
|
|
|
101
103
|
properties: /* @__PURE__ */ __name(async () => {
|
|
102
104
|
const properties = {};
|
|
103
105
|
collection.fields.forEach((field) => {
|
|
104
|
-
if (field.filterable) {
|
|
105
|
-
properties[field.name] = createFieldMetadata(field);
|
|
106
|
+
if (includeNonFilterable || field.filterable) {
|
|
107
|
+
properties[field.name] = createFieldMetadata(field, includeNonFilterable);
|
|
106
108
|
}
|
|
107
109
|
});
|
|
108
110
|
return properties;
|
|
@@ -164,7 +164,7 @@ function inferParentRecordRef(ctx) {
|
|
|
164
164
|
}
|
|
165
165
|
__name(inferParentRecordRef, "inferParentRecordRef");
|
|
166
166
|
async function buildRecordMeta(collectionAccessor, title, paramsBuilder) {
|
|
167
|
-
const base = await (0, import_createCollectionContextMeta.createCollectionContextMeta)(collectionAccessor, title)();
|
|
167
|
+
const base = await (0, import_createCollectionContextMeta.createCollectionContextMeta)(collectionAccessor, title, true)();
|
|
168
168
|
if (!base) return null;
|
|
169
169
|
return {
|
|
170
170
|
...base,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.51",
|
|
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.0.0-alpha.
|
|
12
|
-
"@nocobase/shared": "2.0.0-alpha.
|
|
11
|
+
"@nocobase/sdk": "2.0.0-alpha.51",
|
|
12
|
+
"@nocobase/shared": "2.0.0-alpha.51",
|
|
13
13
|
"ahooks": "^3.7.2",
|
|
14
14
|
"dayjs": "^1.11.9",
|
|
15
15
|
"dompurify": "^3.0.2",
|
|
@@ -36,5 +36,5 @@
|
|
|
36
36
|
],
|
|
37
37
|
"author": "NocoBase Team",
|
|
38
38
|
"license": "AGPL-3.0",
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "a1e34dd97f370d54f3d80a6b83ab7ddb9c72dc18"
|
|
40
40
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
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 { beforeEach, describe, expect, it } from 'vitest';
|
|
11
|
+
import { FlowEngine } from '../flowEngine';
|
|
12
|
+
import { FlowModel } from '../models';
|
|
13
|
+
|
|
14
|
+
describe('FlowEngine removeModel', () => {
|
|
15
|
+
let engine: FlowEngine;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
engine = new FlowEngine();
|
|
19
|
+
engine.registerModels({ FlowModel });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('removeModel should remove model but keep sub-models in cache (current behavior)', () => {
|
|
23
|
+
const parent = engine.createModel({ uid: 'parent', use: 'FlowModel' });
|
|
24
|
+
const child = engine.createModel({
|
|
25
|
+
uid: 'child',
|
|
26
|
+
use: 'FlowModel',
|
|
27
|
+
parentId: 'parent',
|
|
28
|
+
subKey: 'child',
|
|
29
|
+
subType: 'object',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(engine.getModel('parent')).toBe(parent);
|
|
33
|
+
expect(engine.getModel('child')).toBe(child);
|
|
34
|
+
|
|
35
|
+
engine.removeModel('parent');
|
|
36
|
+
|
|
37
|
+
expect(engine.getModel('parent')).toBeUndefined();
|
|
38
|
+
// Current behavior: child is still in cache
|
|
39
|
+
expect(engine.getModel('child')).toBeDefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('removeModelWithSubModels should remove model and all sub-models from cache', () => {
|
|
43
|
+
const parent = engine.createModel({ uid: 'parent', use: 'FlowModel' });
|
|
44
|
+
const child = engine.createModel({
|
|
45
|
+
uid: 'child',
|
|
46
|
+
use: 'FlowModel',
|
|
47
|
+
parentId: 'parent',
|
|
48
|
+
subKey: 'child',
|
|
49
|
+
subType: 'object',
|
|
50
|
+
});
|
|
51
|
+
parent.setSubModel('child', child);
|
|
52
|
+
|
|
53
|
+
const grandChild = engine.createModel({
|
|
54
|
+
uid: 'grandChild',
|
|
55
|
+
use: 'FlowModel',
|
|
56
|
+
parentId: 'child',
|
|
57
|
+
subKey: 'grandChild',
|
|
58
|
+
subType: 'object',
|
|
59
|
+
});
|
|
60
|
+
child.setSubModel('grandChild', grandChild);
|
|
61
|
+
|
|
62
|
+
expect(engine.getModel('parent')).toBe(parent);
|
|
63
|
+
expect(engine.getModel('child')).toBe(child);
|
|
64
|
+
expect(engine.getModel('grandChild')).toBe(grandChild);
|
|
65
|
+
|
|
66
|
+
engine.removeModelWithSubModels('parent');
|
|
67
|
+
|
|
68
|
+
expect(engine.getModel('parent')).toBeUndefined();
|
|
69
|
+
expect(engine.getModel('child')).toBeUndefined();
|
|
70
|
+
expect(engine.getModel('grandChild')).toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -35,7 +35,7 @@ describe('ACL', () => {
|
|
|
35
35
|
};
|
|
36
36
|
const engine = makeEngine(payload);
|
|
37
37
|
const acl = new ACL(engine);
|
|
38
|
-
|
|
38
|
+
acl.setData(payload.data);
|
|
39
39
|
expect(acl.getActionAlias('remove')).toBe('destroy');
|
|
40
40
|
expect(acl.inResources('posts')).toBe(true);
|
|
41
41
|
expect(acl.getResourceActionParams('posts', 'remove')).toEqual({ whitelist: ['title'] });
|
|
@@ -54,6 +54,7 @@ describe('ACL', () => {
|
|
|
54
54
|
};
|
|
55
55
|
const engine = makeEngine(payload);
|
|
56
56
|
const acl = new ACL(engine);
|
|
57
|
+
acl.setData(payload.data);
|
|
57
58
|
const ok = await acl.aclCheck({
|
|
58
59
|
dataSourceKey: 'main',
|
|
59
60
|
resourceName: 'posts',
|
|
@@ -101,15 +102,13 @@ describe('ACL', () => {
|
|
|
101
102
|
engine.context.defineProperty('api', { value: api });
|
|
102
103
|
|
|
103
104
|
const acl = new ACL(engine);
|
|
104
|
-
|
|
105
|
+
acl.setData(payload1.data);
|
|
105
106
|
expect(acl.getActionAlias('remove')).toBe('destroy');
|
|
106
107
|
|
|
107
108
|
// 切换 token,应触发下次校验时的 ACL 重载
|
|
108
109
|
api.auth.token = 't2';
|
|
110
|
+
acl.setData(payload2.data);
|
|
109
111
|
await acl.aclCheck({ dataSourceKey: 'main', resourceName: 'posts', actionName: 'remove' });
|
|
110
112
|
expect(acl.getActionAlias('remove')).toBe('erase');
|
|
111
|
-
|
|
112
|
-
// 确认 roles:check 请求至少调用两次(初次 + 重载)
|
|
113
|
-
expect(api.request).toHaveBeenCalledTimes(2);
|
|
114
113
|
});
|
|
115
114
|
});
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
import { observer } from '@formily/reactive-react';
|
|
46
46
|
import _ from 'lodash';
|
|
47
|
-
import React from 'react';
|
|
47
|
+
import React, { useEffect } from 'react';
|
|
48
48
|
import { ErrorBoundary } from 'react-error-boundary';
|
|
49
49
|
import { FlowModelProvider, useApplyAutoFlows } from '../hooks';
|
|
50
50
|
import { getAutoFlowError, setAutoFlowError } from '../utils';
|
|
@@ -97,6 +97,8 @@ export interface FlowModelRendererProps {
|
|
|
97
97
|
|
|
98
98
|
/** 额外的工具栏项目,仅应用于此实例 */
|
|
99
99
|
extraToolbarItems?: ToolbarItemConfig[];
|
|
100
|
+
|
|
101
|
+
useCache?: boolean;
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
/**
|
|
@@ -139,7 +141,10 @@ const FlowModelRendererWithAutoFlows: React.FC<{
|
|
|
139
141
|
}) => {
|
|
140
142
|
// hidden 占位由模型自身处理;无需在此注入
|
|
141
143
|
|
|
142
|
-
const { loading: pending, error: autoFlowsError } = useApplyAutoFlows(model, inputArgs, {
|
|
144
|
+
const { loading: pending, error: autoFlowsError } = useApplyAutoFlows(model, inputArgs, {
|
|
145
|
+
throwOnError: false,
|
|
146
|
+
useCache: model.context.useCache,
|
|
147
|
+
});
|
|
143
148
|
// 将错误下沉到 model 实例上,供内容层读取(类型安全的 WeakMap 存储)
|
|
144
149
|
setAutoFlowError(model, autoFlowsError || null);
|
|
145
150
|
|
|
@@ -339,7 +344,16 @@ export const FlowModelRenderer: React.FC<FlowModelRendererProps> = observer(
|
|
|
339
344
|
showErrorFallback = true,
|
|
340
345
|
settingsMenuLevel,
|
|
341
346
|
extraToolbarItems,
|
|
347
|
+
useCache,
|
|
342
348
|
}) => {
|
|
349
|
+
useEffect(() => {
|
|
350
|
+
if (model?.context) {
|
|
351
|
+
model.context.defineProperty('useCache', {
|
|
352
|
+
value: typeof useCache === 'boolean' ? useCache : model.context.useCache,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}, [model?.context, useCache]);
|
|
356
|
+
|
|
343
357
|
if (!model || typeof model.render !== 'function') {
|
|
344
358
|
// 可以选择渲染 null 或者一个错误/提示信息
|
|
345
359
|
console.warn('FlowModelRenderer: Invalid model or render method not found.', model);
|
|
@@ -0,0 +1,89 @@
|
|
|
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 { render, waitFor } from '@testing-library/react';
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { vi } from 'vitest';
|
|
13
|
+
import { FlowEngine } from '../../flowEngine';
|
|
14
|
+
import { FlowModel } from '../../models/flowModel';
|
|
15
|
+
import { FlowEngineProvider } from '../../provider';
|
|
16
|
+
import { FlowModelRenderer } from '../FlowModelRenderer';
|
|
17
|
+
|
|
18
|
+
describe('FlowModelRenderer', () => {
|
|
19
|
+
let flowEngine: FlowEngine;
|
|
20
|
+
let model: FlowModel;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
flowEngine = new FlowEngine();
|
|
24
|
+
model = new FlowModel({
|
|
25
|
+
uid: 'test-model',
|
|
26
|
+
flowEngine,
|
|
27
|
+
});
|
|
28
|
+
// Mock dispatchEvent to track calls
|
|
29
|
+
model.dispatchEvent = vi.fn().mockResolvedValue([]);
|
|
30
|
+
// Mock render to return something
|
|
31
|
+
model.render = vi.fn().mockReturnValue(<div>Model Content</div>);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const renderWithProvider = (ui: React.ReactNode) => {
|
|
35
|
+
return render(<FlowEngineProvider engine={flowEngine}>{ui}</FlowEngineProvider>);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
test('should pass useCache to useApplyAutoFlows and set it on context', async () => {
|
|
39
|
+
const { unmount } = renderWithProvider(<FlowModelRenderer model={model} useCache={true} />);
|
|
40
|
+
|
|
41
|
+
// Check if dispatchEvent was called with useCache: true
|
|
42
|
+
// useApplyAutoFlows calls dispatchEvent('beforeRender', inputArgs, { useCache })
|
|
43
|
+
await waitFor(() => {
|
|
44
|
+
expect(model.dispatchEvent).toHaveBeenCalledWith(
|
|
45
|
+
'beforeRender',
|
|
46
|
+
undefined,
|
|
47
|
+
expect.objectContaining({ useCache: true }),
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Check if useCache is set on context
|
|
52
|
+
expect(model.context.useCache).toBe(true);
|
|
53
|
+
|
|
54
|
+
unmount();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('should pass useCache=false to useApplyAutoFlows and set it on context', async () => {
|
|
58
|
+
const { unmount } = renderWithProvider(<FlowModelRenderer model={model} useCache={false} />);
|
|
59
|
+
|
|
60
|
+
await waitFor(() => {
|
|
61
|
+
expect(model.dispatchEvent).toHaveBeenCalledWith(
|
|
62
|
+
'beforeRender',
|
|
63
|
+
undefined,
|
|
64
|
+
expect.objectContaining({ useCache: false }),
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(model.context.useCache).toBe(false);
|
|
69
|
+
|
|
70
|
+
unmount();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('should not pass useCache if not provided', async () => {
|
|
74
|
+
const { unmount } = renderWithProvider(<FlowModelRenderer model={model} />);
|
|
75
|
+
|
|
76
|
+
await waitFor(() => {
|
|
77
|
+
expect(model.dispatchEvent).toHaveBeenCalledWith(
|
|
78
|
+
'beforeRender',
|
|
79
|
+
undefined,
|
|
80
|
+
expect.objectContaining({ useCache: undefined }),
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// context.useCache should be undefined (or default)
|
|
85
|
+
expect(model.context.useCache).toBeUndefined();
|
|
86
|
+
|
|
87
|
+
unmount();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -270,19 +270,6 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
|
|
|
270
270
|
// 仅使用静态流(类级全局注册的 flows)
|
|
271
271
|
const flowsMap = new Map((targetModel.constructor as typeof FlowModel).globalFlowRegistry.getFlows());
|
|
272
272
|
|
|
273
|
-
// 如果有原始 use 且与当前类不同,合并原始模型类的静态 flows(用于入口模型 resolveUse 场景)
|
|
274
|
-
const originUse = targetModel?.use;
|
|
275
|
-
if (typeof originUse === 'string' && originUse !== targetModel.constructor.name) {
|
|
276
|
-
const originCls = targetModel.flowEngine?.getModelClass(originUse) as typeof FlowModel | undefined;
|
|
277
|
-
if (originCls?.globalFlowRegistry) {
|
|
278
|
-
for (const [k, v] of originCls.globalFlowRegistry.getFlows()) {
|
|
279
|
-
if (!flowsMap.has(k)) {
|
|
280
|
-
flowsMap.set(k, v);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
273
|
const flows = flowsMap;
|
|
287
274
|
|
|
288
275
|
const flowsArray = Array.from(flows.values());
|
|
@@ -422,59 +422,4 @@ describe('DefaultSettingsIcon - only static flows are shown', () => {
|
|
|
422
422
|
menu.onClick?.({ key: 'copy-pop-uid:items[0]:popupSettings:stage' });
|
|
423
423
|
expect((navigator as any).clipboard.writeText).toHaveBeenCalledWith('child-2');
|
|
424
424
|
});
|
|
425
|
-
|
|
426
|
-
it('merges static flows from origin use when instance class differs', async () => {
|
|
427
|
-
class TargetModel extends FlowModel {
|
|
428
|
-
static setupFlows() {
|
|
429
|
-
this.registerFlow({
|
|
430
|
-
key: 'targetFlow',
|
|
431
|
-
title: 'Target Flow',
|
|
432
|
-
steps: {
|
|
433
|
-
targetStep: { title: 'Target Step', uiSchema: { x: { type: 'string', 'x-component': 'Input' } } },
|
|
434
|
-
},
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
class EntryModel extends FlowModel {
|
|
439
|
-
static resolveUse() {
|
|
440
|
-
return TargetModel;
|
|
441
|
-
}
|
|
442
|
-
static setupFlows() {
|
|
443
|
-
this.registerFlow({
|
|
444
|
-
key: 'originFlow',
|
|
445
|
-
title: 'Origin Flow',
|
|
446
|
-
steps: {
|
|
447
|
-
originStep: { title: 'Origin Step', uiSchema: { y: { type: 'string', 'x-component': 'Input' } } },
|
|
448
|
-
},
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
TargetModel.setupFlows();
|
|
454
|
-
EntryModel.setupFlows();
|
|
455
|
-
|
|
456
|
-
const engine = new FlowEngine();
|
|
457
|
-
engine.registerModels({ EntryModel, TargetModel });
|
|
458
|
-
|
|
459
|
-
const model = engine.createModel({ use: 'EntryModel', uid: 'merge-origin', flowEngine: engine });
|
|
460
|
-
|
|
461
|
-
render(
|
|
462
|
-
React.createElement(
|
|
463
|
-
ConfigProvider as any,
|
|
464
|
-
null,
|
|
465
|
-
React.createElement(App as any, null, React.createElement(DefaultSettingsIcon as any, { model })),
|
|
466
|
-
),
|
|
467
|
-
);
|
|
468
|
-
|
|
469
|
-
await waitFor(() => {
|
|
470
|
-
const menu = (globalThis as any).__lastDropdownMenu;
|
|
471
|
-
expect(menu).toBeTruthy();
|
|
472
|
-
const items = (menu?.items || []) as any[];
|
|
473
|
-
const groups = items.filter((it) => it.type === 'group').map((it) => String(it.label));
|
|
474
|
-
expect(groups).toContain('Origin Flow');
|
|
475
|
-
expect(groups).toContain('Target Flow');
|
|
476
|
-
expect(items.some((it) => String(it.key || '').startsWith('originFlow:originStep'))).toBe(true);
|
|
477
|
-
expect(items.some((it) => String(it.key || '').startsWith('targetFlow:targetStep'))).toBe(true);
|
|
478
|
-
});
|
|
479
|
-
});
|
|
480
425
|
});
|
package/src/flowEngine.ts
CHANGED
|
@@ -701,6 +701,44 @@ export class FlowEngine {
|
|
|
701
701
|
return true;
|
|
702
702
|
}
|
|
703
703
|
|
|
704
|
+
/**
|
|
705
|
+
* Remove a local model instance and all its sub-models recursively.
|
|
706
|
+
* @param {string} uid UID of the model instance to destroy
|
|
707
|
+
* @returns {boolean} Returns true if successfully destroyed, false otherwise
|
|
708
|
+
*/
|
|
709
|
+
public removeModelWithSubModels(uid: string): boolean {
|
|
710
|
+
const model = this.getModel(uid);
|
|
711
|
+
if (!model) {
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const collectDescendants = (m: FlowModel, acc: FlowModel[]) => {
|
|
716
|
+
if (m.subModels) {
|
|
717
|
+
for (const key in m.subModels) {
|
|
718
|
+
const sub = m.subModels[key];
|
|
719
|
+
if (Array.isArray(sub)) {
|
|
720
|
+
[...sub].forEach((s) => collectDescendants(s, acc));
|
|
721
|
+
} else if (sub) {
|
|
722
|
+
collectDescendants(sub, acc);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
acc.push(m);
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
const allModels: FlowModel[] = [];
|
|
730
|
+
collectDescendants(model, allModels);
|
|
731
|
+
|
|
732
|
+
let success = true;
|
|
733
|
+
for (const m of allModels) {
|
|
734
|
+
if (!this.removeModel(m.uid)) {
|
|
735
|
+
success = false;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return success;
|
|
740
|
+
}
|
|
741
|
+
|
|
704
742
|
/**
|
|
705
743
|
* Check if the model repository is set.
|
|
706
744
|
* @returns {boolean} Returns true if set, false otherwise.
|
|
@@ -21,7 +21,7 @@ import { useFlowEngine } from '../provider';
|
|
|
21
21
|
export function useApplyAutoFlows(
|
|
22
22
|
modelOrUid: FlowModel | string,
|
|
23
23
|
inputArgs?: Record<string, any>,
|
|
24
|
-
options?: { throwOnError?: boolean },
|
|
24
|
+
options?: { throwOnError?: boolean; useCache?: boolean },
|
|
25
25
|
) {
|
|
26
26
|
const flowEngine = useFlowEngine();
|
|
27
27
|
const model = useMemo(() => {
|
|
@@ -37,10 +37,10 @@ export function useApplyAutoFlows(
|
|
|
37
37
|
async () => {
|
|
38
38
|
if (!model) return;
|
|
39
39
|
// beforeRender 在模型层默认顺序执行并默认使用缓存(可覆盖)
|
|
40
|
-
await model.dispatchEvent('beforeRender', inputArgs);
|
|
40
|
+
await model.dispatchEvent('beforeRender', inputArgs, { useCache: options?.useCache });
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
|
-
refreshDeps: [model, inputArgs],
|
|
43
|
+
refreshDeps: [model, inputArgs, options?.useCache],
|
|
44
44
|
},
|
|
45
45
|
);
|
|
46
46
|
|
|
@@ -0,0 +1,51 @@
|
|
|
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 { describe, expect, it } from 'vitest';
|
|
11
|
+
import { createCollectionContextMeta } from '../createCollectionContextMeta';
|
|
12
|
+
import { FlowEngine } from '../../flowEngine';
|
|
13
|
+
import { CollectionFieldInterfaceManager } from '../../../../client/src/data-source/collection-field-interface/CollectionFieldInterfaceManager';
|
|
14
|
+
|
|
15
|
+
describe('createCollectionContextMeta', () => {
|
|
16
|
+
it('filters association sub fields when includeNonFilterable is false', async () => {
|
|
17
|
+
const engine = new FlowEngine();
|
|
18
|
+
const dm = engine.dataSourceManager as any;
|
|
19
|
+
dm.collectionFieldInterfaceManager = new CollectionFieldInterfaceManager([], {}, dm);
|
|
20
|
+
engine.context.defineProperty('app', { value: { dataSourceManager: dm } });
|
|
21
|
+
const ds = dm.getDataSource('main')!;
|
|
22
|
+
|
|
23
|
+
ds.addCollection({
|
|
24
|
+
name: 'users',
|
|
25
|
+
fields: [
|
|
26
|
+
{ name: 'id', type: 'integer', interface: 'number', filterable: true },
|
|
27
|
+
{ name: 'email', type: 'string', interface: 'text', filterable: true },
|
|
28
|
+
{ name: 'nickname', type: 'string', interface: 'text' }, // 未声明 filterable
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
ds.addCollection({
|
|
33
|
+
name: 'posts',
|
|
34
|
+
fields: [
|
|
35
|
+
{ name: 'title', type: 'string', interface: 'text', filterable: true },
|
|
36
|
+
{ name: 'author', type: 'belongsTo', target: 'users', interface: 'm2o', filterable: true },
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const posts = ds.getCollection('posts')!;
|
|
41
|
+
const metaFactory = createCollectionContextMeta(posts, 'Posts', false);
|
|
42
|
+
const meta = await metaFactory();
|
|
43
|
+
const props = await (meta?.properties as any)?.();
|
|
44
|
+
const authorMeta: any = props?.author;
|
|
45
|
+
const authorFields = await authorMeta?.properties?.();
|
|
46
|
+
|
|
47
|
+
expect(authorFields).toBeTruthy();
|
|
48
|
+
expect(authorFields).toHaveProperty('email');
|
|
49
|
+
expect(authorFields).not.toHaveProperty('nickname');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -122,7 +122,7 @@ export function createAssociationAwareObjectMetaFactory(
|
|
|
122
122
|
title: string,
|
|
123
123
|
valueAccessor: (ctx: FlowContext) => any,
|
|
124
124
|
): PropertyMetaFactory {
|
|
125
|
-
const baseFactory = createCollectionContextMeta(collectionAccessor, title);
|
|
125
|
+
const baseFactory = createCollectionContextMeta(collectionAccessor, title, true);
|
|
126
126
|
const factory: PropertyMetaFactory = async () => {
|
|
127
127
|
const base = (await baseFactory()) as PropertyMeta | null;
|
|
128
128
|
if (!base) return null;
|
|
@@ -17,7 +17,7 @@ const NUMERIC_FIELD_TYPES = ['integer', 'float', 'double', 'decimal'] as const;
|
|
|
17
17
|
/**
|
|
18
18
|
* 创建字段的完整元数据(统一处理关联和非关联字段)
|
|
19
19
|
*/
|
|
20
|
-
function createFieldMetadata(field: CollectionField) {
|
|
20
|
+
function createFieldMetadata(field: CollectionField, includeNonFilterable?: boolean) {
|
|
21
21
|
const baseProperties = createMetaBaseProperties(field);
|
|
22
22
|
|
|
23
23
|
if (field.isAssociationField()) {
|
|
@@ -36,7 +36,9 @@ function createFieldMetadata(field: CollectionField) {
|
|
|
36
36
|
properties: async () => {
|
|
37
37
|
const subProperties: Record<string, any> = {};
|
|
38
38
|
targetCollection.fields.forEach((subField) => {
|
|
39
|
-
|
|
39
|
+
if (includeNonFilterable || subField.filterable) {
|
|
40
|
+
subProperties[subField.name] = createFieldMetadata(subField, includeNonFilterable);
|
|
41
|
+
}
|
|
40
42
|
});
|
|
41
43
|
return subProperties;
|
|
42
44
|
},
|
|
@@ -93,6 +95,7 @@ function createMetaBaseProperties(field: CollectionField) {
|
|
|
93
95
|
export function createCollectionContextMeta(
|
|
94
96
|
collectionOrFactory: Collection | (() => Collection | null),
|
|
95
97
|
title?: string,
|
|
98
|
+
includeNonFilterable?: boolean,
|
|
96
99
|
): PropertyMetaFactory {
|
|
97
100
|
const metaFn: PropertyMetaFactory = async () => {
|
|
98
101
|
const collection = typeof collectionOrFactory === 'function' ? collectionOrFactory() : collectionOrFactory;
|
|
@@ -110,8 +113,8 @@ export function createCollectionContextMeta(
|
|
|
110
113
|
|
|
111
114
|
// 添加所有字段
|
|
112
115
|
collection.fields.forEach((field) => {
|
|
113
|
-
if (field.filterable) {
|
|
114
|
-
properties[field.name] = createFieldMetadata(field);
|
|
116
|
+
if (includeNonFilterable || field.filterable) {
|
|
117
|
+
properties[field.name] = createFieldMetadata(field, includeNonFilterable);
|
|
115
118
|
}
|
|
116
119
|
});
|
|
117
120
|
|
|
@@ -174,7 +174,7 @@ export async function buildRecordMeta(
|
|
|
174
174
|
title?: string,
|
|
175
175
|
paramsBuilder?: RecordParamsBuilder,
|
|
176
176
|
): Promise<PropertyMeta | null> {
|
|
177
|
-
const base = await createCollectionContextMeta(collectionAccessor, title)();
|
|
177
|
+
const base = await createCollectionContextMeta(collectionAccessor, title, true)();
|
|
178
178
|
if (!base) return null;
|
|
179
179
|
return {
|
|
180
180
|
...base,
|