@nocobase/flow-engine 2.1.0-beta.9 → 2.1.1
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/FlowContextProvider.d.ts +5 -1
- package/lib/FlowContextProvider.js +9 -2
- 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 +607 -19
- 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 +152 -42
- 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/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 +12 -1
- package/lib/components/subModel/LazyDropdown.js +301 -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 +2 -1
- package/lib/components/subModel/utils.js +15 -5
- 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 +269 -7
- package/lib/executor/FlowExecutor.js +6 -3
- 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 +9 -1
- package/lib/flowContext.js +77 -6
- package/lib/flowEngine.d.ts +136 -4
- package/lib/flowEngine.js +429 -51
- 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 +126 -34
- package/lib/provider.js +38 -23
- package/lib/reactive/observer.js +46 -16
- package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +4 -3
- package/lib/runjs-context/contexts/JSBlockRunJSContext.js +4 -15
- package/lib/runjs-context/contexts/JSColumnRunJSContext.js +5 -2
- package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +5 -8
- package/lib/runjs-context/contexts/JSFieldRunJSContext.js +4 -3
- package/lib/runjs-context/contexts/JSItemRunJSContext.js +4 -3
- package/lib/runjs-context/contexts/base.js +464 -29
- package/lib/runjs-context/contexts/elementDoc.d.ts +11 -0
- package/lib/runjs-context/contexts/elementDoc.js +152 -0
- package/lib/runjs-context/setup.js +1 -0
- 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/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/loadedPageCache.d.ts +24 -0
- package/lib/utils/loadedPageCache.js +139 -0
- package/lib/utils/parsePathnameToViewParams.d.ts +5 -1
- package/lib/utils/parsePathnameToViewParams.js +28 -4
- 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 +12 -3
- package/lib/views/useDrawer.d.ts +2 -1
- package/lib/views/useDrawer.js +12 -3
- package/lib/views/usePage.d.ts +5 -11
- package/lib/views/usePage.js +304 -144
- package/package.json +5 -4
- package/src/FlowContextProvider.tsx +9 -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 +105 -1
- package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
- package/src/__tests__/flowEngine.moveModel.test.ts +81 -1
- 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 +21 -0
- package/src/__tests__/runjsContextImplementations.test.ts +9 -2
- package/src/__tests__/runjsContextRuntime.test.ts +2 -0
- package/src/__tests__/runjsLocales.test.ts +6 -5
- package/src/__tests__/runjsSnippets.test.ts +21 -0
- package/src/__tests__/viewScopedFlowEngine.test.ts +136 -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 +472 -5
- package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
- package/src/components/dnd/gridDragPlanner.ts +750 -17
- 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 +178 -48
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +487 -440
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +344 -8
- 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 +16 -2
- package/src/components/subModel/LazyDropdown.tsx +341 -56
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +524 -38
- package/src/components/subModel/__tests__/utils.test.ts +24 -0
- package/src/components/subModel/index.ts +1 -0
- package/src/components/subModel/utils.ts +13 -2
- 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 +69 -2
- package/src/data-source/index.ts +332 -8
- package/src/executor/FlowExecutor.ts +6 -3
- 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 +85 -6
- package/src/flowEngine.ts +484 -45
- 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__/flowEngine.resolveUse.test.ts +0 -15
- package/src/models/__tests__/flowModel.test.ts +65 -37
- package/src/models/flowModel.tsx +184 -65
- 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/contexts/FormJSFieldItemRunJSContext.ts +4 -3
- package/src/runjs-context/contexts/JSBlockRunJSContext.ts +4 -15
- package/src/runjs-context/contexts/JSColumnRunJSContext.ts +4 -2
- package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +5 -9
- package/src/runjs-context/contexts/JSFieldRunJSContext.ts +4 -3
- package/src/runjs-context/contexts/JSItemRunJSContext.ts +4 -3
- package/src/runjs-context/contexts/base.ts +467 -31
- package/src/runjs-context/contexts/elementDoc.ts +130 -0
- package/src/runjs-context/setup.ts +1 -0
- 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/types.ts +62 -0
- package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +21 -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/loadedPageCache.ts +147 -0
- package/src/utils/parsePathnameToViewParams.ts +45 -5
- 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 +12 -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 +13 -3
- package/src/views/useDrawer.tsx +13 -3
- package/src/views/usePage.tsx +367 -180
|
@@ -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,309 @@ 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
|
+
});
|
|
559
|
+
|
|
560
|
+
it('keeps root group search value when hovering a sibling submenu', async () => {
|
|
561
|
+
const engine = new FlowEngine();
|
|
562
|
+
await engine.flowSettings.forceEnable();
|
|
563
|
+
class Parent extends FlowModel {}
|
|
564
|
+
engine.registerModels({ Parent });
|
|
565
|
+
const parent = engine.createModel<FlowModel>({ use: 'Parent' });
|
|
566
|
+
|
|
567
|
+
const items = [
|
|
568
|
+
{
|
|
569
|
+
key: 'fields',
|
|
570
|
+
label: '',
|
|
571
|
+
type: 'group' as const,
|
|
572
|
+
searchable: true,
|
|
573
|
+
searchPlaceholder: 'Search fields',
|
|
574
|
+
children: [
|
|
575
|
+
{ key: 'nickname', label: 'Nickname', createModelOptions: { use: 'Parent' } },
|
|
576
|
+
{ key: 'email', label: 'Email', createModelOptions: { use: 'Parent' } },
|
|
577
|
+
],
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
key: 'association-fields',
|
|
581
|
+
label: 'Display association fields',
|
|
582
|
+
children: [{ key: 'author', label: 'Author', createModelOptions: { use: 'Parent' } }],
|
|
583
|
+
},
|
|
584
|
+
];
|
|
585
|
+
|
|
586
|
+
const user = userEvent.setup();
|
|
587
|
+
render(
|
|
588
|
+
<FlowEngineProvider engine={engine}>
|
|
589
|
+
<ConfigProvider>
|
|
590
|
+
<App>
|
|
591
|
+
<AddSubModelButton model={parent} subModelKey="items" items={items as any}>
|
|
592
|
+
Open
|
|
593
|
+
</AddSubModelButton>
|
|
594
|
+
</App>
|
|
595
|
+
</ConfigProvider>
|
|
596
|
+
</FlowEngineProvider>,
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
await user.click(screen.getByText('Open'));
|
|
600
|
+
const searchInput = await screen.findByPlaceholderText('Search fields');
|
|
601
|
+
await user.type(searchInput, 'nick');
|
|
602
|
+
await waitFor(() => expect(screen.queryByText('Email')).not.toBeInTheDocument());
|
|
603
|
+
|
|
604
|
+
await user.hover(screen.getByText('Display association fields'));
|
|
605
|
+
|
|
606
|
+
await waitFor(() => expect(screen.getByText('Author')).toBeInTheDocument());
|
|
607
|
+
expect(searchInput).toHaveValue('nick');
|
|
608
|
+
expect(screen.getByText('Nickname')).toBeInTheDocument();
|
|
609
|
+
});
|
|
191
610
|
});
|
|
192
611
|
|
|
193
612
|
describe('transformItems - hide', () => {
|
|
194
613
|
it('filters items by hide flag/function recursively', async () => {
|
|
195
614
|
const engine = new FlowEngine();
|
|
196
|
-
engine.flowSettings.forceEnable();
|
|
615
|
+
await engine.flowSettings.forceEnable();
|
|
197
616
|
class Parent extends FlowModel {}
|
|
198
617
|
engine.registerModels({ Parent });
|
|
199
618
|
const parent = engine.createModel<FlowModel>({ use: 'Parent', uid: 'p-hide' });
|
|
@@ -239,7 +658,7 @@ describe('transformItems - hide', () => {
|
|
|
239
658
|
|
|
240
659
|
it('removes group when all children are hidden (even with async hide)', async () => {
|
|
241
660
|
const engine = new FlowEngine();
|
|
242
|
-
engine.flowSettings.forceEnable();
|
|
661
|
+
await engine.flowSettings.forceEnable();
|
|
243
662
|
class Parent extends FlowModel {}
|
|
244
663
|
engine.registerModels({ Parent });
|
|
245
664
|
const parent = engine.createModel<FlowModel>({ use: 'Parent', uid: 'p-empty-group' });
|
|
@@ -272,7 +691,7 @@ describe('transformItems - hide', () => {
|
|
|
272
691
|
|
|
273
692
|
it('supports async hide functions and disables cache', async () => {
|
|
274
693
|
const engine = new FlowEngine();
|
|
275
|
-
engine.flowSettings.forceEnable();
|
|
694
|
+
await engine.flowSettings.forceEnable();
|
|
276
695
|
class Parent extends FlowModel {}
|
|
277
696
|
engine.registerModels({ Parent });
|
|
278
697
|
const parent = engine.createModel<FlowModel>({ use: 'Parent', uid: 'p-async-hide' });
|
|
@@ -300,7 +719,7 @@ describe('transformItems - hide', () => {
|
|
|
300
719
|
|
|
301
720
|
it('shows items when hide function throws (conservative fallback)', async () => {
|
|
302
721
|
const engine = new FlowEngine();
|
|
303
|
-
engine.flowSettings.forceEnable();
|
|
722
|
+
await engine.flowSettings.forceEnable();
|
|
304
723
|
class Parent extends FlowModel {}
|
|
305
724
|
engine.registerModels({ Parent });
|
|
306
725
|
const parent = engine.createModel<FlowModel>({ use: 'Parent', uid: 'p-hide-throws' });
|
|
@@ -331,15 +750,15 @@ describe('transformItems - toggleable items', () => {
|
|
|
331
750
|
class ToggleParent extends FlowModel {}
|
|
332
751
|
class ToggleChild extends FlowModel {}
|
|
333
752
|
|
|
334
|
-
const setupEngine = () => {
|
|
753
|
+
const setupEngine = async () => {
|
|
335
754
|
const engine = new FlowEngine();
|
|
336
|
-
engine.flowSettings.forceEnable();
|
|
755
|
+
await engine.flowSettings.forceEnable();
|
|
337
756
|
engine.registerModels({ ToggleParent, ToggleChild });
|
|
338
757
|
return engine;
|
|
339
758
|
};
|
|
340
759
|
|
|
341
760
|
it('marks toggleable item as active when matching sub model exists', async () => {
|
|
342
|
-
const engine = setupEngine();
|
|
761
|
+
const engine = await setupEngine();
|
|
343
762
|
const parent = engine.createModel<ToggleParent>({ use: 'ToggleParent', uid: 'toggle-parent-on' });
|
|
344
763
|
const child = engine.createModel<ToggleChild>({ use: 'ToggleChild', uid: 'toggle-child-on' });
|
|
345
764
|
parent.addSubModel('items', child);
|
|
@@ -371,7 +790,7 @@ describe('transformItems - toggleable items', () => {
|
|
|
371
790
|
});
|
|
372
791
|
|
|
373
792
|
it('infers useModel from createModelOptions when toggleable is enabled', async () => {
|
|
374
|
-
const engine = setupEngine();
|
|
793
|
+
const engine = await setupEngine();
|
|
375
794
|
const parent = engine.createModel<ToggleParent>({ use: 'ToggleParent', uid: 'toggle-parent-infer' });
|
|
376
795
|
const child = engine.createModel<ToggleChild>({ use: 'ToggleChild', uid: 'toggle-child-infer' });
|
|
377
796
|
parent.addSubModel('items', child);
|
|
@@ -397,7 +816,7 @@ describe('transformItems - toggleable items', () => {
|
|
|
397
816
|
});
|
|
398
817
|
|
|
399
818
|
it('keeps toggleable item off when sub model missing', async () => {
|
|
400
|
-
const engine = setupEngine();
|
|
819
|
+
const engine = await setupEngine();
|
|
401
820
|
const parent = engine.createModel<ToggleParent>({ use: 'ToggleParent', uid: 'toggle-parent-off' });
|
|
402
821
|
|
|
403
822
|
const definition: SubModelItem[] = [
|
|
@@ -420,7 +839,7 @@ describe('transformItems - toggleable items', () => {
|
|
|
420
839
|
});
|
|
421
840
|
|
|
422
841
|
it('respects keepDropdownOpen override on toggleable items', async () => {
|
|
423
|
-
const engine = setupEngine();
|
|
842
|
+
const engine = await setupEngine();
|
|
424
843
|
const parent = engine.createModel<ToggleParent>({ use: 'ToggleParent', uid: 'toggle-parent-keep' });
|
|
425
844
|
|
|
426
845
|
const definition: SubModelItem[] = [
|
|
@@ -443,7 +862,7 @@ describe('transformItems - toggleable items', () => {
|
|
|
443
862
|
|
|
444
863
|
it('removes object sub model via default remove handler when toggleDetector provided', async () => {
|
|
445
864
|
const engine = new FlowEngine();
|
|
446
|
-
engine.flowSettings.forceEnable();
|
|
865
|
+
await engine.flowSettings.forceEnable();
|
|
447
866
|
|
|
448
867
|
class ObjectParent extends FlowModel {}
|
|
449
868
|
class ObjectChild extends FlowModel {}
|
|
@@ -481,6 +900,8 @@ describe('transformItems - toggleable items', () => {
|
|
|
481
900
|
</FlowEngineProvider>,
|
|
482
901
|
);
|
|
483
902
|
|
|
903
|
+
await waitFor(() => expect(screen.getByText('Toggle Menu')).toBeInTheDocument());
|
|
904
|
+
|
|
484
905
|
await act(async () => {
|
|
485
906
|
await userEvent.click(screen.getByText('Toggle Menu'));
|
|
486
907
|
});
|
|
@@ -519,16 +940,16 @@ describe('transformItems - caching behaviour', () => {
|
|
|
519
940
|
class CacheParent extends FlowModel {}
|
|
520
941
|
class CacheChild extends FlowModel {}
|
|
521
942
|
|
|
522
|
-
const setupEngine = () => {
|
|
943
|
+
const setupEngine = async () => {
|
|
523
944
|
const engine = new FlowEngine();
|
|
524
|
-
engine.flowSettings.forceEnable();
|
|
945
|
+
await engine.flowSettings.forceEnable();
|
|
525
946
|
engine.registerModels({ CacheParent, CacheChild });
|
|
526
947
|
const parent = engine.createModel<CacheParent>({ use: 'CacheParent', uid: 'cache-parent' });
|
|
527
948
|
return { engine, parent };
|
|
528
949
|
};
|
|
529
950
|
|
|
530
951
|
it('reuses cached result when no toggleable items exist', async () => {
|
|
531
|
-
const { parent } = setupEngine();
|
|
952
|
+
const { parent } = await setupEngine();
|
|
532
953
|
const definition: SubModelItem[] = [{ key: 'basic', label: 'Basic', createModelOptions: { use: 'CacheChild' } }];
|
|
533
954
|
|
|
534
955
|
const factory = transformItems(definition, parent, 'items', 'array');
|
|
@@ -541,7 +962,7 @@ describe('transformItems - caching behaviour', () => {
|
|
|
541
962
|
});
|
|
542
963
|
|
|
543
964
|
it('refreshes toggle state after new sub model is added', async () => {
|
|
544
|
-
const { parent, engine } = setupEngine();
|
|
965
|
+
const { parent, engine } = await setupEngine();
|
|
545
966
|
const createDefinition = (): SubModelItem[] => [
|
|
546
967
|
{
|
|
547
968
|
key: 'toggleable',
|
|
@@ -570,7 +991,7 @@ describe('transformItems - caching behaviour', () => {
|
|
|
570
991
|
describe('AddSubModelButton - refreshTargets linkage', () => {
|
|
571
992
|
it('clicking an item with refreshTargets triggers toggle recomputation on target branch', async () => {
|
|
572
993
|
const engine = new FlowEngine();
|
|
573
|
-
engine.flowSettings.forceEnable();
|
|
994
|
+
await engine.flowSettings.forceEnable();
|
|
574
995
|
|
|
575
996
|
class Parent extends FlowModel {}
|
|
576
997
|
class ToggleModel extends FlowModel {}
|
|
@@ -642,7 +1063,7 @@ describe('AddSubModelButton - base class menu groups', () => {
|
|
|
642
1063
|
|
|
643
1064
|
it('renders async children provided by subModelBaseClasses', async () => {
|
|
644
1065
|
const engine = new FlowEngine();
|
|
645
|
-
engine.flowSettings.forceEnable();
|
|
1066
|
+
await engine.flowSettings.forceEnable();
|
|
646
1067
|
|
|
647
1068
|
class Parent extends FlowModel {}
|
|
648
1069
|
class AsyncLeaf extends FlowModel {}
|
|
@@ -687,7 +1108,7 @@ describe('AddSubModelButton - base class menu groups', () => {
|
|
|
687
1108
|
|
|
688
1109
|
it('skips base class groups whose children resolve to empty', async () => {
|
|
689
1110
|
const engine = new FlowEngine();
|
|
690
|
-
engine.flowSettings.forceEnable();
|
|
1111
|
+
await engine.flowSettings.forceEnable();
|
|
691
1112
|
|
|
692
1113
|
class Parent extends FlowModel {}
|
|
693
1114
|
class EmptyLeaf extends FlowModel {}
|
|
@@ -739,7 +1160,7 @@ describe('AddSubModelButton - base class menu groups', () => {
|
|
|
739
1160
|
|
|
740
1161
|
it('renders submenu base class with children and respects meta.sort', async () => {
|
|
741
1162
|
const engine = new FlowEngine();
|
|
742
|
-
engine.flowSettings.forceEnable();
|
|
1163
|
+
await engine.flowSettings.forceEnable();
|
|
743
1164
|
|
|
744
1165
|
class Parent extends FlowModel {}
|
|
745
1166
|
class Leaf extends FlowModel {}
|
|
@@ -803,7 +1224,7 @@ describe('AddSubModelButton - base class menu groups', () => {
|
|
|
803
1224
|
|
|
804
1225
|
it('merges explicit items with base class and grouped sources', async () => {
|
|
805
1226
|
const engine = new FlowEngine();
|
|
806
|
-
engine.flowSettings.forceEnable();
|
|
1227
|
+
await engine.flowSettings.forceEnable();
|
|
807
1228
|
|
|
808
1229
|
class Parent extends FlowModel {}
|
|
809
1230
|
class BaseChild extends FlowModel {}
|
|
@@ -862,7 +1283,7 @@ describe('AddSubModelButton - base class menu groups', () => {
|
|
|
862
1283
|
describe('AddSubModelButton - toggle interactions', () => {
|
|
863
1284
|
it('removes existing toggleable sub model and triggers callbacks', async () => {
|
|
864
1285
|
const engine = new FlowEngine();
|
|
865
|
-
engine.flowSettings.forceEnable();
|
|
1286
|
+
await engine.flowSettings.forceEnable();
|
|
866
1287
|
|
|
867
1288
|
class ToggleParent extends FlowModel {}
|
|
868
1289
|
const destroySpy = vi.fn();
|
|
@@ -926,7 +1347,7 @@ describe('AddSubModelButton - toggle interactions', () => {
|
|
|
926
1347
|
|
|
927
1348
|
it('creates toggleable sub model and runs lifecycle callbacks', async () => {
|
|
928
1349
|
const engine = new FlowEngine();
|
|
929
|
-
engine.flowSettings.forceEnable();
|
|
1350
|
+
await engine.flowSettings.forceEnable();
|
|
930
1351
|
|
|
931
1352
|
class ToggleParent extends FlowModel {}
|
|
932
1353
|
const saveSpy = vi.fn();
|
|
@@ -998,7 +1419,7 @@ describe('AddSubModelButton - toggle interactions', () => {
|
|
|
998
1419
|
|
|
999
1420
|
it('updates toggle state after external sub model removal', async () => {
|
|
1000
1421
|
const engine = new FlowEngine();
|
|
1001
|
-
engine.flowSettings.forceEnable();
|
|
1422
|
+
await engine.flowSettings.forceEnable();
|
|
1002
1423
|
|
|
1003
1424
|
class ToggleParent extends FlowModel {}
|
|
1004
1425
|
class ToggleChild extends FlowModel {}
|
|
@@ -1063,9 +1484,9 @@ describe('AddSubModelButton toggleable behavior', () => {
|
|
|
1063
1484
|
duplicate = vi.fn().mockResolvedValue(null);
|
|
1064
1485
|
}
|
|
1065
1486
|
|
|
1066
|
-
function setup() {
|
|
1487
|
+
async function setup() {
|
|
1067
1488
|
const engine = new FlowEngine();
|
|
1068
|
-
engine.flowSettings.forceEnable();
|
|
1489
|
+
await engine.flowSettings.forceEnable();
|
|
1069
1490
|
engine.registerModels({ ToggleModel });
|
|
1070
1491
|
engine.setModelRepository(new FakeRepo());
|
|
1071
1492
|
|
|
@@ -1118,7 +1539,7 @@ describe('AddSubModelButton toggleable behavior', () => {
|
|
|
1118
1539
|
});
|
|
1119
1540
|
|
|
1120
1541
|
test('keeps dropdown open and preserves loaded children on toggle add/remove', async () => {
|
|
1121
|
-
const { engine, ui } = setup();
|
|
1542
|
+
const { engine, ui } = await setup();
|
|
1122
1543
|
const user = userEvent.setup();
|
|
1123
1544
|
|
|
1124
1545
|
render(ui);
|
|
@@ -1142,6 +1563,7 @@ describe('AddSubModelButton toggleable behavior', () => {
|
|
|
1142
1563
|
},
|
|
1143
1564
|
{ timeout: 3000 },
|
|
1144
1565
|
);
|
|
1566
|
+
await waitFor(() => expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true'));
|
|
1145
1567
|
|
|
1146
1568
|
// dropdown should remain open and children should still be visible (no flicker / reload)
|
|
1147
1569
|
expect(screen.getByText('Async Group')).toBeInTheDocument();
|
|
@@ -1158,12 +1580,13 @@ describe('AddSubModelButton toggleable behavior', () => {
|
|
|
1158
1580
|
|
|
1159
1581
|
// ensure destroy has been called (avoid flakiness on exact call counts)
|
|
1160
1582
|
await waitFor(() => {
|
|
1583
|
+
expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'false');
|
|
1161
1584
|
expect(repo.destroy).toHaveBeenCalled();
|
|
1162
1585
|
});
|
|
1163
1586
|
});
|
|
1164
1587
|
|
|
1165
1588
|
test('toggle state updates without menu closing', async () => {
|
|
1166
|
-
const { ui } = setup();
|
|
1589
|
+
const { ui } = await setup();
|
|
1167
1590
|
const user = userEvent.setup();
|
|
1168
1591
|
|
|
1169
1592
|
render(ui);
|
|
@@ -1181,7 +1604,7 @@ describe('AddSubModelButton toggleable behavior', () => {
|
|
|
1181
1604
|
|
|
1182
1605
|
test('nested submenu (static items) toggle keeps menu open and reflects state', async () => {
|
|
1183
1606
|
const engine = new FlowEngine();
|
|
1184
|
-
engine.flowSettings.forceEnable();
|
|
1607
|
+
await engine.flowSettings.forceEnable();
|
|
1185
1608
|
engine.registerModels({ ToggleModel });
|
|
1186
1609
|
const parent = engine.createModel<FlowModel>({ use: FlowModel });
|
|
1187
1610
|
|
|
@@ -1263,7 +1686,7 @@ describe('AddSubModelButton toggleable behavior', () => {
|
|
|
1263
1686
|
|
|
1264
1687
|
test('submenu (second-level) toggleable stays open and updates state', async () => {
|
|
1265
1688
|
const engine = new FlowEngine();
|
|
1266
|
-
engine.flowSettings.forceEnable();
|
|
1689
|
+
await engine.flowSettings.forceEnable();
|
|
1267
1690
|
engine.registerModels({ ToggleModel });
|
|
1268
1691
|
engine.setModelRepository(new FakeRepo());
|
|
1269
1692
|
vi.spyOn(engine.flowSettings, 'open').mockResolvedValue(false as any);
|
|
@@ -1311,18 +1734,81 @@ describe('AddSubModelButton toggleable behavior', () => {
|
|
|
1311
1734
|
// click leaf toggle to add
|
|
1312
1735
|
await user.click(screen.getByText('Leaf Toggle'));
|
|
1313
1736
|
|
|
1314
|
-
// menu should remain visible
|
|
1737
|
+
// menu and submenu should remain visible after toggling a submenu leaf
|
|
1315
1738
|
expect(screen.getByText('Fields')).toBeInTheDocument();
|
|
1316
|
-
|
|
1317
|
-
// 由于点击叶子项后二级子菜单可能被收起,这里先重新展开再断言开关状态
|
|
1318
|
-
await user.hover(screen.getByText('Fields'));
|
|
1319
1739
|
await waitFor(() => expect(screen.getByText('Leaf Toggle')).toBeInTheDocument());
|
|
1320
1740
|
await waitFor(() => expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true'));
|
|
1321
1741
|
});
|
|
1322
1742
|
|
|
1743
|
+
test('keepDropdownOpen keeps root menu visible after clicking a nested relation-style leaf', async () => {
|
|
1744
|
+
const engine = new FlowEngine();
|
|
1745
|
+
await engine.flowSettings.forceEnable();
|
|
1746
|
+
|
|
1747
|
+
class Parent extends FlowModel {}
|
|
1748
|
+
class RelationLeafModel extends FlowModel {}
|
|
1749
|
+
|
|
1750
|
+
engine.registerModels({ Parent, RelationLeafModel });
|
|
1751
|
+
engine.setModelRepository(new FakeRepo());
|
|
1752
|
+
vi.spyOn(engine.flowSettings, 'open').mockResolvedValue(false as any);
|
|
1753
|
+
|
|
1754
|
+
const parent = engine.createModel<FlowModel>({ use: 'Parent' });
|
|
1755
|
+
|
|
1756
|
+
const items = [
|
|
1757
|
+
{
|
|
1758
|
+
key: 'relation-fields',
|
|
1759
|
+
label: 'Display association fields',
|
|
1760
|
+
children: [
|
|
1761
|
+
{
|
|
1762
|
+
key: 'users',
|
|
1763
|
+
label: 'Users',
|
|
1764
|
+
type: 'group' as const,
|
|
1765
|
+
children: [
|
|
1766
|
+
{
|
|
1767
|
+
key: 'user-name',
|
|
1768
|
+
label: 'User name',
|
|
1769
|
+
createModelOptions: { use: 'RelationLeafModel' },
|
|
1770
|
+
},
|
|
1771
|
+
],
|
|
1772
|
+
},
|
|
1773
|
+
],
|
|
1774
|
+
},
|
|
1775
|
+
];
|
|
1776
|
+
|
|
1777
|
+
render(
|
|
1778
|
+
<FlowEngineProvider engine={engine}>
|
|
1779
|
+
<ConfigProvider>
|
|
1780
|
+
<App>
|
|
1781
|
+
<AddSubModelButton
|
|
1782
|
+
model={parent}
|
|
1783
|
+
items={items as any}
|
|
1784
|
+
subModelType="array"
|
|
1785
|
+
subModelKey="subs"
|
|
1786
|
+
keepDropdownOpen
|
|
1787
|
+
>
|
|
1788
|
+
Open
|
|
1789
|
+
</AddSubModelButton>
|
|
1790
|
+
</App>
|
|
1791
|
+
</ConfigProvider>
|
|
1792
|
+
</FlowEngineProvider>,
|
|
1793
|
+
);
|
|
1794
|
+
|
|
1795
|
+
const user = userEvent.setup();
|
|
1796
|
+
await user.click(screen.getByText('Open'));
|
|
1797
|
+
|
|
1798
|
+
await waitFor(() => expect(screen.getByText('Display association fields')).toBeInTheDocument());
|
|
1799
|
+
await user.hover(screen.getByText('Display association fields'));
|
|
1800
|
+
await waitFor(() => expect(screen.getByText('Users')).toBeInTheDocument());
|
|
1801
|
+
await waitFor(() => expect(getSubmenuTitle('Display association fields')).toHaveAttribute('aria-expanded', 'true'));
|
|
1802
|
+
|
|
1803
|
+
await user.click(screen.getByText('User name'));
|
|
1804
|
+
|
|
1805
|
+
await waitFor(() => expect(screen.getByText('Display association fields')).toBeInTheDocument());
|
|
1806
|
+
await waitFor(() => expect(getSubmenuTitle('Display association fields')).toHaveAttribute('aria-expanded', 'true'));
|
|
1807
|
+
});
|
|
1808
|
+
|
|
1323
1809
|
test('top-level toggle updates after opening a second-level branch', async () => {
|
|
1324
1810
|
const engine = new FlowEngine();
|
|
1325
|
-
engine.flowSettings.forceEnable();
|
|
1811
|
+
await engine.flowSettings.forceEnable();
|
|
1326
1812
|
engine.registerModels({ ToggleModel });
|
|
1327
1813
|
engine.setModelRepository(new FakeRepo());
|
|
1328
1814
|
vi.spyOn(engine.flowSettings, 'open').mockResolvedValue(false as any);
|