@nocobase/flow-engine 2.0.0-beta.2 → 2.0.0-beta.20
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/BlockScopedFlowEngine.js +0 -1
- package/lib/JSRunner.d.ts +6 -0
- package/lib/JSRunner.js +2 -1
- package/lib/ViewScopedFlowEngine.js +3 -0
- package/lib/acl/Acl.js +13 -3
- package/lib/components/dnd/gridDragPlanner.d.ts +1 -0
- package/lib/components/dnd/gridDragPlanner.js +53 -1
- package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +11 -3
- package/lib/components/variables/VariableInput.js +8 -2
- package/lib/data-source/index.js +6 -0
- package/lib/executor/FlowExecutor.d.ts +2 -1
- package/lib/executor/FlowExecutor.js +156 -22
- package/lib/flowContext.d.ts +4 -1
- package/lib/flowContext.js +176 -107
- package/lib/flowEngine.d.ts +21 -0
- package/lib/flowEngine.js +38 -0
- package/lib/flowSettings.js +12 -10
- package/lib/index.d.ts +3 -0
- package/lib/index.js +16 -0
- package/lib/models/CollectionFieldModel.d.ts +1 -0
- package/lib/models/CollectionFieldModel.js +3 -2
- package/lib/models/flowModel.d.ts +7 -0
- package/lib/models/flowModel.js +66 -1
- package/lib/provider.js +7 -6
- package/lib/resources/baseRecordResource.d.ts +5 -0
- package/lib/resources/baseRecordResource.js +24 -0
- package/lib/resources/multiRecordResource.d.ts +1 -0
- package/lib/resources/multiRecordResource.js +11 -4
- package/lib/resources/singleRecordResource.js +2 -0
- package/lib/resources/sqlResource.d.ts +1 -0
- package/lib/resources/sqlResource.js +8 -3
- package/lib/runjs-context/contexts/base.js +10 -4
- package/lib/runjsLibs.d.ts +28 -0
- package/lib/runjsLibs.js +532 -0
- package/lib/scheduler/ModelOperationScheduler.d.ts +2 -0
- package/lib/scheduler/ModelOperationScheduler.js +21 -21
- package/lib/types.d.ts +15 -0
- package/lib/utils/createCollectionContextMeta.js +1 -0
- package/lib/utils/index.d.ts +2 -0
- package/lib/utils/index.js +10 -0
- package/lib/utils/params-resolvers.js +16 -9
- package/lib/utils/resolveModuleUrl.d.ts +58 -0
- package/lib/utils/resolveModuleUrl.js +65 -0
- package/lib/utils/runjsModuleLoader.d.ts +58 -0
- package/lib/utils/runjsModuleLoader.js +422 -0
- package/lib/utils/runjsTemplateCompat.d.ts +35 -0
- package/lib/utils/runjsTemplateCompat.js +743 -0
- package/lib/utils/safeGlobals.d.ts +5 -9
- package/lib/utils/safeGlobals.js +129 -17
- package/lib/views/createViewMeta.d.ts +0 -7
- package/lib/views/createViewMeta.js +19 -70
- package/lib/views/index.d.ts +1 -2
- package/lib/views/index.js +4 -3
- package/lib/views/useDialog.js +8 -3
- package/lib/views/useDrawer.js +7 -2
- package/lib/views/usePage.d.ts +4 -0
- package/lib/views/usePage.js +43 -6
- package/lib/views/usePopover.js +4 -1
- package/lib/views/viewEvents.d.ts +17 -0
- package/lib/views/viewEvents.js +90 -0
- package/package.json +4 -4
- package/src/BlockScopedFlowEngine.ts +2 -5
- package/src/JSRunner.ts +8 -1
- package/src/ViewScopedFlowEngine.ts +4 -0
- package/src/__tests__/createViewMeta.popup.test.ts +62 -1
- package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
- package/src/__tests__/flowSettings.open.test.tsx +69 -15
- package/src/__tests__/provider.test.tsx +0 -5
- package/src/__tests__/runjsExternalLibs.test.ts +242 -0
- package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
- package/src/__tests__/runjsPreprocessDefault.test.ts +49 -0
- package/src/acl/Acl.tsx +3 -3
- package/src/components/__tests__/gridDragPlanner.test.ts +141 -1
- package/src/components/dnd/gridDragPlanner.ts +60 -0
- package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
- package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +11 -3
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +63 -4
- package/src/components/variables/VariableInput.tsx +8 -2
- package/src/data-source/index.ts +6 -0
- package/src/executor/FlowExecutor.ts +193 -23
- package/src/executor/__tests__/flowExecutor.test.ts +66 -0
- package/src/flowContext.ts +234 -118
- package/src/flowEngine.ts +41 -0
- package/src/flowSettings.ts +12 -11
- package/src/index.ts +10 -0
- package/src/models/CollectionFieldModel.tsx +3 -1
- package/src/models/__tests__/dispatchEvent.when.test.ts +356 -0
- package/src/models/__tests__/flowModel.clone.test.ts +416 -0
- package/src/models/__tests__/flowModel.test.ts +16 -0
- package/src/models/flowModel.tsx +94 -1
- package/src/provider.tsx +9 -7
- package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
- package/src/resources/__tests__/sqlResource.test.ts +60 -0
- package/src/resources/baseRecordResource.ts +31 -0
- package/src/resources/multiRecordResource.ts +11 -4
- package/src/resources/singleRecordResource.ts +3 -0
- package/src/resources/sqlResource.ts +8 -3
- package/src/runjs-context/contexts/base.ts +9 -2
- package/src/runjsLibs.ts +622 -0
- package/src/scheduler/ModelOperationScheduler.ts +23 -21
- package/src/types.ts +26 -1
- package/src/utils/__tests__/params-resolvers.test.ts +40 -0
- package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
- package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
- package/src/utils/__tests__/safeGlobals.test.ts +49 -2
- package/src/utils/createCollectionContextMeta.ts +1 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/params-resolvers.ts +23 -9
- package/src/utils/resolveModuleUrl.ts +91 -0
- package/src/utils/runjsModuleLoader.ts +553 -0
- package/src/utils/runjsTemplateCompat.ts +828 -0
- package/src/utils/safeGlobals.ts +133 -16
- package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +35 -8
- package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
- package/src/views/createViewMeta.ts +22 -75
- package/src/views/index.tsx +1 -2
- package/src/views/useDialog.tsx +9 -2
- package/src/views/useDrawer.tsx +8 -1
- package/src/views/usePage.tsx +51 -5
- package/src/views/usePopover.tsx +4 -1
- package/src/views/viewEvents.ts +55 -0
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
15
|
+
var __export = (target, all) => {
|
|
16
|
+
for (var name in all)
|
|
17
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
18
|
+
};
|
|
19
|
+
var __copyProps = (to, from, except, desc) => {
|
|
20
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
21
|
+
for (let key of __getOwnPropNames(from))
|
|
22
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
23
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
24
|
+
}
|
|
25
|
+
return to;
|
|
26
|
+
};
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
var viewEvents_exports = {};
|
|
29
|
+
__export(viewEvents_exports, {
|
|
30
|
+
DATA_SOURCE_DIRTY_EVENT: () => DATA_SOURCE_DIRTY_EVENT,
|
|
31
|
+
ENGINE_SCOPE_KEY: () => ENGINE_SCOPE_KEY,
|
|
32
|
+
VIEW_ACTIVATED_EVENT: () => VIEW_ACTIVATED_EVENT,
|
|
33
|
+
VIEW_ACTIVATED_VERSION: () => VIEW_ACTIVATED_VERSION,
|
|
34
|
+
VIEW_ENGINE_SCOPE: () => VIEW_ENGINE_SCOPE,
|
|
35
|
+
bumpViewActivatedVersion: () => bumpViewActivatedVersion,
|
|
36
|
+
getEmitterViewActivatedVersion: () => getEmitterViewActivatedVersion,
|
|
37
|
+
resolveOpenerEngine: () => resolveOpenerEngine
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(viewEvents_exports);
|
|
40
|
+
const VIEW_ACTIVATED_VERSION = Symbol.for("__NOCOBASE_VIEW_ACTIVATED_VERSION__");
|
|
41
|
+
const VIEW_ACTIVATED_EVENT = "view:activated";
|
|
42
|
+
const DATA_SOURCE_DIRTY_EVENT = "dataSource:dirty";
|
|
43
|
+
const ENGINE_SCOPE_KEY = "__NOCOBASE_ENGINE_SCOPE__";
|
|
44
|
+
const VIEW_ENGINE_SCOPE = "view";
|
|
45
|
+
function getEmitterViewActivatedVersion(emitter) {
|
|
46
|
+
const raw = Reflect.get(emitter, VIEW_ACTIVATED_VERSION);
|
|
47
|
+
const num = typeof raw === "number" ? raw : Number(raw);
|
|
48
|
+
return Number.isFinite(num) && num > 0 ? num : 0;
|
|
49
|
+
}
|
|
50
|
+
__name(getEmitterViewActivatedVersion, "getEmitterViewActivatedVersion");
|
|
51
|
+
function bumpViewActivatedVersion(emitter) {
|
|
52
|
+
const current = getEmitterViewActivatedVersion(emitter);
|
|
53
|
+
if (!Object.isExtensible(emitter)) return current;
|
|
54
|
+
const next = current + 1;
|
|
55
|
+
Reflect.set(emitter, VIEW_ACTIVATED_VERSION, next);
|
|
56
|
+
return next;
|
|
57
|
+
}
|
|
58
|
+
__name(bumpViewActivatedVersion, "bumpViewActivatedVersion");
|
|
59
|
+
function isViewEngine(engine) {
|
|
60
|
+
return Reflect.get(engine, ENGINE_SCOPE_KEY) === VIEW_ENGINE_SCOPE;
|
|
61
|
+
}
|
|
62
|
+
__name(isViewEngine, "isViewEngine");
|
|
63
|
+
function findNearestViewEngine(engine) {
|
|
64
|
+
let cur = engine;
|
|
65
|
+
let guard = 0;
|
|
66
|
+
while (cur && guard++ < 50) {
|
|
67
|
+
if (isViewEngine(cur)) return cur;
|
|
68
|
+
cur = cur.previousEngine;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
__name(findNearestViewEngine, "findNearestViewEngine");
|
|
72
|
+
function resolveOpenerEngine(parentEngine, scopedEngine) {
|
|
73
|
+
if (!parentEngine) return void 0;
|
|
74
|
+
const parentViewEngine = findNearestViewEngine(parentEngine);
|
|
75
|
+
if (parentViewEngine) return parentViewEngine;
|
|
76
|
+
const previousEngine = scopedEngine == null ? void 0 : scopedEngine.previousEngine;
|
|
77
|
+
return findNearestViewEngine(previousEngine) || parentEngine;
|
|
78
|
+
}
|
|
79
|
+
__name(resolveOpenerEngine, "resolveOpenerEngine");
|
|
80
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
81
|
+
0 && (module.exports = {
|
|
82
|
+
DATA_SOURCE_DIRTY_EVENT,
|
|
83
|
+
ENGINE_SCOPE_KEY,
|
|
84
|
+
VIEW_ACTIVATED_EVENT,
|
|
85
|
+
VIEW_ACTIVATED_VERSION,
|
|
86
|
+
VIEW_ENGINE_SCOPE,
|
|
87
|
+
bumpViewActivatedVersion,
|
|
88
|
+
getEmitterViewActivatedVersion,
|
|
89
|
+
resolveOpenerEngine
|
|
90
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.20",
|
|
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-beta.
|
|
12
|
-
"@nocobase/shared": "2.0.0-beta.
|
|
11
|
+
"@nocobase/sdk": "2.0.0-beta.20",
|
|
12
|
+
"@nocobase/shared": "2.0.0-beta.20",
|
|
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": "f4ab788fc6c17915157617026dfbba6f0d78eaac"
|
|
40
40
|
}
|
|
@@ -31,16 +31,13 @@ export function createBlockScopedEngine(parent: FlowEngine): FlowEngine {
|
|
|
31
31
|
local.context.addDelegate(parent.context);
|
|
32
32
|
|
|
33
33
|
// 覆盖 unlinkFromStack:BlockScoped 引擎被移除时,修复前后指针,避免“截断”后续视图/作用域
|
|
34
|
-
const originalUnlink = local.unlinkFromStack.bind(local);
|
|
35
34
|
local.unlinkFromStack = function () {
|
|
36
|
-
// 修复指针:prev -> next,next -> prev,然后清理自身指针
|
|
37
|
-
// 若不这么做,移除位于中间的 block 引擎会导致后续整段链丢失
|
|
38
35
|
const prev = (local as any)._previousEngine as FlowEngine | undefined;
|
|
39
36
|
const next = (local as any)._nextEngine as FlowEngine | undefined;
|
|
40
37
|
if (prev) (prev as any)._nextEngine = next;
|
|
41
38
|
if (next) (next as any)._previousEngine = prev;
|
|
42
|
-
(local as any)._previousEngine = undefined
|
|
43
|
-
(local as any)._nextEngine = undefined
|
|
39
|
+
(local as any)._previousEngine = undefined;
|
|
40
|
+
(local as any)._nextEngine = undefined;
|
|
44
41
|
};
|
|
45
42
|
|
|
46
43
|
// 默认全部代理到父引擎,只有少数字段(实例/缓存/执行器/上下文/链表指针)使用本地值
|
package/src/JSRunner.ts
CHANGED
|
@@ -13,6 +13,12 @@ export interface JSRunnerOptions {
|
|
|
13
13
|
timeoutMs?: number;
|
|
14
14
|
globals?: Record<string, any>;
|
|
15
15
|
version?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Enable RunJS template compatibility preprocessing for `{{ ... }}`.
|
|
18
|
+
* When enabled via `ctx.runjs(code, vars, { preprocessTemplates: true })` (default),
|
|
19
|
+
* the code will be rewritten to call `ctx.resolveJsonTemplate(...)` at runtime.
|
|
20
|
+
*/
|
|
21
|
+
preprocessTemplates?: boolean;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
export class JSRunner {
|
|
@@ -55,7 +61,8 @@ export class JSRunner {
|
|
|
55
61
|
error?: any;
|
|
56
62
|
timeout?: boolean;
|
|
57
63
|
}> {
|
|
58
|
-
|
|
64
|
+
const search = typeof location !== 'undefined' ? location.search : undefined;
|
|
65
|
+
if (typeof search === 'string' && search.includes('skipRunJs=true')) {
|
|
59
66
|
return { success: true, value: null };
|
|
60
67
|
}
|
|
61
68
|
const wrapped = `(async () => {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { FlowEngine } from './flowEngine';
|
|
11
|
+
import { ENGINE_SCOPE_KEY, VIEW_ENGINE_SCOPE } from './views/viewEvents';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* ViewScopedFlowEngine(视图作用域引擎)
|
|
@@ -24,6 +25,8 @@ import { FlowEngine } from './flowEngine';
|
|
|
24
25
|
*/
|
|
25
26
|
export function createViewScopedEngine(parent: FlowEngine): FlowEngine {
|
|
26
27
|
const local = new FlowEngine();
|
|
28
|
+
// Mark for view-stack traversal (used by view activation events).
|
|
29
|
+
Object.defineProperty(local, ENGINE_SCOPE_KEY, { value: VIEW_ENGINE_SCOPE, configurable: true });
|
|
27
30
|
if (parent.modelRepository) {
|
|
28
31
|
local.setModelRepository(parent.modelRepository);
|
|
29
32
|
}
|
|
@@ -43,6 +46,7 @@ export function createViewScopedEngine(parent: FlowEngine): FlowEngine {
|
|
|
43
46
|
'_applyFlowCache',
|
|
44
47
|
'executor',
|
|
45
48
|
'context',
|
|
49
|
+
ENGINE_SCOPE_KEY,
|
|
46
50
|
'previousEngine',
|
|
47
51
|
'nextEngine',
|
|
48
52
|
// 调度器与事件总线局部化
|
|
@@ -13,7 +13,7 @@ import { FlowEngine } from '../flowEngine';
|
|
|
13
13
|
import type { FlowView } from '../views/FlowView';
|
|
14
14
|
import { createPopupMeta } from '../views/createViewMeta';
|
|
15
15
|
|
|
16
|
-
describe('
|
|
16
|
+
describe('createPopupMeta - popup variables', () => {
|
|
17
17
|
function makeCtx() {
|
|
18
18
|
const engine = new FlowEngine();
|
|
19
19
|
const ctx = new FlowContext();
|
|
@@ -139,4 +139,65 @@ describe('createViewMeta - popup variables', () => {
|
|
|
139
139
|
expect((props.record as any).title).toBe('Current popup record');
|
|
140
140
|
expect((props.record as any).hasChildren).toBe(true);
|
|
141
141
|
});
|
|
142
|
+
|
|
143
|
+
it('treats views with openerUids as popup (meta visible)', async () => {
|
|
144
|
+
const { ctx } = makeCtx();
|
|
145
|
+
|
|
146
|
+
// openerUids 作为路由栈缺失时的兜底标记:即使没有 navigation.viewStack,也应展示 ctx.popup 元信息
|
|
147
|
+
const anchorView: FlowView = {
|
|
148
|
+
type: 'dialog',
|
|
149
|
+
inputArgs: {
|
|
150
|
+
openerUids: ['opener-uid-1'],
|
|
151
|
+
viewUid: 'popup-uid',
|
|
152
|
+
dataSourceKey: 'main',
|
|
153
|
+
collectionName: 'posts',
|
|
154
|
+
filterByTk: 1,
|
|
155
|
+
},
|
|
156
|
+
Header: null,
|
|
157
|
+
Footer: null,
|
|
158
|
+
close: () => void 0,
|
|
159
|
+
update: () => void 0,
|
|
160
|
+
} as any;
|
|
161
|
+
|
|
162
|
+
const meta = (await createPopupMeta(ctx, anchorView)())!;
|
|
163
|
+
expect(typeof meta.hidden).toBe('function');
|
|
164
|
+
expect((meta.hidden as any)()).toBe(false);
|
|
165
|
+
expect(typeof meta.disabled).toBe('function');
|
|
166
|
+
expect((meta.disabled as any)()).toBe(false);
|
|
167
|
+
|
|
168
|
+
const vars = (await meta.buildVariablesParams!(ctx)) as any;
|
|
169
|
+
expect(vars).toBeTruthy();
|
|
170
|
+
expect(vars.record).toEqual({
|
|
171
|
+
collection: 'posts',
|
|
172
|
+
dataSourceKey: 'main',
|
|
173
|
+
filterByTk: 1,
|
|
174
|
+
associationName: undefined,
|
|
175
|
+
sourceId: undefined,
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('does not expose popup.record without filterByTk', async () => {
|
|
180
|
+
const { ctx } = makeCtx();
|
|
181
|
+
|
|
182
|
+
const anchorView: FlowView = {
|
|
183
|
+
type: 'dialog',
|
|
184
|
+
inputArgs: {
|
|
185
|
+
openerUids: ['opener-uid-1'],
|
|
186
|
+
viewUid: 'popup-uid',
|
|
187
|
+
dataSourceKey: 'main',
|
|
188
|
+
collectionName: 'posts',
|
|
189
|
+
// filterByTk 缺失:不应展示“当前弹窗记录”变量
|
|
190
|
+
},
|
|
191
|
+
Header: null,
|
|
192
|
+
Footer: null,
|
|
193
|
+
close: () => void 0,
|
|
194
|
+
update: () => void 0,
|
|
195
|
+
} as any;
|
|
196
|
+
|
|
197
|
+
ctx.defineProperty('view', { value: anchorView });
|
|
198
|
+
|
|
199
|
+
const meta = (await createPopupMeta(ctx, anchorView)())!;
|
|
200
|
+
const props = typeof meta.properties === 'function' ? await (meta.properties as any)() : meta.properties || {};
|
|
201
|
+
expect(props.record).toBeUndefined();
|
|
202
|
+
});
|
|
142
203
|
});
|
|
@@ -0,0 +1,63 @@
|
|
|
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, vi } from 'vitest';
|
|
11
|
+
import { FlowEngine } from '../flowEngine';
|
|
12
|
+
import { MultiRecordResource } from '../resources/multiRecordResource';
|
|
13
|
+
import { SingleRecordResource } from '../resources/singleRecordResource';
|
|
14
|
+
|
|
15
|
+
describe('FlowEngine dataSource dirty registry', () => {
|
|
16
|
+
it('tracks versions per dataSourceKey + resourceName', () => {
|
|
17
|
+
const engine = new FlowEngine();
|
|
18
|
+
|
|
19
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(0);
|
|
20
|
+
expect(engine.markDataSourceDirty('main', 'posts')).toBe(1);
|
|
21
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
22
|
+
|
|
23
|
+
expect(engine.markDataSourceDirty('main', 'posts')).toBe(2);
|
|
24
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(2);
|
|
25
|
+
|
|
26
|
+
// different resource
|
|
27
|
+
expect(engine.getDataSourceDirtyVersion('main', 'users')).toBe(0);
|
|
28
|
+
expect(engine.markDataSourceDirty('main', 'users')).toBe(1);
|
|
29
|
+
expect(engine.getDataSourceDirtyVersion('main', 'users')).toBe(1);
|
|
30
|
+
|
|
31
|
+
// different data source
|
|
32
|
+
expect(engine.getDataSourceDirtyVersion('ds2', 'posts')).toBe(0);
|
|
33
|
+
expect(engine.markDataSourceDirty('ds2', 'posts')).toBe(1);
|
|
34
|
+
expect(engine.getDataSourceDirtyVersion('ds2', 'posts')).toBe(1);
|
|
35
|
+
// main unchanged
|
|
36
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(2);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('marks dirty on record write operations (single & multi)', async () => {
|
|
40
|
+
const engine = new FlowEngine();
|
|
41
|
+
const markSpy = vi.spyOn(engine, 'markDataSourceDirty');
|
|
42
|
+
|
|
43
|
+
const single = engine.createResource(SingleRecordResource);
|
|
44
|
+
single.setDataSourceKey('main');
|
|
45
|
+
single.setResourceName('posts');
|
|
46
|
+
// avoid network: stub runAction + refresh
|
|
47
|
+
(single as any).runAction = vi.fn().mockResolvedValue({ data: {}, meta: {} });
|
|
48
|
+
(single as any).refresh = vi.fn().mockResolvedValue(undefined);
|
|
49
|
+
await single.save({ title: 't' } as any, { refresh: false });
|
|
50
|
+
expect(markSpy).toHaveBeenCalledWith('main', 'posts');
|
|
51
|
+
|
|
52
|
+
const multi = engine.createResource(MultiRecordResource);
|
|
53
|
+
multi.setDataSourceKey('main');
|
|
54
|
+
multi.setResourceName('users.profile');
|
|
55
|
+
(multi as any).runAction = vi.fn().mockResolvedValue({ data: [], meta: {} });
|
|
56
|
+
(multi as any).refresh = vi.fn().mockResolvedValue(undefined);
|
|
57
|
+
await multi.create({ name: 'n' } as any, { refresh: false });
|
|
58
|
+
// exact association
|
|
59
|
+
expect(markSpy).toHaveBeenCalledWith('main', 'users.profile');
|
|
60
|
+
// plus root collection (safety)
|
|
61
|
+
expect(markSpy).toHaveBeenCalledWith('main', 'users');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -12,6 +12,7 @@ import { screen } from '@testing-library/react';
|
|
|
12
12
|
import { FlowSettings } from '../flowSettings';
|
|
13
13
|
import { FlowModel } from '../models';
|
|
14
14
|
import { FlowEngine } from '../flowEngine';
|
|
15
|
+
import { GLOBAL_EMBED_CONTAINER_ID } from '../views';
|
|
15
16
|
|
|
16
17
|
// We will stub viewer directly on model.context in tests
|
|
17
18
|
|
|
@@ -1087,18 +1088,18 @@ describe('FlowSettings.open rendering behavior', () => {
|
|
|
1087
1088
|
|
|
1088
1089
|
// Create mock DOM element for embed target
|
|
1089
1090
|
const mockTarget = document.createElement('div');
|
|
1090
|
-
mockTarget.id =
|
|
1091
|
+
mockTarget.id = GLOBAL_EMBED_CONTAINER_ID;
|
|
1091
1092
|
mockTarget.style.width = 'auto';
|
|
1092
1093
|
mockTarget.style.maxWidth = 'none';
|
|
1093
1094
|
document.body.appendChild(mockTarget);
|
|
1094
1095
|
|
|
1095
1096
|
// Mock querySelector to return our mock element
|
|
1096
|
-
const originalQuerySelector = document.querySelector;
|
|
1097
|
-
|
|
1098
|
-
if (selector ===
|
|
1097
|
+
const originalQuerySelector = document.querySelector.bind(document);
|
|
1098
|
+
const querySelectorSpy = vi.spyOn(document, 'querySelector').mockImplementation((selector: string) => {
|
|
1099
|
+
if (selector === `#${GLOBAL_EMBED_CONTAINER_ID}`) {
|
|
1099
1100
|
return mockTarget;
|
|
1100
1101
|
}
|
|
1101
|
-
return originalQuerySelector
|
|
1102
|
+
return originalQuerySelector(selector);
|
|
1102
1103
|
});
|
|
1103
1104
|
|
|
1104
1105
|
const M = model.constructor as any;
|
|
@@ -1164,7 +1165,61 @@ describe('FlowSettings.open rendering behavior', () => {
|
|
|
1164
1165
|
|
|
1165
1166
|
// Cleanup
|
|
1166
1167
|
document.body.removeChild(mockTarget);
|
|
1167
|
-
|
|
1168
|
+
querySelectorSpy.mockRestore();
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
it('does not clear embed target DOM before opening (avoids portal unmount errors)', async () => {
|
|
1172
|
+
const engine = new FlowEngine();
|
|
1173
|
+
const flowSettings = new FlowSettings(engine);
|
|
1174
|
+
const model = new FlowModel({ uid: 'm-embed-no-clear', flowEngine: engine });
|
|
1175
|
+
|
|
1176
|
+
const mockTarget = document.createElement('div');
|
|
1177
|
+
mockTarget.id = GLOBAL_EMBED_CONTAINER_ID;
|
|
1178
|
+
mockTarget.innerHTML = '<div data-testid="existing">Existing</div>';
|
|
1179
|
+
document.body.appendChild(mockTarget);
|
|
1180
|
+
|
|
1181
|
+
const originalQuerySelector = document.querySelector.bind(document);
|
|
1182
|
+
const querySelectorSpy = vi.spyOn(document, 'querySelector').mockImplementation((selector: string) => {
|
|
1183
|
+
if (selector === `#${GLOBAL_EMBED_CONTAINER_ID}`) {
|
|
1184
|
+
return mockTarget;
|
|
1185
|
+
}
|
|
1186
|
+
return originalQuerySelector(selector);
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
const M = model.constructor as any;
|
|
1190
|
+
M.registerFlow({
|
|
1191
|
+
key: 'embedNoClearFlow',
|
|
1192
|
+
steps: {
|
|
1193
|
+
step: {
|
|
1194
|
+
title: 'Step',
|
|
1195
|
+
uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
|
|
1196
|
+
},
|
|
1197
|
+
},
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
const embed = vi.fn((opts: any) => {
|
|
1201
|
+
// The existing DOM should not be wiped out before opening the embed view.
|
|
1202
|
+
expect(mockTarget.querySelector('[data-testid="existing"]')).toBeTruthy();
|
|
1203
|
+
const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
1204
|
+
if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
|
|
1205
|
+
return dlg;
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
model.context.defineProperty('viewer', { value: { embed } });
|
|
1209
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
1210
|
+
|
|
1211
|
+
await flowSettings.open({
|
|
1212
|
+
model,
|
|
1213
|
+
flowKey: 'embedNoClearFlow',
|
|
1214
|
+
stepKey: 'step',
|
|
1215
|
+
uiMode: 'embed',
|
|
1216
|
+
} as any);
|
|
1217
|
+
|
|
1218
|
+
expect(embed).toHaveBeenCalledTimes(1);
|
|
1219
|
+
expect(mockTarget.querySelector('[data-testid="existing"]')).toBeTruthy();
|
|
1220
|
+
|
|
1221
|
+
document.body.removeChild(mockTarget);
|
|
1222
|
+
querySelectorSpy.mockRestore();
|
|
1168
1223
|
});
|
|
1169
1224
|
|
|
1170
1225
|
it('uses embed uiMode with default props when target element exists', async () => {
|
|
@@ -1174,16 +1229,16 @@ describe('FlowSettings.open rendering behavior', () => {
|
|
|
1174
1229
|
|
|
1175
1230
|
// Create mock DOM element for embed target
|
|
1176
1231
|
const mockTarget = document.createElement('div');
|
|
1177
|
-
mockTarget.id =
|
|
1232
|
+
mockTarget.id = GLOBAL_EMBED_CONTAINER_ID;
|
|
1178
1233
|
document.body.appendChild(mockTarget);
|
|
1179
1234
|
|
|
1180
1235
|
// Mock querySelector
|
|
1181
|
-
const originalQuerySelector = document.querySelector;
|
|
1182
|
-
|
|
1183
|
-
if (selector ===
|
|
1236
|
+
const originalQuerySelector = document.querySelector.bind(document);
|
|
1237
|
+
const querySelectorSpy = vi.spyOn(document, 'querySelector').mockImplementation((selector: string) => {
|
|
1238
|
+
if (selector === `#${GLOBAL_EMBED_CONTAINER_ID}`) {
|
|
1184
1239
|
return mockTarget;
|
|
1185
1240
|
}
|
|
1186
|
-
return originalQuerySelector
|
|
1241
|
+
return originalQuerySelector(selector);
|
|
1187
1242
|
});
|
|
1188
1243
|
|
|
1189
1244
|
const M = model.constructor as any;
|
|
@@ -1223,7 +1278,7 @@ describe('FlowSettings.open rendering behavior', () => {
|
|
|
1223
1278
|
|
|
1224
1279
|
// Cleanup
|
|
1225
1280
|
document.body.removeChild(mockTarget);
|
|
1226
|
-
|
|
1281
|
+
querySelectorSpy.mockRestore();
|
|
1227
1282
|
});
|
|
1228
1283
|
|
|
1229
1284
|
it('handles embed uiMode when target element is not found', async () => {
|
|
@@ -1232,8 +1287,7 @@ describe('FlowSettings.open rendering behavior', () => {
|
|
|
1232
1287
|
const model = new FlowModel({ uid: 'm-embed-no-target', flowEngine: engine });
|
|
1233
1288
|
|
|
1234
1289
|
// Mock querySelector to return null (target not found)
|
|
1235
|
-
const
|
|
1236
|
-
document.querySelector = vi.fn(() => null);
|
|
1290
|
+
const querySelectorSpy = vi.spyOn(document, 'querySelector').mockReturnValue(null);
|
|
1237
1291
|
|
|
1238
1292
|
const M = model.constructor as any;
|
|
1239
1293
|
M.registerFlow({
|
|
@@ -1266,7 +1320,7 @@ describe('FlowSettings.open rendering behavior', () => {
|
|
|
1266
1320
|
expect(embed).toHaveBeenCalledTimes(1);
|
|
1267
1321
|
|
|
1268
1322
|
// Restore querySelector
|
|
1269
|
-
|
|
1323
|
+
querySelectorSpy.mockRestore();
|
|
1270
1324
|
});
|
|
1271
1325
|
|
|
1272
1326
|
it('handles error in function-based step uiMode gracefully', async () => {
|
|
@@ -14,11 +14,6 @@ import { FlowEngine } from '../flowEngine';
|
|
|
14
14
|
import { FlowEngineProvider, useFlowEngine } from '../provider';
|
|
15
15
|
|
|
16
16
|
describe('FlowEngineProvider/useFlowEngine', () => {
|
|
17
|
-
it('throws without provider', () => {
|
|
18
|
-
const run = () => renderHook(() => useFlowEngine());
|
|
19
|
-
expect(run).toThrow(/FlowEngineProvider/);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
17
|
it('returns engine within provider', () => {
|
|
23
18
|
const engine = new FlowEngine();
|
|
24
19
|
const wrapper = ({ children }: any) => <FlowEngineProvider engine={engine}>{children}</FlowEngineProvider>;
|