@nocobase/client-v2 2.2.11 → 2.2.13
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/filter/VariableFilterItem.d.ts +7 -0
- package/es/index.mjs +1 -1
- package/lib/index.js +24 -24
- package/package.json +7 -7
- package/src/flow/components/filter/VariableFilterItem.tsx +11 -3
- package/src/flow/components/filter/__tests__/VariableFilterItem.rightMetaTree.test.tsx +158 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.13",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.mjs",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"@formily/antd-v5": "1.2.3",
|
|
28
28
|
"@formily/react": "^2.2.27",
|
|
29
29
|
"@formily/shared": "^2.2.27",
|
|
30
|
-
"@nocobase/evaluators": "2.2.
|
|
31
|
-
"@nocobase/flow-engine": "2.2.
|
|
32
|
-
"@nocobase/sdk": "2.2.
|
|
33
|
-
"@nocobase/shared": "2.2.
|
|
34
|
-
"@nocobase/utils": "2.2.
|
|
30
|
+
"@nocobase/evaluators": "2.2.13",
|
|
31
|
+
"@nocobase/flow-engine": "2.2.13",
|
|
32
|
+
"@nocobase/sdk": "2.2.13",
|
|
33
|
+
"@nocobase/shared": "2.2.13",
|
|
34
|
+
"@nocobase/utils": "2.2.13",
|
|
35
35
|
"ahooks": "^3.7.2",
|
|
36
36
|
"antd": "5.24.2",
|
|
37
37
|
"antd-style": "3.7.1",
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"react-router-dom": "^6.30.1",
|
|
50
50
|
"zxing-wasm": "^3.1.3"
|
|
51
51
|
},
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "d548ca65faf20d60c7747e6b072c2f2f2c786cf3"
|
|
53
53
|
}
|
|
@@ -139,9 +139,16 @@ export interface VariableFilterItemProps {
|
|
|
139
139
|
rightVariableConverters?: Pick<Converters, 'resolvePathFromValue' | 'resolveValueFromPath'>;
|
|
140
140
|
ignoreFieldNames?: string[];
|
|
141
141
|
maxAssociationFieldDepth?: number;
|
|
142
|
+
/**
|
|
143
|
+
* 右侧变量树的关联层级上限,默认与 `maxAssociationFieldDepth` 一致。
|
|
144
|
+
* 传 `null` 表示不限制:右侧是变量树而非集合字段树,其深度可能已由调用方约束
|
|
145
|
+
* (例如工作流的变量树由触发器的「预加载关系数据」决定),此时再按左侧的层级上限裁剪
|
|
146
|
+
* 会让已配置好的深层变量选不到。
|
|
147
|
+
*/
|
|
148
|
+
rightMaxAssociationFieldDepth?: number | null;
|
|
142
149
|
}
|
|
143
150
|
|
|
144
|
-
function limitMetaTreeIfNeeded(nodes: MetaTreeNode[], maxAssociationFieldDepth?: number) {
|
|
151
|
+
function limitMetaTreeIfNeeded(nodes: MetaTreeNode[], maxAssociationFieldDepth?: number | null) {
|
|
145
152
|
if (typeof maxAssociationFieldDepth !== 'number') {
|
|
146
153
|
return nodes;
|
|
147
154
|
}
|
|
@@ -367,6 +374,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
367
374
|
rightVariableConverters,
|
|
368
375
|
ignoreFieldNames,
|
|
369
376
|
maxAssociationFieldDepth,
|
|
377
|
+
rightMaxAssociationFieldDepth = maxAssociationFieldDepth,
|
|
370
378
|
}) => {
|
|
371
379
|
// 使用 View 上下文,确保可访问 ctx.view 的异步子树
|
|
372
380
|
const ctx = useFlowViewContext();
|
|
@@ -685,10 +693,10 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
685
693
|
{ title: t('Null'), name: 'null', type: 'object', paths: ['null'], render: NullComponent },
|
|
686
694
|
...nodes,
|
|
687
695
|
],
|
|
688
|
-
|
|
696
|
+
rightMaxAssociationFieldDepth,
|
|
689
697
|
);
|
|
690
698
|
};
|
|
691
|
-
}, [rightMetaTree, ctx, staticInputRenderer, NullComponent, t,
|
|
699
|
+
}, [rightMetaTree, ctx, staticInputRenderer, NullComponent, t, rightMaxAssociationFieldDepth]);
|
|
692
700
|
|
|
693
701
|
// 当启用右侧变量输入时,构造 VariableInput 的 converters:
|
|
694
702
|
// - 变量模式:返回 null 让 VariableInput 渲染 VariableTag
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
12
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
13
|
+
import type { MetaTreeNode } from '@nocobase/flow-engine';
|
|
14
|
+
import { FlowEngine, FlowModel } from '@nocobase/flow-engine';
|
|
15
|
+
import { VariableFilterItem } from '../VariableFilterItem';
|
|
16
|
+
import { createMockFlowApp, TestCollectionFieldInterface } from '../../../__tests__/helpers/mockFlowApp';
|
|
17
|
+
|
|
18
|
+
const captured = vi.hoisted(() => ({ metaTrees: [] as any[] }));
|
|
19
|
+
|
|
20
|
+
vi.mock('@nocobase/flow-engine', async () => {
|
|
21
|
+
const actual = await vi.importActual<any>('@nocobase/flow-engine');
|
|
22
|
+
const MockVariableInput = ({ onChange, metaTree }: any) => {
|
|
23
|
+
if (metaTree) {
|
|
24
|
+
captured.metaTrees.push(metaTree);
|
|
25
|
+
}
|
|
26
|
+
return (
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
data-testid="variable-input"
|
|
30
|
+
onClick={() =>
|
|
31
|
+
onChange?.('title', {
|
|
32
|
+
interface: 'input',
|
|
33
|
+
uiSchema: { 'x-component': 'Input' },
|
|
34
|
+
paths: ['collection', 'title'],
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
>
|
|
38
|
+
mock-variable-input
|
|
39
|
+
</button>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
return { ...actual, VariableInput: MockVariableInput };
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// $context.data (not an association) -> category (m2o) -> book (m2o) -> createdBy (not counted) -> departments (m2m).
|
|
46
|
+
// Mirrors a workflow collection-event trigger that preloads `category.book.createdBy.departments`.
|
|
47
|
+
function buildRightMetaTree(): MetaTreeNode[] {
|
|
48
|
+
const departments: MetaTreeNode = {
|
|
49
|
+
name: 'departments',
|
|
50
|
+
title: 'Departments',
|
|
51
|
+
type: 'object',
|
|
52
|
+
interface: 'm2m',
|
|
53
|
+
paths: ['$context', 'data', 'category', 'book', 'createdBy', 'departments'],
|
|
54
|
+
};
|
|
55
|
+
const createdBy: MetaTreeNode = {
|
|
56
|
+
name: 'createdBy',
|
|
57
|
+
title: 'Created by',
|
|
58
|
+
type: 'object',
|
|
59
|
+
interface: 'createdBy',
|
|
60
|
+
paths: ['$context', 'data', 'category', 'book', 'createdBy'],
|
|
61
|
+
children: async () => [departments],
|
|
62
|
+
};
|
|
63
|
+
const book: MetaTreeNode = {
|
|
64
|
+
name: 'book',
|
|
65
|
+
title: 'Book',
|
|
66
|
+
type: 'object',
|
|
67
|
+
interface: 'obo',
|
|
68
|
+
paths: ['$context', 'data', 'category', 'book'],
|
|
69
|
+
children: async () => [createdBy],
|
|
70
|
+
};
|
|
71
|
+
const category: MetaTreeNode = {
|
|
72
|
+
name: 'category',
|
|
73
|
+
title: 'Category',
|
|
74
|
+
type: 'object',
|
|
75
|
+
interface: 'm2o',
|
|
76
|
+
paths: ['$context', 'data', 'category'],
|
|
77
|
+
children: async () => [book],
|
|
78
|
+
};
|
|
79
|
+
const data: MetaTreeNode = {
|
|
80
|
+
name: 'data',
|
|
81
|
+
title: 'Trigger data',
|
|
82
|
+
type: 'object',
|
|
83
|
+
paths: ['$context', 'data'],
|
|
84
|
+
children: async () => [category],
|
|
85
|
+
};
|
|
86
|
+
return [{ name: '$context', title: 'Trigger variables', type: '', paths: ['$context'], children: [data] }];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function resolveChildren(node: MetaTreeNode | undefined) {
|
|
90
|
+
const children = node?.children;
|
|
91
|
+
if (typeof children === 'function') {
|
|
92
|
+
return (await (children as () => Promise<MetaTreeNode[]>)()) ?? [];
|
|
93
|
+
}
|
|
94
|
+
return Array.isArray(children) ? children : [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function walkToCreatedByChildren(metaTree: any) {
|
|
98
|
+
const roots: MetaTreeNode[] = typeof metaTree === 'function' ? await metaTree() : metaTree;
|
|
99
|
+
const context = roots.find((node) => node.name === '$context');
|
|
100
|
+
const data = (await resolveChildren(context)).find((node) => node.name === 'data');
|
|
101
|
+
const category = (await resolveChildren(data)).find((node) => node.name === 'category');
|
|
102
|
+
const book = (await resolveChildren(category)).find((node) => node.name === 'book');
|
|
103
|
+
const createdBy = (await resolveChildren(book)).find((node) => node.name === 'createdBy');
|
|
104
|
+
return (await resolveChildren(createdBy)).map((node) => node.name);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function createModel() {
|
|
108
|
+
const engine = new FlowEngine();
|
|
109
|
+
const model = new FlowModel({ uid: 'm-variable-filter-right', flowEngine: engine });
|
|
110
|
+
const app = createMockFlowApp();
|
|
111
|
+
model.context.defineProperty('app', { value: app });
|
|
112
|
+
|
|
113
|
+
class InputInterface extends TestCollectionFieldInterface {
|
|
114
|
+
name = 'input';
|
|
115
|
+
group = 'basic';
|
|
116
|
+
filterable = { operators: [{ value: '$eq', label: 'Equals' }] };
|
|
117
|
+
}
|
|
118
|
+
app.addFieldInterfaces([InputInterface]);
|
|
119
|
+
|
|
120
|
+
const ds = engine.dataSourceManager.getDataSource('main');
|
|
121
|
+
ds.addCollection({
|
|
122
|
+
name: 'posts',
|
|
123
|
+
fields: [{ name: 'title', type: 'string', interface: 'input', uiSchema: { 'x-component': 'Input' } }],
|
|
124
|
+
});
|
|
125
|
+
model.context.defineProperty('collection', { get: () => ds.getCollection('posts') });
|
|
126
|
+
|
|
127
|
+
return model as any;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function renderItem(props: Record<string, unknown>) {
|
|
131
|
+
const value = { path: 'title', operator: '$eq', value: '' } as any;
|
|
132
|
+
const model = createModel();
|
|
133
|
+
render(
|
|
134
|
+
<VariableFilterItem value={value} model={model} rightAsVariable rightMetaTree={buildRightMetaTree()} {...props} />,
|
|
135
|
+
);
|
|
136
|
+
fireEvent.click(screen.getAllByTestId('variable-input')[0]);
|
|
137
|
+
return captured.metaTrees.at(-1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
describe('VariableFilterItem right-side association depth', () => {
|
|
141
|
+
beforeEach(() => {
|
|
142
|
+
document.body.innerHTML = '';
|
|
143
|
+
captured.metaTrees = [];
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('applies maxAssociationFieldDepth to the right tree by default', async () => {
|
|
147
|
+
const metaTree = renderItem({ maxAssociationFieldDepth: 2 });
|
|
148
|
+
// `departments` is the third association down the path, so the default cap removes it.
|
|
149
|
+
expect(await walkToCreatedByChildren(metaTree)).toEqual([]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// The right side is a variable tree whose depth is decided by its provider (in workflow, by the trigger's
|
|
153
|
+
// "Preload associations" config). Capping it by the left-side field-picker limit hid preloaded variables.
|
|
154
|
+
it('leaves the right tree untouched when rightMaxAssociationFieldDepth is null', async () => {
|
|
155
|
+
const metaTree = renderItem({ maxAssociationFieldDepth: 2, rightMaxAssociationFieldDepth: null });
|
|
156
|
+
expect(await walkToCreatedByChildren(metaTree)).toEqual(['departments']);
|
|
157
|
+
});
|
|
158
|
+
});
|