@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.
Files changed (77) hide show
  1. package/lib/FlowContextProvider.d.ts +5 -1
  2. package/lib/FlowContextProvider.js +9 -2
  3. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +84 -32
  4. package/lib/components/subModel/LazyDropdown.js +208 -16
  5. package/lib/components/subModel/utils.d.ts +1 -0
  6. package/lib/components/subModel/utils.js +6 -2
  7. package/lib/data-source/index.d.ts +9 -0
  8. package/lib/data-source/index.js +12 -0
  9. package/lib/executor/FlowExecutor.js +0 -3
  10. package/lib/flowContext.d.ts +6 -1
  11. package/lib/flowContext.js +38 -6
  12. package/lib/flowEngine.d.ts +4 -3
  13. package/lib/flowEngine.js +72 -40
  14. package/lib/models/flowModel.js +48 -16
  15. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +4 -3
  16. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +4 -15
  17. package/lib/runjs-context/contexts/JSColumnRunJSContext.js +5 -2
  18. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +5 -8
  19. package/lib/runjs-context/contexts/JSFieldRunJSContext.js +4 -3
  20. package/lib/runjs-context/contexts/JSItemRunJSContext.js +4 -3
  21. package/lib/runjs-context/contexts/base.js +464 -29
  22. package/lib/runjs-context/contexts/elementDoc.d.ts +11 -0
  23. package/lib/runjs-context/contexts/elementDoc.js +152 -0
  24. package/lib/utils/loadedPageCache.d.ts +24 -0
  25. package/lib/utils/loadedPageCache.js +139 -0
  26. package/lib/utils/parsePathnameToViewParams.d.ts +5 -1
  27. package/lib/utils/parsePathnameToViewParams.js +28 -4
  28. package/lib/views/ViewNavigation.d.ts +12 -2
  29. package/lib/views/ViewNavigation.js +22 -7
  30. package/lib/views/createViewMeta.js +114 -50
  31. package/lib/views/inheritLayoutContext.d.ts +10 -0
  32. package/lib/views/inheritLayoutContext.js +50 -0
  33. package/lib/views/useDialog.js +2 -0
  34. package/lib/views/useDrawer.js +2 -0
  35. package/lib/views/usePage.js +2 -0
  36. package/package.json +4 -4
  37. package/src/FlowContextProvider.tsx +9 -1
  38. package/src/__tests__/createViewMeta.popup.test.ts +115 -1
  39. package/src/__tests__/flowContext.test.ts +23 -0
  40. package/src/__tests__/flowEngine.moveModel.test.ts +81 -1
  41. package/src/__tests__/flowEngine.removeModel.test.ts +47 -3
  42. package/src/__tests__/runjsContext.test.ts +18 -0
  43. package/src/__tests__/runjsContextImplementations.test.ts +9 -2
  44. package/src/__tests__/runjsLocales.test.ts +6 -5
  45. package/src/__tests__/viewScopedFlowEngine.test.ts +133 -0
  46. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +90 -38
  47. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +155 -5
  48. package/src/components/subModel/LazyDropdown.tsx +237 -16
  49. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +254 -1
  50. package/src/components/subModel/utils.ts +6 -1
  51. package/src/data-source/index.ts +18 -0
  52. package/src/executor/FlowExecutor.ts +0 -3
  53. package/src/executor/__tests__/flowExecutor.test.ts +26 -0
  54. package/src/flowContext.ts +43 -6
  55. package/src/flowEngine.ts +75 -38
  56. package/src/models/__tests__/flowEngine.resolveUse.test.ts +0 -15
  57. package/src/models/__tests__/flowModel.test.ts +46 -62
  58. package/src/models/flowModel.tsx +65 -32
  59. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +4 -3
  60. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +4 -15
  61. package/src/runjs-context/contexts/JSColumnRunJSContext.ts +4 -2
  62. package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +5 -9
  63. package/src/runjs-context/contexts/JSFieldRunJSContext.ts +4 -3
  64. package/src/runjs-context/contexts/JSItemRunJSContext.ts +4 -3
  65. package/src/runjs-context/contexts/base.ts +467 -31
  66. package/src/runjs-context/contexts/elementDoc.ts +130 -0
  67. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +21 -0
  68. package/src/utils/loadedPageCache.ts +147 -0
  69. package/src/utils/parsePathnameToViewParams.ts +45 -5
  70. package/src/views/ViewNavigation.ts +40 -7
  71. package/src/views/__tests__/ViewNavigation.test.ts +52 -0
  72. package/src/views/__tests__/inheritLayoutContext.test.ts +53 -0
  73. package/src/views/createViewMeta.ts +106 -34
  74. package/src/views/inheritLayoutContext.ts +26 -0
  75. package/src/views/useDialog.tsx +2 -0
  76. package/src/views/useDrawer.tsx +2 -0
  77. 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';
@@ -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) => ({
@@ -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
- * - 不返回自动补全字段(例如 completion
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 ['description', 'examples', 'ref', 'params', 'returns']) {
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 ['title', 'type', 'interface', 'description', 'examples', 'ref', 'params', 'returns']) {
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>);