@nocobase/client-v2 2.1.0-beta.26 → 2.1.0-beta.27
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/es/flow/components/code-editor/types.d.ts +1 -0
- package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +15 -6
- package/es/flow/models/blocks/shared/filterOperators.d.ts +9 -0
- package/es/flow-compat/data.d.ts +9 -2
- package/es/flow-compat/index.d.ts +1 -1
- package/es/index.d.ts +1 -1
- package/es/index.mjs +90 -90
- package/lib/index.js +83 -83
- package/package.json +5 -5
- package/src/BaseApplication.tsx +1 -1
- package/src/__tests__/app.test.tsx +23 -6
- package/src/flow/actions/titleField.tsx +8 -3
- package/src/flow/components/FieldAssignValueInput.tsx +1 -0
- package/src/flow/components/code-editor/__tests__/linter.test.ts +18 -0
- package/src/flow/components/code-editor/__tests__/runjsDiagnostics.test.ts +23 -0
- package/src/flow/components/code-editor/index.tsx +18 -17
- package/src/flow/components/code-editor/linter.ts +222 -158
- package/src/flow/components/code-editor/runjsDiagnostics.ts +161 -97
- package/src/flow/components/code-editor/types.ts +1 -0
- package/src/flow/components/filter/LinkageFilterItem.tsx +6 -5
- package/src/flow/components/filter/VariableFilterItem.tsx +14 -13
- package/src/flow/components/filter/__tests__/LinkageFilterItem.test.tsx +33 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +48 -5
- package/src/flow/internal/utils/__tests__/titleFieldQuickSync.test.ts +1 -0
- package/src/flow/internal/utils/titleFieldQuickSync.ts +2 -2
- package/src/flow/models/actions/FilterActionModel.tsx +17 -9
- package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +200 -36
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +270 -1
- package/src/flow/models/blocks/filter-form/__tests__/customFieldOperators.test.tsx +23 -0
- package/src/flow/models/blocks/filter-form/customFieldOperators.ts +12 -1
- package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +22 -8
- package/src/flow/models/blocks/filter-form/fields/__tests__/FilterFormCustomFieldModel.recordSelect.test.tsx +18 -0
- package/src/flow/models/blocks/filter-manager/FilterManager.ts +51 -1
- package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +75 -0
- package/src/flow/models/blocks/form/FormItemModel.tsx +48 -28
- package/src/flow/models/blocks/shared/filterOperators.ts +14 -0
- package/src/flow/models/blocks/table/TableBlockModel.tsx +19 -3
- package/src/flow/models/fields/DividerItemModel.tsx +30 -15
- package/src/flow-compat/data.ts +25 -3
- package/src/flow-compat/index.ts +7 -1
- 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(
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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(
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
callee
|
|
656
|
-
|
|
657
|
-
callee
|
|
658
|
-
callee.type === '
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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(
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
name =
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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(
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
(parent
|
|
722
|
-
(
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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
|
}
|
|
@@ -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
|
-
? (
|
|
231
|
+
? (getFlowFieldInterfaceOptions(
|
|
233
232
|
leftFieldMeta.interface,
|
|
234
|
-
|
|
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
|
|
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
|
|
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
|
-
}, [
|
|
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 !!
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
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(
|
|
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 =
|
|
321
|
+
const fi = getFlowFieldInterfaceOptions(field.interface, ...dataSourceManagers);
|
|
314
322
|
return !!fi?.filterable;
|
|
315
323
|
});
|
|
316
324
|
}
|