@nocobase/flow-engine 2.1.0-alpha.4 → 2.1.0-alpha.45
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/FlowContextProvider.d.ts +5 -1
- package/lib/FlowContextProvider.js +9 -2
- 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 +76 -11
- 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 +293 -52
- 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 +84 -0
- package/lib/data-source/index.js +259 -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 +46 -1
- package/lib/flowEngine.d.ts +151 -1
- package/lib/flowEngine.js +392 -18
- 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 +81 -21
- 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.d.ts +5 -1
- package/lib/utils/parsePathnameToViewParams.js +29 -5
- 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.d.ts +12 -2
- package/lib/views/ViewNavigation.js +28 -9
- package/lib/views/createViewMeta.js +114 -50
- package/lib/views/inheritLayoutContext.d.ts +10 -0
- package/lib/views/inheritLayoutContext.js +50 -0
- 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 +22 -3
- package/lib/views/useDrawer.d.ts +2 -1
- package/lib/views/useDrawer.js +22 -3
- package/lib/views/usePage.d.ts +5 -11
- package/lib/views/usePage.js +304 -144
- package/package.json +6 -5
- package/src/FlowContextProvider.tsx +9 -1
- package/src/JSRunner.ts +68 -4
- package/src/ViewScopedFlowEngine.ts +4 -0
- package/src/__tests__/JSRunner.test.ts +27 -1
- package/src/__tests__/createViewMeta.popup.test.ts +115 -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__/flowEngine.removeModel.test.ts +47 -3
- 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 +99 -11
- 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 +194 -5
- 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 +332 -56
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +522 -37
- 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 +322 -6
- package/src/executor/FlowExecutor.ts +35 -10
- package/src/executor/__tests__/flowExecutor.test.ts +85 -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 +50 -3
- package/src/flowEngine.ts +449 -14
- 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__/flowEngine.resolveUse.test.ts +0 -15
- package/src/models/__tests__/flowModel.test.ts +80 -37
- package/src/models/flowModel.tsx +122 -36
- 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 +28 -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 +47 -7
- 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 +46 -9
- package/src/views/__tests__/FlowView.usePage.test.tsx +243 -3
- package/src/views/__tests__/ViewNavigation.test.ts +52 -0
- package/src/views/__tests__/inheritLayoutContext.test.ts +53 -0
- package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +13 -12
- package/src/views/createViewMeta.ts +106 -34
- package/src/views/inheritLayoutContext.ts +26 -0
- package/src/views/runViewBeforeClose.ts +19 -0
- package/src/views/useDialog.tsx +27 -3
- package/src/views/useDrawer.tsx +27 -3
- package/src/views/usePage.tsx +367 -179
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
|
-
import { act, render, screen, userEvent, waitFor } from '@nocobase/test/client';
|
|
11
|
+
import { act, fireEvent, render, screen, userEvent, waitFor } from '@nocobase/test/client';
|
|
12
12
|
import { vi, beforeEach } from 'vitest';
|
|
13
13
|
import {
|
|
14
14
|
AddSubModelButton,
|
|
@@ -21,11 +21,13 @@ import {
|
|
|
21
21
|
import { SubModelItem, mergeSubModelItems, transformItems } from '../AddSubModelButton';
|
|
22
22
|
import { App, ConfigProvider } from 'antd';
|
|
23
23
|
|
|
24
|
+
const getSubmenuTitle = (label: string) => screen.getByText(label).closest('.ant-dropdown-menu-submenu-title');
|
|
25
|
+
|
|
24
26
|
describe('AddSubModelButton - preset settings open on add', () => {
|
|
25
27
|
test('calls openFlowSettings with preset=true for subModel with preset steps', async () => {
|
|
26
28
|
// Arrange: set up engine and models
|
|
27
29
|
const engine = new FlowEngine();
|
|
28
|
-
engine.flowSettings.forceEnable();
|
|
30
|
+
await engine.flowSettings.forceEnable();
|
|
29
31
|
|
|
30
32
|
class ParentModel extends FlowModel {}
|
|
31
33
|
|
|
@@ -99,12 +101,70 @@ describe('AddSubModelButton - preset settings open on add', () => {
|
|
|
99
101
|
});
|
|
100
102
|
});
|
|
101
103
|
|
|
104
|
+
describe('AddSubModelButton - model loader integration', () => {
|
|
105
|
+
test('resolves model loaders before creating sub models', async () => {
|
|
106
|
+
const engine = new FlowEngine();
|
|
107
|
+
await engine.flowSettings.forceEnable();
|
|
108
|
+
|
|
109
|
+
class ParentModel extends FlowModel {}
|
|
110
|
+
class ChildModel extends FlowModel {}
|
|
111
|
+
|
|
112
|
+
const childLoader = vi.fn(async () => ({ ChildModel }));
|
|
113
|
+
|
|
114
|
+
engine.registerModels({ ParentModel });
|
|
115
|
+
engine.registerModelLoaders({
|
|
116
|
+
ChildModel: {
|
|
117
|
+
loader: childLoader,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const parent = engine.createModel<ParentModel>({ use: 'ParentModel', uid: 'parent-loader' });
|
|
122
|
+
|
|
123
|
+
render(
|
|
124
|
+
<FlowEngineProvider engine={engine}>
|
|
125
|
+
<ConfigProvider>
|
|
126
|
+
<App>
|
|
127
|
+
<AddSubModelButton
|
|
128
|
+
model={parent}
|
|
129
|
+
subModelKey="items"
|
|
130
|
+
items={[
|
|
131
|
+
{
|
|
132
|
+
key: 'child',
|
|
133
|
+
label: 'Add Child',
|
|
134
|
+
createModelOptions: { use: 'ChildModel' },
|
|
135
|
+
},
|
|
136
|
+
]}
|
|
137
|
+
>
|
|
138
|
+
Add SubModel
|
|
139
|
+
</AddSubModelButton>
|
|
140
|
+
</App>
|
|
141
|
+
</ConfigProvider>
|
|
142
|
+
</FlowEngineProvider>,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
await act(async () => {
|
|
146
|
+
await userEvent.click(screen.getByText('Add SubModel'));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await waitFor(() => expect(screen.getByText('Add Child')).toBeInTheDocument());
|
|
150
|
+
|
|
151
|
+
await act(async () => {
|
|
152
|
+
await userEvent.click(screen.getByText('Add Child'));
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await waitFor(() => expect(childLoader).toHaveBeenCalledTimes(1));
|
|
156
|
+
const items = parent.subModels.items as FlowModel[];
|
|
157
|
+
expect(Array.isArray(items)).toBe(true);
|
|
158
|
+
expect(items[0]).toBeInstanceOf(ChildModel);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
102
162
|
describe('AddSubModelButton - async group children (nested)', () => {
|
|
103
163
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
104
164
|
|
|
105
165
|
it('renders group and nested async group leaf items', async () => {
|
|
106
166
|
const engine = new FlowEngine();
|
|
107
|
-
engine.flowSettings.forceEnable();
|
|
167
|
+
await engine.flowSettings.forceEnable();
|
|
108
168
|
class Parent extends FlowModel {}
|
|
109
169
|
engine.registerModels({ Parent });
|
|
110
170
|
const parent = engine.createModel<FlowModel>({ use: 'Parent', uid: 'p1' });
|
|
@@ -157,12 +217,74 @@ describe('AddSubModelButton - async group children (nested)', () => {
|
|
|
157
217
|
await waitFor(() => expect(screen.getByText('Nested-Leaf-1')).toBeInTheDocument());
|
|
158
218
|
await waitFor(() => expect(screen.getByText('Nested-Leaf-2')).toBeInTheDocument());
|
|
159
219
|
});
|
|
220
|
+
|
|
221
|
+
it('keeps root dropdown open while only the current nested group stays expanded', async () => {
|
|
222
|
+
const engine = new FlowEngine();
|
|
223
|
+
await engine.flowSettings.forceEnable();
|
|
224
|
+
class Parent extends FlowModel {}
|
|
225
|
+
engine.registerModels({ Parent });
|
|
226
|
+
const parent = engine.createModel<FlowModel>({ use: 'Parent', uid: 'p-multi-open' });
|
|
227
|
+
|
|
228
|
+
const items = [
|
|
229
|
+
{
|
|
230
|
+
key: 'group-a',
|
|
231
|
+
label: 'Group A',
|
|
232
|
+
children: [
|
|
233
|
+
{ key: 'a-1', label: 'A-1', createModelOptions: { use: 'Parent' } },
|
|
234
|
+
{ key: 'a-2', label: 'A-2', createModelOptions: { use: 'Parent' } },
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
key: 'group-b',
|
|
239
|
+
label: 'Group B',
|
|
240
|
+
children: [
|
|
241
|
+
{ key: 'b-1', label: 'B-1', createModelOptions: { use: 'Parent' } },
|
|
242
|
+
{ key: 'b-2', label: 'B-2', createModelOptions: { use: 'Parent' } },
|
|
243
|
+
],
|
|
244
|
+
},
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
render(
|
|
248
|
+
<FlowEngineProvider engine={engine}>
|
|
249
|
+
<ConfigProvider>
|
|
250
|
+
<App>
|
|
251
|
+
<AddSubModelButton model={parent} subModelKey="items" items={items as any}>
|
|
252
|
+
Open Menu
|
|
253
|
+
</AddSubModelButton>
|
|
254
|
+
</App>
|
|
255
|
+
</ConfigProvider>
|
|
256
|
+
</FlowEngineProvider>,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
await act(async () => {
|
|
260
|
+
await userEvent.click(screen.getByText('Open Menu'));
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
await waitFor(() => expect(screen.getByText('Group A')).toBeInTheDocument());
|
|
264
|
+
await waitFor(() => expect(screen.getByText('Group B')).toBeInTheDocument());
|
|
265
|
+
|
|
266
|
+
await act(async () => {
|
|
267
|
+
await userEvent.hover(screen.getByText('Group A'));
|
|
268
|
+
});
|
|
269
|
+
await waitFor(() => expect(screen.getByText('A-1')).toBeInTheDocument());
|
|
270
|
+
await waitFor(() => expect(getSubmenuTitle('Group A')).toHaveAttribute('aria-expanded', 'true'));
|
|
271
|
+
expect(getSubmenuTitle('Group B')).toHaveAttribute('aria-expanded', 'false');
|
|
272
|
+
|
|
273
|
+
await act(async () => {
|
|
274
|
+
await userEvent.hover(screen.getByText('Group B'));
|
|
275
|
+
});
|
|
276
|
+
await waitFor(() => expect(screen.getByText('B-1')).toBeInTheDocument());
|
|
277
|
+
await waitFor(() => expect(getSubmenuTitle('Group B')).toHaveAttribute('aria-expanded', 'true'));
|
|
278
|
+
expect(getSubmenuTitle('Group A')).toHaveAttribute('aria-expanded', 'false');
|
|
279
|
+
expect(screen.getByText('Group A')).toBeInTheDocument();
|
|
280
|
+
expect(screen.getByText('Group B')).toBeInTheDocument();
|
|
281
|
+
});
|
|
160
282
|
});
|
|
161
283
|
|
|
162
284
|
describe('transformItems - searchable flags', () => {
|
|
163
285
|
it('preserves searchable + placeholder on non-group submenu items', async () => {
|
|
164
286
|
const engine = new FlowEngine();
|
|
165
|
-
engine.flowSettings.forceEnable();
|
|
287
|
+
await engine.flowSettings.forceEnable();
|
|
166
288
|
class Parent extends FlowModel {}
|
|
167
289
|
engine.registerModels({ Parent });
|
|
168
290
|
const parent = engine.createModel<FlowModel>({ use: 'Parent' });
|
|
@@ -188,12 +310,258 @@ describe('transformItems - searchable flags', () => {
|
|
|
188
310
|
expect(submenu.searchPlaceholder).toBe('Search blocks');
|
|
189
311
|
expect(Array.isArray(submenu.children)).toBe(true);
|
|
190
312
|
});
|
|
313
|
+
|
|
314
|
+
it('filters searchable field menus by display label instead of item key', async () => {
|
|
315
|
+
const engine = new FlowEngine();
|
|
316
|
+
await engine.flowSettings.forceEnable();
|
|
317
|
+
const parent = engine.createModel<FlowModel>({ use: FlowModel });
|
|
318
|
+
const user = userEvent.setup();
|
|
319
|
+
|
|
320
|
+
const items = [
|
|
321
|
+
{
|
|
322
|
+
key: 'fields',
|
|
323
|
+
label: '',
|
|
324
|
+
type: 'group' as const,
|
|
325
|
+
searchable: true,
|
|
326
|
+
searchPlaceholder: 'Search fields',
|
|
327
|
+
children: [
|
|
328
|
+
{ key: 'field_name', label: 'Field display name' },
|
|
329
|
+
{ key: 'other_field', label: 'Other field' },
|
|
330
|
+
],
|
|
331
|
+
},
|
|
332
|
+
];
|
|
333
|
+
|
|
334
|
+
render(
|
|
335
|
+
<FlowEngineProvider engine={engine}>
|
|
336
|
+
<ConfigProvider>
|
|
337
|
+
<App>
|
|
338
|
+
<AddSubModelButton model={parent} items={items as any} subModelKey="items">
|
|
339
|
+
Open
|
|
340
|
+
</AddSubModelButton>
|
|
341
|
+
</App>
|
|
342
|
+
</ConfigProvider>
|
|
343
|
+
</FlowEngineProvider>,
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
await user.click(screen.getByText('Open'));
|
|
347
|
+
const searchInput = await screen.findByPlaceholderText('Search fields');
|
|
348
|
+
expect(screen.getByText('Field display name')).toBeInTheDocument();
|
|
349
|
+
|
|
350
|
+
await user.type(searchInput, 'field_name');
|
|
351
|
+
await waitFor(() => expect(screen.queryByText('Field display name')).not.toBeInTheDocument());
|
|
352
|
+
|
|
353
|
+
await user.clear(searchInput);
|
|
354
|
+
await user.type(searchInput, 'display');
|
|
355
|
+
await waitFor(() => expect(screen.getByText('Field display name')).toBeInTheDocument());
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('keeps searchable submenu children during IME composition', async () => {
|
|
359
|
+
const engine = new FlowEngine();
|
|
360
|
+
await engine.flowSettings.forceEnable();
|
|
361
|
+
class Parent extends FlowModel {}
|
|
362
|
+
engine.registerModels({ Parent });
|
|
363
|
+
const parent = engine.createModel<FlowModel>({ use: 'Parent' });
|
|
364
|
+
|
|
365
|
+
const items = [
|
|
366
|
+
{
|
|
367
|
+
key: 'fields',
|
|
368
|
+
label: 'Fields',
|
|
369
|
+
searchable: true,
|
|
370
|
+
children: [
|
|
371
|
+
{ key: 'f1', label: 'Field 1', createModelOptions: { use: 'Parent' } },
|
|
372
|
+
{ key: 'f2', label: 'Field 2', createModelOptions: { use: 'Parent' } },
|
|
373
|
+
],
|
|
374
|
+
},
|
|
375
|
+
];
|
|
376
|
+
|
|
377
|
+
const user = userEvent.setup();
|
|
378
|
+
render(
|
|
379
|
+
<FlowEngineProvider engine={engine}>
|
|
380
|
+
<ConfigProvider>
|
|
381
|
+
<App>
|
|
382
|
+
<AddSubModelButton model={parent} subModelKey="items" items={items as any}>
|
|
383
|
+
Open
|
|
384
|
+
</AddSubModelButton>
|
|
385
|
+
</App>
|
|
386
|
+
</ConfigProvider>
|
|
387
|
+
</FlowEngineProvider>,
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
await user.click(screen.getByText('Open'));
|
|
391
|
+
await waitFor(() => expect(screen.getByText('Fields')).toBeInTheDocument());
|
|
392
|
+
await user.hover(screen.getByText('Fields'));
|
|
393
|
+
await waitFor(() => expect(screen.getByText('Field 1')).toBeInTheDocument());
|
|
394
|
+
|
|
395
|
+
const input = screen.getByRole('textbox');
|
|
396
|
+
await user.click(input);
|
|
397
|
+
fireEvent.compositionStart(input);
|
|
398
|
+
fireEvent.change(input, { target: { value: 'zzzz' }, nativeEvent: { isComposing: true } });
|
|
399
|
+
fireEvent.mouseLeave(screen.getByText('Fields'));
|
|
400
|
+
await act(async () => {
|
|
401
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
expect(input).toHaveValue('zzzz');
|
|
405
|
+
expect(screen.getByText('Field 1')).toBeInTheDocument();
|
|
406
|
+
expect(screen.getByText('Field 2')).toBeInTheDocument();
|
|
407
|
+
|
|
408
|
+
fireEvent.compositionEnd(input);
|
|
409
|
+
fireEvent.change(input, { target: { value: 'zzzz' } });
|
|
410
|
+
|
|
411
|
+
await waitFor(() => expect(screen.getAllByText('No data').length).toBeGreaterThan(0));
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('closes searchable submenu after focus without input', async () => {
|
|
415
|
+
const engine = new FlowEngine();
|
|
416
|
+
await engine.flowSettings.forceEnable();
|
|
417
|
+
class Parent extends FlowModel {}
|
|
418
|
+
engine.registerModels({ Parent });
|
|
419
|
+
const parent = engine.createModel<FlowModel>({ use: 'Parent' });
|
|
420
|
+
|
|
421
|
+
const items = [
|
|
422
|
+
{
|
|
423
|
+
key: 'fields',
|
|
424
|
+
label: 'Fields',
|
|
425
|
+
searchable: true,
|
|
426
|
+
children: [
|
|
427
|
+
{ key: 'f1', label: 'Field 1', createModelOptions: { use: 'Parent' } },
|
|
428
|
+
{ key: 'f2', label: 'Field 2', createModelOptions: { use: 'Parent' } },
|
|
429
|
+
],
|
|
430
|
+
},
|
|
431
|
+
];
|
|
432
|
+
|
|
433
|
+
const user = userEvent.setup();
|
|
434
|
+
render(
|
|
435
|
+
<FlowEngineProvider engine={engine}>
|
|
436
|
+
<ConfigProvider>
|
|
437
|
+
<App>
|
|
438
|
+
<AddSubModelButton model={parent} subModelKey="items" items={items as any}>
|
|
439
|
+
Open
|
|
440
|
+
</AddSubModelButton>
|
|
441
|
+
</App>
|
|
442
|
+
</ConfigProvider>
|
|
443
|
+
</FlowEngineProvider>,
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
await user.click(screen.getByText('Open'));
|
|
447
|
+
await waitFor(() => expect(screen.getByText('Fields')).toBeInTheDocument());
|
|
448
|
+
await user.hover(screen.getByText('Fields'));
|
|
449
|
+
await waitFor(() => expect(screen.getByText('Field 1')).toBeInTheDocument());
|
|
450
|
+
|
|
451
|
+
await user.click(screen.getByRole('textbox'));
|
|
452
|
+
fireEvent.mouseLeave(screen.getByText('Fields'));
|
|
453
|
+
|
|
454
|
+
await waitFor(() => expect(screen.queryByText('Field 1')).not.toBeInTheDocument());
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it('closes active searchable submenu after outside click', async () => {
|
|
458
|
+
const engine = new FlowEngine();
|
|
459
|
+
await engine.flowSettings.forceEnable();
|
|
460
|
+
class Parent extends FlowModel {}
|
|
461
|
+
engine.registerModels({ Parent });
|
|
462
|
+
const parent = engine.createModel<FlowModel>({ use: 'Parent' });
|
|
463
|
+
|
|
464
|
+
const items = [
|
|
465
|
+
{
|
|
466
|
+
key: 'fields',
|
|
467
|
+
label: 'Fields',
|
|
468
|
+
searchable: true,
|
|
469
|
+
children: [
|
|
470
|
+
{ key: 'f1', label: 'Field 1', createModelOptions: { use: 'Parent' } },
|
|
471
|
+
{ key: 'f2', label: 'Field 2', createModelOptions: { use: 'Parent' } },
|
|
472
|
+
],
|
|
473
|
+
},
|
|
474
|
+
];
|
|
475
|
+
|
|
476
|
+
const user = userEvent.setup();
|
|
477
|
+
render(
|
|
478
|
+
<FlowEngineProvider engine={engine}>
|
|
479
|
+
<ConfigProvider>
|
|
480
|
+
<App>
|
|
481
|
+
<AddSubModelButton model={parent} subModelKey="items" items={items as any}>
|
|
482
|
+
Open
|
|
483
|
+
</AddSubModelButton>
|
|
484
|
+
</App>
|
|
485
|
+
</ConfigProvider>
|
|
486
|
+
</FlowEngineProvider>,
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
await user.click(screen.getByText('Open'));
|
|
490
|
+
await waitFor(() => expect(screen.getByText('Fields')).toBeInTheDocument());
|
|
491
|
+
await user.hover(screen.getByText('Fields'));
|
|
492
|
+
await waitFor(() => expect(screen.getByText('Field 1')).toBeInTheDocument());
|
|
493
|
+
|
|
494
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'Field' } });
|
|
495
|
+
expect(screen.getByText('Field 1')).toBeInTheDocument();
|
|
496
|
+
|
|
497
|
+
fireEvent.pointerDown(document.body);
|
|
498
|
+
|
|
499
|
+
await waitFor(() => expect(screen.queryByText('Fields')).not.toBeInTheDocument());
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it('switches away from active searchable submenu and resets its input', async () => {
|
|
503
|
+
const engine = new FlowEngine();
|
|
504
|
+
await engine.flowSettings.forceEnable();
|
|
505
|
+
class Parent extends FlowModel {}
|
|
506
|
+
engine.registerModels({ Parent });
|
|
507
|
+
const parent = engine.createModel<FlowModel>({ use: 'Parent' });
|
|
508
|
+
|
|
509
|
+
const items = [
|
|
510
|
+
{
|
|
511
|
+
key: 'fields',
|
|
512
|
+
label: 'Fields',
|
|
513
|
+
searchable: true,
|
|
514
|
+
children: [
|
|
515
|
+
{ key: 'f1', label: 'Field 1', createModelOptions: { use: 'Parent' } },
|
|
516
|
+
{ key: 'f2', label: 'Field 2', createModelOptions: { use: 'Parent' } },
|
|
517
|
+
],
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
key: 'blocks',
|
|
521
|
+
label: 'Blocks',
|
|
522
|
+
searchable: true,
|
|
523
|
+
children: [
|
|
524
|
+
{ key: 'b1', label: 'Block 1', createModelOptions: { use: 'Parent' } },
|
|
525
|
+
{ key: 'b2', label: 'Block 2', createModelOptions: { use: 'Parent' } },
|
|
526
|
+
],
|
|
527
|
+
},
|
|
528
|
+
];
|
|
529
|
+
|
|
530
|
+
const user = userEvent.setup();
|
|
531
|
+
render(
|
|
532
|
+
<FlowEngineProvider engine={engine}>
|
|
533
|
+
<ConfigProvider>
|
|
534
|
+
<App>
|
|
535
|
+
<AddSubModelButton model={parent} subModelKey="items" items={items as any}>
|
|
536
|
+
Open
|
|
537
|
+
</AddSubModelButton>
|
|
538
|
+
</App>
|
|
539
|
+
</ConfigProvider>
|
|
540
|
+
</FlowEngineProvider>,
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
await user.click(screen.getByText('Open'));
|
|
544
|
+
await waitFor(() => expect(screen.getByText('Fields')).toBeInTheDocument());
|
|
545
|
+
await user.hover(screen.getByText('Fields'));
|
|
546
|
+
await waitFor(() => expect(screen.getByText('Field 1')).toBeInTheDocument());
|
|
547
|
+
|
|
548
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'zzzz' } });
|
|
549
|
+
await waitFor(() => expect(screen.getAllByText('No data').length).toBeGreaterThan(0));
|
|
550
|
+
|
|
551
|
+
await user.hover(screen.getByText('Blocks'));
|
|
552
|
+
await waitFor(() => expect(screen.getByText('Block 1')).toBeInTheDocument());
|
|
553
|
+
expect(screen.queryByText('No data')).not.toBeInTheDocument();
|
|
554
|
+
|
|
555
|
+
await user.hover(screen.getByText('Fields'));
|
|
556
|
+
await waitFor(() => expect(screen.getByText('Field 1')).toBeInTheDocument());
|
|
557
|
+
expect(screen.getByRole('textbox')).toHaveValue('');
|
|
558
|
+
});
|
|
191
559
|
});
|
|
192
560
|
|
|
193
561
|
describe('transformItems - hide', () => {
|
|
194
562
|
it('filters items by hide flag/function recursively', async () => {
|
|
195
563
|
const engine = new FlowEngine();
|
|
196
|
-
engine.flowSettings.forceEnable();
|
|
564
|
+
await engine.flowSettings.forceEnable();
|
|
197
565
|
class Parent extends FlowModel {}
|
|
198
566
|
engine.registerModels({ Parent });
|
|
199
567
|
const parent = engine.createModel<FlowModel>({ use: 'Parent', uid: 'p-hide' });
|
|
@@ -239,7 +607,7 @@ describe('transformItems - hide', () => {
|
|
|
239
607
|
|
|
240
608
|
it('removes group when all children are hidden (even with async hide)', async () => {
|
|
241
609
|
const engine = new FlowEngine();
|
|
242
|
-
engine.flowSettings.forceEnable();
|
|
610
|
+
await engine.flowSettings.forceEnable();
|
|
243
611
|
class Parent extends FlowModel {}
|
|
244
612
|
engine.registerModels({ Parent });
|
|
245
613
|
const parent = engine.createModel<FlowModel>({ use: 'Parent', uid: 'p-empty-group' });
|
|
@@ -272,7 +640,7 @@ describe('transformItems - hide', () => {
|
|
|
272
640
|
|
|
273
641
|
it('supports async hide functions and disables cache', async () => {
|
|
274
642
|
const engine = new FlowEngine();
|
|
275
|
-
engine.flowSettings.forceEnable();
|
|
643
|
+
await engine.flowSettings.forceEnable();
|
|
276
644
|
class Parent extends FlowModel {}
|
|
277
645
|
engine.registerModels({ Parent });
|
|
278
646
|
const parent = engine.createModel<FlowModel>({ use: 'Parent', uid: 'p-async-hide' });
|
|
@@ -300,7 +668,7 @@ describe('transformItems - hide', () => {
|
|
|
300
668
|
|
|
301
669
|
it('shows items when hide function throws (conservative fallback)', async () => {
|
|
302
670
|
const engine = new FlowEngine();
|
|
303
|
-
engine.flowSettings.forceEnable();
|
|
671
|
+
await engine.flowSettings.forceEnable();
|
|
304
672
|
class Parent extends FlowModel {}
|
|
305
673
|
engine.registerModels({ Parent });
|
|
306
674
|
const parent = engine.createModel<FlowModel>({ use: 'Parent', uid: 'p-hide-throws' });
|
|
@@ -331,15 +699,15 @@ describe('transformItems - toggleable items', () => {
|
|
|
331
699
|
class ToggleParent extends FlowModel {}
|
|
332
700
|
class ToggleChild extends FlowModel {}
|
|
333
701
|
|
|
334
|
-
const setupEngine = () => {
|
|
702
|
+
const setupEngine = async () => {
|
|
335
703
|
const engine = new FlowEngine();
|
|
336
|
-
engine.flowSettings.forceEnable();
|
|
704
|
+
await engine.flowSettings.forceEnable();
|
|
337
705
|
engine.registerModels({ ToggleParent, ToggleChild });
|
|
338
706
|
return engine;
|
|
339
707
|
};
|
|
340
708
|
|
|
341
709
|
it('marks toggleable item as active when matching sub model exists', async () => {
|
|
342
|
-
const engine = setupEngine();
|
|
710
|
+
const engine = await setupEngine();
|
|
343
711
|
const parent = engine.createModel<ToggleParent>({ use: 'ToggleParent', uid: 'toggle-parent-on' });
|
|
344
712
|
const child = engine.createModel<ToggleChild>({ use: 'ToggleChild', uid: 'toggle-child-on' });
|
|
345
713
|
parent.addSubModel('items', child);
|
|
@@ -371,7 +739,7 @@ describe('transformItems - toggleable items', () => {
|
|
|
371
739
|
});
|
|
372
740
|
|
|
373
741
|
it('infers useModel from createModelOptions when toggleable is enabled', async () => {
|
|
374
|
-
const engine = setupEngine();
|
|
742
|
+
const engine = await setupEngine();
|
|
375
743
|
const parent = engine.createModel<ToggleParent>({ use: 'ToggleParent', uid: 'toggle-parent-infer' });
|
|
376
744
|
const child = engine.createModel<ToggleChild>({ use: 'ToggleChild', uid: 'toggle-child-infer' });
|
|
377
745
|
parent.addSubModel('items', child);
|
|
@@ -397,7 +765,7 @@ describe('transformItems - toggleable items', () => {
|
|
|
397
765
|
});
|
|
398
766
|
|
|
399
767
|
it('keeps toggleable item off when sub model missing', async () => {
|
|
400
|
-
const engine = setupEngine();
|
|
768
|
+
const engine = await setupEngine();
|
|
401
769
|
const parent = engine.createModel<ToggleParent>({ use: 'ToggleParent', uid: 'toggle-parent-off' });
|
|
402
770
|
|
|
403
771
|
const definition: SubModelItem[] = [
|
|
@@ -420,7 +788,7 @@ describe('transformItems - toggleable items', () => {
|
|
|
420
788
|
});
|
|
421
789
|
|
|
422
790
|
it('respects keepDropdownOpen override on toggleable items', async () => {
|
|
423
|
-
const engine = setupEngine();
|
|
791
|
+
const engine = await setupEngine();
|
|
424
792
|
const parent = engine.createModel<ToggleParent>({ use: 'ToggleParent', uid: 'toggle-parent-keep' });
|
|
425
793
|
|
|
426
794
|
const definition: SubModelItem[] = [
|
|
@@ -443,7 +811,7 @@ describe('transformItems - toggleable items', () => {
|
|
|
443
811
|
|
|
444
812
|
it('removes object sub model via default remove handler when toggleDetector provided', async () => {
|
|
445
813
|
const engine = new FlowEngine();
|
|
446
|
-
engine.flowSettings.forceEnable();
|
|
814
|
+
await engine.flowSettings.forceEnable();
|
|
447
815
|
|
|
448
816
|
class ObjectParent extends FlowModel {}
|
|
449
817
|
class ObjectChild extends FlowModel {}
|
|
@@ -481,6 +849,8 @@ describe('transformItems - toggleable items', () => {
|
|
|
481
849
|
</FlowEngineProvider>,
|
|
482
850
|
);
|
|
483
851
|
|
|
852
|
+
await waitFor(() => expect(screen.getByText('Toggle Menu')).toBeInTheDocument());
|
|
853
|
+
|
|
484
854
|
await act(async () => {
|
|
485
855
|
await userEvent.click(screen.getByText('Toggle Menu'));
|
|
486
856
|
});
|
|
@@ -519,16 +889,16 @@ describe('transformItems - caching behaviour', () => {
|
|
|
519
889
|
class CacheParent extends FlowModel {}
|
|
520
890
|
class CacheChild extends FlowModel {}
|
|
521
891
|
|
|
522
|
-
const setupEngine = () => {
|
|
892
|
+
const setupEngine = async () => {
|
|
523
893
|
const engine = new FlowEngine();
|
|
524
|
-
engine.flowSettings.forceEnable();
|
|
894
|
+
await engine.flowSettings.forceEnable();
|
|
525
895
|
engine.registerModels({ CacheParent, CacheChild });
|
|
526
896
|
const parent = engine.createModel<CacheParent>({ use: 'CacheParent', uid: 'cache-parent' });
|
|
527
897
|
return { engine, parent };
|
|
528
898
|
};
|
|
529
899
|
|
|
530
900
|
it('reuses cached result when no toggleable items exist', async () => {
|
|
531
|
-
const { parent } = setupEngine();
|
|
901
|
+
const { parent } = await setupEngine();
|
|
532
902
|
const definition: SubModelItem[] = [{ key: 'basic', label: 'Basic', createModelOptions: { use: 'CacheChild' } }];
|
|
533
903
|
|
|
534
904
|
const factory = transformItems(definition, parent, 'items', 'array');
|
|
@@ -541,7 +911,7 @@ describe('transformItems - caching behaviour', () => {
|
|
|
541
911
|
});
|
|
542
912
|
|
|
543
913
|
it('refreshes toggle state after new sub model is added', async () => {
|
|
544
|
-
const { parent, engine } = setupEngine();
|
|
914
|
+
const { parent, engine } = await setupEngine();
|
|
545
915
|
const createDefinition = (): SubModelItem[] => [
|
|
546
916
|
{
|
|
547
917
|
key: 'toggleable',
|
|
@@ -570,7 +940,7 @@ describe('transformItems - caching behaviour', () => {
|
|
|
570
940
|
describe('AddSubModelButton - refreshTargets linkage', () => {
|
|
571
941
|
it('clicking an item with refreshTargets triggers toggle recomputation on target branch', async () => {
|
|
572
942
|
const engine = new FlowEngine();
|
|
573
|
-
engine.flowSettings.forceEnable();
|
|
943
|
+
await engine.flowSettings.forceEnable();
|
|
574
944
|
|
|
575
945
|
class Parent extends FlowModel {}
|
|
576
946
|
class ToggleModel extends FlowModel {}
|
|
@@ -642,7 +1012,7 @@ describe('AddSubModelButton - base class menu groups', () => {
|
|
|
642
1012
|
|
|
643
1013
|
it('renders async children provided by subModelBaseClasses', async () => {
|
|
644
1014
|
const engine = new FlowEngine();
|
|
645
|
-
engine.flowSettings.forceEnable();
|
|
1015
|
+
await engine.flowSettings.forceEnable();
|
|
646
1016
|
|
|
647
1017
|
class Parent extends FlowModel {}
|
|
648
1018
|
class AsyncLeaf extends FlowModel {}
|
|
@@ -687,7 +1057,7 @@ describe('AddSubModelButton - base class menu groups', () => {
|
|
|
687
1057
|
|
|
688
1058
|
it('skips base class groups whose children resolve to empty', async () => {
|
|
689
1059
|
const engine = new FlowEngine();
|
|
690
|
-
engine.flowSettings.forceEnable();
|
|
1060
|
+
await engine.flowSettings.forceEnable();
|
|
691
1061
|
|
|
692
1062
|
class Parent extends FlowModel {}
|
|
693
1063
|
class EmptyLeaf extends FlowModel {}
|
|
@@ -739,7 +1109,7 @@ describe('AddSubModelButton - base class menu groups', () => {
|
|
|
739
1109
|
|
|
740
1110
|
it('renders submenu base class with children and respects meta.sort', async () => {
|
|
741
1111
|
const engine = new FlowEngine();
|
|
742
|
-
engine.flowSettings.forceEnable();
|
|
1112
|
+
await engine.flowSettings.forceEnable();
|
|
743
1113
|
|
|
744
1114
|
class Parent extends FlowModel {}
|
|
745
1115
|
class Leaf extends FlowModel {}
|
|
@@ -803,7 +1173,7 @@ describe('AddSubModelButton - base class menu groups', () => {
|
|
|
803
1173
|
|
|
804
1174
|
it('merges explicit items with base class and grouped sources', async () => {
|
|
805
1175
|
const engine = new FlowEngine();
|
|
806
|
-
engine.flowSettings.forceEnable();
|
|
1176
|
+
await engine.flowSettings.forceEnable();
|
|
807
1177
|
|
|
808
1178
|
class Parent extends FlowModel {}
|
|
809
1179
|
class BaseChild extends FlowModel {}
|
|
@@ -862,7 +1232,7 @@ describe('AddSubModelButton - base class menu groups', () => {
|
|
|
862
1232
|
describe('AddSubModelButton - toggle interactions', () => {
|
|
863
1233
|
it('removes existing toggleable sub model and triggers callbacks', async () => {
|
|
864
1234
|
const engine = new FlowEngine();
|
|
865
|
-
engine.flowSettings.forceEnable();
|
|
1235
|
+
await engine.flowSettings.forceEnable();
|
|
866
1236
|
|
|
867
1237
|
class ToggleParent extends FlowModel {}
|
|
868
1238
|
const destroySpy = vi.fn();
|
|
@@ -926,7 +1296,7 @@ describe('AddSubModelButton - toggle interactions', () => {
|
|
|
926
1296
|
|
|
927
1297
|
it('creates toggleable sub model and runs lifecycle callbacks', async () => {
|
|
928
1298
|
const engine = new FlowEngine();
|
|
929
|
-
engine.flowSettings.forceEnable();
|
|
1299
|
+
await engine.flowSettings.forceEnable();
|
|
930
1300
|
|
|
931
1301
|
class ToggleParent extends FlowModel {}
|
|
932
1302
|
const saveSpy = vi.fn();
|
|
@@ -995,6 +1365,56 @@ describe('AddSubModelButton - toggle interactions', () => {
|
|
|
995
1365
|
const subModels = ((parent.subModels as any).items as FlowModel[]) || [];
|
|
996
1366
|
expect(subModels).toHaveLength(1);
|
|
997
1367
|
});
|
|
1368
|
+
|
|
1369
|
+
it('updates toggle state after external sub model removal', async () => {
|
|
1370
|
+
const engine = new FlowEngine();
|
|
1371
|
+
await engine.flowSettings.forceEnable();
|
|
1372
|
+
|
|
1373
|
+
class ToggleParent extends FlowModel {}
|
|
1374
|
+
class ToggleChild extends FlowModel {}
|
|
1375
|
+
|
|
1376
|
+
engine.registerModels({ ToggleParent, ToggleChild });
|
|
1377
|
+
const parent = engine.createModel<ToggleParent>({ use: 'ToggleParent', uid: 'toggle-parent-external-remove' });
|
|
1378
|
+
const existing = engine.createModel<ToggleChild>({ use: 'ToggleChild', uid: 'toggle-child-external-remove' });
|
|
1379
|
+
parent.addSubModel('items', existing);
|
|
1380
|
+
|
|
1381
|
+
render(
|
|
1382
|
+
<FlowEngineProvider engine={engine}>
|
|
1383
|
+
<ConfigProvider>
|
|
1384
|
+
<App>
|
|
1385
|
+
<AddSubModelButton
|
|
1386
|
+
model={parent}
|
|
1387
|
+
subModelKey="items"
|
|
1388
|
+
items={[
|
|
1389
|
+
{
|
|
1390
|
+
key: 'toggle-child',
|
|
1391
|
+
label: 'Toggle Child',
|
|
1392
|
+
toggleable: true,
|
|
1393
|
+
useModel: 'ToggleChild',
|
|
1394
|
+
createModelOptions: { use: 'ToggleChild' },
|
|
1395
|
+
},
|
|
1396
|
+
]}
|
|
1397
|
+
>
|
|
1398
|
+
Toggle Menu
|
|
1399
|
+
</AddSubModelButton>
|
|
1400
|
+
</App>
|
|
1401
|
+
</ConfigProvider>
|
|
1402
|
+
</FlowEngineProvider>,
|
|
1403
|
+
);
|
|
1404
|
+
|
|
1405
|
+
await act(async () => {
|
|
1406
|
+
await userEvent.click(screen.getByText('Toggle Menu'));
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
await waitFor(() => expect(screen.getByText('Toggle Child')).toBeInTheDocument());
|
|
1410
|
+
await waitFor(() => expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true'));
|
|
1411
|
+
|
|
1412
|
+
await act(async () => {
|
|
1413
|
+
await existing.destroy();
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
await waitFor(() => expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'false'));
|
|
1417
|
+
});
|
|
998
1418
|
});
|
|
999
1419
|
|
|
1000
1420
|
// ========================
|
|
@@ -1013,9 +1433,9 @@ describe('AddSubModelButton toggleable behavior', () => {
|
|
|
1013
1433
|
duplicate = vi.fn().mockResolvedValue(null);
|
|
1014
1434
|
}
|
|
1015
1435
|
|
|
1016
|
-
function setup() {
|
|
1436
|
+
async function setup() {
|
|
1017
1437
|
const engine = new FlowEngine();
|
|
1018
|
-
engine.flowSettings.forceEnable();
|
|
1438
|
+
await engine.flowSettings.forceEnable();
|
|
1019
1439
|
engine.registerModels({ ToggleModel });
|
|
1020
1440
|
engine.setModelRepository(new FakeRepo());
|
|
1021
1441
|
|
|
@@ -1068,7 +1488,7 @@ describe('AddSubModelButton toggleable behavior', () => {
|
|
|
1068
1488
|
});
|
|
1069
1489
|
|
|
1070
1490
|
test('keeps dropdown open and preserves loaded children on toggle add/remove', async () => {
|
|
1071
|
-
const { engine, ui } = setup();
|
|
1491
|
+
const { engine, ui } = await setup();
|
|
1072
1492
|
const user = userEvent.setup();
|
|
1073
1493
|
|
|
1074
1494
|
render(ui);
|
|
@@ -1092,6 +1512,7 @@ describe('AddSubModelButton toggleable behavior', () => {
|
|
|
1092
1512
|
},
|
|
1093
1513
|
{ timeout: 3000 },
|
|
1094
1514
|
);
|
|
1515
|
+
await waitFor(() => expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true'));
|
|
1095
1516
|
|
|
1096
1517
|
// dropdown should remain open and children should still be visible (no flicker / reload)
|
|
1097
1518
|
expect(screen.getByText('Async Group')).toBeInTheDocument();
|
|
@@ -1108,12 +1529,13 @@ describe('AddSubModelButton toggleable behavior', () => {
|
|
|
1108
1529
|
|
|
1109
1530
|
// ensure destroy has been called (avoid flakiness on exact call counts)
|
|
1110
1531
|
await waitFor(() => {
|
|
1532
|
+
expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'false');
|
|
1111
1533
|
expect(repo.destroy).toHaveBeenCalled();
|
|
1112
1534
|
});
|
|
1113
1535
|
});
|
|
1114
1536
|
|
|
1115
1537
|
test('toggle state updates without menu closing', async () => {
|
|
1116
|
-
const { ui } = setup();
|
|
1538
|
+
const { ui } = await setup();
|
|
1117
1539
|
const user = userEvent.setup();
|
|
1118
1540
|
|
|
1119
1541
|
render(ui);
|
|
@@ -1131,7 +1553,7 @@ describe('AddSubModelButton toggleable behavior', () => {
|
|
|
1131
1553
|
|
|
1132
1554
|
test('nested submenu (static items) toggle keeps menu open and reflects state', async () => {
|
|
1133
1555
|
const engine = new FlowEngine();
|
|
1134
|
-
engine.flowSettings.forceEnable();
|
|
1556
|
+
await engine.flowSettings.forceEnable();
|
|
1135
1557
|
engine.registerModels({ ToggleModel });
|
|
1136
1558
|
const parent = engine.createModel<FlowModel>({ use: FlowModel });
|
|
1137
1559
|
|
|
@@ -1213,7 +1635,7 @@ describe('AddSubModelButton toggleable behavior', () => {
|
|
|
1213
1635
|
|
|
1214
1636
|
test('submenu (second-level) toggleable stays open and updates state', async () => {
|
|
1215
1637
|
const engine = new FlowEngine();
|
|
1216
|
-
engine.flowSettings.forceEnable();
|
|
1638
|
+
await engine.flowSettings.forceEnable();
|
|
1217
1639
|
engine.registerModels({ ToggleModel });
|
|
1218
1640
|
engine.setModelRepository(new FakeRepo());
|
|
1219
1641
|
vi.spyOn(engine.flowSettings, 'open').mockResolvedValue(false as any);
|
|
@@ -1261,18 +1683,81 @@ describe('AddSubModelButton toggleable behavior', () => {
|
|
|
1261
1683
|
// click leaf toggle to add
|
|
1262
1684
|
await user.click(screen.getByText('Leaf Toggle'));
|
|
1263
1685
|
|
|
1264
|
-
// menu should remain visible
|
|
1686
|
+
// menu and submenu should remain visible after toggling a submenu leaf
|
|
1265
1687
|
expect(screen.getByText('Fields')).toBeInTheDocument();
|
|
1266
|
-
|
|
1267
|
-
// 由于点击叶子项后二级子菜单可能被收起,这里先重新展开再断言开关状态
|
|
1268
|
-
await user.hover(screen.getByText('Fields'));
|
|
1269
1688
|
await waitFor(() => expect(screen.getByText('Leaf Toggle')).toBeInTheDocument());
|
|
1270
1689
|
await waitFor(() => expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true'));
|
|
1271
1690
|
});
|
|
1272
1691
|
|
|
1692
|
+
test('keepDropdownOpen keeps root menu visible after clicking a nested relation-style leaf', async () => {
|
|
1693
|
+
const engine = new FlowEngine();
|
|
1694
|
+
await engine.flowSettings.forceEnable();
|
|
1695
|
+
|
|
1696
|
+
class Parent extends FlowModel {}
|
|
1697
|
+
class RelationLeafModel extends FlowModel {}
|
|
1698
|
+
|
|
1699
|
+
engine.registerModels({ Parent, RelationLeafModel });
|
|
1700
|
+
engine.setModelRepository(new FakeRepo());
|
|
1701
|
+
vi.spyOn(engine.flowSettings, 'open').mockResolvedValue(false as any);
|
|
1702
|
+
|
|
1703
|
+
const parent = engine.createModel<FlowModel>({ use: 'Parent' });
|
|
1704
|
+
|
|
1705
|
+
const items = [
|
|
1706
|
+
{
|
|
1707
|
+
key: 'relation-fields',
|
|
1708
|
+
label: 'Display association fields',
|
|
1709
|
+
children: [
|
|
1710
|
+
{
|
|
1711
|
+
key: 'users',
|
|
1712
|
+
label: 'Users',
|
|
1713
|
+
type: 'group' as const,
|
|
1714
|
+
children: [
|
|
1715
|
+
{
|
|
1716
|
+
key: 'user-name',
|
|
1717
|
+
label: 'User name',
|
|
1718
|
+
createModelOptions: { use: 'RelationLeafModel' },
|
|
1719
|
+
},
|
|
1720
|
+
],
|
|
1721
|
+
},
|
|
1722
|
+
],
|
|
1723
|
+
},
|
|
1724
|
+
];
|
|
1725
|
+
|
|
1726
|
+
render(
|
|
1727
|
+
<FlowEngineProvider engine={engine}>
|
|
1728
|
+
<ConfigProvider>
|
|
1729
|
+
<App>
|
|
1730
|
+
<AddSubModelButton
|
|
1731
|
+
model={parent}
|
|
1732
|
+
items={items as any}
|
|
1733
|
+
subModelType="array"
|
|
1734
|
+
subModelKey="subs"
|
|
1735
|
+
keepDropdownOpen
|
|
1736
|
+
>
|
|
1737
|
+
Open
|
|
1738
|
+
</AddSubModelButton>
|
|
1739
|
+
</App>
|
|
1740
|
+
</ConfigProvider>
|
|
1741
|
+
</FlowEngineProvider>,
|
|
1742
|
+
);
|
|
1743
|
+
|
|
1744
|
+
const user = userEvent.setup();
|
|
1745
|
+
await user.click(screen.getByText('Open'));
|
|
1746
|
+
|
|
1747
|
+
await waitFor(() => expect(screen.getByText('Display association fields')).toBeInTheDocument());
|
|
1748
|
+
await user.hover(screen.getByText('Display association fields'));
|
|
1749
|
+
await waitFor(() => expect(screen.getByText('Users')).toBeInTheDocument());
|
|
1750
|
+
await waitFor(() => expect(getSubmenuTitle('Display association fields')).toHaveAttribute('aria-expanded', 'true'));
|
|
1751
|
+
|
|
1752
|
+
await user.click(screen.getByText('User name'));
|
|
1753
|
+
|
|
1754
|
+
await waitFor(() => expect(screen.getByText('Display association fields')).toBeInTheDocument());
|
|
1755
|
+
await waitFor(() => expect(getSubmenuTitle('Display association fields')).toHaveAttribute('aria-expanded', 'true'));
|
|
1756
|
+
});
|
|
1757
|
+
|
|
1273
1758
|
test('top-level toggle updates after opening a second-level branch', async () => {
|
|
1274
1759
|
const engine = new FlowEngine();
|
|
1275
|
-
engine.flowSettings.forceEnable();
|
|
1760
|
+
await engine.flowSettings.forceEnable();
|
|
1276
1761
|
engine.registerModels({ ToggleModel });
|
|
1277
1762
|
engine.setModelRepository(new FakeRepo());
|
|
1278
1763
|
vi.spyOn(engine.flowSettings, 'open').mockResolvedValue(false as any);
|