@nocobase/client-v2 2.1.0-alpha.45 → 2.1.0-alpha.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/authRedirect.d.ts +12 -0
- package/es/components/form/TypedVariableInput.d.ts +75 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
- package/es/flow/components/ConditionBuilder.d.ts +1 -0
- package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
- package/es/flow/components/FieldAssignValueInput.d.ts +1 -0
- package/es/flow/components/fieldAssignOptions.d.ts +10 -0
- package/es/flow/components/filter/LinkageFilterItem.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +1 -0
- package/es/flow/components/filter/metaTreeAssociationDepth.d.ts +13 -0
- package/es/flow/models/base/AssociationFieldGroupModel.d.ts +2 -33
- package/es/flow/models/blocks/assign-form/assignFieldValuesFlow.d.ts +8 -0
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +2 -0
- package/es/index.mjs +103 -103
- package/lib/index.js +105 -105
- package/package.json +7 -7
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +5 -0
- package/src/authRedirect.ts +43 -0
- package/src/components/README.md +48 -0
- package/src/components/README.zh-CN.md +48 -0
- package/src/components/form/TypedVariableInput.tsx +441 -0
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +152 -0
- package/src/components/form/index.tsx +1 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +8 -1
- package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +100 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +23 -1
- package/src/flow/actions/dataScope.tsx +8 -1
- package/src/flow/actions/linkageRules.tsx +8 -5
- package/src/flow/actions/linkageRulesRefresh.tsx +2 -3
- package/src/flow/actions/setTargetDataScope.tsx +1 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +15 -1
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +75 -1
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +11 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +2 -4
- package/src/flow/common/Markdown/Edit.tsx +32 -4
- package/src/flow/components/ConditionBuilder.tsx +12 -2
- package/src/flow/components/FieldAssignRulesEditor.tsx +37 -46
- package/src/flow/components/FieldAssignValueInput.tsx +8 -4
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +112 -1
- package/src/flow/components/__tests__/fieldAssignOptions.test.ts +71 -1
- package/src/flow/components/code-editor/__tests__/runjsCompletions.test.ts +272 -27
- package/src/flow/components/code-editor/runjsCompletions.ts +490 -9
- package/src/flow/components/fieldAssignOptions.ts +23 -6
- package/src/flow/components/filter/LinkageFilterItem.tsx +22 -9
- package/src/flow/components/filter/VariableFilterItem.tsx +27 -9
- package/src/flow/components/filter/__tests__/fieldsToOptions.test.ts +48 -0
- package/src/flow/components/filter/__tests__/metaTreeAssociationDepth.test.ts +60 -0
- package/src/flow/components/filter/fieldsToOptions.ts +9 -4
- package/src/flow/components/filter/metaTreeAssociationDepth.ts +69 -0
- package/src/flow/models/actions/FilterActionModel.tsx +15 -3
- package/src/flow/models/actions/__tests__/AssignFormRefill.test.ts +90 -0
- package/src/flow/models/base/AssociationFieldGroupModel.tsx +20 -3
- package/src/flow/models/base/GridModel.tsx +1 -1
- package/src/flow/models/base/PageModel/ChildPageModel.tsx +5 -1
- package/src/flow/models/base/PageModel/__tests__/ChildPageModel.test.ts +12 -0
- package/src/flow/models/base/__tests__/AssociationFieldGroupModel.test.ts +86 -0
- package/src/flow/models/base/__tests__/GridModel.render.test.tsx +53 -0
- package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +12 -3
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +14 -4
- package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +216 -0
- package/src/flow/models/blocks/assign-form/assignFieldValuesFlow.tsx +142 -40
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +1 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +6 -1
- package/src/flow/models/blocks/filter-manager/flow-actions/connectFields.tsx +48 -6
- package/src/flow/models/blocks/form/FormAssociationItemModel.tsx +9 -0
- package/src/flow/models/blocks/form/FormItemModel.tsx +9 -0
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
- package/src/flow/models/blocks/form/__tests__/FormItemModel.defineChildren.test.ts +27 -2
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +29 -0
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +5 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +15 -5
- package/src/flow/models/blocks/table/TableCustomColumnModel.tsx +2 -2
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +18 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +6 -7
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +5 -3
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +70 -0
|
@@ -27,6 +27,7 @@ import _ from 'lodash';
|
|
|
27
27
|
import { NumberPicker } from '@formily/antd-v5';
|
|
28
28
|
import { enumToOptions, normalizeSelectRenderValue, UiSchemaEnumItem } from '../../internal/utils/enumOptionsUtils';
|
|
29
29
|
import { resolveOperatorComponent } from '../../internal/utils/operatorSchemaHelper';
|
|
30
|
+
import { limitAssociationMetaTree } from './metaTreeAssociationDepth';
|
|
30
31
|
|
|
31
32
|
const { DateFilterDynamicComponent: DateFilterDynamicComponentLazy } = lazy(
|
|
32
33
|
() => import('../../models/blocks/filter-form/fields/date-time/components/DateFilterDynamicComponent'),
|
|
@@ -85,6 +86,14 @@ export interface VariableFilterItemProps {
|
|
|
85
86
|
*/
|
|
86
87
|
rightMetaTree?: MetaTreeNode[] | (() => MetaTreeNode[] | Promise<MetaTreeNode[]>);
|
|
87
88
|
ignoreFieldNames?: string[];
|
|
89
|
+
maxAssociationFieldDepth?: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function limitMetaTreeIfNeeded(nodes: MetaTreeNode[], maxAssociationFieldDepth?: number) {
|
|
93
|
+
if (typeof maxAssociationFieldDepth !== 'number') {
|
|
94
|
+
return nodes;
|
|
95
|
+
}
|
|
96
|
+
return limitAssociationMetaTree(nodes, { maxAssociationDepth: maxAssociationFieldDepth });
|
|
88
97
|
}
|
|
89
98
|
|
|
90
99
|
function createStaticInputRenderer(
|
|
@@ -260,7 +269,7 @@ function normalizeRightValueInput(input: unknown) {
|
|
|
260
269
|
* 上下文筛选项组件
|
|
261
270
|
*/
|
|
262
271
|
export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
263
|
-
({ value, model, rightAsVariable, rightMetaTree, ignoreFieldNames }) => {
|
|
272
|
+
({ value, model, rightAsVariable, rightMetaTree, ignoreFieldNames, maxAssociationFieldDepth }) => {
|
|
264
273
|
// 使用 View 上下文,确保可访问 ctx.view 的异步子树
|
|
265
274
|
const ctx = useFlowViewContext();
|
|
266
275
|
const t = model.translate;
|
|
@@ -539,13 +548,22 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
539
548
|
const nodes: MetaTreeNode[] = Array.isArray(raw)
|
|
540
549
|
? (raw as MetaTreeNode[])
|
|
541
550
|
: await (raw as () => Promise<MetaTreeNode[]>)();
|
|
542
|
-
return
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
551
|
+
return limitMetaTreeIfNeeded(
|
|
552
|
+
[
|
|
553
|
+
{
|
|
554
|
+
title: t('Constant'),
|
|
555
|
+
name: 'constant',
|
|
556
|
+
type: 'string',
|
|
557
|
+
paths: ['constant'],
|
|
558
|
+
render: staticInputRenderer,
|
|
559
|
+
},
|
|
560
|
+
{ title: t('Null'), name: 'null', type: 'object', paths: ['null'], render: NullComponent },
|
|
561
|
+
...nodes,
|
|
562
|
+
],
|
|
563
|
+
maxAssociationFieldDepth,
|
|
564
|
+
);
|
|
547
565
|
};
|
|
548
|
-
}, [rightMetaTree, ctx, staticInputRenderer, NullComponent, t]);
|
|
566
|
+
}, [rightMetaTree, ctx, staticInputRenderer, NullComponent, t, maxAssociationFieldDepth]);
|
|
549
567
|
|
|
550
568
|
// 当启用右侧变量输入时,构造 VariableInput 的 converters:
|
|
551
569
|
// - 变量模式:返回 null 让 VariableInput 渲染 VariableTag
|
|
@@ -629,9 +647,9 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
629
647
|
for (const n of nodes) {
|
|
630
648
|
out.push(await enhanceNode(n));
|
|
631
649
|
}
|
|
632
|
-
return out;
|
|
650
|
+
return limitMetaTreeIfNeeded(out, maxAssociationFieldDepth);
|
|
633
651
|
};
|
|
634
|
-
}, [getFieldInterface, model]);
|
|
652
|
+
}, [getFieldInterface, model, maxAssociationFieldDepth]);
|
|
635
653
|
|
|
636
654
|
return (
|
|
637
655
|
<Space wrap style={{ width: '100%' }}>
|
|
@@ -0,0 +1,48 @@
|
|
|
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 { describe, expect, it } from 'vitest';
|
|
11
|
+
|
|
12
|
+
import { fieldsToOptions } from '../fieldsToOptions';
|
|
13
|
+
|
|
14
|
+
describe('fieldsToOptions', () => {
|
|
15
|
+
it('skips nested fields when target collection is unavailable', () => {
|
|
16
|
+
const field = {
|
|
17
|
+
name: 'space',
|
|
18
|
+
type: 'belongsTo',
|
|
19
|
+
target: 'spaces',
|
|
20
|
+
title: 'Space',
|
|
21
|
+
interface: 'm2o',
|
|
22
|
+
filterable: true,
|
|
23
|
+
uiSchema: {},
|
|
24
|
+
getInterfaceOptions: () => ({
|
|
25
|
+
filterable: {
|
|
26
|
+
nested: true,
|
|
27
|
+
operators: [{ value: '$eq', label: 'is' }],
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
getFields: () => {
|
|
31
|
+
throw new Error('Target collection spaces not found for field space');
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
expect(() => fieldsToOptions([field], 1, [], (key: string) => key)).not.toThrow();
|
|
36
|
+
expect(fieldsToOptions([field], 1, [], (key: string) => key)).toEqual([
|
|
37
|
+
{
|
|
38
|
+
name: 'space',
|
|
39
|
+
type: 'belongsTo',
|
|
40
|
+
target: 'spaces',
|
|
41
|
+
title: 'Space',
|
|
42
|
+
schema: {},
|
|
43
|
+
operators: [{ value: '$eq', label: 'is' }],
|
|
44
|
+
children: [],
|
|
45
|
+
},
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
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 { describe, expect, it } from 'vitest';
|
|
11
|
+
import type { MetaTreeNode } from '@nocobase/flow-engine';
|
|
12
|
+
import { limitAssociationMetaTree } from '../metaTreeAssociationDepth';
|
|
13
|
+
|
|
14
|
+
describe('limitAssociationMetaTree', () => {
|
|
15
|
+
it('keeps relation / relation / normal field and removes deeper relation fields', async () => {
|
|
16
|
+
const tree: MetaTreeNode[] = [
|
|
17
|
+
{
|
|
18
|
+
name: 'user',
|
|
19
|
+
title: 'User',
|
|
20
|
+
type: 'object',
|
|
21
|
+
interface: 'm2o',
|
|
22
|
+
options: { target: 'users' },
|
|
23
|
+
paths: ['collection', 'user'],
|
|
24
|
+
children: async () => [
|
|
25
|
+
{
|
|
26
|
+
name: 'department',
|
|
27
|
+
title: 'Department',
|
|
28
|
+
type: 'object',
|
|
29
|
+
interface: 'm2o',
|
|
30
|
+
options: { target: 'departments' },
|
|
31
|
+
paths: ['collection', 'user', 'department'],
|
|
32
|
+
children: async () => [
|
|
33
|
+
{
|
|
34
|
+
name: 'name',
|
|
35
|
+
title: 'Name',
|
|
36
|
+
type: 'string',
|
|
37
|
+
interface: 'input',
|
|
38
|
+
paths: ['collection', 'user', 'department', 'name'],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'company',
|
|
42
|
+
title: 'Company',
|
|
43
|
+
type: 'object',
|
|
44
|
+
interface: 'm2o',
|
|
45
|
+
options: { target: 'companies' },
|
|
46
|
+
paths: ['collection', 'user', 'department', 'company'],
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const limited = limitAssociationMetaTree(tree);
|
|
55
|
+
const userChildren = await (limited[0].children as () => Promise<MetaTreeNode[]>)();
|
|
56
|
+
const departmentChildren = await (userChildren[0].children as () => Promise<MetaTreeNode[]>)();
|
|
57
|
+
|
|
58
|
+
expect(departmentChildren.map((node) => node.name)).toEqual(['name']);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -59,10 +59,15 @@ const field2option = (field: CollectionField, depth, nonfilterable, t) => {
|
|
|
59
59
|
option['children'] = children;
|
|
60
60
|
}
|
|
61
61
|
if (nested) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
let targetFields: CollectionField[] = [];
|
|
63
|
+
try {
|
|
64
|
+
targetFields = field.getFields().filter((f) => {
|
|
65
|
+
// 过滤掉附件字段,因为会报错:Target collection attachments not found for field xxx
|
|
66
|
+
return f.target !== 'attachments' && f.interface !== 'formula';
|
|
67
|
+
});
|
|
68
|
+
} catch {
|
|
69
|
+
targetFields = [];
|
|
70
|
+
}
|
|
66
71
|
const options = fieldsToOptions(targetFields, depth + 1, nonfilterable, t).filter(Boolean);
|
|
67
72
|
option['children'] = option['children'] || [];
|
|
68
73
|
option['children'].push(...options);
|
|
@@ -0,0 +1,69 @@
|
|
|
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 type { MetaTreeNode } from '@nocobase/flow-engine';
|
|
11
|
+
|
|
12
|
+
const ASSOCIATION_FIELD_INTERFACES = new Set(['m2o', 'o2o', 'oho', 'obo', 'o2m', 'm2m', 'linkTo']);
|
|
13
|
+
|
|
14
|
+
function isAssociationMetaNode(node: MetaTreeNode) {
|
|
15
|
+
const options = (node as any)?.options || {};
|
|
16
|
+
return Boolean(
|
|
17
|
+
options.target ||
|
|
18
|
+
options.targetCollection ||
|
|
19
|
+
options.foreignKey ||
|
|
20
|
+
options.through ||
|
|
21
|
+
ASSOCIATION_FIELD_INTERFACES.has(String(node?.interface || '')),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function limitNodeAssociationDepth(node: MetaTreeNode, associationDepth: number, maxAssociationDepth: number) {
|
|
26
|
+
const isAssociation = isAssociationMetaNode(node);
|
|
27
|
+
const nextAssociationDepth = associationDepth + (isAssociation ? 1 : 0);
|
|
28
|
+
|
|
29
|
+
if (isAssociation && nextAssociationDepth > maxAssociationDepth) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const wrapChildren = (children: MetaTreeNode[] | (() => Promise<MetaTreeNode[]>)) => {
|
|
34
|
+
if (Array.isArray(children)) {
|
|
35
|
+
return limitAssociationMetaTree(children, {
|
|
36
|
+
associationDepth: nextAssociationDepth,
|
|
37
|
+
maxAssociationDepth,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return async () => {
|
|
42
|
+
const loaded = await children();
|
|
43
|
+
return limitAssociationMetaTree(Array.isArray(loaded) ? loaded : [], {
|
|
44
|
+
associationDepth: nextAssociationDepth,
|
|
45
|
+
maxAssociationDepth,
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
...node,
|
|
52
|
+
children: node.children ? wrapChildren(node.children as any) : undefined,
|
|
53
|
+
} as MetaTreeNode;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function limitAssociationMetaTree(
|
|
57
|
+
nodes: MetaTreeNode[] | undefined,
|
|
58
|
+
options: {
|
|
59
|
+
associationDepth?: number;
|
|
60
|
+
maxAssociationDepth?: number;
|
|
61
|
+
} = {},
|
|
62
|
+
): MetaTreeNode[] {
|
|
63
|
+
const { associationDepth = 0, maxAssociationDepth = 2 } = options;
|
|
64
|
+
if (!Array.isArray(nodes)) return [];
|
|
65
|
+
|
|
66
|
+
return nodes
|
|
67
|
+
.map((node) => limitNodeAssociationDepth(node, associationDepth, maxAssociationDepth))
|
|
68
|
+
.filter(Boolean) as MetaTreeNode[];
|
|
69
|
+
}
|
|
@@ -75,7 +75,12 @@ export class FilterActionModel extends ActionModel {
|
|
|
75
75
|
value={this.props.filterValue || this.props.defaultFilterValue}
|
|
76
76
|
ctx={this.context}
|
|
77
77
|
FilterItem={(props) => (
|
|
78
|
-
<VariableFilterItem
|
|
78
|
+
<VariableFilterItem
|
|
79
|
+
{...props}
|
|
80
|
+
model={this}
|
|
81
|
+
ignoreFieldNames={this.getIgnoreFieldNames()}
|
|
82
|
+
maxAssociationFieldDepth={2}
|
|
83
|
+
/>
|
|
79
84
|
)}
|
|
80
85
|
/>
|
|
81
86
|
<div style={{ height: 150 }}></div>
|
|
@@ -94,7 +99,12 @@ export class FilterActionModel extends ActionModel {
|
|
|
94
99
|
value={this.props.filterValue || this.props.defaultFilterValue}
|
|
95
100
|
ctx={this.context}
|
|
96
101
|
FilterItem={(props) => (
|
|
97
|
-
<VariableFilterItem
|
|
102
|
+
<VariableFilterItem
|
|
103
|
+
{...props}
|
|
104
|
+
model={this}
|
|
105
|
+
ignoreFieldNames={this.getIgnoreFieldNames()}
|
|
106
|
+
maxAssociationFieldDepth={2}
|
|
107
|
+
/>
|
|
98
108
|
)}
|
|
99
109
|
/>
|
|
100
110
|
}
|
|
@@ -187,7 +197,9 @@ FilterActionModel.registerFlow({
|
|
|
187
197
|
return (
|
|
188
198
|
<FilterGroup
|
|
189
199
|
value={props.value || {}}
|
|
190
|
-
FilterItem={(props) =>
|
|
200
|
+
FilterItem={(props) => (
|
|
201
|
+
<VariableFilterItem {...props} model={modelInstance} maxAssociationFieldDepth={2} />
|
|
202
|
+
)}
|
|
191
203
|
/>
|
|
192
204
|
);
|
|
193
205
|
},
|
|
@@ -53,6 +53,51 @@ describe('AssignForm value refill and save (beforeParamsSave)', () => {
|
|
|
53
53
|
expect(saved?.assignedValues).toEqual({ nickname: 'Alice', score: 99 });
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
it('UpdateRecordActionModel: saves assignedValues from assignForm subModel when assignFormUid is not ready', async () => {
|
|
57
|
+
const root = new FlowEngine();
|
|
58
|
+
root.registerModels({ UpdateRecordActionModel, AssignFormModel: TestAssignFormModel });
|
|
59
|
+
|
|
60
|
+
const action = root.createModel<UpdateRecordActionModel>({ use: 'UpdateRecordActionModel', uid: 'act-u-sub' });
|
|
61
|
+
const form = root.createModel<TestAssignFormModel>({
|
|
62
|
+
use: 'AssignFormModel',
|
|
63
|
+
uid: 'form-u-sub',
|
|
64
|
+
parentId: action.uid,
|
|
65
|
+
subKey: 'assignForm',
|
|
66
|
+
});
|
|
67
|
+
form.setAssignedValues({ nickname: 'Bob' });
|
|
68
|
+
action.setSubModel('assignForm', form);
|
|
69
|
+
|
|
70
|
+
const flow = action.getFlow('assignSettings') as any;
|
|
71
|
+
const step = flow?.steps?.assignFieldValues;
|
|
72
|
+
|
|
73
|
+
await step.beforeParamsSave({ engine: root, model: action }, {}, {});
|
|
74
|
+
|
|
75
|
+
const saved = action.getStepParams('assignSettings', 'assignFieldValues');
|
|
76
|
+
expect(saved?.assignedValues).toEqual({ nickname: 'Bob' });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('UpdateRecordActionModel: keeps previous assignedValues when AssignForm is unavailable during save', async () => {
|
|
80
|
+
const root = new FlowEngine();
|
|
81
|
+
root.registerModels({ UpdateRecordActionModel, AssignFormModel: TestAssignFormModel });
|
|
82
|
+
|
|
83
|
+
const action = root.createModel<UpdateRecordActionModel>({ use: 'UpdateRecordActionModel', uid: 'act-u-missing' });
|
|
84
|
+
action.setStepParams('assignSettings', 'assignFieldValues', {});
|
|
85
|
+
|
|
86
|
+
const flow = action.getFlow('assignSettings') as any;
|
|
87
|
+
const step = flow?.steps?.assignFieldValues;
|
|
88
|
+
|
|
89
|
+
await step.beforeParamsSave(
|
|
90
|
+
{ engine: root, model: action },
|
|
91
|
+
{},
|
|
92
|
+
{
|
|
93
|
+
assignedValues: { nickname: 'Previous' },
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const saved = action.getStepParams('assignSettings', 'assignFieldValues');
|
|
98
|
+
expect(saved?.assignedValues).toEqual({ nickname: 'Previous' });
|
|
99
|
+
});
|
|
100
|
+
|
|
56
101
|
it('FormSubmitActionModel: reuses assignFieldValues step and saves assignedValues from AssignForm', async () => {
|
|
57
102
|
const root = new FlowEngine();
|
|
58
103
|
|
|
@@ -77,4 +122,49 @@ describe('AssignForm value refill and save (beforeParamsSave)', () => {
|
|
|
77
122
|
const saved = action.getStepParams('submitSettings', 'assignFieldValues');
|
|
78
123
|
expect(saved?.assignedValues).toEqual({ status: 'published' });
|
|
79
124
|
});
|
|
125
|
+
|
|
126
|
+
it('FormSubmitActionModel: saves assignedValues from assignForm subModel when assignFormUid is not ready', async () => {
|
|
127
|
+
const root = new FlowEngine();
|
|
128
|
+
root.registerModels({ FormSubmitActionModel, AssignFormModel: TestAssignFormModel });
|
|
129
|
+
|
|
130
|
+
const action = root.createModel<FormSubmitActionModel>({ use: 'FormSubmitActionModel', uid: 'submit-u-sub' });
|
|
131
|
+
const form = root.createModel<TestAssignFormModel>({
|
|
132
|
+
use: 'AssignFormModel',
|
|
133
|
+
uid: 'submit-form-u-sub',
|
|
134
|
+
parentId: action.uid,
|
|
135
|
+
subKey: 'assignForm',
|
|
136
|
+
});
|
|
137
|
+
form.setAssignedValues({ status: 'draft' });
|
|
138
|
+
action.setSubModel('assignForm', form);
|
|
139
|
+
|
|
140
|
+
const flow = action.getFlow('submitSettings') as any;
|
|
141
|
+
const step = flow?.steps?.assignFieldValues;
|
|
142
|
+
|
|
143
|
+
await step.beforeParamsSave({ engine: root, model: action }, {}, {});
|
|
144
|
+
|
|
145
|
+
const saved = action.getStepParams('submitSettings', 'assignFieldValues');
|
|
146
|
+
expect(saved?.assignedValues).toEqual({ status: 'draft' });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('FormSubmitActionModel: keeps previous assignedValues when AssignForm is unavailable during save', async () => {
|
|
150
|
+
const root = new FlowEngine();
|
|
151
|
+
root.registerModels({ FormSubmitActionModel, AssignFormModel: TestAssignFormModel });
|
|
152
|
+
|
|
153
|
+
const action = root.createModel<FormSubmitActionModel>({ use: 'FormSubmitActionModel', uid: 'submit-u-missing' });
|
|
154
|
+
action.setStepParams('submitSettings', 'assignFieldValues', {});
|
|
155
|
+
|
|
156
|
+
const flow = action.getFlow('submitSettings') as any;
|
|
157
|
+
const step = flow?.steps?.assignFieldValues;
|
|
158
|
+
|
|
159
|
+
await step.beforeParamsSave(
|
|
160
|
+
{ engine: root, model: action },
|
|
161
|
+
{},
|
|
162
|
+
{
|
|
163
|
+
assignedValues: { status: 'published' },
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const saved = action.getStepParams('submitSettings', 'assignFieldValues');
|
|
168
|
+
expect(saved?.assignedValues).toEqual({ status: 'published' });
|
|
169
|
+
});
|
|
80
170
|
});
|
|
@@ -11,13 +11,20 @@ import { Collection, DisplayItemModel, FlowModel, FlowModelContext } from '@noco
|
|
|
11
11
|
|
|
12
12
|
export class AssociationFieldGroupModel extends FlowModel {
|
|
13
13
|
static itemModelName = 'DetailsItemModel';
|
|
14
|
+
static maxAssociationFieldDepth = 2;
|
|
14
15
|
static defineChildren(ctx: FlowModelContext) {
|
|
15
16
|
const itemModel = this.itemModelName;
|
|
16
|
-
const
|
|
17
|
+
const maxAssociationFieldDepth = this.maxAssociationFieldDepth ?? 2;
|
|
18
|
+
const displayAssociationFields = (targetCollection: Collection, fieldPath = '', associationDepth = 0) => {
|
|
19
|
+
if (associationDepth >= maxAssociationFieldDepth) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
return targetCollection
|
|
18
24
|
.getToOneAssociationFields()
|
|
19
25
|
.map((field) => {
|
|
20
26
|
const fPath = fieldPath ? `${fieldPath}.${field.name}` : field.name;
|
|
27
|
+
const nextAssociationDepth = associationDepth + 1;
|
|
21
28
|
if (!field.targetCollection) {
|
|
22
29
|
console.error(
|
|
23
30
|
`AssociationFieldGroupModel: target collection ${field.target} not found for field ${field.name}`,
|
|
@@ -37,6 +44,12 @@ export class AssociationFieldGroupModel extends FlowModel {
|
|
|
37
44
|
children: field.targetCollection
|
|
38
45
|
.getFields()
|
|
39
46
|
.map((f) => {
|
|
47
|
+
if (
|
|
48
|
+
nextAssociationDepth >= maxAssociationFieldDepth &&
|
|
49
|
+
(f.isAssociationField?.() || f.target || f.targetCollection)
|
|
50
|
+
) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
40
53
|
const fp = `${fPath}.${f.name}`;
|
|
41
54
|
const binding = DisplayItemModel.getDefaultBindingByField(ctx, f, {
|
|
42
55
|
fallbackToTargetTitleField: true,
|
|
@@ -81,9 +94,13 @@ export class AssociationFieldGroupModel extends FlowModel {
|
|
|
81
94
|
key: `${fPath}-children-associationField`,
|
|
82
95
|
label: 'Display association fields',
|
|
83
96
|
type: 'group',
|
|
84
|
-
children: (
|
|
97
|
+
children: (
|
|
98
|
+
displayAssociationFields(field.targetCollection, fPath, nextAssociationDepth) || []
|
|
99
|
+
).filter(Boolean),
|
|
85
100
|
},
|
|
86
|
-
]
|
|
101
|
+
].filter((item) => {
|
|
102
|
+
return item.key !== `${fPath}-children-associationField` || (item.children as any[])?.length > 0;
|
|
103
|
+
});
|
|
87
104
|
},
|
|
88
105
|
};
|
|
89
106
|
})
|
|
@@ -1006,7 +1006,7 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
|
|
|
1006
1006
|
<Space
|
|
1007
1007
|
ref={this.gridContainerRef}
|
|
1008
1008
|
direction={'vertical'}
|
|
1009
|
-
style={{ width: '100%', marginBottom: this.props.rowGap }}
|
|
1009
|
+
style={{ width: '100%', marginBottom: this.props.rowGap, display: 'flex' }}
|
|
1010
1010
|
size={this.props.rowGap}
|
|
1011
1011
|
>
|
|
1012
1012
|
<DndProvider
|
|
@@ -49,7 +49,11 @@ export class ChildPageModel extends PageModel {
|
|
|
49
49
|
throw new Error('Invalid drag event');
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
if (event.active.id === event.over.id) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await this.flowEngine.moveModel(event.active.id, event.over.id);
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
render() {
|
|
@@ -107,6 +107,18 @@ describe('ChildPageModel', () => {
|
|
|
107
107
|
expect(mockFlowEngine.moveModel).toHaveBeenCalledWith('active-model-id', 'over-model-id');
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
+
it('should ignore self-drop', async () => {
|
|
111
|
+
await childPageModel.handleDragEnd({
|
|
112
|
+
...mockDragEndEvent,
|
|
113
|
+
over: {
|
|
114
|
+
...(mockDragEndEvent.over as NonNullable<DragEndEvent['over']>),
|
|
115
|
+
id: 'active-model-id',
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(mockFlowEngine.moveModel).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
110
122
|
it('should handle case when over is null', async () => {
|
|
111
123
|
const eventWithNullOver = {
|
|
112
124
|
...mockDragEndEvent,
|
|
@@ -0,0 +1,86 @@
|
|
|
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 { afterEach, describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import { DisplayItemModel } from '@nocobase/flow-engine';
|
|
12
|
+
import { AssociationFieldGroupModel } from '../AssociationFieldGroupModel';
|
|
13
|
+
|
|
14
|
+
function createCollection(name: string, fields: any[]) {
|
|
15
|
+
return {
|
|
16
|
+
name,
|
|
17
|
+
dataSourceKey: 'main',
|
|
18
|
+
getFields: () => fields,
|
|
19
|
+
getToOneAssociationFields: () => fields.filter((field) => field?.type === 'belongsTo' || field?.type === 'hasOne'),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('AssociationFieldGroupModel', () => {
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vi.restoreAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('limits display association fields to relation / relation / field', () => {
|
|
29
|
+
vi.spyOn(DisplayItemModel, 'getDefaultBindingByField').mockReturnValue({ modelName: 'InputFieldModel' } as any);
|
|
30
|
+
|
|
31
|
+
const companies = createCollection('companies', [
|
|
32
|
+
{ name: 'title', title: 'Company title', interface: 'input', type: 'string' },
|
|
33
|
+
]);
|
|
34
|
+
const departments = createCollection('departments', [
|
|
35
|
+
{ name: 'name', title: 'Department name', interface: 'input', type: 'string' },
|
|
36
|
+
{
|
|
37
|
+
name: 'company',
|
|
38
|
+
title: 'Company',
|
|
39
|
+
interface: 'm2o',
|
|
40
|
+
type: 'belongsTo',
|
|
41
|
+
target: 'companies',
|
|
42
|
+
targetCollection: companies,
|
|
43
|
+
isAssociationField: () => true,
|
|
44
|
+
},
|
|
45
|
+
]);
|
|
46
|
+
const users = createCollection('users', [
|
|
47
|
+
{
|
|
48
|
+
name: 'department',
|
|
49
|
+
title: 'Department',
|
|
50
|
+
interface: 'm2o',
|
|
51
|
+
type: 'belongsTo',
|
|
52
|
+
target: 'departments',
|
|
53
|
+
targetCollection: departments,
|
|
54
|
+
isAssociationField: () => true,
|
|
55
|
+
},
|
|
56
|
+
]);
|
|
57
|
+
const posts = createCollection('posts', [
|
|
58
|
+
{
|
|
59
|
+
name: 'user',
|
|
60
|
+
title: 'User',
|
|
61
|
+
interface: 'm2o',
|
|
62
|
+
type: 'belongsTo',
|
|
63
|
+
target: 'users',
|
|
64
|
+
targetCollection: users,
|
|
65
|
+
isAssociationField: () => true,
|
|
66
|
+
},
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
const ctx = {
|
|
70
|
+
collection: posts,
|
|
71
|
+
t: (key: string) => key,
|
|
72
|
+
} as any;
|
|
73
|
+
|
|
74
|
+
const firstLevel = AssociationFieldGroupModel.defineChildren(ctx) as any[];
|
|
75
|
+
const firstChildren = firstLevel[0].children();
|
|
76
|
+
const firstAssociationGroup = firstChildren.find((item) => item.key === 'user-children-associationField');
|
|
77
|
+
const secondLevel = firstAssociationGroup.children;
|
|
78
|
+
const secondChildren = secondLevel[0].children();
|
|
79
|
+
const secondFieldsGroup = secondChildren.find((item) => item.key === 'user.department-children-collectionField');
|
|
80
|
+
|
|
81
|
+
expect(secondChildren.map((item) => item.key)).toContain('user.department-children-collectionField');
|
|
82
|
+
expect(secondChildren.map((item) => item.key)).not.toContain('user.department-children-associationField');
|
|
83
|
+
expect(secondFieldsGroup.children.map((item) => item.key)).toContain('c-user.department.name');
|
|
84
|
+
expect(secondFieldsGroup.children.map((item) => item.key)).not.toContain('c-user.department.company');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
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 { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
|
|
11
|
+
import { render } from '@testing-library/react';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
14
|
+
import { GridModel } from '../GridModel';
|
|
15
|
+
|
|
16
|
+
describe('GridModel.render', () => {
|
|
17
|
+
let engine: FlowEngine;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
engine = new FlowEngine();
|
|
21
|
+
engine.registerModels({ GridModel });
|
|
22
|
+
await engine.flowSettings.disable();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('renders the Space wrapper as block-level flex to avoid inline baseline spacing', () => {
|
|
26
|
+
const item = engine.createModel({ use: 'FlowModel', uid: 'item-1' });
|
|
27
|
+
const model = engine.createModel<GridModel>({
|
|
28
|
+
use: 'GridModel',
|
|
29
|
+
uid: 'grid-render',
|
|
30
|
+
props: {
|
|
31
|
+
rows: {
|
|
32
|
+
row1: [['item-1']],
|
|
33
|
+
},
|
|
34
|
+
sizes: {
|
|
35
|
+
row1: [24],
|
|
36
|
+
},
|
|
37
|
+
rowGap: 16,
|
|
38
|
+
},
|
|
39
|
+
structure: {} as any,
|
|
40
|
+
});
|
|
41
|
+
(model as any).subModels = { items: [item] };
|
|
42
|
+
|
|
43
|
+
expect(Object.keys((model as any).getVisibleLayout().rows)).toEqual(['row1']);
|
|
44
|
+
|
|
45
|
+
const { container } = render(<FlowEngineProvider engine={engine}>{model.render()}</FlowEngineProvider>);
|
|
46
|
+
const space = container.querySelector('.ant-space') as HTMLElement;
|
|
47
|
+
|
|
48
|
+
expect(space).toBeTruthy();
|
|
49
|
+
expect(space.style.width).toBe('100%');
|
|
50
|
+
expect(space.style.marginBottom).toBe('16px');
|
|
51
|
+
expect(space.style.display).toBe('flex');
|
|
52
|
+
});
|
|
53
|
+
});
|