@nocobase/flow-engine 2.0.0-beta.9 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/BlockScopedFlowEngine.js +0 -1
- package/lib/FlowDefinition.d.ts +2 -0
- package/lib/JSRunner.d.ts +6 -0
- package/lib/JSRunner.js +32 -2
- package/lib/ViewScopedFlowEngine.js +3 -0
- package/lib/acl/Acl.js +13 -3
- package/lib/components/FlowContextSelector.js +155 -10
- package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -15
- package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +24 -4
- package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +5 -1
- package/lib/components/variables/VariableInput.js +9 -4
- package/lib/components/variables/VariableTag.js +46 -39
- package/lib/components/variables/utils.d.ts +7 -0
- package/lib/components/variables/utils.js +42 -2
- package/lib/data-source/index.d.ts +7 -27
- package/lib/data-source/index.js +81 -51
- package/lib/executor/FlowExecutor.d.ts +2 -1
- package/lib/executor/FlowExecutor.js +163 -22
- package/lib/flowContext.d.ts +230 -7
- package/lib/flowContext.js +2267 -148
- package/lib/flowEngine.d.ts +21 -0
- package/lib/flowEngine.js +56 -8
- package/lib/flowI18n.js +6 -4
- package/lib/flowSettings.js +17 -11
- package/lib/index.d.ts +7 -1
- package/lib/index.js +21 -0
- package/lib/locale/en-US.json +9 -2
- package/lib/locale/index.d.ts +14 -0
- package/lib/locale/zh-CN.json +8 -1
- package/lib/models/CollectionFieldModel.d.ts +1 -0
- package/lib/models/CollectionFieldModel.js +3 -2
- package/lib/models/flowModel.js +12 -1
- package/lib/provider.js +5 -5
- package/lib/resources/baseRecordResource.d.ts +5 -0
- package/lib/resources/baseRecordResource.js +24 -0
- package/lib/resources/multiRecordResource.d.ts +1 -0
- package/lib/resources/multiRecordResource.js +11 -4
- package/lib/resources/singleRecordResource.js +2 -0
- package/lib/resources/sqlResource.d.ts +4 -3
- package/lib/resources/sqlResource.js +8 -3
- package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +12 -2
- package/lib/runjs-context/contexts/JSBlockRunJSContext.js +2 -2
- package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.d.ts +16 -0
- package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +125 -0
- package/lib/runjs-context/contexts/JSItemRunJSContext.js +12 -2
- package/lib/runjs-context/contexts/base.js +706 -41
- package/lib/runjs-context/contributions.d.ts +33 -0
- package/lib/runjs-context/contributions.js +88 -0
- package/lib/runjs-context/helpers.js +12 -1
- package/lib/runjs-context/setup.js +6 -0
- package/lib/runjs-context/snippets/global/api-request.snippet.js +3 -3
- package/lib/runjs-context/snippets/global/import-esm.snippet.js +2 -3
- package/lib/runjs-context/snippets/global/query-selector.snippet.js +8 -3
- package/lib/runjs-context/snippets/global/require-amd.snippet.js +1 -1
- package/lib/runjs-context/snippets/index.d.ts +11 -1
- package/lib/runjs-context/snippets/index.js +61 -40
- package/lib/runjs-context/snippets/scene/block/add-event-listener.snippet.js +10 -7
- package/lib/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/block/chartjs-bar.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/echarts-init.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/render-iframe.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/render-react.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/render-statistics.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/render-timeline.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/resource-example.snippet.js +5 -5
- package/lib/runjs-context/snippets/scene/block/three-users-orbit.snippet.js +6 -6
- package/lib/runjs-context/snippets/scene/block/vue-component.snippet.js +3 -4
- package/lib/runjs-context/snippets/scene/detail/color-by-value.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.js +20 -3
- package/lib/runjs-context/snippets/scene/detail/format-number.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/innerHTML-value.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/percentage-bar.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/detail/relative-time.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/detail/status-tag.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/form/cascade-select.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/form/render-basic.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/table/cell-open-dialog.snippet.js +6 -3
- package/lib/runjs-context/snippets/scene/table/concat-fields.snippet.js +3 -1
- package/lib/runjsLibs.d.ts +28 -0
- package/lib/runjsLibs.js +532 -0
- package/lib/scheduler/ModelOperationScheduler.d.ts +2 -0
- package/lib/scheduler/ModelOperationScheduler.js +25 -21
- package/lib/types.d.ts +27 -0
- package/lib/utils/associationObjectVariable.d.ts +2 -2
- package/lib/utils/createCollectionContextMeta.js +1 -0
- package/lib/utils/createEphemeralContext.js +2 -2
- package/lib/utils/dateVariable.d.ts +16 -0
- package/lib/utils/dateVariable.js +380 -0
- package/lib/utils/exceptions.d.ts +7 -0
- package/lib/utils/exceptions.js +10 -0
- package/lib/utils/index.d.ts +8 -3
- package/lib/utils/index.js +45 -0
- package/lib/utils/params-resolvers.js +16 -9
- package/lib/utils/resolveModuleUrl.d.ts +58 -0
- package/lib/utils/resolveModuleUrl.js +65 -0
- package/lib/utils/resolveRunJSObjectValues.d.ts +16 -0
- package/lib/utils/resolveRunJSObjectValues.js +61 -0
- package/lib/utils/runjsModuleLoader.d.ts +58 -0
- package/lib/utils/runjsModuleLoader.js +422 -0
- package/lib/utils/runjsTemplateCompat.d.ts +35 -0
- package/lib/utils/runjsTemplateCompat.js +743 -0
- package/lib/utils/runjsValue.d.ts +29 -0
- package/lib/utils/runjsValue.js +275 -0
- package/lib/utils/safeGlobals.d.ts +18 -8
- package/lib/utils/safeGlobals.js +164 -17
- package/lib/utils/schema-utils.d.ts +10 -0
- package/lib/utils/schema-utils.js +61 -0
- package/lib/views/createViewMeta.d.ts +0 -7
- package/lib/views/createViewMeta.js +19 -70
- package/lib/views/index.d.ts +1 -2
- package/lib/views/index.js +4 -3
- package/lib/views/useDialog.js +7 -2
- package/lib/views/useDrawer.js +7 -2
- package/lib/views/usePage.d.ts +4 -0
- package/lib/views/usePage.js +43 -6
- package/lib/views/usePopover.js +4 -1
- package/lib/views/viewEvents.d.ts +17 -0
- package/lib/views/viewEvents.js +90 -0
- package/package.json +4 -4
- package/src/BlockScopedFlowEngine.ts +2 -5
- package/src/JSRunner.ts +44 -2
- package/src/ViewScopedFlowEngine.ts +4 -0
- package/src/__tests__/JSRunner.test.ts +64 -0
- package/src/__tests__/createViewMeta.popup.test.ts +62 -1
- package/src/__tests__/flowContext.test.ts +693 -1
- package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
- package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
- package/src/__tests__/flowRunJSContextDefine.test.ts +63 -0
- package/src/__tests__/flowRuntimeContext.test.ts +2 -1
- package/src/__tests__/flowSettings.open.test.tsx +123 -19
- package/src/__tests__/runjsContext.test.ts +10 -7
- package/src/__tests__/runjsContextImplementations.test.ts +34 -3
- package/src/__tests__/runjsContextRuntime.test.ts +3 -3
- package/src/__tests__/runjsContributions.test.ts +89 -0
- package/src/__tests__/runjsExternalLibs.test.ts +242 -0
- package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
- package/src/__tests__/runjsLocales.test.ts +4 -1
- package/src/__tests__/runjsPreprocessDefault.test.ts +49 -0
- package/src/__tests__/runjsRuntimeFeatures.test.ts +166 -0
- package/src/__tests__/runjsSnippets.test.ts +40 -3
- package/src/acl/Acl.tsx +3 -3
- package/src/components/FlowContextSelector.tsx +208 -12
- package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
- package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +109 -16
- package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +41 -7
- package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +13 -2
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +157 -5
- package/src/components/variables/VariableInput.tsx +12 -4
- package/src/components/variables/VariableTag.tsx +54 -45
- package/src/components/variables/__tests__/FlowContextSelector.test.tsx +260 -3
- package/src/components/variables/__tests__/VariableTag.test.tsx +50 -0
- package/src/components/variables/__tests__/utils.test.ts +81 -3
- package/src/components/variables/utils.ts +67 -6
- package/src/data-source/index.ts +85 -110
- package/src/executor/FlowExecutor.ts +200 -23
- package/src/executor/__tests__/flowExecutor.test.ts +66 -0
- package/src/flowContext.ts +2986 -211
- package/src/flowEngine.ts +59 -8
- package/src/flowI18n.ts +7 -5
- package/src/flowSettings.ts +18 -12
- package/src/index.ts +14 -1
- package/src/locale/en-US.json +9 -2
- package/src/locale/zh-CN.json +8 -1
- package/src/models/CollectionFieldModel.tsx +3 -1
- package/src/models/__tests__/dispatchEvent.when.test.ts +554 -0
- package/src/models/__tests__/flowModel.test.ts +20 -4
- package/src/models/flowModel.tsx +13 -1
- package/src/provider.tsx +7 -6
- package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
- package/src/resources/__tests__/sqlResource.test.ts +60 -0
- package/src/resources/baseRecordResource.ts +31 -0
- package/src/resources/multiRecordResource.ts +11 -4
- package/src/resources/singleRecordResource.ts +3 -0
- package/src/resources/sqlResource.ts +11 -6
- package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +10 -0
- package/src/runjs-context/contexts/JSBlockRunJSContext.ts +6 -2
- package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +106 -0
- package/src/runjs-context/contexts/JSItemRunJSContext.ts +10 -0
- package/src/runjs-context/contexts/base.ts +715 -44
- package/src/runjs-context/contributions.ts +88 -0
- package/src/runjs-context/helpers.ts +11 -1
- package/src/runjs-context/setup.ts +6 -0
- package/src/runjs-context/snippets/global/api-request.snippet.ts +3 -3
- package/src/runjs-context/snippets/global/import-esm.snippet.ts +2 -3
- package/src/runjs-context/snippets/global/query-selector.snippet.ts +8 -3
- package/src/runjs-context/snippets/global/require-amd.snippet.ts +1 -1
- package/src/runjs-context/snippets/index.ts +75 -41
- package/src/runjs-context/snippets/scene/block/add-event-listener.snippet.ts +11 -13
- package/src/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/block/chartjs-bar.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/echarts-init.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/render-iframe.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/render-react.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/render-statistics.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/render-timeline.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/resource-example.snippet.ts +6 -11
- package/src/runjs-context/snippets/scene/block/three-users-orbit.snippet.ts +6 -6
- package/src/runjs-context/snippets/scene/block/vue-component.snippet.ts +3 -4
- package/src/runjs-context/snippets/scene/detail/color-by-value.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.ts +20 -3
- package/src/runjs-context/snippets/scene/detail/format-number.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/innerHTML-value.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/percentage-bar.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/detail/relative-time.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/detail/status-tag.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/form/cascade-select.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/form/render-basic.snippet.ts +3 -8
- package/src/runjs-context/snippets/scene/table/cell-open-dialog.snippet.ts +6 -3
- package/src/runjs-context/snippets/scene/table/concat-fields.snippet.ts +3 -1
- package/src/runjsLibs.ts +622 -0
- package/src/scheduler/ModelOperationScheduler.ts +27 -21
- package/src/types.ts +38 -1
- package/src/utils/__tests__/dateVariable.test.ts +101 -0
- package/src/utils/__tests__/params-resolvers.test.ts +40 -0
- package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
- package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
- package/src/utils/__tests__/runjsValue.test.ts +44 -0
- package/src/utils/__tests__/safeGlobals.test.ts +57 -2
- package/src/utils/__tests__/utils.test.ts +95 -0
- package/src/utils/associationObjectVariable.ts +2 -2
- package/src/utils/createCollectionContextMeta.ts +1 -0
- package/src/utils/createEphemeralContext.ts +5 -4
- package/src/utils/dateVariable.ts +397 -0
- package/src/utils/exceptions.ts +11 -0
- package/src/utils/index.ts +37 -3
- package/src/utils/params-resolvers.ts +23 -9
- package/src/utils/resolveModuleUrl.ts +91 -0
- package/src/utils/resolveRunJSObjectValues.ts +46 -0
- package/src/utils/runjsModuleLoader.ts +553 -0
- package/src/utils/runjsTemplateCompat.ts +828 -0
- package/src/utils/runjsValue.ts +287 -0
- package/src/utils/safeGlobals.ts +188 -17
- package/src/utils/schema-utils.ts +79 -0
- package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +35 -8
- package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
- package/src/views/createViewMeta.ts +22 -75
- package/src/views/index.tsx +1 -2
- package/src/views/useDialog.tsx +8 -1
- package/src/views/useDrawer.tsx +8 -1
- package/src/views/usePage.tsx +51 -5
- package/src/views/usePopover.tsx +4 -1
- package/src/views/viewEvents.ts +55 -0
|
@@ -11,6 +11,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
|
11
11
|
import React from 'react';
|
|
12
12
|
import { describe, expect, it, vi } from 'vitest';
|
|
13
13
|
import * as FlowContextSelectorModule from '../../FlowContextSelector';
|
|
14
|
+
import type { MetaTreeNode } from '../../../flowContext';
|
|
14
15
|
import { createTestFlowContext, TestFlowContextWrapper } from './test-utils';
|
|
15
16
|
|
|
16
17
|
const FlowContextSelector = FlowContextSelectorModule.FlowContextSelector;
|
|
@@ -176,13 +177,242 @@ describe('FlowContextSelector', () => {
|
|
|
176
177
|
const cascader = screen.getByRole('button');
|
|
177
178
|
fireEvent.click(cascader);
|
|
178
179
|
|
|
180
|
+
const searchInput = await screen.findByPlaceholderText('Search');
|
|
181
|
+
|
|
179
182
|
await waitFor(() => {
|
|
180
183
|
expect(screen.getByText('User')).toBeInTheDocument();
|
|
181
184
|
});
|
|
182
185
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
+
fireEvent.change(searchInput, { target: { value: 'config' } });
|
|
187
|
+
|
|
188
|
+
await waitFor(() => {
|
|
189
|
+
expect(screen.getByText('Config')).toBeInTheDocument();
|
|
190
|
+
expect(screen.queryByText('User')).not.toBeInTheDocument();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
fireEvent.change(searchInput, { target: { value: '' } });
|
|
194
|
+
|
|
195
|
+
await waitFor(() => {
|
|
196
|
+
expect(screen.getByText('User')).toBeInTheDocument();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should not render search input when search is disabled', async () => {
|
|
201
|
+
const flowContext = createTestFlowContext();
|
|
202
|
+
render(
|
|
203
|
+
<TestFlowContextWrapper context={flowContext}>
|
|
204
|
+
<FlowContextSelector metaTree={() => flowContext.getPropertyMetaTree()} showSearch={false} />
|
|
205
|
+
</TestFlowContextWrapper>,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
fireEvent.click(screen.getByRole('button'));
|
|
209
|
+
|
|
210
|
+
await waitFor(() => {
|
|
211
|
+
expect(screen.getByText('User')).toBeInTheDocument();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
expect(screen.queryByPlaceholderText('Search')).not.toBeInTheDocument();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should load and expand lazy relation node after searching', async () => {
|
|
218
|
+
const flowContext = createTestFlowContext();
|
|
219
|
+
const loadOrgChildren = vi.fn(async () => [
|
|
220
|
+
{
|
|
221
|
+
name: 'org_name',
|
|
222
|
+
title: 'Org Name',
|
|
223
|
+
type: 'string',
|
|
224
|
+
paths: ['org_oho', 'org_name'],
|
|
225
|
+
parentTitles: ['org_oho'],
|
|
226
|
+
},
|
|
227
|
+
]);
|
|
228
|
+
const metaTree: MetaTreeNode[] = [
|
|
229
|
+
{
|
|
230
|
+
name: 'org_oho',
|
|
231
|
+
title: 'org_oho',
|
|
232
|
+
type: 'object',
|
|
233
|
+
paths: ['org_oho'],
|
|
234
|
+
children: loadOrgChildren,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'staff',
|
|
238
|
+
title: 'staff',
|
|
239
|
+
type: 'string',
|
|
240
|
+
paths: ['staff'],
|
|
241
|
+
},
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
render(
|
|
245
|
+
<TestFlowContextWrapper context={flowContext}>
|
|
246
|
+
<FlowContextSelector metaTree={metaTree} showSearch onlyLeafSelectable={true} />
|
|
247
|
+
</TestFlowContextWrapper>,
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
fireEvent.click(screen.getByRole('button'));
|
|
251
|
+
const searchInput = await screen.findByPlaceholderText('Search');
|
|
252
|
+
|
|
253
|
+
fireEvent.change(searchInput, { target: { value: 'oho' } });
|
|
254
|
+
|
|
255
|
+
await waitFor(() => {
|
|
256
|
+
expect(screen.getByText('org_oho')).toBeInTheDocument();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
fireEvent.click(screen.getByText('org_oho'));
|
|
260
|
+
|
|
261
|
+
await waitFor(() => {
|
|
262
|
+
expect(loadOrgChildren).toHaveBeenCalledTimes(1);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await waitFor(() => {
|
|
266
|
+
expect(screen.getByText('Org Name')).toBeInTheDocument();
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should support inline search input when children is null and keep lazy expand', async () => {
|
|
271
|
+
const flowContext = createTestFlowContext();
|
|
272
|
+
const loadOrgChildren = vi.fn(async () => [
|
|
273
|
+
{
|
|
274
|
+
name: 'org_name',
|
|
275
|
+
title: 'Org Name',
|
|
276
|
+
type: 'string',
|
|
277
|
+
paths: ['org_oho', 'org_name'],
|
|
278
|
+
parentTitles: ['org_oho'],
|
|
279
|
+
},
|
|
280
|
+
]);
|
|
281
|
+
|
|
282
|
+
const metaTree: MetaTreeNode[] = [
|
|
283
|
+
{
|
|
284
|
+
name: 'org_oho',
|
|
285
|
+
title: 'org_oho',
|
|
286
|
+
type: 'object',
|
|
287
|
+
paths: ['org_oho'],
|
|
288
|
+
children: loadOrgChildren,
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
name: 'staff',
|
|
292
|
+
title: 'staff',
|
|
293
|
+
type: 'string',
|
|
294
|
+
paths: ['staff'],
|
|
295
|
+
},
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
render(
|
|
299
|
+
<TestFlowContextWrapper context={flowContext}>
|
|
300
|
+
<FlowContextSelector metaTree={metaTree} onlyLeafSelectable={true}>
|
|
301
|
+
{null}
|
|
302
|
+
</FlowContextSelector>
|
|
303
|
+
</TestFlowContextWrapper>,
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const inlineInput = screen.getByRole('textbox');
|
|
307
|
+
fireEvent.focus(inlineInput);
|
|
308
|
+
fireEvent.change(inlineInput, { target: { value: 'oho' } });
|
|
309
|
+
|
|
310
|
+
await waitFor(() => {
|
|
311
|
+
expect(screen.getByText('org_oho')).toBeInTheDocument();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
fireEvent.click(screen.getByText('org_oho'));
|
|
315
|
+
|
|
316
|
+
await waitFor(() => {
|
|
317
|
+
expect(loadOrgChildren).toHaveBeenCalledTimes(1);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
await waitFor(() => {
|
|
321
|
+
expect(screen.getByText('Org Name')).toBeInTheDocument();
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should keep dropdown open on first pointer click in inline input mode', async () => {
|
|
326
|
+
const flowContext = createTestFlowContext();
|
|
327
|
+
|
|
328
|
+
render(
|
|
329
|
+
<TestFlowContextWrapper context={flowContext}>
|
|
330
|
+
<FlowContextSelector metaTree={() => flowContext.getPropertyMetaTree()}>{null}</FlowContextSelector>
|
|
331
|
+
</TestFlowContextWrapper>,
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const inlineInput = await screen.findByRole('textbox');
|
|
335
|
+
|
|
336
|
+
fireEvent.mouseDown(inlineInput);
|
|
337
|
+
fireEvent.focus(inlineInput);
|
|
338
|
+
fireEvent.mouseUp(inlineInput);
|
|
339
|
+
fireEvent.click(inlineInput);
|
|
340
|
+
|
|
341
|
+
await waitFor(() => {
|
|
342
|
+
expect(screen.getByText('User')).toBeInTheDocument();
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should show selected path text when inline input dropdown is closed', async () => {
|
|
347
|
+
const flowContext = createTestFlowContext();
|
|
348
|
+
|
|
349
|
+
render(
|
|
350
|
+
<TestFlowContextWrapper context={flowContext}>
|
|
351
|
+
<FlowContextSelector metaTree={() => flowContext.getPropertyMetaTree()} value="{{ ctx.user.name }}">
|
|
352
|
+
{null}
|
|
353
|
+
</FlowContextSelector>
|
|
354
|
+
</TestFlowContextWrapper>,
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
const inlineInput = screen.getByRole('textbox') as HTMLInputElement;
|
|
358
|
+
|
|
359
|
+
await waitFor(() => {
|
|
360
|
+
expect(inlineInput.value).toBe('User / Name');
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should clear selected value when clearing inline input while dropdown is closed', async () => {
|
|
365
|
+
const onChange = vi.fn();
|
|
366
|
+
const flowContext = createTestFlowContext();
|
|
367
|
+
|
|
368
|
+
render(
|
|
369
|
+
<TestFlowContextWrapper context={flowContext}>
|
|
370
|
+
<FlowContextSelector
|
|
371
|
+
metaTree={() => flowContext.getPropertyMetaTree()}
|
|
372
|
+
value="{{ ctx.user.name }}"
|
|
373
|
+
onChange={onChange}
|
|
374
|
+
>
|
|
375
|
+
{null}
|
|
376
|
+
</FlowContextSelector>
|
|
377
|
+
</TestFlowContextWrapper>,
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const inlineInput = screen.getByRole('textbox');
|
|
381
|
+
fireEvent.change(inlineInput, { target: { value: '' } });
|
|
382
|
+
|
|
383
|
+
await waitFor(() => {
|
|
384
|
+
expect(onChange).toHaveBeenCalledWith('', undefined);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('should clear selected value when clicking inline clear icon while dropdown is closed', async () => {
|
|
389
|
+
const onChange = vi.fn();
|
|
390
|
+
const flowContext = createTestFlowContext();
|
|
391
|
+
|
|
392
|
+
render(
|
|
393
|
+
<TestFlowContextWrapper context={flowContext}>
|
|
394
|
+
<FlowContextSelector
|
|
395
|
+
metaTree={() => flowContext.getPropertyMetaTree()}
|
|
396
|
+
value="{{ ctx.user.name }}"
|
|
397
|
+
onChange={onChange}
|
|
398
|
+
>
|
|
399
|
+
{null}
|
|
400
|
+
</FlowContextSelector>
|
|
401
|
+
</TestFlowContextWrapper>,
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
await screen.findByRole('textbox');
|
|
405
|
+
|
|
406
|
+
const clearIcon = document.querySelector('.ant-input-clear-icon') as HTMLElement | null;
|
|
407
|
+
expect(clearIcon).toBeInTheDocument();
|
|
408
|
+
|
|
409
|
+
fireEvent.mouseDown(clearIcon!);
|
|
410
|
+
fireEvent.mouseUp(clearIcon!);
|
|
411
|
+
fireEvent.click(clearIcon!);
|
|
412
|
+
|
|
413
|
+
await waitFor(() => {
|
|
414
|
+
expect(onChange).toHaveBeenCalledWith('', undefined);
|
|
415
|
+
});
|
|
186
416
|
});
|
|
187
417
|
|
|
188
418
|
it('should handle FlowContext metaTree', async () => {
|
|
@@ -456,6 +686,33 @@ describe('FlowContextSelector', () => {
|
|
|
456
686
|
});
|
|
457
687
|
});
|
|
458
688
|
|
|
689
|
+
it('should handle non-array parsed path in inline input mode', async () => {
|
|
690
|
+
const flowContext = createTestFlowContext();
|
|
691
|
+
const customParseValueToPath = vi.fn().mockReturnValue('' as any);
|
|
692
|
+
|
|
693
|
+
render(
|
|
694
|
+
<TestFlowContextWrapper context={flowContext}>
|
|
695
|
+
<FlowContextSelector
|
|
696
|
+
metaTree={() => flowContext.getPropertyMetaTree()}
|
|
697
|
+
value="invalid.path"
|
|
698
|
+
parseValueToPath={customParseValueToPath as any}
|
|
699
|
+
>
|
|
700
|
+
{null}
|
|
701
|
+
</FlowContextSelector>
|
|
702
|
+
</TestFlowContextWrapper>,
|
|
703
|
+
);
|
|
704
|
+
|
|
705
|
+
const inlineInput = await screen.findByRole('textbox');
|
|
706
|
+
expect(inlineInput).toBeInTheDocument();
|
|
707
|
+
expect((inlineInput as HTMLInputElement).value).toBe('');
|
|
708
|
+
|
|
709
|
+
fireEvent.focus(inlineInput);
|
|
710
|
+
|
|
711
|
+
await waitFor(() => {
|
|
712
|
+
expect(screen.getByText('User')).toBeInTheDocument();
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
|
|
459
716
|
it('should handle metaTree function returning non-array', async () => {
|
|
460
717
|
const invalidMetaTree = vi.fn().mockResolvedValue(null);
|
|
461
718
|
const flowContext = createTestFlowContext();
|
|
@@ -272,6 +272,56 @@ describe('VariableTag', () => {
|
|
|
272
272
|
);
|
|
273
273
|
});
|
|
274
274
|
|
|
275
|
+
it('should display full path when metaTreeNode has no parentTitles but metaTree is provided', async () => {
|
|
276
|
+
const metaTree = [
|
|
277
|
+
{
|
|
278
|
+
name: 'item',
|
|
279
|
+
title: 'Current item',
|
|
280
|
+
type: 'object',
|
|
281
|
+
paths: ['item'],
|
|
282
|
+
children: [
|
|
283
|
+
{
|
|
284
|
+
name: 'value',
|
|
285
|
+
title: 'Attributes',
|
|
286
|
+
type: 'object',
|
|
287
|
+
paths: ['item', 'value'],
|
|
288
|
+
children: [
|
|
289
|
+
{
|
|
290
|
+
name: 'nickname',
|
|
291
|
+
title: 'Nickname',
|
|
292
|
+
type: 'string',
|
|
293
|
+
paths: ['item', 'value', 'nickname'],
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
},
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
const mockMetaTreeNode = {
|
|
302
|
+
name: 'nickname',
|
|
303
|
+
title: 'Nickname',
|
|
304
|
+
type: 'string',
|
|
305
|
+
paths: ['item', 'value', 'nickname'],
|
|
306
|
+
// 没有 parentTitles 属性
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
renderWithCtx(
|
|
310
|
+
<VariableTag
|
|
311
|
+
value="{{ ctx.item.value.nickname }}"
|
|
312
|
+
metaTreeNode={mockMetaTreeNode as any}
|
|
313
|
+
metaTree={metaTree as any}
|
|
314
|
+
/>,
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
await waitFor(
|
|
318
|
+
() => {
|
|
319
|
+
expect(screen.getByText('Current item/Attributes/Nickname')).toBeInTheDocument();
|
|
320
|
+
},
|
|
321
|
+
{ timeout: 3000 },
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
|
|
275
325
|
it('should handle function type metaTree gracefully', async () => {
|
|
276
326
|
const mockMetaTreeFunction = () => [
|
|
277
327
|
{
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
formatPathToValue,
|
|
14
14
|
loadMetaTreeChildren,
|
|
15
15
|
searchInLoadedNodes,
|
|
16
|
+
filterLoadedContextSelectorItems,
|
|
16
17
|
buildContextSelectorItems,
|
|
17
18
|
isVariableValue,
|
|
18
19
|
createDefaultConverters,
|
|
@@ -150,6 +151,83 @@ describe('Variable Utils', () => {
|
|
|
150
151
|
});
|
|
151
152
|
});
|
|
152
153
|
|
|
154
|
+
describe('filterLoadedContextSelectorItems', () => {
|
|
155
|
+
const loadedChildren = [
|
|
156
|
+
{
|
|
157
|
+
label: 'Org Name',
|
|
158
|
+
value: 'org_name',
|
|
159
|
+
paths: ['org', 'org_name'],
|
|
160
|
+
isLeaf: true,
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
label: 'Org Code',
|
|
164
|
+
value: 'org_code',
|
|
165
|
+
paths: ['org', 'org_code'],
|
|
166
|
+
isLeaf: true,
|
|
167
|
+
},
|
|
168
|
+
] satisfies ContextSelectorItem[];
|
|
169
|
+
|
|
170
|
+
const asyncChildLoader = async () => [];
|
|
171
|
+
|
|
172
|
+
const mockOptions: ContextSelectorItem[] = [
|
|
173
|
+
{
|
|
174
|
+
label: 'Organization',
|
|
175
|
+
value: 'org',
|
|
176
|
+
paths: ['org'],
|
|
177
|
+
children: loadedChildren,
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
label: 'Staff',
|
|
181
|
+
value: 'staff',
|
|
182
|
+
paths: ['staff'],
|
|
183
|
+
meta: {
|
|
184
|
+
name: 'staff',
|
|
185
|
+
title: 'Staff',
|
|
186
|
+
type: 'object',
|
|
187
|
+
paths: ['staff'],
|
|
188
|
+
children: asyncChildLoader,
|
|
189
|
+
} satisfies MetaTreeNode,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
label: 'Config',
|
|
193
|
+
value: 'config',
|
|
194
|
+
paths: ['config'],
|
|
195
|
+
isLeaf: true,
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
it('should keep original tree when keyword is empty', () => {
|
|
200
|
+
const result = filterLoadedContextSelectorItems(mockOptions, ' ');
|
|
201
|
+
expect(result).toBe(mockOptions);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should keep parent node reference when parent matches', () => {
|
|
205
|
+
const result = filterLoadedContextSelectorItems(mockOptions, 'orga');
|
|
206
|
+
expect(result).toHaveLength(1);
|
|
207
|
+
expect(result[0]).toBe(mockOptions[0]);
|
|
208
|
+
expect(result[0].children).toBe(loadedChildren);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should keep tree shape and trim children when only child matches', () => {
|
|
212
|
+
const result = filterLoadedContextSelectorItems(mockOptions, 'code');
|
|
213
|
+
expect(result).toHaveLength(1);
|
|
214
|
+
expect(result[0]).not.toBe(mockOptions[0]);
|
|
215
|
+
expect(result[0].value).toBe('org');
|
|
216
|
+
expect(result[0].children).toHaveLength(1);
|
|
217
|
+
expect(result[0].children?.[0]).toBe(loadedChildren[1]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should not recurse into unloaded children', () => {
|
|
221
|
+
const result = filterLoadedContextSelectorItems(mockOptions, 'staff child');
|
|
222
|
+
expect(result).toHaveLength(0);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should return empty array when no node matches', () => {
|
|
226
|
+
const result = filterLoadedContextSelectorItems(mockOptions, 'not-exists');
|
|
227
|
+
expect(result).toEqual([]);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
153
231
|
describe('buildContextSelectorItems', () => {
|
|
154
232
|
it('should convert MetaTreeNode[] to ContextSelectorItem[]', () => {
|
|
155
233
|
const metaTree: MetaTreeNode[] = [
|
|
@@ -234,9 +312,9 @@ describe('Variable Utils', () => {
|
|
|
234
312
|
it('should handle invalid metaTree input', () => {
|
|
235
313
|
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
236
314
|
|
|
237
|
-
expect(buildContextSelectorItems(null as
|
|
238
|
-
expect(buildContextSelectorItems(undefined as
|
|
239
|
-
expect(buildContextSelectorItems({} as
|
|
315
|
+
expect(buildContextSelectorItems(null as unknown as MetaTreeNode[])).toEqual([]);
|
|
316
|
+
expect(buildContextSelectorItems(undefined as unknown as MetaTreeNode[])).toEqual([]);
|
|
317
|
+
expect(buildContextSelectorItems({} as unknown as MetaTreeNode[])).toEqual([]);
|
|
240
318
|
|
|
241
319
|
expect(consoleSpy).toHaveBeenCalledTimes(3);
|
|
242
320
|
consoleSpy.mockRestore();
|
|
@@ -13,6 +13,18 @@ import type { MetaTreeNode } from '../../flowContext';
|
|
|
13
13
|
import type { ContextSelectorItem, Converters } from './types';
|
|
14
14
|
import { isVariableExpression } from '../../utils';
|
|
15
15
|
|
|
16
|
+
const getContextSelectorLabelText = (node: ContextSelectorItem) => {
|
|
17
|
+
if (typeof node.label === 'string') {
|
|
18
|
+
return node.label;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (typeof node.meta?.title === 'string') {
|
|
22
|
+
return node.meta.title;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return node.value;
|
|
26
|
+
};
|
|
27
|
+
|
|
16
28
|
export const parseValueToPath = (value: string): string[] | undefined => {
|
|
17
29
|
if (typeof value !== 'string') return undefined;
|
|
18
30
|
|
|
@@ -66,12 +78,7 @@ export const searchInLoadedNodes = (
|
|
|
66
78
|
const nodePath = [...currentPath, node.value];
|
|
67
79
|
|
|
68
80
|
// 计算可搜索的纯文本标签
|
|
69
|
-
const labelText =
|
|
70
|
-
typeof node.label === 'string'
|
|
71
|
-
? node.label
|
|
72
|
-
: typeof node.meta?.title === 'string'
|
|
73
|
-
? node.meta!.title
|
|
74
|
-
: String(node.value);
|
|
81
|
+
const labelText = getContextSelectorLabelText(node);
|
|
75
82
|
|
|
76
83
|
// 检查节点标签是否匹配搜索文本
|
|
77
84
|
if (labelText.toLowerCase().includes(lowerSearchText)) {
|
|
@@ -89,6 +96,60 @@ export const searchInLoadedNodes = (
|
|
|
89
96
|
return results;
|
|
90
97
|
};
|
|
91
98
|
|
|
99
|
+
/**
|
|
100
|
+
* 仅在“已加载节点”范围内按关键字过滤 options(保留树结构)。
|
|
101
|
+
* - 匹配父节点:保留原节点引用(含原 children),避免不必要的实体重建。
|
|
102
|
+
* - 匹配子节点:返回裁剪后的父节点副本,children 仅包含命中分支。
|
|
103
|
+
* - 未加载 children(即 children 不为数组)不会递归搜索。
|
|
104
|
+
*/
|
|
105
|
+
export const filterLoadedContextSelectorItems = (
|
|
106
|
+
options: ContextSelectorItem[] | undefined,
|
|
107
|
+
keyword: string,
|
|
108
|
+
): ContextSelectorItem[] => {
|
|
109
|
+
if (!Array.isArray(options) || options.length === 0) return [];
|
|
110
|
+
|
|
111
|
+
const normalizedKeyword = keyword.trim().toLowerCase();
|
|
112
|
+
if (!normalizedKeyword) {
|
|
113
|
+
return options;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const filterNode = (node: ContextSelectorItem): ContextSelectorItem | null => {
|
|
117
|
+
const labelText = getContextSelectorLabelText(node).toLowerCase();
|
|
118
|
+
const selfMatched = labelText.includes(normalizedKeyword);
|
|
119
|
+
|
|
120
|
+
if (selfMatched) {
|
|
121
|
+
return node;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!Array.isArray(node.children) || node.children.length === 0) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const filteredChildren = node.children
|
|
129
|
+
.map((child) => filterNode(child))
|
|
130
|
+
.filter((item): item is ContextSelectorItem => item !== null);
|
|
131
|
+
|
|
132
|
+
if (filteredChildren.length === 0) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 所有子节点都保留原引用时,直接复用父节点对象。
|
|
137
|
+
if (
|
|
138
|
+
filteredChildren.length === node.children.length &&
|
|
139
|
+
filteredChildren.every((child, idx) => child === node.children![idx])
|
|
140
|
+
) {
|
|
141
|
+
return node;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
...node,
|
|
146
|
+
children: filteredChildren,
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return options.map((node) => filterNode(node)).filter((item): item is ContextSelectorItem => item !== null);
|
|
151
|
+
};
|
|
152
|
+
|
|
92
153
|
export const buildContextSelectorItems = (metaTree: MetaTreeNode[]): ContextSelectorItem[] => {
|
|
93
154
|
if (!metaTree || !Array.isArray(metaTree)) {
|
|
94
155
|
console.warn('buildContextSelectorItems received invalid metaTree:', metaTree);
|