@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,242 @@
|
|
|
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, vi } from 'vitest';
|
|
11
|
+
import { observable } from '@formily/reactive';
|
|
12
|
+
|
|
13
|
+
vi.mock('../utils/runjsModuleLoader', async (importOriginal) => {
|
|
14
|
+
const actual = await importOriginal<typeof import('../utils/runjsModuleLoader')>();
|
|
15
|
+
return {
|
|
16
|
+
...actual,
|
|
17
|
+
runjsImportAsync: vi.fn(),
|
|
18
|
+
runjsRequireAsync: vi.fn(),
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
import { runjsImportAsync } from '../utils/runjsModuleLoader';
|
|
23
|
+
import { FlowEngine, FlowRunJSContext } from '..';
|
|
24
|
+
import { externalReactRender } from '../runjsLibs';
|
|
25
|
+
|
|
26
|
+
function newEngine(): FlowEngine {
|
|
27
|
+
const engine = new FlowEngine();
|
|
28
|
+
// 提供最小 api,避免 ctx.auth getter 在打印对象时抛错
|
|
29
|
+
engine.context.defineProperty('api', { value: { auth: { role: 'guest', locale: 'zh-CN', token: '' } } });
|
|
30
|
+
return engine;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
(globalThis as any).__nocobaseImportAsyncCache = undefined;
|
|
35
|
+
(globalThis as any).__nbRunjsRoots = undefined;
|
|
36
|
+
(runjsImportAsync as any).mockReset();
|
|
37
|
+
|
|
38
|
+
if (typeof window !== 'undefined') {
|
|
39
|
+
(window as any).__esm_cdn_base_url__ = 'https://esm.sh';
|
|
40
|
+
(window as any).__esm_cdn_suffix__ = '';
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('RunJS external libs', () => {
|
|
45
|
+
it('should override ctx.React/ReactDOM when importing external react', async () => {
|
|
46
|
+
const engine = newEngine();
|
|
47
|
+
const ctx = new FlowRunJSContext(engine.context);
|
|
48
|
+
|
|
49
|
+
const fakeReact = { createElement: vi.fn(), Fragment: Symbol('Fragment') };
|
|
50
|
+
const fakeReactDOM = { createRoot: vi.fn(() => ({ render: vi.fn(), unmount: vi.fn() })) };
|
|
51
|
+
|
|
52
|
+
(runjsImportAsync as any).mockImplementation(async (url: string) => {
|
|
53
|
+
if (url === 'https://esm.sh/react@18.2.0') return fakeReact;
|
|
54
|
+
if (url === 'https://esm.sh/react-dom@18.2.0/client') return fakeReactDOM;
|
|
55
|
+
throw new Error(`unexpected import url: ${url}`);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await ctx.importAsync('react@18.2.0');
|
|
59
|
+
|
|
60
|
+
expect(ctx.React).toBe(fakeReact);
|
|
61
|
+
expect(ctx.libs.React).toBe(fakeReact);
|
|
62
|
+
expect(ctx.ReactDOM).toBe(fakeReactDOM);
|
|
63
|
+
expect(ctx.libs.ReactDOM).toBe(fakeReactDOM);
|
|
64
|
+
|
|
65
|
+
expect(runjsImportAsync).toHaveBeenNthCalledWith(1, 'https://esm.sh/react@18.2.0');
|
|
66
|
+
expect(runjsImportAsync).toHaveBeenNthCalledWith(2, 'https://esm.sh/react-dom@18.2.0/client');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should override ctx.antd and ctx.libs.antd when importing antd', async () => {
|
|
70
|
+
const engine = newEngine();
|
|
71
|
+
const ctx = new FlowRunJSContext(engine.context);
|
|
72
|
+
|
|
73
|
+
const fakeAntd = { Button: 'Button' };
|
|
74
|
+
|
|
75
|
+
(runjsImportAsync as any).mockImplementation(async (url: string) => {
|
|
76
|
+
if (url === 'https://esm.sh/antd@5.29.3?bundle=1') return fakeAntd;
|
|
77
|
+
throw new Error(`unexpected import url: ${url}`);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await ctx.importAsync('antd@5.29.3');
|
|
81
|
+
|
|
82
|
+
expect(ctx.antd).toBe(fakeAntd);
|
|
83
|
+
expect(ctx.libs.antd).toBe(fakeAntd);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should isolate roots by rendererKey and unmount on ReactDOM switch', () => {
|
|
87
|
+
const engine = newEngine();
|
|
88
|
+
const ctx = new FlowRunJSContext(engine.context);
|
|
89
|
+
|
|
90
|
+
const root1 = { render: vi.fn(), unmount: vi.fn() };
|
|
91
|
+
const root2 = { render: vi.fn(), unmount: vi.fn() };
|
|
92
|
+
|
|
93
|
+
const renderer1 = { createRoot: vi.fn(() => root1) };
|
|
94
|
+
const renderer2 = { createRoot: vi.fn(() => root2) };
|
|
95
|
+
|
|
96
|
+
ctx.defineProperty('ReactDOM', { value: renderer1 });
|
|
97
|
+
|
|
98
|
+
const container = document.createElement('div');
|
|
99
|
+
|
|
100
|
+
ctx.render({ step: 1 }, container);
|
|
101
|
+
ctx.render({ step: 2 }, container);
|
|
102
|
+
|
|
103
|
+
expect(renderer1.createRoot).toHaveBeenCalledTimes(1);
|
|
104
|
+
expect(root1.render).toHaveBeenCalledTimes(2);
|
|
105
|
+
|
|
106
|
+
ctx.defineProperty('ReactDOM', { value: renderer2 });
|
|
107
|
+
ctx.render({ step: 3 }, container);
|
|
108
|
+
|
|
109
|
+
expect(root1.unmount).toHaveBeenCalledTimes(1);
|
|
110
|
+
expect(renderer2.createRoot).toHaveBeenCalledTimes(1);
|
|
111
|
+
expect(root2.render).toHaveBeenCalledTimes(1);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should not reuse roots across different ctx owners when rendererKey is the same', () => {
|
|
115
|
+
const engine1 = newEngine();
|
|
116
|
+
const ctx1 = new FlowRunJSContext(engine1.context);
|
|
117
|
+
|
|
118
|
+
const engine2 = newEngine();
|
|
119
|
+
const ctx2 = new FlowRunJSContext(engine2.context);
|
|
120
|
+
|
|
121
|
+
const root1 = { render: vi.fn(), unmount: vi.fn() };
|
|
122
|
+
const root2 = { render: vi.fn(), unmount: vi.fn() };
|
|
123
|
+
|
|
124
|
+
// 两个 ctx 共享同一个 ReactDOM 实例引用(rendererKey 相同),但 owner 不同;
|
|
125
|
+
// 若错误复用 entry,会导致旧 ctx 的 autorun 闭包继续持有并驱动新渲染,进而泄漏。
|
|
126
|
+
const renderer = {
|
|
127
|
+
createRoot: vi.fn(() => root1),
|
|
128
|
+
};
|
|
129
|
+
(renderer.createRoot as any).mockImplementationOnce(() => root1).mockImplementationOnce(() => root2);
|
|
130
|
+
|
|
131
|
+
ctx1.defineProperty('ReactDOM', { value: renderer });
|
|
132
|
+
ctx2.defineProperty('ReactDOM', { value: renderer });
|
|
133
|
+
|
|
134
|
+
const container = document.createElement('div');
|
|
135
|
+
|
|
136
|
+
ctx1.render({ step: 1 } as any, container);
|
|
137
|
+
ctx2.render({ step: 2 } as any, container);
|
|
138
|
+
|
|
139
|
+
expect(renderer.createRoot).toHaveBeenCalledTimes(2);
|
|
140
|
+
expect(root1.unmount).toHaveBeenCalledTimes(1);
|
|
141
|
+
expect(root2.render).toHaveBeenCalledTimes(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should wrap external antd ConfigProvider and rerender on themeToken change', async () => {
|
|
145
|
+
const engine = newEngine();
|
|
146
|
+
|
|
147
|
+
const themeState = observable.shallow({ token: { colorPrimary: 'red' } });
|
|
148
|
+
engine.context.defineProperty('themeToken', {
|
|
149
|
+
get: () => themeState.token,
|
|
150
|
+
cache: false,
|
|
151
|
+
});
|
|
152
|
+
engine.context.defineProperty('antdConfig', {
|
|
153
|
+
value: {
|
|
154
|
+
theme: { token: { colorPrimary: 'red' } },
|
|
155
|
+
prefixCls: 'ant',
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const ctx = new FlowRunJSContext(engine.context);
|
|
160
|
+
|
|
161
|
+
const createElement = vi.fn((type: any, props: any, ...children: any[]) => ({ type, props, children }));
|
|
162
|
+
const fakeReact = { createElement };
|
|
163
|
+
const fakeAntdConfigProvider = function ConfigProvider() {};
|
|
164
|
+
const fakeAntdApp = function App() {};
|
|
165
|
+
const fakeAntd = { ConfigProvider: fakeAntdConfigProvider, App: fakeAntdApp };
|
|
166
|
+
|
|
167
|
+
const root = { render: vi.fn(), unmount: vi.fn() };
|
|
168
|
+
const fakeReactDOM = { createRoot: vi.fn(() => root) };
|
|
169
|
+
|
|
170
|
+
ctx.defineProperty('React', { value: fakeReact });
|
|
171
|
+
ctx.defineProperty('ReactDOM', { value: fakeReactDOM });
|
|
172
|
+
ctx.defineProperty('antd', { value: fakeAntd });
|
|
173
|
+
|
|
174
|
+
const container = document.createElement('div');
|
|
175
|
+
const vnode = { v: 1 };
|
|
176
|
+
|
|
177
|
+
ctx.render(vnode as any, container);
|
|
178
|
+
|
|
179
|
+
expect(root.render).toHaveBeenCalledTimes(1);
|
|
180
|
+
// createElement is called twice: App first, then ConfigProvider
|
|
181
|
+
expect(createElement).toHaveBeenNthCalledWith(
|
|
182
|
+
2,
|
|
183
|
+
fakeAntdConfigProvider,
|
|
184
|
+
expect.objectContaining({ prefixCls: 'ant' }),
|
|
185
|
+
expect.anything(),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
themeState.token = { colorPrimary: 'blue' };
|
|
189
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
190
|
+
|
|
191
|
+
expect(root.render).toHaveBeenCalledTimes(2);
|
|
192
|
+
|
|
193
|
+
// cleanup (dispose autorun + unmount root)
|
|
194
|
+
ctx.render('', container);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should enhance hooks dispatcher-null TypeError with a helpful hint', () => {
|
|
198
|
+
const original = new TypeError(`Cannot read properties of null (reading 'useMemo')`);
|
|
199
|
+
// Mimic a real browser stack from ESM CDN where a dependency brings its own React.
|
|
200
|
+
(original as any).stack = [
|
|
201
|
+
`TypeError: Cannot read properties of null (reading 'useMemo')`,
|
|
202
|
+
` at u.useMemo (https://esm.sh/react@19.2.4/es2022/react.mjs:2:7636)`,
|
|
203
|
+
` at to (https://esm.sh/@dnd-kit/core@6.1.0/es2022/core.mjs:6:1574)`,
|
|
204
|
+
].join('\n');
|
|
205
|
+
|
|
206
|
+
const root = {
|
|
207
|
+
render: vi.fn(() => {
|
|
208
|
+
throw original;
|
|
209
|
+
}),
|
|
210
|
+
unmount: vi.fn(),
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const internalReact = {};
|
|
214
|
+
const internalAntd = {};
|
|
215
|
+
const ctx: any = {
|
|
216
|
+
React: internalReact,
|
|
217
|
+
ReactDOM: { __nbRunjsInternalShim: true },
|
|
218
|
+
antd: internalAntd,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const entry: any = { root };
|
|
222
|
+
const containerEl = document.createElement('div');
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
externalReactRender({
|
|
226
|
+
ctx,
|
|
227
|
+
entry,
|
|
228
|
+
vnode: { v: 1 },
|
|
229
|
+
containerEl,
|
|
230
|
+
rootMap: new WeakMap(),
|
|
231
|
+
unmountContainerRoot: vi.fn(),
|
|
232
|
+
internalReact,
|
|
233
|
+
internalAntd,
|
|
234
|
+
});
|
|
235
|
+
expect.fail('expected externalReactRender to throw');
|
|
236
|
+
} catch (e: any) {
|
|
237
|
+
expect(String(e?.message || '')).toContain('[RunJS Hint]');
|
|
238
|
+
expect(String(e?.message || '')).toContain('await ctx.importAsync("react@19.2.4")');
|
|
239
|
+
expect(e?.cause).toBe(original);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
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, it, expect } from 'vitest';
|
|
11
|
+
import { FlowEngine } from '../flowEngine';
|
|
12
|
+
import { registerRunJSLib } from '../runjsLibs';
|
|
13
|
+
|
|
14
|
+
describe('RunJS ctx.libs lazy loading', () => {
|
|
15
|
+
it('preloads member access via prepareRunJsCode injection', async () => {
|
|
16
|
+
registerRunJSLib('testLib', async () => ({ foo: 123 }));
|
|
17
|
+
|
|
18
|
+
const engine = new FlowEngine();
|
|
19
|
+
const ctx: any = engine.context;
|
|
20
|
+
const r = await ctx.runjs(`return ctx.libs.testLib.foo;`);
|
|
21
|
+
expect(r.success).toBe(true);
|
|
22
|
+
expect(r.value).toBe(123);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('preloads bracket access via prepareRunJsCode injection', async () => {
|
|
26
|
+
registerRunJSLib('testLib', async () => ({ foo: 456 }));
|
|
27
|
+
|
|
28
|
+
const engine = new FlowEngine();
|
|
29
|
+
const ctx: any = engine.context;
|
|
30
|
+
const r = await ctx.runjs(`return ctx.libs['testLib'].foo;`);
|
|
31
|
+
expect(r.success).toBe(true);
|
|
32
|
+
expect(r.value).toBe(456);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('preloads object destructuring via prepareRunJsCode injection', async () => {
|
|
36
|
+
registerRunJSLib('testLib', async () => ({ foo: 789 }));
|
|
37
|
+
|
|
38
|
+
const engine = new FlowEngine();
|
|
39
|
+
const ctx: any = engine.context;
|
|
40
|
+
const r = await ctx.runjs(`const { testLib } = ctx.libs; return testLib.foo;`);
|
|
41
|
+
expect(r.success).toBe(true);
|
|
42
|
+
expect(r.value).toBe(789);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
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, it, expect } from 'vitest';
|
|
11
|
+
import { FlowEngine } from '../flowEngine';
|
|
12
|
+
import { prepareRunJsCode } from '../utils/runjsTemplateCompat';
|
|
13
|
+
|
|
14
|
+
describe('ctx.runjs preprocessTemplates default', () => {
|
|
15
|
+
it('enables template preprocess by default', async () => {
|
|
16
|
+
const engine = new FlowEngine();
|
|
17
|
+
const ctx = engine.context as any;
|
|
18
|
+
ctx.defineProperty('user', { value: { id: 123 } });
|
|
19
|
+
|
|
20
|
+
const r1 = await ctx.runjs('return {{ctx.user.id}};');
|
|
21
|
+
expect(r1.success).toBe(true);
|
|
22
|
+
expect(r1.value).toBe(123);
|
|
23
|
+
|
|
24
|
+
const r2 = await ctx.runjs('return "{{ctx.user.id}}";');
|
|
25
|
+
expect(r2.success).toBe(true);
|
|
26
|
+
expect(r2.value).toBe('123');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('can disable template preprocess explicitly', async () => {
|
|
30
|
+
const engine = new FlowEngine();
|
|
31
|
+
const ctx = engine.context as any;
|
|
32
|
+
ctx.defineProperty('user', { value: { id: 123 } });
|
|
33
|
+
|
|
34
|
+
const r = await ctx.runjs('return "{{ctx.user.id}}";', undefined, { preprocessTemplates: false });
|
|
35
|
+
expect(r.success).toBe(true);
|
|
36
|
+
expect(r.value).toBe('{{ctx.user.id}}');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('does not double-preprocess already prepared code', async () => {
|
|
40
|
+
const engine = new FlowEngine();
|
|
41
|
+
const ctx = engine.context as any;
|
|
42
|
+
ctx.defineProperty('user', { value: { id: 123 } });
|
|
43
|
+
|
|
44
|
+
const prepared = await prepareRunJsCode('return "{{ctx.user.id}}";', { preprocessTemplates: true });
|
|
45
|
+
const r = await ctx.runjs(prepared);
|
|
46
|
+
expect(r.success).toBe(true);
|
|
47
|
+
expect(r.value).toBe('123');
|
|
48
|
+
});
|
|
49
|
+
});
|
package/src/acl/Acl.tsx
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
|
-
import { omit } from 'lodash';
|
|
9
|
+
import _, { omit } from 'lodash';
|
|
10
10
|
import { FlowEngine } from '../flowEngine';
|
|
11
11
|
import { FlowModel } from '../models/flowModel';
|
|
12
12
|
|
|
@@ -31,11 +31,11 @@ export class ACL {
|
|
|
31
31
|
constructor(private flowEngine: FlowEngine) {}
|
|
32
32
|
|
|
33
33
|
setData(data: Record<string, any>) {
|
|
34
|
-
this.data = data;
|
|
34
|
+
this.data = _.cloneDeep(data);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
setMeta(data: Record<string, any>) {
|
|
38
|
-
this.meta = data;
|
|
38
|
+
this.meta = _.cloneDeep(data);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
async load() {
|
|
@@ -19,9 +19,14 @@ import {
|
|
|
19
19
|
|
|
20
20
|
const rect = { top: 0, left: 0, width: 100, height: 100 };
|
|
21
21
|
|
|
22
|
-
const createLayout = (
|
|
22
|
+
const createLayout = (
|
|
23
|
+
rows: Record<string, string[][]>,
|
|
24
|
+
sizes: Record<string, number[]>,
|
|
25
|
+
rowOrder?: string[],
|
|
26
|
+
): GridLayoutData => ({
|
|
23
27
|
rows,
|
|
24
28
|
sizes,
|
|
29
|
+
rowOrder,
|
|
25
30
|
});
|
|
26
31
|
|
|
27
32
|
describe('getSlotKey', () => {
|
|
@@ -275,6 +280,7 @@ describe('simulateLayoutForSlot', () => {
|
|
|
275
280
|
rowA: [24],
|
|
276
281
|
rowB: [24],
|
|
277
282
|
},
|
|
283
|
+
['rowA', 'rowB'],
|
|
278
284
|
);
|
|
279
285
|
|
|
280
286
|
const slot: LayoutSlot = {
|
|
@@ -315,6 +321,33 @@ describe('simulateLayoutForSlot', () => {
|
|
|
315
321
|
expect(result.sizes['row-new']).toEqual([24]);
|
|
316
322
|
});
|
|
317
323
|
|
|
324
|
+
it('removes empty source row when moving item into empty container slot', () => {
|
|
325
|
+
const layout = createLayout(
|
|
326
|
+
{
|
|
327
|
+
rowA: [['block-x']],
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
rowA: [24],
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const slot: LayoutSlot = {
|
|
335
|
+
type: 'empty-row',
|
|
336
|
+
rect,
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const result = simulateLayoutForSlot({
|
|
340
|
+
slot,
|
|
341
|
+
sourceUid: 'block-x',
|
|
342
|
+
layout,
|
|
343
|
+
generateRowId: () => 'row-new',
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
expect(result.rows['row-new']).toEqual([['block-x']]);
|
|
347
|
+
expect(result.rows.rowA).toBeUndefined();
|
|
348
|
+
expect(result.sizes.rowA).toBeUndefined();
|
|
349
|
+
});
|
|
350
|
+
|
|
318
351
|
it('handles column slot with after position', () => {
|
|
319
352
|
const layout = createLayout(
|
|
320
353
|
{
|
|
@@ -373,6 +406,7 @@ describe('simulateLayoutForSlot', () => {
|
|
|
373
406
|
rowA: [24],
|
|
374
407
|
rowB: [24],
|
|
375
408
|
},
|
|
409
|
+
['rowA', 'rowB'],
|
|
376
410
|
);
|
|
377
411
|
|
|
378
412
|
const slot: LayoutSlot = {
|
|
@@ -392,6 +426,112 @@ describe('simulateLayoutForSlot', () => {
|
|
|
392
426
|
expect(Object.keys(result.rows)).toEqual(['rowA', 'row-inserted', 'rowB']);
|
|
393
427
|
});
|
|
394
428
|
|
|
429
|
+
it('inserts row into rowOrder when dropping below target row', () => {
|
|
430
|
+
const layout = createLayout(
|
|
431
|
+
{
|
|
432
|
+
rowA: [['a']],
|
|
433
|
+
rowB: [['b']],
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
rowA: [24],
|
|
437
|
+
rowB: [24],
|
|
438
|
+
},
|
|
439
|
+
['rowA', 'rowB'],
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
const slot: LayoutSlot = {
|
|
443
|
+
type: 'row-gap',
|
|
444
|
+
targetRowId: 'rowA',
|
|
445
|
+
position: 'below',
|
|
446
|
+
rect,
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const result = simulateLayoutForSlot({
|
|
450
|
+
slot,
|
|
451
|
+
sourceUid: 'c',
|
|
452
|
+
layout,
|
|
453
|
+
generateRowId: () => 'row-new',
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
expect(result.rowOrder).toEqual(['rowA', 'row-new', 'rowB']);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('maintains rowOrder and inserts new row before target when provided', () => {
|
|
460
|
+
const layout = createLayout(
|
|
461
|
+
{
|
|
462
|
+
rowA: [['a']],
|
|
463
|
+
rowB: [['b']],
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
rowA: [24],
|
|
467
|
+
rowB: [24],
|
|
468
|
+
},
|
|
469
|
+
['rowA', 'rowB'],
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
const slot: LayoutSlot = {
|
|
473
|
+
type: 'row-gap',
|
|
474
|
+
targetRowId: 'rowB',
|
|
475
|
+
position: 'above',
|
|
476
|
+
rect,
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const result = simulateLayoutForSlot({
|
|
480
|
+
slot,
|
|
481
|
+
sourceUid: 'c',
|
|
482
|
+
layout,
|
|
483
|
+
generateRowId: () => 'row-new',
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
expect(result.rowOrder).toEqual(['rowA', 'row-new', 'rowB']);
|
|
487
|
+
expect(result.rows).toEqual({
|
|
488
|
+
rowA: [['a']],
|
|
489
|
+
'row-new': [['c']],
|
|
490
|
+
rowB: [['b']],
|
|
491
|
+
});
|
|
492
|
+
expect(result.sizes).toEqual({
|
|
493
|
+
rowA: [24],
|
|
494
|
+
'row-new': [24],
|
|
495
|
+
rowB: [24],
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('derives rowOrder from rows when missing and removes empty rows from order', () => {
|
|
500
|
+
const layout = createLayout(
|
|
501
|
+
{
|
|
502
|
+
row1: [['a']],
|
|
503
|
+
row2: [['b']],
|
|
504
|
+
row3: [['c']],
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
row1: [24],
|
|
508
|
+
row2: [24],
|
|
509
|
+
row3: [24],
|
|
510
|
+
},
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
const slot: LayoutSlot = {
|
|
514
|
+
type: 'column',
|
|
515
|
+
rowId: 'row1',
|
|
516
|
+
columnIndex: 0,
|
|
517
|
+
insertIndex: 0,
|
|
518
|
+
position: 'before',
|
|
519
|
+
rect,
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const result = simulateLayoutForSlot({ slot, sourceUid: 'b', layout });
|
|
523
|
+
|
|
524
|
+
expect(result.rowOrder).toEqual(['row1', 'row3']);
|
|
525
|
+
expect(result.rows).toEqual({
|
|
526
|
+
row1: [['b', 'a']],
|
|
527
|
+
row3: [['c']],
|
|
528
|
+
});
|
|
529
|
+
expect(result.sizes).toEqual({
|
|
530
|
+
row1: [24],
|
|
531
|
+
row3: [24],
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
395
535
|
it('handles empty-column slot by replacing empty column', () => {
|
|
396
536
|
const layout = createLayout(
|
|
397
537
|
{
|
|
@@ -46,6 +46,7 @@ export interface Point {
|
|
|
46
46
|
export interface GridLayoutData {
|
|
47
47
|
rows: Record<string, string[][]>;
|
|
48
48
|
sizes: Record<string, number[]>;
|
|
49
|
+
rowOrder?: string[];
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export interface ColumnSlot {
|
|
@@ -142,6 +143,49 @@ export interface LayoutSnapshot {
|
|
|
142
143
|
containerRect: Rect;
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
const deriveRowOrder = (rows: Record<string, string[][]>, provided?: string[]) => {
|
|
147
|
+
const order: string[] = [];
|
|
148
|
+
const used = new Set<string>();
|
|
149
|
+
|
|
150
|
+
(provided || Object.keys(rows)).forEach((rowId) => {
|
|
151
|
+
if (rows[rowId] && !used.has(rowId)) {
|
|
152
|
+
order.push(rowId);
|
|
153
|
+
used.add(rowId);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
Object.keys(rows).forEach((rowId) => {
|
|
158
|
+
if (!used.has(rowId)) {
|
|
159
|
+
order.push(rowId);
|
|
160
|
+
used.add(rowId);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return order;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const normalizeRowsWithOrder = (rows: Record<string, string[][]>, order: string[]) => {
|
|
168
|
+
const next: Record<string, string[][]> = {};
|
|
169
|
+
order.forEach((rowId) => {
|
|
170
|
+
if (rows[rowId]) {
|
|
171
|
+
next[rowId] = rows[rowId];
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
Object.keys(rows).forEach((rowId) => {
|
|
175
|
+
if (!next[rowId]) {
|
|
176
|
+
next[rowId] = rows[rowId];
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
return next;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const ensureRowOrder = (layout: GridLayoutData) => {
|
|
183
|
+
const order = deriveRowOrder(layout.rows, layout.rowOrder);
|
|
184
|
+
layout.rowOrder = order;
|
|
185
|
+
layout.rows = normalizeRowsWithOrder(layout.rows, order);
|
|
186
|
+
return order;
|
|
187
|
+
};
|
|
188
|
+
|
|
145
189
|
export interface BuildLayoutSnapshotOptions {
|
|
146
190
|
container: HTMLElement | null;
|
|
147
191
|
}
|
|
@@ -465,10 +509,12 @@ const removeItemFromLayout = (layout: GridLayoutData, uidValue: string) => {
|
|
|
465
509
|
if (columns.length === 0) {
|
|
466
510
|
delete layout.rows[rowId];
|
|
467
511
|
delete layout.sizes[rowId];
|
|
512
|
+
ensureRowOrder(layout);
|
|
468
513
|
return;
|
|
469
514
|
}
|
|
470
515
|
|
|
471
516
|
normalizeRowSizes(rowId, layout);
|
|
517
|
+
ensureRowOrder(layout);
|
|
472
518
|
};
|
|
473
519
|
|
|
474
520
|
const toIntSizes = (weights: number[], count: number): number[] => {
|
|
@@ -592,8 +638,10 @@ export const simulateLayoutForSlot = ({
|
|
|
592
638
|
const cloned: GridLayoutData = {
|
|
593
639
|
rows: _.cloneDeep(layout.rows),
|
|
594
640
|
sizes: _.cloneDeep(layout.sizes),
|
|
641
|
+
rowOrder: layout.rowOrder ? [...layout.rowOrder] : undefined,
|
|
595
642
|
};
|
|
596
643
|
|
|
644
|
+
ensureRowOrder(cloned);
|
|
597
645
|
removeItemFromLayout(cloned, sourceUid);
|
|
598
646
|
|
|
599
647
|
const createRowId = generateRowId ?? uid;
|
|
@@ -638,8 +686,16 @@ export const simulateLayoutForSlot = ({
|
|
|
638
686
|
case 'row-gap': {
|
|
639
687
|
const newRowId = createRowId();
|
|
640
688
|
const rowPosition: 'before' | 'after' = slot.position === 'above' ? 'before' : 'after';
|
|
689
|
+
const currentOrder = deriveRowOrder(cloned.rows, cloned.rowOrder);
|
|
641
690
|
cloned.rows = insertRow(cloned.rows, slot.targetRowId, newRowId, rowPosition, [[sourceUid]]);
|
|
642
691
|
cloned.sizes[newRowId] = [DEFAULT_GRID_COLUMNS];
|
|
692
|
+
const targetIndex = currentOrder.indexOf(slot.targetRowId);
|
|
693
|
+
const insertIndex =
|
|
694
|
+
targetIndex === -1 ? currentOrder.length : rowPosition === 'before' ? targetIndex : targetIndex + 1;
|
|
695
|
+
const nextOrder = [...currentOrder];
|
|
696
|
+
nextOrder.splice(insertIndex, 0, newRowId);
|
|
697
|
+
cloned.rowOrder = nextOrder;
|
|
698
|
+
cloned.rows = normalizeRowsWithOrder(cloned.rows, nextOrder);
|
|
643
699
|
break;
|
|
644
700
|
}
|
|
645
701
|
case 'empty-row': {
|
|
@@ -649,11 +705,15 @@ export const simulateLayoutForSlot = ({
|
|
|
649
705
|
[newRowId]: [[sourceUid]],
|
|
650
706
|
};
|
|
651
707
|
cloned.sizes[newRowId] = [DEFAULT_GRID_COLUMNS];
|
|
708
|
+
const currentOrder = deriveRowOrder(cloned.rows, cloned.rowOrder);
|
|
709
|
+
cloned.rowOrder = [...currentOrder.filter((id) => id !== newRowId), newRowId];
|
|
710
|
+
cloned.rows = normalizeRowsWithOrder(cloned.rows, cloned.rowOrder);
|
|
652
711
|
break;
|
|
653
712
|
}
|
|
654
713
|
default:
|
|
655
714
|
break;
|
|
656
715
|
}
|
|
657
716
|
|
|
717
|
+
ensureRowOrder(cloned);
|
|
658
718
|
return cloned;
|
|
659
719
|
};
|