@nocobase/flow-engine 2.1.0-alpha.4 → 2.1.0-alpha.40
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/LICENSE +201 -661
- package/README.md +79 -10
- package/lib/JSRunner.d.ts +10 -1
- package/lib/JSRunner.js +50 -5
- package/lib/ViewScopedFlowEngine.js +5 -1
- package/lib/components/FieldModelRenderer.js +2 -2
- package/lib/components/FlowModelRenderer.d.ts +3 -1
- package/lib/components/FlowModelRenderer.js +12 -6
- package/lib/components/FormItem.d.ts +6 -0
- package/lib/components/FormItem.js +11 -3
- package/lib/components/MobilePopup.js +6 -5
- package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
- package/lib/components/dnd/gridDragPlanner.js +613 -21
- package/lib/components/dnd/index.d.ts +31 -2
- package/lib/components/dnd/index.js +244 -23
- package/lib/components/settings/wrappers/component/SelectWithTitle.d.ts +2 -1
- package/lib/components/settings/wrappers/component/SelectWithTitle.js +14 -12
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +68 -10
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +23 -43
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +352 -295
- package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +16 -2
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +274 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +315 -0
- package/lib/components/subModel/AddSubModelButton.js +27 -1
- package/lib/components/subModel/LazyDropdown.js +96 -39
- package/lib/components/subModel/index.d.ts +1 -0
- package/lib/components/subModel/index.js +19 -0
- package/lib/components/subModel/utils.d.ts +1 -1
- package/lib/components/subModel/utils.js +9 -3
- package/lib/components/variables/VariableHybridInput.d.ts +27 -0
- package/lib/components/variables/VariableHybridInput.js +499 -0
- package/lib/components/variables/index.d.ts +2 -0
- package/lib/components/variables/index.js +3 -0
- package/lib/data-source/index.d.ts +75 -0
- package/lib/data-source/index.js +247 -5
- package/lib/executor/FlowExecutor.js +32 -9
- package/lib/flow-registry/DetachedFlowRegistry.d.ts +21 -0
- package/lib/flow-registry/DetachedFlowRegistry.js +80 -0
- package/lib/flow-registry/index.d.ts +1 -0
- package/lib/flow-registry/index.js +3 -1
- package/lib/flowContext.d.ts +3 -0
- package/lib/flowContext.js +43 -1
- package/lib/flowEngine.d.ts +151 -1
- package/lib/flowEngine.js +389 -15
- package/lib/flowI18n.js +2 -1
- package/lib/flowSettings.d.ts +14 -6
- package/lib/flowSettings.js +34 -6
- package/lib/index.d.ts +2 -0
- package/lib/index.js +7 -0
- package/lib/lazy-helper.d.ts +14 -0
- package/lib/lazy-helper.js +71 -0
- package/lib/locale/en-US.json +1 -0
- package/lib/locale/index.d.ts +2 -0
- package/lib/locale/zh-CN.json +1 -0
- package/lib/models/DisplayItemModel.d.ts +1 -1
- package/lib/models/EditableItemModel.d.ts +1 -1
- package/lib/models/FilterableItemModel.d.ts +1 -1
- package/lib/models/flowModel.d.ts +13 -10
- package/lib/models/flowModel.js +78 -18
- package/lib/provider.js +38 -23
- package/lib/reactive/observer.js +46 -16
- package/lib/runjs-context/registry.d.ts +1 -1
- package/lib/runjs-context/setup.js +20 -12
- package/lib/runjs-context/snippets/index.js +13 -2
- package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
- package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
- package/lib/scheduler/ModelOperationScheduler.d.ts +5 -1
- package/lib/scheduler/ModelOperationScheduler.js +3 -2
- package/lib/types.d.ts +50 -2
- package/lib/types.js +1 -0
- package/lib/utils/createCollectionContextMeta.js +6 -2
- package/lib/utils/index.d.ts +3 -2
- package/lib/utils/index.js +7 -0
- package/lib/utils/parsePathnameToViewParams.js +1 -1
- package/lib/utils/randomId.d.ts +39 -0
- package/lib/utils/randomId.js +45 -0
- package/lib/utils/runjsTemplateCompat.js +1 -1
- package/lib/utils/runjsValue.js +41 -11
- package/lib/utils/schema-utils.d.ts +7 -1
- package/lib/utils/schema-utils.js +19 -0
- package/lib/views/FlowView.d.ts +7 -1
- package/lib/views/FlowView.js +11 -1
- package/lib/views/PageComponent.js +8 -6
- package/lib/views/ViewNavigation.js +6 -2
- package/lib/views/runViewBeforeClose.d.ts +10 -0
- package/lib/views/runViewBeforeClose.js +45 -0
- package/lib/views/useDialog.d.ts +2 -1
- package/lib/views/useDialog.js +20 -3
- package/lib/views/useDrawer.d.ts +2 -1
- package/lib/views/useDrawer.js +20 -3
- package/lib/views/usePage.d.ts +5 -11
- package/lib/views/usePage.js +302 -144
- package/package.json +6 -5
- package/src/JSRunner.ts +68 -4
- package/src/ViewScopedFlowEngine.ts +4 -0
- package/src/__tests__/JSRunner.test.ts +27 -1
- package/src/__tests__/flow-engine.test.ts +166 -0
- package/src/__tests__/flowContext.test.ts +82 -1
- package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
- package/src/__tests__/flowSettings.test.ts +94 -15
- package/src/__tests__/objectVariable.test.ts +24 -0
- package/src/__tests__/provider.test.tsx +24 -2
- package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
- package/src/__tests__/runjsContext.test.ts +16 -0
- package/src/__tests__/runjsContextRuntime.test.ts +2 -0
- package/src/__tests__/runjsPreprocessDefault.test.ts +23 -0
- package/src/__tests__/runjsSnippets.test.ts +21 -0
- package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
- package/src/components/FieldModelRenderer.tsx +2 -1
- package/src/components/FlowModelRenderer.tsx +18 -6
- package/src/components/FormItem.tsx +7 -1
- package/src/components/MobilePopup.tsx +4 -2
- package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
- package/src/components/__tests__/FormItem.test.tsx +25 -0
- package/src/components/__tests__/dnd.test.ts +44 -0
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
- package/src/components/__tests__/gridDragPlanner.test.ts +558 -3
- package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
- package/src/components/dnd/gridDragPlanner.ts +758 -19
- package/src/components/dnd/index.tsx +305 -28
- package/src/components/settings/wrappers/component/SelectWithTitle.tsx +21 -9
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +88 -10
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +487 -440
- package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +18 -2
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +189 -3
- package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +778 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +360 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +361 -0
- package/src/components/subModel/AddSubModelButton.tsx +32 -2
- package/src/components/subModel/LazyDropdown.tsx +107 -43
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +319 -36
- package/src/components/subModel/__tests__/utils.test.ts +24 -0
- package/src/components/subModel/index.ts +1 -0
- package/src/components/subModel/utils.ts +7 -1
- package/src/components/variables/VariableHybridInput.tsx +531 -0
- package/src/components/variables/index.ts +2 -0
- package/src/data-source/__tests__/collection.test.ts +41 -2
- package/src/data-source/__tests__/index.test.ts +68 -1
- package/src/data-source/index.ts +304 -6
- package/src/executor/FlowExecutor.ts +35 -10
- package/src/executor/__tests__/flowExecutor.test.ts +57 -0
- package/src/flow-registry/DetachedFlowRegistry.ts +46 -0
- package/src/flow-registry/__tests__/detachedFlowRegistry.test.ts +47 -0
- package/src/flow-registry/index.ts +1 -0
- package/src/flowContext.ts +47 -3
- package/src/flowEngine.ts +445 -11
- package/src/flowI18n.ts +2 -1
- package/src/flowSettings.ts +40 -6
- package/src/index.ts +2 -0
- package/src/lazy-helper.tsx +57 -0
- package/src/locale/en-US.json +1 -0
- package/src/locale/zh-CN.json +1 -0
- package/src/models/DisplayItemModel.tsx +1 -1
- package/src/models/EditableItemModel.tsx +1 -1
- package/src/models/FilterableItemModel.tsx +1 -1
- package/src/models/__tests__/dispatchEvent.when.test.ts +214 -0
- package/src/models/__tests__/flowModel.test.ts +47 -3
- package/src/models/flowModel.tsx +119 -33
- package/src/provider.tsx +41 -25
- package/src/reactive/__tests__/observer.test.tsx +82 -0
- package/src/reactive/observer.tsx +87 -25
- package/src/runjs-context/registry.ts +1 -1
- package/src/runjs-context/setup.ts +22 -12
- package/src/runjs-context/snippets/index.ts +12 -1
- package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
- package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
- package/src/scheduler/ModelOperationScheduler.ts +14 -3
- package/src/types.ts +62 -0
- package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +7 -0
- package/src/utils/__tests__/runjsValue.test.ts +11 -0
- package/src/utils/__tests__/utils.test.ts +62 -0
- package/src/utils/createCollectionContextMeta.ts +6 -2
- package/src/utils/index.ts +5 -1
- package/src/utils/parsePathnameToViewParams.ts +2 -2
- package/src/utils/randomId.ts +48 -0
- package/src/utils/runjsTemplateCompat.ts +1 -1
- package/src/utils/runjsValue.ts +50 -11
- package/src/utils/schema-utils.ts +30 -1
- package/src/views/FlowView.tsx +22 -2
- package/src/views/PageComponent.tsx +7 -4
- package/src/views/ViewNavigation.ts +6 -2
- package/src/views/__tests__/FlowView.usePage.test.tsx +243 -3
- package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +13 -12
- package/src/views/runViewBeforeClose.ts +19 -0
- package/src/views/useDialog.tsx +25 -3
- package/src/views/useDrawer.tsx +25 -3
- package/src/views/usePage.tsx +365 -179
|
@@ -26,6 +26,7 @@ function setupEngineWithCollections() {
|
|
|
26
26
|
fields: [
|
|
27
27
|
{ name: 'id', type: 'integer', interface: 'number' },
|
|
28
28
|
{ name: 'name', type: 'string', interface: 'text' },
|
|
29
|
+
{ name: 'rawUserPayload', type: 'json', filterable: true },
|
|
29
30
|
],
|
|
30
31
|
});
|
|
31
32
|
ds.addCollection({
|
|
@@ -41,6 +42,8 @@ function setupEngineWithCollections() {
|
|
|
41
42
|
filterTargetKey: 'id',
|
|
42
43
|
fields: [
|
|
43
44
|
{ name: 'title', type: 'string', interface: 'text' },
|
|
45
|
+
{ name: 'internalName', type: 'string', interface: 'text' },
|
|
46
|
+
{ name: 'rawPostPayload', type: 'json', filterable: true },
|
|
44
47
|
{ name: 'author', type: 'belongsTo', target: 'users', interface: 'm2o' },
|
|
45
48
|
{ name: 'tags', type: 'belongsToMany', target: 'tags', interface: 'm2m' },
|
|
46
49
|
],
|
|
@@ -91,6 +94,27 @@ describe('objectVariable utilities', () => {
|
|
|
91
94
|
});
|
|
92
95
|
});
|
|
93
96
|
|
|
97
|
+
it('createAssociationAwareObjectMetaFactory should hide fields without interface from object variable meta', async () => {
|
|
98
|
+
const { collection } = setupEngineWithCollections();
|
|
99
|
+
const obj = { title: 'hello', internalName: 'internal', rawPostPayload: { secret: true }, author: 1 };
|
|
100
|
+
const metaFactory = createAssociationAwareObjectMetaFactory(
|
|
101
|
+
() => collection,
|
|
102
|
+
'Current object',
|
|
103
|
+
() => obj,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const meta = await metaFactory();
|
|
107
|
+
const props = await (meta?.properties as any)?.();
|
|
108
|
+
const authorFields = await props?.author?.properties?.();
|
|
109
|
+
|
|
110
|
+
expect(props).toHaveProperty('title');
|
|
111
|
+
expect(props).toHaveProperty('internalName');
|
|
112
|
+
expect(props).toHaveProperty('author');
|
|
113
|
+
expect(props).not.toHaveProperty('rawPostPayload');
|
|
114
|
+
expect(authorFields).toHaveProperty('name');
|
|
115
|
+
expect(authorFields).not.toHaveProperty('rawUserPayload');
|
|
116
|
+
});
|
|
117
|
+
|
|
94
118
|
it('integrates with FlowContext.resolveJsonTemplate to call variables:resolve with flattened contextParams', async () => {
|
|
95
119
|
const { engine, collection } = setupEngineWithCollections();
|
|
96
120
|
const obj = { author: 1 };
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { describe, expect, it } from 'vitest';
|
|
12
|
-
import { renderHook } from '@testing-library/react';
|
|
12
|
+
import { render, renderHook } from '@testing-library/react';
|
|
13
|
+
import { App, ConfigProvider, theme } from 'antd';
|
|
13
14
|
import { FlowEngine } from '../flowEngine';
|
|
14
|
-
import { FlowEngineProvider, useFlowEngine } from '../provider';
|
|
15
|
+
import { FlowEngineGlobalsContextProvider, FlowEngineProvider, useFlowEngine } from '../provider';
|
|
15
16
|
|
|
16
17
|
describe('FlowEngineProvider/useFlowEngine', () => {
|
|
17
18
|
it('returns engine within provider', () => {
|
|
@@ -20,4 +21,25 @@ describe('FlowEngineProvider/useFlowEngine', () => {
|
|
|
20
21
|
const { result } = renderHook(() => useFlowEngine(), { wrapper });
|
|
21
22
|
expect(result.current).toBe(engine);
|
|
22
23
|
});
|
|
24
|
+
|
|
25
|
+
it('registers isDarkTheme within globals provider before first child render', () => {
|
|
26
|
+
const engine = new FlowEngine();
|
|
27
|
+
const reads: boolean[] = [];
|
|
28
|
+
const Reader = () => {
|
|
29
|
+
reads.push(engine.context.isDarkTheme);
|
|
30
|
+
return null;
|
|
31
|
+
};
|
|
32
|
+
const wrapper = ({ children }: any) => (
|
|
33
|
+
<ConfigProvider theme={{ algorithm: theme.darkAlgorithm }}>
|
|
34
|
+
<App>
|
|
35
|
+
<FlowEngineProvider engine={engine}>
|
|
36
|
+
<FlowEngineGlobalsContextProvider>{children}</FlowEngineGlobalsContextProvider>
|
|
37
|
+
</FlowEngineProvider>
|
|
38
|
+
</App>
|
|
39
|
+
</ConfigProvider>
|
|
40
|
+
);
|
|
41
|
+
render(<Reader />, { wrapper });
|
|
42
|
+
expect(reads[0]).toBe(true);
|
|
43
|
+
expect(engine.context.isDarkTheme).toBe(true);
|
|
44
|
+
});
|
|
23
45
|
});
|
|
@@ -14,7 +14,7 @@ import { FlowEngine } from '../flowEngine';
|
|
|
14
14
|
import { FlowModel, ModelRenderMode } from '../models/flowModel';
|
|
15
15
|
|
|
16
16
|
describe('FlowModel.renderHiddenInConfig', () => {
|
|
17
|
-
it('renders via renderHiddenInConfig when hidden and config enabled (React element mode, mounted)', () => {
|
|
17
|
+
it('renders via renderHiddenInConfig when hidden and config enabled (React element mode, mounted)', async () => {
|
|
18
18
|
class ElemModel extends FlowModel {
|
|
19
19
|
render() {
|
|
20
20
|
return <div data-testid="content">Content</div>;
|
|
@@ -28,14 +28,14 @@ describe('FlowModel.renderHiddenInConfig', () => {
|
|
|
28
28
|
const model = new ElemModel({ uid: 'elem-1', flowEngine: engine });
|
|
29
29
|
|
|
30
30
|
// runtime hidden => mounted result should be empty (no content/hidden)
|
|
31
|
-
engine.flowSettings.disable();
|
|
31
|
+
await engine.flowSettings.disable();
|
|
32
32
|
model.setHidden(true);
|
|
33
33
|
const { container, unmount, rerender } = render(model.render() as React.ReactElement);
|
|
34
34
|
expect(screen.queryByTestId('content')).toBeNull();
|
|
35
35
|
expect(screen.queryByTestId('hidden')).toBeNull();
|
|
36
36
|
|
|
37
37
|
// config enabled + hidden => should show renderHiddenInConfig result
|
|
38
|
-
engine.flowSettings.enable();
|
|
38
|
+
await engine.flowSettings.enable();
|
|
39
39
|
rerender(model.render() as React.ReactElement);
|
|
40
40
|
expect(screen.getByTestId('hidden').textContent).toBe('HiddenViaAPI');
|
|
41
41
|
|
|
@@ -46,7 +46,7 @@ describe('FlowModel.renderHiddenInConfig', () => {
|
|
|
46
46
|
unmount();
|
|
47
47
|
cleanup();
|
|
48
48
|
});
|
|
49
|
-
it('returns a render function when hidden and config enabled (RenderFunction mode)', () => {
|
|
49
|
+
it('returns a render function when hidden and config enabled (RenderFunction mode)', async () => {
|
|
50
50
|
class FuncModel extends FlowModel {
|
|
51
51
|
static override renderMode = ModelRenderMode.RenderFunction;
|
|
52
52
|
render() {
|
|
@@ -63,13 +63,13 @@ describe('FlowModel.renderHiddenInConfig', () => {
|
|
|
63
63
|
const model = engine.createModel({ use: 'FuncModel' }) as FuncModel;
|
|
64
64
|
|
|
65
65
|
// runtime hidden => null
|
|
66
|
-
engine.flowSettings.disable();
|
|
66
|
+
await engine.flowSettings.disable();
|
|
67
67
|
model.setHidden(true);
|
|
68
68
|
const runtimeHidden = model.render();
|
|
69
69
|
expect(runtimeHidden).toBeNull();
|
|
70
70
|
|
|
71
71
|
// config enabled + hidden => renderHiddenInConfig (function)
|
|
72
|
-
engine.flowSettings.enable();
|
|
72
|
+
await engine.flowSettings.enable();
|
|
73
73
|
const cfgHidden = model.render();
|
|
74
74
|
expect(typeof cfgHidden).toBe('function');
|
|
75
75
|
const cellNode = (cfgHidden as any)();
|
|
@@ -29,11 +29,16 @@ describe('flowRunJSContext registry and doc', () => {
|
|
|
29
29
|
expect(RunJSContextRegistry['resolve']('v1' as any, '*')).toBeTruthy();
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
+
it('should register v2 mapping', () => {
|
|
33
|
+
expect(RunJSContextRegistry['resolve']('v2' as any, '*')).toBeTruthy();
|
|
34
|
+
});
|
|
35
|
+
|
|
32
36
|
it('should register all context types', () => {
|
|
33
37
|
const contextTypes = [
|
|
34
38
|
'JSBlockModel',
|
|
35
39
|
'JSFieldModel',
|
|
36
40
|
'JSItemModel',
|
|
41
|
+
'JSItemActionModel',
|
|
37
42
|
'JSColumnModel',
|
|
38
43
|
'FormJSFieldItemModel',
|
|
39
44
|
'JSRecordActionModel',
|
|
@@ -44,12 +49,22 @@ describe('flowRunJSContext registry and doc', () => {
|
|
|
44
49
|
const ctor = RunJSContextRegistry['resolve']('v1' as any, modelClass);
|
|
45
50
|
expect(ctor).toBeTruthy();
|
|
46
51
|
});
|
|
52
|
+
|
|
53
|
+
contextTypes.forEach((modelClass) => {
|
|
54
|
+
const ctor = RunJSContextRegistry['resolve']('v2' as any, modelClass);
|
|
55
|
+
expect(ctor).toBeTruthy();
|
|
56
|
+
});
|
|
47
57
|
});
|
|
48
58
|
|
|
49
59
|
it('should expose scene metadata for contexts', () => {
|
|
50
60
|
expect(getRunJSScenesForModel('JSBlockModel', 'v1')).toEqual(['block']);
|
|
51
61
|
expect(getRunJSScenesForModel('JSFieldModel', 'v1')).toEqual(['detail']);
|
|
62
|
+
expect(getRunJSScenesForModel('JSItemActionModel', 'v1')).toEqual(['table']);
|
|
63
|
+
expect(getRunJSScenesForModel('JSBlockModel', 'v2')).toEqual(['block']);
|
|
64
|
+
expect(getRunJSScenesForModel('JSFieldModel', 'v2')).toEqual(['detail']);
|
|
65
|
+
expect(getRunJSScenesForModel('JSItemActionModel', 'v2')).toEqual(['table']);
|
|
52
66
|
expect(getRunJSScenesForModel('UnknownModel', 'v1')).toEqual([]);
|
|
67
|
+
expect(getRunJSScenesForModel('UnknownModel', 'v2')).toEqual([]);
|
|
53
68
|
});
|
|
54
69
|
|
|
55
70
|
it('should only execute once (idempotent)', async () => {
|
|
@@ -175,6 +190,7 @@ describe('flowRunJSContext registry and doc', () => {
|
|
|
175
190
|
const ctx = new FlowContext();
|
|
176
191
|
ctx.defineProperty('model', { value: { constructor: { name: 'JSColumnModel' } } });
|
|
177
192
|
expect(getRunJSScenesForContext(ctx as any, { version: 'v1' })).toEqual(['table']);
|
|
193
|
+
expect(getRunJSScenesForContext(ctx as any, { version: 'v2' })).toEqual(['table']);
|
|
178
194
|
});
|
|
179
195
|
|
|
180
196
|
it('JSBlockModel context should have element property in doc', () => {
|
|
@@ -186,6 +186,7 @@ describe('RunJS Context Runtime Behavior', () => {
|
|
|
186
186
|
'JSBlockModel',
|
|
187
187
|
'JSFieldModel',
|
|
188
188
|
'JSItemModel',
|
|
189
|
+
'JSItemActionModel',
|
|
189
190
|
'JSColumnModel',
|
|
190
191
|
'FormJSFieldItemModel',
|
|
191
192
|
'JSRecordActionModel',
|
|
@@ -237,6 +238,7 @@ describe('RunJS Context Runtime Behavior', () => {
|
|
|
237
238
|
'JSBlockModel',
|
|
238
239
|
'JSFieldModel',
|
|
239
240
|
'JSItemModel',
|
|
241
|
+
'JSItemActionModel',
|
|
240
242
|
'JSColumnModel',
|
|
241
243
|
'FormJSFieldItemModel',
|
|
242
244
|
'JSRecordActionModel',
|
|
@@ -36,6 +36,29 @@ describe('ctx.runjs preprocessTemplates default', () => {
|
|
|
36
36
|
expect(r.value).toBe('{{ctx.user.id}}');
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
+
it('disables template preprocess by default for version v2', async () => {
|
|
40
|
+
const engine = new FlowEngine();
|
|
41
|
+
const ctx = engine.context as any;
|
|
42
|
+
ctx.defineProperty('user', { value: { id: 123 } });
|
|
43
|
+
|
|
44
|
+
const r = await ctx.runjs('return "{{ctx.user.id}}";', undefined, { version: 'v2' });
|
|
45
|
+
expect(r.success).toBe(true);
|
|
46
|
+
expect(r.value).toBe('{{ctx.user.id}}');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('keeps explicit preprocessTemplates override higher priority than version', async () => {
|
|
50
|
+
const engine = new FlowEngine();
|
|
51
|
+
const ctx = engine.context as any;
|
|
52
|
+
ctx.defineProperty('user', { value: { id: 123 } });
|
|
53
|
+
|
|
54
|
+
const r = await ctx.runjs('return "{{ctx.user.id}}";', undefined, {
|
|
55
|
+
version: 'v2',
|
|
56
|
+
preprocessTemplates: true,
|
|
57
|
+
});
|
|
58
|
+
expect(r.success).toBe(true);
|
|
59
|
+
expect(r.value).toBe('123');
|
|
60
|
+
});
|
|
61
|
+
|
|
39
62
|
it('does not double-preprocess already prepared code', async () => {
|
|
40
63
|
const engine = new FlowEngine();
|
|
41
64
|
const ctx = engine.context as any;
|
|
@@ -112,6 +112,27 @@ describe('RunJS Snippets', () => {
|
|
|
112
112
|
expect(multiScene?.scenes).toEqual(expect.arrayContaining(['detail', 'table']));
|
|
113
113
|
expect(multiScene?.groups).toEqual(expect.arrayContaining(['scene/detail', 'scene/table']));
|
|
114
114
|
});
|
|
115
|
+
|
|
116
|
+
it('should expose new style snippets for matching contexts', async () => {
|
|
117
|
+
const tableSnippets = await listSnippetsForContext('JSColumnRunJSContext', 'v1', 'zh-CN');
|
|
118
|
+
const fieldSnippets = await listSnippetsForContext('FormJSFieldItemRunJSContext', 'v1', 'zh-CN');
|
|
119
|
+
const detailEventSnippets = await listSnippetsForContext('DetailsItemModel', 'v1', 'zh-CN');
|
|
120
|
+
const tableEventSnippets = await listSnippetsForContext('TableColumnModel', 'v1', 'zh-CN');
|
|
121
|
+
|
|
122
|
+
const tableStyle = tableSnippets.find((s) => s.ref === 'scene/table/set-cell-style');
|
|
123
|
+
expect(tableStyle?.name).toBe('表格字段样式设置');
|
|
124
|
+
expect(tableStyle?.body).toContain('ctx.model.props.onCell');
|
|
125
|
+
expect(tableStyle?.scenes).toEqual(['tableFieldEvent']);
|
|
126
|
+
|
|
127
|
+
const fieldStyle = fieldSnippets.find((s) => s.ref === 'scene/detail/set-field-style');
|
|
128
|
+
expect(fieldStyle?.name).toBe('设置表单项/详情项样式');
|
|
129
|
+
expect(fieldStyle?.body).toContain('ctx.model.props.style');
|
|
130
|
+
expect(fieldStyle?.scenes).toEqual(expect.arrayContaining(['detailFieldEvent', 'formFieldEvent']));
|
|
131
|
+
expect(fieldStyle?.groups).toEqual(expect.arrayContaining(['scene/detail', 'scene/form']));
|
|
132
|
+
|
|
133
|
+
expect(detailEventSnippets.some((s) => s.ref === 'scene/detail/set-field-style')).toBe(true);
|
|
134
|
+
expect(tableEventSnippets.some((s) => s.ref === 'scene/table/set-cell-style')).toBe(true);
|
|
135
|
+
});
|
|
115
136
|
});
|
|
116
137
|
|
|
117
138
|
describe('New snippets', () => {
|
|
@@ -256,11 +256,11 @@ describe('ViewScopedFlowEngine', () => {
|
|
|
256
256
|
|
|
257
257
|
// Both children should return null from hydration because parent has flowSettingsEnabled
|
|
258
258
|
// This is the bug fix: previously only children with their own flowSettingsEnabled would return null
|
|
259
|
-
const result1 = (scoped as any).hydrateModelFromPreviousEngines({
|
|
259
|
+
const result1 = await (scoped as any).hydrateModelFromPreviousEngines({
|
|
260
260
|
parentId: 'parent-with-settings',
|
|
261
261
|
subKey: 'popup',
|
|
262
262
|
});
|
|
263
|
-
const result2 = (scoped as any).hydrateModelFromPreviousEngines({
|
|
263
|
+
const result2 = await (scoped as any).hydrateModelFromPreviousEngines({
|
|
264
264
|
parentId: 'parent-with-settings',
|
|
265
265
|
subKey: 'items',
|
|
266
266
|
});
|
|
@@ -298,7 +298,7 @@ describe('ViewScopedFlowEngine', () => {
|
|
|
298
298
|
const scoped = createViewScopedEngine(root);
|
|
299
299
|
|
|
300
300
|
// Call the private method hydrateModelFromPreviousEngines directly
|
|
301
|
-
const result = (scoped as any).hydrateModelFromPreviousEngines({
|
|
301
|
+
const result = await (scoped as any).hydrateModelFromPreviousEngines({
|
|
302
302
|
parentId: 'parent-normal',
|
|
303
303
|
subKey: 'content',
|
|
304
304
|
});
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import type { FlowModelRendererProps } from './FlowModelRenderer';
|
|
11
|
+
import { FlowModelRenderer } from './FlowModelRenderer';
|
|
11
12
|
import _ from 'lodash';
|
|
12
13
|
import React, { useEffect, useMemo, useRef } from 'react';
|
|
13
14
|
|
|
@@ -69,7 +69,9 @@ export interface FlowModelRendererProps {
|
|
|
69
69
|
showBackground?: boolean;
|
|
70
70
|
showBorder?: boolean;
|
|
71
71
|
showDragHandle?: boolean;
|
|
72
|
-
/**
|
|
72
|
+
/** 是否显示事件流入口,默认 true */
|
|
73
|
+
showDynamicFlowsEditor?: boolean;
|
|
74
|
+
/** 自定义工具栏样式,`top/left/right/bottom` 会作为 portal overlay 的 inset 使用 */
|
|
73
75
|
style?: React.CSSProperties;
|
|
74
76
|
/**
|
|
75
77
|
* @default 'inside'
|
|
@@ -112,6 +114,8 @@ const FlowModelRendererWithAutoFlows: React.FC<{
|
|
|
112
114
|
showBackground?: boolean;
|
|
113
115
|
showBorder?: boolean;
|
|
114
116
|
showDragHandle?: boolean;
|
|
117
|
+
showDynamicFlowsEditor?: boolean;
|
|
118
|
+
/** `top/left/right/bottom` 会作为 portal overlay 的 inset 使用 */
|
|
115
119
|
style?: React.CSSProperties;
|
|
116
120
|
/**
|
|
117
121
|
* @default 'inside'
|
|
@@ -126,6 +130,7 @@ const FlowModelRendererWithAutoFlows: React.FC<{
|
|
|
126
130
|
settingsMenuLevel?: number;
|
|
127
131
|
extraToolbarItems?: ToolbarItemConfig[];
|
|
128
132
|
fallback?: React.ReactNode;
|
|
133
|
+
useCache?: boolean;
|
|
129
134
|
}> = observer(
|
|
130
135
|
({
|
|
131
136
|
model,
|
|
@@ -138,12 +143,12 @@ const FlowModelRendererWithAutoFlows: React.FC<{
|
|
|
138
143
|
settingsMenuLevel,
|
|
139
144
|
extraToolbarItems,
|
|
140
145
|
fallback,
|
|
146
|
+
useCache,
|
|
141
147
|
}) => {
|
|
142
148
|
// hidden 占位由模型自身处理;无需在此注入
|
|
143
|
-
|
|
144
149
|
const { loading: pending, error: autoFlowsError } = useApplyAutoFlows(model, inputArgs, {
|
|
145
150
|
throwOnError: false,
|
|
146
|
-
useCache
|
|
151
|
+
useCache,
|
|
147
152
|
});
|
|
148
153
|
// 将错误下沉到 model 实例上,供内容层读取(类型安全的 WeakMap 存储)
|
|
149
154
|
setAutoFlowError(model, autoFlowsError || null);
|
|
@@ -182,6 +187,8 @@ const FlowModelRendererCore: React.FC<{
|
|
|
182
187
|
showBackground?: boolean;
|
|
183
188
|
showBorder?: boolean;
|
|
184
189
|
showDragHandle?: boolean;
|
|
190
|
+
showDynamicFlowsEditor?: boolean;
|
|
191
|
+
/** `top/left/right/bottom` 会作为 portal overlay 的 inset 使用 */
|
|
185
192
|
style?: React.CSSProperties;
|
|
186
193
|
/**
|
|
187
194
|
* @default 'inside'
|
|
@@ -251,6 +258,7 @@ const FlowModelRendererCore: React.FC<{
|
|
|
251
258
|
showBackground={_.isObject(showFlowSettings) ? showFlowSettings.showBackground : undefined}
|
|
252
259
|
showBorder={_.isObject(showFlowSettings) ? showFlowSettings.showBorder : undefined}
|
|
253
260
|
showDragHandle={_.isObject(showFlowSettings) ? showFlowSettings.showDragHandle : undefined}
|
|
261
|
+
showDynamicFlowsEditor={_.isObject(showFlowSettings) ? showFlowSettings.showDynamicFlowsEditor : undefined}
|
|
254
262
|
settingsMenuLevel={settingsMenuLevel}
|
|
255
263
|
extraToolbarItems={extraToolbarItems}
|
|
256
264
|
toolbarStyle={_.isObject(showFlowSettings) ? showFlowSettings.style : undefined}
|
|
@@ -297,6 +305,7 @@ const FlowModelRendererCore: React.FC<{
|
|
|
297
305
|
showBackground={_.isObject(showFlowSettings) ? showFlowSettings.showBackground : undefined}
|
|
298
306
|
showBorder={_.isObject(showFlowSettings) ? showFlowSettings.showBorder : undefined}
|
|
299
307
|
showDragHandle={_.isObject(showFlowSettings) ? showFlowSettings.showDragHandle : undefined}
|
|
308
|
+
showDynamicFlowsEditor={_.isObject(showFlowSettings) ? showFlowSettings.showDynamicFlowsEditor : undefined}
|
|
300
309
|
settingsMenuLevel={settingsMenuLevel}
|
|
301
310
|
extraToolbarItems={extraToolbarItems}
|
|
302
311
|
toolbarStyle={_.isObject(showFlowSettings) ? showFlowSettings.style : undefined}
|
|
@@ -346,13 +355,15 @@ export const FlowModelRenderer: React.FC<FlowModelRendererProps> = observer(
|
|
|
346
355
|
extraToolbarItems,
|
|
347
356
|
useCache,
|
|
348
357
|
}) => {
|
|
358
|
+
const resolvedUseCache = typeof useCache === 'boolean' ? useCache : model?.context?.useCache;
|
|
359
|
+
|
|
349
360
|
useEffect(() => {
|
|
350
|
-
if (model?.context) {
|
|
361
|
+
if (model?.context && typeof resolvedUseCache !== 'undefined') {
|
|
351
362
|
model.context.defineProperty('useCache', {
|
|
352
|
-
value:
|
|
363
|
+
value: resolvedUseCache,
|
|
353
364
|
});
|
|
354
365
|
}
|
|
355
|
-
}, [model?.context,
|
|
366
|
+
}, [model?.context, resolvedUseCache]);
|
|
356
367
|
|
|
357
368
|
if (!model || typeof model.render !== 'function') {
|
|
358
369
|
// 可以选择渲染 null 或者一个错误/提示信息
|
|
@@ -373,6 +384,7 @@ export const FlowModelRenderer: React.FC<FlowModelRendererProps> = observer(
|
|
|
373
384
|
settingsMenuLevel={settingsMenuLevel}
|
|
374
385
|
extraToolbarItems={extraToolbarItems}
|
|
375
386
|
fallback={fallback}
|
|
387
|
+
useCache={resolvedUseCache}
|
|
376
388
|
/>
|
|
377
389
|
);
|
|
378
390
|
|
|
@@ -19,6 +19,9 @@ interface ExtendedFormItemProps extends FormItemProps {
|
|
|
19
19
|
showLabel?: boolean;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export const verticalFormItemLabelStyle = { paddingBottom: 0 };
|
|
23
|
+
export const formItemStyle = { marginBottom: 12 };
|
|
24
|
+
|
|
22
25
|
const formItemPropKeys: (keyof ExtendedFormItemProps)[] = [
|
|
23
26
|
'colon',
|
|
24
27
|
'dependencies',
|
|
@@ -73,6 +76,8 @@ export const FormItem = ({
|
|
|
73
76
|
});
|
|
74
77
|
const { label, labelWrap, colon = true, layout } = rest;
|
|
75
78
|
const effectiveLabelWrap = !layout || layout === 'vertical' ? true : labelWrap;
|
|
79
|
+
const labelColStyle =
|
|
80
|
+
layout === 'vertical' ? { width: labelWidth, ...verticalFormItemLabelStyle } : { width: labelWidth };
|
|
76
81
|
const renderLabel = () => {
|
|
77
82
|
if (!showLabel) return null;
|
|
78
83
|
if (effectiveLabelWrap) {
|
|
@@ -118,7 +123,8 @@ export const FormItem = ({
|
|
|
118
123
|
return (
|
|
119
124
|
<Form.Item
|
|
120
125
|
{...rest}
|
|
121
|
-
|
|
126
|
+
style={{ ...formItemStyle, ...rest.style }}
|
|
127
|
+
labelCol={{ style: labelColStyle }}
|
|
122
128
|
layout={layout}
|
|
123
129
|
label={renderLabel()}
|
|
124
130
|
colon={false}
|
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { ConfigProvider } from 'antd';
|
|
11
|
-
import { Popup } from 'antd-mobile';
|
|
12
11
|
import React, { FC, ReactNode, useMemo } from 'react';
|
|
13
|
-
import { CloseOutline } from 'antd-mobile-icons';
|
|
14
12
|
import { useMobileActionDrawerStyle } from './MobilePopup.style';
|
|
15
13
|
import { useTranslation } from 'react-i18next';
|
|
14
|
+
import { lazy } from '../lazy-helper';
|
|
15
|
+
|
|
16
|
+
const { Popup } = lazy(() => import('antd-mobile'), 'Popup');
|
|
17
|
+
const { CloseOutline } = lazy(() => import('antd-mobile-icons'), 'CloseOutline');
|
|
16
18
|
|
|
17
19
|
interface MobilePopupProps {
|
|
18
20
|
title?: string;
|
|
@@ -38,9 +38,8 @@ describe('FlowModelRenderer', () => {
|
|
|
38
38
|
test('should pass useCache to useApplyAutoFlows and set it on context', async () => {
|
|
39
39
|
const { unmount } = renderWithProvider(<FlowModelRenderer model={model} useCache={true} />);
|
|
40
40
|
|
|
41
|
-
// Check if dispatchEvent was called with useCache: true
|
|
42
|
-
// useApplyAutoFlows calls dispatchEvent('beforeRender', inputArgs, { useCache })
|
|
43
41
|
await waitFor(() => {
|
|
42
|
+
expect(model.dispatchEvent).toHaveBeenCalledTimes(1);
|
|
44
43
|
expect(model.dispatchEvent).toHaveBeenCalledWith(
|
|
45
44
|
'beforeRender',
|
|
46
45
|
undefined,
|
|
@@ -58,6 +57,7 @@ describe('FlowModelRenderer', () => {
|
|
|
58
57
|
const { unmount } = renderWithProvider(<FlowModelRenderer model={model} useCache={false} />);
|
|
59
58
|
|
|
60
59
|
await waitFor(() => {
|
|
60
|
+
expect(model.dispatchEvent).toHaveBeenCalledTimes(1);
|
|
61
61
|
expect(model.dispatchEvent).toHaveBeenCalledWith(
|
|
62
62
|
'beforeRender',
|
|
63
63
|
undefined,
|
|
@@ -74,6 +74,7 @@ describe('FlowModelRenderer', () => {
|
|
|
74
74
|
const { unmount } = renderWithProvider(<FlowModelRenderer model={model} />);
|
|
75
75
|
|
|
76
76
|
await waitFor(() => {
|
|
77
|
+
expect(model.dispatchEvent).toHaveBeenCalledTimes(1);
|
|
77
78
|
expect(model.dispatchEvent).toHaveBeenCalledWith(
|
|
78
79
|
'beforeRender',
|
|
79
80
|
undefined,
|
|
@@ -86,4 +87,66 @@ describe('FlowModelRenderer', () => {
|
|
|
86
87
|
|
|
87
88
|
unmount();
|
|
88
89
|
});
|
|
90
|
+
|
|
91
|
+
test('should clear stale beforeRender state after unmount when reusing the same model', async () => {
|
|
92
|
+
const statefulEngine = new FlowEngine();
|
|
93
|
+
const onMountSpy = vi.fn();
|
|
94
|
+
const onUnmountSpy = vi.fn();
|
|
95
|
+
|
|
96
|
+
class StatefulModel extends FlowModel {
|
|
97
|
+
render(): any {
|
|
98
|
+
return <div>Stateful Content</div>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
protected onMount(): void {
|
|
102
|
+
onMountSpy();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
protected onUnmount(): void {
|
|
106
|
+
onUnmountSpy();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const statefulModel = new StatefulModel({
|
|
111
|
+
uid: 'stateful-model',
|
|
112
|
+
flowEngine: statefulEngine,
|
|
113
|
+
});
|
|
114
|
+
const executorSpy = vi.spyOn((statefulEngine as any).executor, 'dispatchEvent').mockResolvedValue([]);
|
|
115
|
+
|
|
116
|
+
const firstRender = renderWithProvider(<FlowModelRenderer model={statefulModel} />);
|
|
117
|
+
await waitFor(() => {
|
|
118
|
+
expect(executorSpy).toHaveBeenCalledTimes(1);
|
|
119
|
+
});
|
|
120
|
+
await waitFor(() => {
|
|
121
|
+
expect(onMountSpy).toHaveBeenCalledTimes(1);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
firstRender.unmount();
|
|
125
|
+
await waitFor(() => {
|
|
126
|
+
expect(onUnmountSpy).toHaveBeenCalledTimes(1);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
executorSpy.mockClear();
|
|
130
|
+
statefulModel.setStepParams('anyFlow', 'anyStep', { x: 1 });
|
|
131
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
132
|
+
expect(executorSpy.mock.calls.length).toBe(0);
|
|
133
|
+
|
|
134
|
+
const secondRender = renderWithProvider(<FlowModelRenderer model={statefulModel} />);
|
|
135
|
+
await waitFor(() => {
|
|
136
|
+
expect(executorSpy).toHaveBeenCalledTimes(1);
|
|
137
|
+
});
|
|
138
|
+
await waitFor(() => {
|
|
139
|
+
expect(onMountSpy).toHaveBeenCalledTimes(2);
|
|
140
|
+
});
|
|
141
|
+
const [target, eventName, inputArgs, options] = executorSpy.mock.calls[0];
|
|
142
|
+
expect(target).toBe(statefulModel);
|
|
143
|
+
expect(eventName).toBe('beforeRender');
|
|
144
|
+
expect(inputArgs).toBeUndefined();
|
|
145
|
+
expect(options).toMatchObject({ useCache: true });
|
|
146
|
+
|
|
147
|
+
secondRender.unmount();
|
|
148
|
+
await waitFor(() => {
|
|
149
|
+
expect(onUnmountSpy).toHaveBeenCalledTimes(2);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
89
152
|
});
|
|
@@ -0,0 +1,25 @@
|
|
|
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 { formItemStyle, verticalFormItemLabelStyle } from '../FormItem';
|
|
12
|
+
|
|
13
|
+
describe('FormItem', () => {
|
|
14
|
+
it('keeps vertical label-to-value spacing consistent with v1', () => {
|
|
15
|
+
expect(verticalFormItemLabelStyle).toEqual({
|
|
16
|
+
paddingBottom: 0,
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('keeps spacing between form items consistent with v1', () => {
|
|
21
|
+
expect(formItemStyle).toEqual({
|
|
22
|
+
marginBottom: 12,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -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, expect, it } from 'vitest';
|
|
11
|
+
import { resolveOverlayAnchorTransform } from '../dnd';
|
|
12
|
+
|
|
13
|
+
describe('resolveOverlayAnchorTransform', () => {
|
|
14
|
+
it('should keep the original transform when anchor point is missing', () => {
|
|
15
|
+
const transform = { x: 24, y: 36, scaleX: 1, scaleY: 1 };
|
|
16
|
+
|
|
17
|
+
expect(
|
|
18
|
+
resolveOverlayAnchorTransform({
|
|
19
|
+
activeId: 'menu-item-1',
|
|
20
|
+
active: { id: 'menu-item-1' },
|
|
21
|
+
transform,
|
|
22
|
+
activeNodeRect: { top: 80, left: 120 },
|
|
23
|
+
dragAnchorPoint: null,
|
|
24
|
+
}),
|
|
25
|
+
).toEqual(transform);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should align the overlay origin to the pointer position when dragging from toolbar handle', () => {
|
|
29
|
+
expect(
|
|
30
|
+
resolveOverlayAnchorTransform({
|
|
31
|
+
activeId: 'menu-item-1',
|
|
32
|
+
active: { id: 'menu-item-1' },
|
|
33
|
+
transform: { x: 20, y: 30, scaleX: 1, scaleY: 1 },
|
|
34
|
+
activeNodeRect: { top: 200, left: 100 },
|
|
35
|
+
dragAnchorPoint: { x: 180, y: 260 },
|
|
36
|
+
}),
|
|
37
|
+
).toEqual({
|
|
38
|
+
x: 100,
|
|
39
|
+
y: 90,
|
|
40
|
+
scaleX: 1,
|
|
41
|
+
scaleY: 1,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|