@nocobase/client-v2 2.1.0-beta.26 → 2.1.0-beta.29

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 (83) hide show
  1. package/es/components/form/JsonTextArea.d.ts +18 -0
  2. package/es/components/index.d.ts +1 -0
  3. package/es/flow/actions/dateRangeLimit.d.ts +9 -0
  4. package/es/flow/actions/index.d.ts +1 -0
  5. package/es/flow/components/code-editor/types.d.ts +1 -0
  6. package/es/flow/models/base/PageModel/PageModel.d.ts +4 -0
  7. package/es/flow/models/base/PageModel/RootPageModel.d.ts +9 -0
  8. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +15 -6
  9. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +7 -0
  10. package/es/flow/models/blocks/shared/filterOperators.d.ts +9 -0
  11. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +2 -0
  12. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +20 -0
  13. package/es/flow/models/fields/JSEditableFieldModel.d.ts +4 -0
  14. package/es/flow-compat/data.d.ts +9 -2
  15. package/es/flow-compat/index.d.ts +1 -1
  16. package/es/index.d.ts +1 -1
  17. package/es/index.mjs +97 -90
  18. package/lib/index.js +99 -92
  19. package/package.json +6 -5
  20. package/src/BaseApplication.tsx +1 -1
  21. package/src/__tests__/app.test.tsx +23 -6
  22. package/src/components/form/JsonTextArea.tsx +129 -0
  23. package/src/components/index.ts +1 -0
  24. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +478 -0
  25. package/src/flow/actions/__tests__/pattern.test.ts +190 -0
  26. package/src/flow/actions/dateRangeLimit.tsx +66 -0
  27. package/src/flow/actions/index.ts +1 -0
  28. package/src/flow/actions/linkageRules.tsx +117 -19
  29. package/src/flow/actions/openView.tsx +2 -1
  30. package/src/flow/actions/pattern.tsx +25 -2
  31. package/src/flow/actions/titleField.tsx +8 -3
  32. package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +7 -1
  33. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +117 -0
  34. package/src/flow/components/FieldAssignValueInput.tsx +1 -0
  35. package/src/flow/components/code-editor/__tests__/linter.test.ts +18 -0
  36. package/src/flow/components/code-editor/__tests__/runjsDiagnostics.test.ts +23 -0
  37. package/src/flow/components/code-editor/index.tsx +18 -17
  38. package/src/flow/components/code-editor/linter.ts +222 -158
  39. package/src/flow/components/code-editor/runjsDiagnostics.ts +161 -97
  40. package/src/flow/components/code-editor/types.ts +1 -0
  41. package/src/flow/components/filter/LinkageFilterItem.tsx +6 -5
  42. package/src/flow/components/filter/VariableFilterItem.tsx +14 -13
  43. package/src/flow/components/filter/__tests__/LinkageFilterItem.test.tsx +33 -0
  44. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +48 -5
  45. package/src/flow/internal/utils/__tests__/titleFieldQuickSync.test.ts +1 -0
  46. package/src/flow/internal/utils/titleFieldQuickSync.ts +2 -2
  47. package/src/flow/models/actions/FilterActionModel.tsx +17 -9
  48. package/src/flow/models/base/PageModel/PageModel.tsx +15 -3
  49. package/src/flow/models/base/PageModel/RootPageModel.tsx +37 -2
  50. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +73 -0
  51. package/src/flow/models/base/PageModel/__tests__/RootPageModel.test.ts +116 -0
  52. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +200 -36
  53. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +270 -1
  54. package/src/flow/models/blocks/filter-form/__tests__/customFieldOperators.test.tsx +23 -0
  55. package/src/flow/models/blocks/filter-form/customFieldOperators.ts +12 -1
  56. package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +22 -8
  57. package/src/flow/models/blocks/filter-form/fields/__tests__/FilterFormCustomFieldModel.recordSelect.test.tsx +18 -0
  58. package/src/flow/models/blocks/filter-manager/FilterManager.ts +51 -1
  59. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +75 -0
  60. package/src/flow/models/blocks/form/FormItemModel.tsx +48 -28
  61. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +167 -1
  62. package/src/flow/models/blocks/form/value-runtime/runtime.ts +103 -11
  63. package/src/flow/models/blocks/shared/filterOperators.ts +14 -0
  64. package/src/flow/models/blocks/table/TableBlockModel.tsx +19 -3
  65. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +27 -3
  66. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +47 -0
  67. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +42 -0
  68. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableField.refresh.test.tsx +122 -0
  69. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +2 -0
  70. package/src/flow/models/fields/ClickableFieldModel.tsx +21 -9
  71. package/src/flow/models/fields/DateTimeFieldModel/DateOnlyFieldModel.tsx +9 -0
  72. package/src/flow/models/fields/DateTimeFieldModel/DateTimeFieldModel.tsx +4 -0
  73. package/src/flow/models/fields/DateTimeFieldModel/DateTimeNoTzFieldModel.tsx +9 -0
  74. package/src/flow/models/fields/DateTimeFieldModel/DateTimeTzFieldModel.tsx +9 -0
  75. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +242 -0
  76. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +152 -0
  77. package/src/flow/models/fields/DividerItemModel.tsx +30 -15
  78. package/src/flow/models/fields/JSEditableFieldModel.tsx +110 -14
  79. package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +87 -0
  80. package/src/flow/models/fields/__tests__/JSEditableFieldModel.test.tsx +210 -0
  81. package/src/flow-compat/data.ts +25 -3
  82. package/src/flow-compat/index.ts +7 -1
  83. package/src/index.ts +1 -1
@@ -27,6 +27,50 @@ import {
27
27
  shouldPreprocessRunJSTemplates,
28
28
  } from '@nocobase/flow-engine';
29
29
 
30
+ const acornWalkBase = {
31
+ ...(acornWalk as any).base,
32
+ JSXElement(node: any, state: any, callback: any) {
33
+ callback(node.openingElement, state);
34
+ for (const child of node.children || []) callback(child, state);
35
+ if (node.closingElement) callback(node.closingElement, state);
36
+ },
37
+ JSXFragment(node: any, state: any, callback: any) {
38
+ callback(node.openingFragment, state);
39
+ for (const child of node.children || []) callback(child, state);
40
+ callback(node.closingFragment, state);
41
+ },
42
+ JSXOpeningElement(node: any, state: any, callback: any) {
43
+ callback(node.name, state);
44
+ for (const attribute of node.attributes || []) callback(attribute, state);
45
+ },
46
+ JSXClosingElement(node: any, state: any, callback: any) {
47
+ callback(node.name, state);
48
+ },
49
+ JSXAttribute(node: any, state: any, callback: any) {
50
+ callback(node.name, state);
51
+ if (node.value) callback(node.value, state);
52
+ },
53
+ JSXExpressionContainer(node: any, state: any, callback: any) {
54
+ callback(node.expression, state);
55
+ },
56
+ JSXSpreadAttribute(node: any, state: any, callback: any) {
57
+ callback(node.argument, state);
58
+ },
59
+ JSXMemberExpression(node: any, state: any, callback: any) {
60
+ callback(node.object, state);
61
+ callback(node.property, state);
62
+ },
63
+ JSXNamespacedName(node: any, state: any, callback: any) {
64
+ callback(node.namespace, state);
65
+ callback(node.name, state);
66
+ },
67
+ JSXIdentifier() {},
68
+ JSXText() {},
69
+ JSXEmptyExpression() {},
70
+ JSXOpeningFragment() {},
71
+ JSXClosingFragment() {},
72
+ };
73
+
30
74
  export type RunJSIssue = {
31
75
  type: 'lint' | 'runtime';
32
76
  message: string;
@@ -615,55 +659,63 @@ function collectHeuristicIssues(code: string): RunJSIssue[] {
615
659
 
616
660
  // Collect declared identifiers (very coarse, best-effort).
617
661
  try {
618
- acornWalk.full(ast, (node: any) => {
619
- switch (node?.type) {
620
- case 'VariableDeclarator':
621
- addPatternIds(node.id);
622
- break;
623
- case 'FunctionDeclaration':
624
- addId(node.id);
625
- (node.params || []).forEach(addPatternIds);
626
- break;
627
- case 'FunctionExpression':
628
- addId(node.id);
629
- (node.params || []).forEach(addPatternIds);
630
- break;
631
- case 'ArrowFunctionExpression':
632
- (node.params || []).forEach(addPatternIds);
633
- break;
634
- case 'CatchClause':
635
- addPatternIds((node as any).param);
636
- break;
637
- case 'ClassDeclaration':
638
- addId(node.id);
639
- break;
640
- default:
641
- break;
642
- }
643
- });
662
+ acornWalk.full(
663
+ ast,
664
+ (node: any) => {
665
+ switch (node?.type) {
666
+ case 'VariableDeclarator':
667
+ addPatternIds(node.id);
668
+ break;
669
+ case 'FunctionDeclaration':
670
+ addId(node.id);
671
+ (node.params || []).forEach(addPatternIds);
672
+ break;
673
+ case 'FunctionExpression':
674
+ addId(node.id);
675
+ (node.params || []).forEach(addPatternIds);
676
+ break;
677
+ case 'ArrowFunctionExpression':
678
+ (node.params || []).forEach(addPatternIds);
679
+ break;
680
+ case 'CatchClause':
681
+ addPatternIds((node as any).param);
682
+ break;
683
+ case 'ClassDeclaration':
684
+ addId(node.id);
685
+ break;
686
+ default:
687
+ break;
688
+ }
689
+ },
690
+ acornWalkBase,
691
+ );
644
692
  } catch (_) {
645
693
  // ignore
646
694
  }
647
695
 
648
696
  // 1) Non-callable call: 123(), 'x'(), (1+2)(), ({})()
649
697
  try {
650
- acornWalk.full(ast, (node: any) => {
651
- if (!node || typeof node.type !== 'string') return;
652
- if (node.type !== 'CallExpression') return;
653
- const callee = node.callee;
654
- const isCallableLike =
655
- callee &&
656
- (callee.type === 'Identifier' ||
657
- callee.type === 'MemberExpression' ||
658
- callee.type === 'FunctionExpression' ||
659
- callee.type === 'ArrowFunctionExpression' ||
660
- callee.type === 'CallExpression' ||
661
- callee.type === 'ChainExpression');
662
- if (!isCallableLike) {
663
- const pos = (callee as any)?.start ?? node.start ?? 0;
664
- pushAtPos(pos, 'no-noncallable-call', 'This expression is not callable.');
665
- }
666
- });
698
+ acornWalk.full(
699
+ ast,
700
+ (node: any) => {
701
+ if (!node || typeof node.type !== 'string') return;
702
+ if (node.type !== 'CallExpression') return;
703
+ const callee = node.callee;
704
+ const isCallableLike =
705
+ callee &&
706
+ (callee.type === 'Identifier' ||
707
+ callee.type === 'MemberExpression' ||
708
+ callee.type === 'FunctionExpression' ||
709
+ callee.type === 'ArrowFunctionExpression' ||
710
+ callee.type === 'CallExpression' ||
711
+ callee.type === 'ChainExpression');
712
+ if (!isCallableLike) {
713
+ const pos = (callee as any)?.start ?? node.start ?? 0;
714
+ pushAtPos(pos, 'no-noncallable-call', 'This expression is not callable.');
715
+ }
716
+ },
717
+ acornWalkBase,
718
+ );
667
719
  } catch (_) {
668
720
  // ignore
669
721
  }
@@ -673,37 +725,45 @@ function collectHeuristicIssues(code: string): RunJSIssue[] {
673
725
  try {
674
726
  const reported = new Set<string>();
675
727
  const allowedShort = new Set<string>(['t']);
676
- acornWalk.full(ast, (node: any) => {
677
- if (!node || typeof node.type !== 'string') return;
678
- if (node.type !== 'CallExpression') return;
679
- let callee = node.callee;
680
- if (callee?.type === 'ChainExpression') callee = callee.expression;
681
- if (!callee || callee.type !== 'MemberExpression') return;
682
- const obj = callee.object;
683
- if (!obj || obj.type !== 'Identifier' || obj.name !== 'ctx') return;
684
-
685
- let name: string | null = null;
686
- if (!callee.computed && callee.property?.type === 'Identifier') {
687
- name = callee.property.name;
688
- } else if (callee.computed && callee.property?.type === 'Literal' && typeof callee.property.value === 'string') {
689
- name = callee.property.value;
690
- }
728
+ acornWalk.full(
729
+ ast,
730
+ (node: any) => {
731
+ if (!node || typeof node.type !== 'string') return;
732
+ if (node.type !== 'CallExpression') return;
733
+ let callee = node.callee;
734
+ if (callee?.type === 'ChainExpression') callee = callee.expression;
735
+ if (!callee || callee.type !== 'MemberExpression') return;
736
+ const obj = callee.object;
737
+ if (!obj || obj.type !== 'Identifier' || obj.name !== 'ctx') return;
738
+
739
+ let name: string | null = null;
740
+ if (!callee.computed && callee.property?.type === 'Identifier') {
741
+ name = callee.property.name;
742
+ } else if (
743
+ callee.computed &&
744
+ callee.property?.type === 'Literal' &&
745
+ typeof callee.property.value === 'string'
746
+ ) {
747
+ name = callee.property.value;
748
+ }
691
749
 
692
- if (!name || typeof name !== 'string') return;
693
- const normalized = name.trim();
694
- if (!normalized || normalized.startsWith('_')) return;
695
- if (normalized.length > 2) return;
696
- if (allowedShort.has(normalized)) return;
697
- if (reported.has(normalized)) return;
698
-
699
- const pos = (callee.property as any)?.start ?? callee.start ?? node.start ?? 0;
700
- pushAtPos(
701
- pos,
702
- 'possible-undefined-ctx-member-call',
703
- `Possible undefined ctx method call: ctx.${normalized}(). This may be a typo or not available in the current ctx API.`,
704
- );
705
- reported.add(normalized);
706
- });
750
+ if (!name || typeof name !== 'string') return;
751
+ const normalized = name.trim();
752
+ if (!normalized || normalized.startsWith('_')) return;
753
+ if (normalized.length > 2) return;
754
+ if (allowedShort.has(normalized)) return;
755
+ if (reported.has(normalized)) return;
756
+
757
+ const pos = (callee.property as any)?.start ?? callee.start ?? node.start ?? 0;
758
+ pushAtPos(
759
+ pos,
760
+ 'possible-undefined-ctx-member-call',
761
+ `Possible undefined ctx method call: ctx.${normalized}(). This may be a typo or not available in the current ctx API.`,
762
+ );
763
+ reported.add(normalized);
764
+ },
765
+ acornWalkBase,
766
+ );
707
767
  } catch (_) {
708
768
  // ignore
709
769
  }
@@ -711,31 +771,35 @@ function collectHeuristicIssues(code: string): RunJSIssue[] {
711
771
  // 2) Possible undefined variable (exclude declarations and property keys)
712
772
  try {
713
773
  const reported = new Set<string>();
714
- acornWalk.ancestor(ast, {
715
- Identifier(node: any, ancestors: any[]) {
716
- const name = node?.name;
717
- if (!name || declared.has(name) || reported.has(name)) return;
718
- const parent = ancestors[ancestors.length - 2];
719
- if (!parent) return;
720
- if (
721
- (parent.type === 'VariableDeclarator' && parent.id === node) ||
722
- (parent.type === 'FunctionDeclaration' && parent.id === node) ||
723
- (parent.type === 'FunctionExpression' && parent.id === node) ||
724
- (parent.type === 'ClassDeclaration' && parent.id === node) ||
725
- (parent.type === 'ClassExpression' && parent.id === node) ||
726
- (parent.type === 'Property' && parent.key === node && parent.computed !== true) ||
727
- (parent.type === 'MemberExpression' && parent.property === node && parent.computed !== true) ||
728
- (parent.type === 'LabeledStatement' && parent.label === node) ||
729
- (parent.type === 'BreakStatement' && parent.label === node) ||
730
- (parent.type === 'ContinueStatement' && parent.label === node)
731
- ) {
732
- return;
733
- }
734
- const pos = (node as any).start ?? 0;
735
- pushAtPos(pos, 'possible-undefined-variable', `Possible undefined variable: ${name}`);
736
- reported.add(name);
774
+ acornWalk.ancestor(
775
+ ast,
776
+ {
777
+ Identifier(node: any, ancestors: any[]) {
778
+ const name = node?.name;
779
+ if (!name || declared.has(name) || reported.has(name)) return;
780
+ const parent = ancestors[ancestors.length - 2];
781
+ if (!parent) return;
782
+ if (
783
+ (parent.type === 'VariableDeclarator' && parent.id === node) ||
784
+ (parent.type === 'FunctionDeclaration' && parent.id === node) ||
785
+ (parent.type === 'FunctionExpression' && parent.id === node) ||
786
+ (parent.type === 'ClassDeclaration' && parent.id === node) ||
787
+ (parent.type === 'ClassExpression' && parent.id === node) ||
788
+ (parent.type === 'Property' && parent.key === node && parent.computed !== true) ||
789
+ (parent.type === 'MemberExpression' && parent.property === node && parent.computed !== true) ||
790
+ (parent.type === 'LabeledStatement' && parent.label === node) ||
791
+ (parent.type === 'BreakStatement' && parent.label === node) ||
792
+ (parent.type === 'ContinueStatement' && parent.label === node)
793
+ ) {
794
+ return;
795
+ }
796
+ const pos = (node as any).start ?? 0;
797
+ pushAtPos(pos, 'possible-undefined-variable', `Possible undefined variable: ${name}`);
798
+ reported.add(name);
799
+ },
737
800
  },
738
- });
801
+ acornWalkBase,
802
+ );
739
803
  } catch (_) {
740
804
  // ignore
741
805
  }
@@ -17,6 +17,7 @@ import { RunLog } from './hooks/useCodeRunner';
17
17
  export interface EditorRef {
18
18
  write(document: string): void;
19
19
  read(): string;
20
+ run?(): Promise<unknown>;
20
21
  buttonGroupHeight?: number;
21
22
  snippetEntries: SnippetEntry[];
22
23
  logs: RunLog[];
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
11
- import { lazy } from '../../../flow-compat';
11
+ import { getFlowFieldInterfaceOptions, lazy } from '../../../flow-compat';
12
12
  import { Input, InputNumber, Select, Space, Switch } from 'antd';
13
13
  import merge from 'lodash/merge';
14
14
  import uniqBy from 'lodash/uniqBy';
@@ -227,11 +227,12 @@ export const LinkageFilterItem: React.FC<LinkageFilterItemProps> = observer((pro
227
227
 
228
228
  const operatorMetadataList: OperatorMeta[] = useMemo(() => {
229
229
  if (leftFieldMeta) {
230
- const dataSourceManager = model.context.app.dataSourceManager;
231
230
  const fieldInterface = leftFieldMeta.interface
232
- ? (dataSourceManager.collectionFieldInterfaceManager.getFieldInterface(
231
+ ? (getFlowFieldInterfaceOptions(
233
232
  leftFieldMeta.interface,
234
- ) as FieldInterfaceDef)
233
+ model.context.dataSourceManager,
234
+ model.context.app?.dataSourceManager,
235
+ ) as FieldInterfaceDef | undefined)
235
236
  : undefined;
236
237
  const schemaOperators = (leftFieldMeta as any)?.uiSchema?.['x-filter-operators'] as
237
238
  | Array<OperatorMeta & { visible?: (meta: MetaTreeNode) => boolean }>
@@ -392,7 +393,7 @@ export const LinkageFilterItem: React.FC<LinkageFilterItemProps> = observer((pro
392
393
  const base = Array.isArray(tree) ? tree : [];
393
394
  const merged = mergeExtraMetaTreeWithBase(base, extraMetaTree);
394
395
  const getFieldInterface = (name: string) =>
395
- model.context.app?.dataSourceManager?.collectionFieldInterfaceManager?.getFieldInterface?.(name) as
396
+ getFlowFieldInterfaceOptions(name, model.context.dataSourceManager, model.context.app?.dataSourceManager) as
396
397
  | FieldInterfaceDef
397
398
  | undefined;
398
399
  return await enhanceMetaTreeWithFilterableChildren(merged, getFieldInterface);
@@ -9,7 +9,7 @@
9
9
 
10
10
  import React, { useState, useMemo, useCallback, useEffect } from 'react';
11
11
  import { Cascader, Checkbox, Input, InputNumber, Radio, Select, Space, Switch } from 'antd';
12
- import { lazy } from '../../../flow-compat';
12
+ import { getFlowFieldInterfaceOptions, lazy } from '../../../flow-compat';
13
13
  import merge from 'lodash/merge';
14
14
  import type { ISchema } from '@formily/json-schema';
15
15
  import {
@@ -286,18 +286,24 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
286
286
  children?: Array<{ name: string; title?: string; schema?: ISchema; operators?: OperatorMeta[] }>;
287
287
  };
288
288
  };
289
+ const getFieldInterface = useCallback(
290
+ (interfaceName: string | undefined) =>
291
+ getFlowFieldInterfaceOptions(
292
+ interfaceName,
293
+ model.context.dataSourceManager,
294
+ model.context.app?.dataSourceManager,
295
+ ) as FieldInterfaceDef | undefined,
296
+ [model],
297
+ );
289
298
 
290
299
  // 基于字段接口的动态操作符元数据(优先使用子菜单 schema 中自定义的 operators,其次再用接口默认 operators)
291
300
  const operatorMetaList: OperatorMeta[] = useMemo(() => {
292
301
  if (!leftMeta) return [];
293
- const dm = model.context.app?.dataSourceManager;
294
- const fi = leftMeta.interface
295
- ? (dm?.collectionFieldInterfaceManager?.getFieldInterface(leftMeta.interface) as FieldInterfaceDef | undefined)
296
- : undefined;
302
+ const fi = leftMeta.interface ? getFieldInterface(leftMeta.interface) : undefined;
297
303
  const schemaOps: OperatorMeta[] | undefined = (leftMeta as any)?.uiSchema?.['x-filter-operators'];
298
304
  const baseOps = (Array.isArray(schemaOps) && schemaOps.length ? schemaOps : fi?.filterable?.operators) || [];
299
305
  return baseOps.filter((op) => !op.visible || op.visible(leftMeta));
300
- }, [leftMeta, model]);
306
+ }, [getFieldInterface, leftMeta]);
301
307
 
302
308
  useEffect(() => {
303
309
  if (!operatorMetaList.length) return;
@@ -578,16 +584,11 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
578
584
  const enhancedMetaTree = useMemo(() => {
579
585
  type MetaTreeProvider = () => MetaTreeNode[] | Promise<MetaTreeNode[]>;
580
586
  return async () => {
581
- const dm = model.context.app?.dataSourceManager;
582
- const fiMgr = dm?.collectionFieldInterfaceManager;
583
-
584
587
  // 优先复用已注入 meta;否则在本组件范围内临时构建
585
588
  const nodes: MetaTreeNode[] = await buildCollectionLeftMetaTreeLocal(model.context);
586
589
 
587
590
  const enhanceNode = async (node: MetaTreeNode): Promise<MetaTreeNode> => {
588
- const fi = node.interface
589
- ? (fiMgr?.getFieldInterface(node.interface) as FieldInterfaceDef | undefined)
590
- : undefined;
591
+ const fi = node.interface ? getFieldInterface(node.interface) : undefined;
591
592
  const extraChildren: MetaTreeNode[] = [];
592
593
  const filterable = fi?.filterable;
593
594
  const childrenDefs = filterable?.children as
@@ -634,7 +635,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
634
635
  }
635
636
  return out;
636
637
  };
637
- }, [model]);
638
+ }, [getFieldInterface, model]);
638
639
 
639
640
  return (
640
641
  <Space wrap style={{ width: '100%' }}>
@@ -110,6 +110,39 @@ describe('LinkageFilterItem', () => {
110
110
  expect(screen.queryByText('[object Object]')).toBeNull();
111
111
  });
112
112
 
113
+ it('uses scoped context dataSourceManager when app dataSourceManager has no field interface manager', async () => {
114
+ const value = observable({ path: '', operator: '', value: '' }) as any;
115
+ const { model, app } = createModel();
116
+ const getRuntimeFieldInterface = vi.fn((name: string) => ({
117
+ name,
118
+ filterable: {
119
+ operators: [{ value: '$eq', label: 'Equals', selected: true }],
120
+ },
121
+ }));
122
+ model.context.dataSourceManager.setCollectionFieldInterfaceManager({
123
+ getFieldInterface: getRuntimeFieldInterface,
124
+ });
125
+ (app as any).dataSourceManager = {};
126
+
127
+ (globalThis as any).__TEST_PATH__ = 'assignee';
128
+ (globalThis as any).__TEST_META__ = {
129
+ interface: 'belongsTo',
130
+ uiSchema: { 'x-component': 'RecordPicker' },
131
+ paths: ['collection', 'assignee'],
132
+ name: 'assignee',
133
+ title: 'Assignee',
134
+ type: 'object',
135
+ };
136
+
137
+ render(<LinkageFilterItem value={value} model={model} />);
138
+ fireEvent.click(screen.getByTestId('variable-input'));
139
+
140
+ await waitFor(() => {
141
+ expect(value.operator).toBe('$eq');
142
+ expect(getRuntimeFieldInterface).toHaveBeenCalledWith('belongsTo');
143
+ });
144
+ });
145
+
113
146
  it('renders operator schema component for multi-keyword constants', async () => {
114
147
  const value = observable({ path: '', operator: '', value: 'foo\nbar' }) as any;
115
148
  const { model, app } = createModel();
@@ -121,6 +121,8 @@ describe('VariableFilterItem', () => {
121
121
  // Ensure document body for antd portals if needed
122
122
  document.body.innerHTML = '';
123
123
  delete (globalThis as any).__LAST_VARIABLE_INPUT_PROPS__;
124
+ delete (globalThis as any).__TEST_PATH__;
125
+ delete (globalThis as any).__TEST_META__;
124
126
  });
125
127
 
126
128
  it('returns undefined path for empty left value in converter', () => {
@@ -153,9 +155,53 @@ describe('VariableFilterItem', () => {
153
155
  expect(value.value).toBe('abc');
154
156
  });
155
157
 
158
+ it('uses scoped context dataSourceManager when app dataSourceManager has no field interface manager', async () => {
159
+ const value = observable({ path: '', operator: '', value: '' }) as any;
160
+ const model = CreateModel();
161
+ const getRuntimeFieldInterface = vi.fn((name: string) => ({
162
+ name,
163
+ filterable: {
164
+ operators: [{ value: '$eq', label: 'Equals' }],
165
+ children: [
166
+ {
167
+ name: 'runtimeChild',
168
+ title: 'Runtime child',
169
+ schema: { 'x-component': 'Input' },
170
+ operators: [{ value: '$includes', label: 'contains' }],
171
+ },
172
+ ],
173
+ },
174
+ }));
175
+
176
+ model.context.dataSourceManager.setCollectionFieldInterfaceManager({
177
+ getFieldInterface: getRuntimeFieldInterface,
178
+ });
179
+ (model.context.app as any).dataSourceManager = {};
180
+ (globalThis as any).__TEST_PATH__ = 'assignee';
181
+ (globalThis as any).__TEST_META__ = {
182
+ interface: 'belongsTo',
183
+ uiSchema: { 'x-component': 'RecordPicker' },
184
+ paths: ['collection', 'assignee'],
185
+ name: 'assignee',
186
+ title: 'Assignee',
187
+ type: 'object',
188
+ };
189
+
190
+ render(<VariableFilterItem value={value} model={model} rightAsVariable={false} />);
191
+ const leftVariableInputProps = (globalThis as any).__LAST_VARIABLE_INPUT_PROPS__;
192
+ const metaTree = await leftVariableInputProps.metaTree();
193
+ const nameNode = metaTree.find((node: any) => node.name === 'name');
194
+ expect(nameNode?.children?.some((child: any) => child.name === 'runtimeChild')).toBe(true);
195
+
196
+ fireEvent.click(screen.getByTestId('variable-input'));
197
+
198
+ await waitFor(() => {
199
+ expect(value.operator).toBe('$eq');
200
+ expect(getRuntimeFieldInterface).toHaveBeenCalledWith('belongsTo');
201
+ });
202
+ });
203
+
156
204
  it('keeps numeric string when x-component uses InputNumber with stringMode', async () => {
157
- const prevMeta = (globalThis as any).__TEST_META__;
158
- const prevPath = (globalThis as any).__TEST_PATH__;
159
205
  (globalThis as any).__TEST_PATH__ = 'price';
160
206
  (globalThis as any).__TEST_META__ = {
161
207
  interface: 'input',
@@ -177,9 +223,6 @@ describe('VariableFilterItem', () => {
177
223
  expect(input.value).toBe('123.45');
178
224
  expect(value.value).toBe('123.45');
179
225
  });
180
-
181
- (globalThis as any).__TEST_META__ = prevMeta;
182
- (globalThis as any).__TEST_PATH__ = prevPath;
183
226
  });
184
227
 
185
228
  it('normalizes synthetic event value when formula field renders Input from app components', async () => {
@@ -26,6 +26,7 @@ describe('titleFieldQuickSync', () => {
26
26
  expect(isTitleUsableField(dm, { interface: 'formula' } as any)).toBe(false);
27
27
  expect(isTitleUsableField(dm, { interface: 'unknown' } as any)).toBe(false);
28
28
  expect(isTitleUsableField(undefined, { interface: 'input' } as any)).toBe(false);
29
+ expect(isTitleUsableField({} as any, { interface: 'input' } as any)).toBe(false);
29
30
  });
30
31
 
31
32
  it('syncs title field to main data source collection', async () => {
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { DataSourceManager } from '@nocobase/flow-engine';
10
+ import { DataSourceManager, getCollectionFieldInterface } from '@nocobase/flow-engine';
11
11
  import type { CollectionFieldOptions } from '../../../flow-compat';
12
12
  import { DEFAULT_DATA_SOURCE_KEY } from '../../../flow-compat';
13
13
 
@@ -33,7 +33,7 @@ export function isTitleUsableField(
33
33
  ): boolean {
34
34
  const ifaceName = typeof field?.interface === 'string' ? field.interface : undefined;
35
35
  if (!dm || !ifaceName) return false;
36
- return !!dm.collectionFieldInterfaceManager.getFieldInterface(ifaceName)?.titleUsable;
36
+ return !!getCollectionFieldInterface(ifaceName, dm)?.titleUsable;
37
37
  }
38
38
 
39
39
  function resolveDataSourceKey(targetCollection: CollectionLike | null | undefined): string {
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { tExpr, MobilePopup, MultiRecordResource, useFlowSettingsContext } from '@nocobase/flow-engine';
11
+ import type { CollectionFieldInterfaceDataSourceManager } from '@nocobase/flow-engine';
11
12
  import { isEmptyFilter, transformFilter } from '@nocobase/utils/client';
12
13
  import { ButtonProps, Popover, Transfer } from 'antd';
13
14
  import React from 'react';
@@ -15,6 +16,7 @@ import { FilterGroup, VariableFilterItem } from '../../components/filter';
15
16
  import { ActionModel, CollectionBlockModel } from '../base';
16
17
  import { FilterContainer } from '../../components/filter/FilterContainer';
17
18
  import _ from 'lodash';
19
+ import { getFlowFieldInterfaceOptions } from '../../../flow-compat';
18
20
 
19
21
  export class FilterActionModel extends ActionModel {
20
22
  static scene = 'collection';
@@ -135,9 +137,11 @@ FilterActionModel.registerFlow({
135
137
  'x-component': (props) => {
136
138
  // eslint-disable-next-line react-hooks/rules-of-hooks
137
139
  const { model } = useFlowSettingsContext();
138
- const dm = model?.context?.app?.dataSourceManager;
139
- const fiMgr = dm?.collectionFieldInterfaceManager;
140
- const filterable = getFilterableFields(model.context.blockModel.collection, fiMgr);
140
+ const filterable = getFilterableFields(
141
+ model.context.blockModel.collection,
142
+ model.context.dataSourceManager,
143
+ model.context.app?.dataSourceManager,
144
+ );
141
145
  const dataSource = filterable.map((field: any) => ({ key: field.name, title: field.title }));
142
146
  return (
143
147
  <Transfer
@@ -157,9 +161,11 @@ FilterActionModel.registerFlow({
157
161
  },
158
162
  defaultParams(ctx) {
159
163
  // 默认仅包含“可筛选”的字段(与 1.0 一致),以避免 JSON 等未提供 operators 的字段出现在默认允许集合中
160
- const dm = ctx?.model?.context?.app?.dataSourceManager;
161
- const fiMgr = dm?.collectionFieldInterfaceManager;
162
- const names = getFilterableFields(ctx.blockModel.collection, fiMgr).map((field: any) => field.name);
164
+ const names = getFilterableFields(
165
+ ctx.blockModel.collection,
166
+ ctx.model?.context?.dataSourceManager,
167
+ ctx.model?.context?.app?.dataSourceManager,
168
+ ).map((field: any) => field.name);
163
169
  return {
164
170
  filterableFieldNames: names || [],
165
171
  };
@@ -304,13 +310,15 @@ FilterActionModel.registerFlow({
304
310
  },
305
311
  });
306
312
 
307
- function getFilterableFields(collection: any, fiMgr: any) {
313
+ function getFilterableFields(
314
+ collection: any,
315
+ ...dataSourceManagers: Array<CollectionFieldInterfaceDataSourceManager | null | undefined>
316
+ ) {
308
317
  const fields = collection?.getFields?.() || [];
309
- if (!fiMgr) return [];
310
318
  return fields.filter((field: any) => {
311
319
  if (!field?.interface) return false;
312
320
  if (field?.filterable === false) return false;
313
- const fi = fiMgr.getFieldInterface(field.interface);
321
+ const fi = getFlowFieldInterfaceOptions(field.interface, ...dataSourceManagers);
314
322
  return !!fi?.filterable;
315
323
  });
316
324
  }
@@ -49,6 +49,17 @@ export class PageModel extends FlowModel<PageModelStructure> {
49
49
  private unmounted = false;
50
50
  private documentTitleUpdateVersion = 0;
51
51
 
52
+ /**
53
+ * 根页面标签页开关以路由表为准,避免 flow model 里的旧配置覆盖路由管理设置。
54
+ */
55
+ private getEnableTabs(): boolean {
56
+ const routeEnableTabs = (this.context as any)?.currentRoute?.enableTabs;
57
+ if (this.props.routeId != null && typeof routeEnableTabs === 'boolean') {
58
+ return routeEnableTabs;
59
+ }
60
+ return !!this.props.enableTabs;
61
+ }
62
+
52
63
  private getActiveTabKey(): string | undefined {
53
64
  const viewParams = this.context.view?.navigation?.viewParams;
54
65
  if (viewParams) {
@@ -193,7 +204,7 @@ export class PageModel extends FlowModel<PageModelStructure> {
193
204
  };
194
205
 
195
206
  let nextTitle = '';
196
- if (this.props.enableTabs) {
207
+ if (this.getEnableTabs()) {
197
208
  const activeTabKey = preferredActiveTabKey || this.getActiveTabKey();
198
209
  const activeTabModel = activeTabKey
199
210
  ? (this.flowEngine.getModel(activeTabKey) as BasePageTabModel | undefined)
@@ -356,13 +367,14 @@ export class PageModel extends FlowModel<PageModelStructure> {
356
367
  headerStyle.paddingBlock = token.paddingSM;
357
368
  headerStyle.paddingInline = token.paddingLG;
358
369
  }
359
- if (this.props.enableTabs) {
370
+ const enableTabs = this.getEnableTabs();
371
+ if (enableTabs) {
360
372
  headerStyle.paddingBottom = 0;
361
373
  }
362
374
  return (
363
375
  <>
364
376
  {this.props.displayTitle && <PageHeader title={this.props.title} style={headerStyle} />}
365
- {this.props.enableTabs ? this.renderTabs() : this.renderFirstTab()}
377
+ {enableTabs ? this.renderTabs() : this.renderFirstTab()}
366
378
  </>
367
379
  );
368
380
  }