@nocobase/flow-engine 2.1.0-alpha.40 → 2.1.0-alpha.46
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/settings/wrappers/contextual/DefaultSettingsIcon.js +84 -32
- package/lib/components/subModel/LazyDropdown.js +208 -16
- package/lib/components/subModel/utils.d.ts +1 -0
- package/lib/components/subModel/utils.js +6 -2
- package/lib/data-source/index.d.ts +9 -0
- package/lib/data-source/index.js +12 -0
- package/lib/executor/FlowExecutor.js +0 -3
- package/lib/flowContext.d.ts +6 -1
- package/lib/flowContext.js +38 -6
- package/lib/flowEngine.d.ts +4 -3
- package/lib/flowEngine.js +72 -40
- package/lib/models/flowModel.js +48 -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/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/views/ViewNavigation.d.ts +12 -2
- package/lib/views/ViewNavigation.js +22 -7
- 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/useDialog.js +2 -0
- package/lib/views/useDrawer.js +2 -0
- package/lib/views/usePage.js +2 -0
- package/package.json +4 -4
- package/src/FlowContextProvider.tsx +9 -1
- package/src/__tests__/createViewMeta.popup.test.ts +115 -1
- package/src/__tests__/flowContext.test.ts +23 -0
- package/src/__tests__/flowEngine.moveModel.test.ts +81 -1
- package/src/__tests__/flowEngine.removeModel.test.ts +47 -3
- package/src/__tests__/runjsContext.test.ts +18 -0
- package/src/__tests__/runjsContextImplementations.test.ts +9 -2
- package/src/__tests__/runjsLocales.test.ts +6 -5
- package/src/__tests__/viewScopedFlowEngine.test.ts +133 -0
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +90 -38
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +155 -5
- package/src/components/subModel/LazyDropdown.tsx +237 -16
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +254 -1
- package/src/components/subModel/utils.ts +6 -1
- package/src/data-source/index.ts +18 -0
- package/src/executor/FlowExecutor.ts +0 -3
- package/src/executor/__tests__/flowExecutor.test.ts +26 -0
- package/src/flowContext.ts +43 -6
- package/src/flowEngine.ts +75 -38
- package/src/models/__tests__/flowEngine.resolveUse.test.ts +0 -15
- package/src/models/__tests__/flowModel.test.ts +46 -62
- package/src/models/flowModel.tsx +65 -32
- 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/utils/__tests__/parsePathnameToViewParams.test.ts +21 -0
- package/src/utils/loadedPageCache.ts +147 -0
- package/src/utils/parsePathnameToViewParams.ts +45 -5
- package/src/views/ViewNavigation.ts +40 -7
- package/src/views/__tests__/ViewNavigation.test.ts +52 -0
- package/src/views/__tests__/inheritLayoutContext.test.ts +53 -0
- package/src/views/createViewMeta.ts +106 -34
- package/src/views/inheritLayoutContext.ts +26 -0
- package/src/views/useDialog.tsx +2 -0
- package/src/views/useDrawer.tsx +2 -0
- package/src/views/usePage.tsx +2 -0
|
@@ -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,
|
|
@@ -354,6 +354,259 @@ describe('transformItems - searchable flags', () => {
|
|
|
354
354
|
await user.type(searchInput, 'display');
|
|
355
355
|
await waitFor(() => expect(screen.getByText('Field display name')).toBeInTheDocument());
|
|
356
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
|
+
});
|
|
357
610
|
});
|
|
358
611
|
|
|
359
612
|
describe('transformItems - hide', () => {
|
|
@@ -232,6 +232,7 @@ export interface BuildFieldChildrenOptions {
|
|
|
232
232
|
fieldUseModel?: string | ((field: any) => string);
|
|
233
233
|
collection?: Collection;
|
|
234
234
|
associationPathName?: string;
|
|
235
|
+
maxAssociationFieldDepth?: number;
|
|
235
236
|
/**
|
|
236
237
|
* 点击这些子项后,除自身路径外,还需要联动刷新的其他菜单路径前缀
|
|
237
238
|
*/
|
|
@@ -239,13 +240,17 @@ export interface BuildFieldChildrenOptions {
|
|
|
239
240
|
}
|
|
240
241
|
|
|
241
242
|
export function buildWrapperFieldChildren(ctx: FlowModelContext, options: BuildFieldChildrenOptions) {
|
|
242
|
-
const { useModel, fieldUseModel, associationPathName, refreshTargets } = options;
|
|
243
|
+
const { useModel, fieldUseModel, associationPathName, refreshTargets, maxAssociationFieldDepth = 2 } = options;
|
|
243
244
|
const collection: Collection = options.collection || ctx.model['collection'] || ctx.collection;
|
|
244
245
|
const fields = collection.getFields();
|
|
245
246
|
const defaultItemKeys = ['fieldSettings', 'init'];
|
|
246
247
|
const children: SubModelItem[] = [];
|
|
248
|
+
const associationDepth = associationPathName ? associationPathName.split('.').filter(Boolean).length : 0;
|
|
247
249
|
for (const f of fields) {
|
|
248
250
|
if (!f?.options?.interface) continue;
|
|
251
|
+
if (associationDepth >= maxAssociationFieldDepth && (f.isAssociationField?.() || f.target || f.targetCollection)) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
249
254
|
const fieldPath = associationPathName ? `${associationPathName}.${f.name}` : f.name;
|
|
250
255
|
|
|
251
256
|
const childUse = typeof fieldUseModel === 'function' ? fieldUseModel(f) : fieldUseModel ?? 'FieldModel';
|
package/src/data-source/index.ts
CHANGED
|
@@ -37,7 +37,13 @@ export class DataSourceManager {
|
|
|
37
37
|
addFieldInterfaceGroups?: (groups: Record<string, { label: string; order?: number }>) => void;
|
|
38
38
|
addFieldInterfaceComponentOption?: (name: string, option: any) => void;
|
|
39
39
|
addFieldInterfaceOperator?: (name: string, operator: any) => void;
|
|
40
|
+
registerFieldFilterOperator?: (operator: any) => void;
|
|
41
|
+
registerFieldFilterOperatorGroup?: (name: string, operators?: any[]) => void;
|
|
42
|
+
addFieldFilterOperatorsToGroup?: (name: string, operators?: any[]) => void;
|
|
40
43
|
getFieldInterface?: (name: string) => any;
|
|
44
|
+
registerFieldInterfaceConfigure?: (options: unknown) => void;
|
|
45
|
+
getFieldInterfaceConfigure?: (name: string, collectionInfo?: unknown) => unknown;
|
|
46
|
+
getFieldInterfaceConfigureProperties?: (name: string, collectionInfo?: any) => Record<string, any>;
|
|
41
47
|
};
|
|
42
48
|
loaders = new Map<string, DataSourceLoader>();
|
|
43
49
|
loadedKeys = new Set<string>();
|
|
@@ -77,6 +83,18 @@ export class DataSourceManager {
|
|
|
77
83
|
this.collectionFieldInterfaceManager?.addFieldInterfaceOperator?.(name, operator);
|
|
78
84
|
}
|
|
79
85
|
|
|
86
|
+
registerFieldFilterOperator(operator: any) {
|
|
87
|
+
this.collectionFieldInterfaceManager?.registerFieldFilterOperator?.(operator);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
registerFieldFilterOperatorGroup(name: string, operators: any[] = []) {
|
|
91
|
+
this.collectionFieldInterfaceManager?.registerFieldFilterOperatorGroup?.(name, operators);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
addFieldFilterOperatorsToGroup(name: string, operators: any[] = []) {
|
|
95
|
+
this.collectionFieldInterfaceManager?.addFieldFilterOperatorsToGroup?.(name, operators);
|
|
96
|
+
}
|
|
97
|
+
|
|
80
98
|
registerLoader(key: string, loader: DataSourceLoader) {
|
|
81
99
|
this.loaders.set(key, loader);
|
|
82
100
|
}
|
|
@@ -158,9 +158,6 @@ export class FlowExecutor {
|
|
|
158
158
|
const stepDefaultParams = await resolveDefaultParams(step.defaultParams, runtimeCtx);
|
|
159
159
|
combinedParams = { ...stepDefaultParams };
|
|
160
160
|
} else {
|
|
161
|
-
flowContext.logger.warn(
|
|
162
|
-
`BaseModel.applyFlow: Step '${stepKey}' in flow '${flowKey}' has neither 'use' nor 'handler'. Skipping.`,
|
|
163
|
-
);
|
|
164
161
|
continue;
|
|
165
162
|
}
|
|
166
163
|
|
|
@@ -81,6 +81,32 @@ describe('FlowExecutor', () => {
|
|
|
81
81
|
expect(result.step2).toBe('step2-ok');
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
+
it('runFlow silently skips steps without use or handler', async () => {
|
|
85
|
+
const flows = {
|
|
86
|
+
referenceSettings: {
|
|
87
|
+
steps: {
|
|
88
|
+
target: {},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
} satisfies Record<string, Omit<FlowDefinitionOptions, 'key'>>;
|
|
92
|
+
const model = createModelWithFlows('m-empty-step', flows);
|
|
93
|
+
const loggerChildSpy = vi.spyOn(engine.logger, 'child').mockReturnValue(engine.logger);
|
|
94
|
+
const loggerWarnSpy = vi.spyOn(engine.logger, 'warn').mockImplementation(() => {});
|
|
95
|
+
const loggerErrorSpy = vi.spyOn(engine.logger, 'error').mockImplementation(() => {});
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const result = await engine.executor.runFlow(model, 'referenceSettings');
|
|
99
|
+
|
|
100
|
+
expect(result).toEqual({});
|
|
101
|
+
expect(loggerWarnSpy).not.toHaveBeenCalled();
|
|
102
|
+
expect(loggerErrorSpy).not.toHaveBeenCalled();
|
|
103
|
+
} finally {
|
|
104
|
+
loggerChildSpy.mockRestore();
|
|
105
|
+
loggerWarnSpy.mockRestore();
|
|
106
|
+
loggerErrorSpy.mockRestore();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
84
110
|
it("dispatchEvent('beforeRender') executes flows in sort order and caches result (when options specify)", async () => {
|
|
85
111
|
const calls: string[] = [];
|
|
86
112
|
const mkFlow = (key: string, sort: number) => ({
|
package/src/flowContext.ts
CHANGED
|
@@ -400,6 +400,10 @@ export type FlowContextGetApiInfosOptions = {
|
|
|
400
400
|
* RunJS 文档版本(默认 v1)。
|
|
401
401
|
*/
|
|
402
402
|
version?: RunJSVersion;
|
|
403
|
+
/**
|
|
404
|
+
* Include editor completion metadata. Defaults to false so API-doc callers keep the compact public shape.
|
|
405
|
+
*/
|
|
406
|
+
includeCompletion?: boolean;
|
|
403
407
|
};
|
|
404
408
|
|
|
405
409
|
export type FlowContextGetVarInfosOptions = {
|
|
@@ -704,10 +708,11 @@ export class FlowContext {
|
|
|
704
708
|
* - 输出仅来自 RunJS doc 与 defineProperty/defineMethod 的 info
|
|
705
709
|
* - 不读取/展开 PropertyMeta(变量结构)
|
|
706
710
|
* - 不自动展开深层 properties
|
|
707
|
-
* -
|
|
711
|
+
* - 默认不返回自动补全字段(例如 completion),传入 includeCompletion=true 时返回
|
|
708
712
|
*/
|
|
709
713
|
async getApiInfos(options: FlowContextGetApiInfosOptions = {}): Promise<Record<string, FlowContextApiInfo>> {
|
|
710
714
|
const version = (options.version as RunJSVersion) || ('v1' as RunJSVersion);
|
|
715
|
+
const includeCompletion = !!options.includeCompletion;
|
|
711
716
|
const evalCtx = this.createProxy();
|
|
712
717
|
|
|
713
718
|
const isPrivateKey = (key: string) => typeof key === 'string' && key.startsWith('_');
|
|
@@ -759,7 +764,14 @@ export class FlowContext {
|
|
|
759
764
|
const src = toDocObject(obj);
|
|
760
765
|
if (!src) return {};
|
|
761
766
|
const out: any = {};
|
|
762
|
-
for (const k of [
|
|
767
|
+
for (const k of [
|
|
768
|
+
'description',
|
|
769
|
+
'examples',
|
|
770
|
+
...(includeCompletion ? ['completion'] : []),
|
|
771
|
+
'ref',
|
|
772
|
+
'params',
|
|
773
|
+
'returns',
|
|
774
|
+
]) {
|
|
763
775
|
const v = (src as any)[k];
|
|
764
776
|
if (typeof v !== 'undefined') out[k] = v;
|
|
765
777
|
}
|
|
@@ -773,7 +785,17 @@ export class FlowContext {
|
|
|
773
785
|
const src = toDocObject(obj);
|
|
774
786
|
if (!src) return {};
|
|
775
787
|
const out: any = {};
|
|
776
|
-
for (const k of [
|
|
788
|
+
for (const k of [
|
|
789
|
+
'title',
|
|
790
|
+
'type',
|
|
791
|
+
'interface',
|
|
792
|
+
'description',
|
|
793
|
+
'examples',
|
|
794
|
+
...(includeCompletion ? ['completion'] : []),
|
|
795
|
+
'ref',
|
|
796
|
+
'params',
|
|
797
|
+
'returns',
|
|
798
|
+
]) {
|
|
777
799
|
const v = (src as any)[k];
|
|
778
800
|
if (typeof v !== 'undefined') out[k] = v;
|
|
779
801
|
}
|
|
@@ -872,7 +894,7 @@ export class FlowContext {
|
|
|
872
894
|
node = { ...node, ...pickPropertyInfo(docObj) };
|
|
873
895
|
node = { ...node, ...pickPropertyInfo(infoObj) };
|
|
874
896
|
delete (node as any).properties;
|
|
875
|
-
delete (node as any).completion;
|
|
897
|
+
if (!includeCompletion) delete (node as any).completion;
|
|
876
898
|
if (!Object.keys(node).length) continue;
|
|
877
899
|
const outKey = mapDocKeyToApiKey(key, docNode);
|
|
878
900
|
// Avoid exposing ctx.React/ctx.ReactDOM/ctx.antd in api docs when mapping to ctx.libs.*.
|
|
@@ -890,7 +912,7 @@ export class FlowContext {
|
|
|
890
912
|
node = { ...node, ...pickMethodInfo(docObj) };
|
|
891
913
|
node = { ...node, ...pickMethodInfo(info) };
|
|
892
914
|
delete (node as any).properties;
|
|
893
|
-
delete (node as any).completion;
|
|
915
|
+
if (!includeCompletion) delete (node as any).completion;
|
|
894
916
|
if (!Object.keys(node).length) continue;
|
|
895
917
|
node.type = 'function';
|
|
896
918
|
|
|
@@ -913,7 +935,7 @@ export class FlowContext {
|
|
|
913
935
|
let node: FlowContextApiInfo = {};
|
|
914
936
|
node = { ...node, ...pickPropertyInfo(childObj) };
|
|
915
937
|
delete (node as any).properties;
|
|
916
|
-
delete (node as any).completion;
|
|
938
|
+
if (!includeCompletion) delete (node as any).completion;
|
|
917
939
|
if (!node.description || !String(node.description).trim()) continue;
|
|
918
940
|
out[outKey] = node;
|
|
919
941
|
}
|
|
@@ -3075,6 +3097,17 @@ class BaseFlowEngineContext extends FlowContext {
|
|
|
3075
3097
|
const jsCode = await prepareRunJsCode(String(code ?? ''), { preprocessTemplates: shouldPreprocessTemplates });
|
|
3076
3098
|
return runner.run(jsCode);
|
|
3077
3099
|
},
|
|
3100
|
+
{
|
|
3101
|
+
description: 'Execute a RunJS code string in the current Flow context.',
|
|
3102
|
+
detail: '(code: string, variables?: Record<string, any>, options?: JSRunnerOptions) => Promise<RunJSResult>',
|
|
3103
|
+
params: [
|
|
3104
|
+
{ name: 'code', type: 'string', description: 'RunJS code to execute.' },
|
|
3105
|
+
{ name: 'variables', type: 'Record<string, any>', optional: true, description: 'Additional globals.' },
|
|
3106
|
+
{ name: 'options', type: 'JSRunnerOptions', optional: true, description: 'Runner options.' },
|
|
3107
|
+
],
|
|
3108
|
+
returns: { type: 'Promise<{ success: boolean; value?: any; error?: any; timeout?: boolean }>' },
|
|
3109
|
+
completion: { insertText: `await ctx.runjs('return 1')` },
|
|
3110
|
+
},
|
|
3078
3111
|
);
|
|
3079
3112
|
}
|
|
3080
3113
|
}
|
|
@@ -3581,6 +3614,9 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
3581
3614
|
},
|
|
3582
3615
|
});
|
|
3583
3616
|
this.defineMethod('aclCheck', function (params) {
|
|
3617
|
+
if (this.skipAclCheck) {
|
|
3618
|
+
return true;
|
|
3619
|
+
}
|
|
3584
3620
|
return this.acl.aclCheck(params);
|
|
3585
3621
|
});
|
|
3586
3622
|
this.defineMethod('createResource', function (this: BaseFlowEngineContext, resourceType) {
|
|
@@ -3924,6 +3960,7 @@ export type FlowSettingsContext<TModel extends FlowModel = FlowModel> = FlowRunt
|
|
|
3924
3960
|
|
|
3925
3961
|
export type RunJSDocCompletionDoc = {
|
|
3926
3962
|
insertText?: string;
|
|
3963
|
+
requires?: Array<'element'>;
|
|
3927
3964
|
};
|
|
3928
3965
|
|
|
3929
3966
|
export type RunJSDocHiddenDoc = boolean | ((ctx: any) => boolean | Promise<boolean>);
|