@nocobase/client-v2 2.1.0-beta.44 → 2.1.0-beta.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/BaseApplication.d.ts +9 -0
- package/es/authRedirect.d.ts +12 -0
- package/es/collection-manager/filter-operators/index.d.ts +1 -0
- package/es/components/form/filter/CollectionFilterItem.d.ts +5 -1
- package/es/components/form/filter/FilterValueInput.d.ts +5 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +11 -0
- package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/useApplications.d.ts +3 -2
- 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/internal/utils/operatorSchemaHelper.d.ts +26 -3
- package/es/flow/models/base/AssociationFieldGroupModel.d.ts +2 -33
- package/es/flow/models/blocks/filter-manager/utils.d.ts +23 -1
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +2 -0
- package/es/index.mjs +178 -100
- package/lib/index.js +187 -109
- package/package.json +7 -7
- package/src/BaseApplication.tsx +13 -0
- package/src/__tests__/browserChecker.test.ts +112 -0
- package/src/__tests__/dataSourceRuntime.test.tsx +22 -1
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +5 -0
- package/src/authRedirect.ts +43 -0
- package/src/collection-field-interface/CollectionFieldInterface.ts +14 -7
- package/src/collection-manager/filter-operators/index.ts +29 -2
- package/src/components/form/filter/CollectionFilterItem.tsx +10 -2
- package/src/components/form/filter/FilterValueInput.tsx +27 -2
- package/src/components/form/filter/__tests__/FilterValueInput.test.tsx +39 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +8 -1
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +369 -2
- package/src/flow/actions/__tests__/sortingRules.test.ts +22 -0
- package/src/flow/actions/dataScope.tsx +8 -1
- package/src/flow/actions/linkageRules.tsx +56 -1
- package/src/flow/actions/setTargetDataScope.tsx +1 -1
- package/src/flow/actions/sortingRules.tsx +3 -4
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +31 -3
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +1 -0
- package/src/flow/admin-shell/admin-layout/AppListRender.tsx +139 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +8 -1
- package/src/flow/admin-shell/admin-layout/index.ts +1 -0
- 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/admin-shell/admin-layout/useApplications.tsx +34 -1
- package/src/flow/common/Markdown/Edit.tsx +32 -4
- package/src/flow/components/ConditionBuilder.tsx +12 -2
- package/src/flow/components/DefaultValue.tsx +14 -8
- package/src/flow/components/FieldAssignRulesEditor.tsx +37 -46
- package/src/flow/components/FieldAssignValueInput.tsx +14 -14
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +112 -1
- package/src/flow/components/__tests__/fieldAssignOptions.test.ts +71 -1
- package/src/flow/components/fieldAssignOptions.ts +23 -6
- package/src/flow/components/filter/LinkageFilterItem.tsx +26 -12
- package/src/flow/components/filter/VariableFilterItem.tsx +32 -13
- 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/components/placeholders/BlockPlaceholder.tsx +76 -15
- package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +105 -0
- package/src/flow/internal/utils/operatorSchemaHelper.ts +112 -5
- package/src/flow/models/actions/FilterActionModel.tsx +15 -3
- package/src/flow/models/base/AssociationFieldGroupModel.tsx +20 -3
- package/src/flow/models/base/CollectionBlockModel.tsx +10 -6
- package/src/flow/models/base/__tests__/AssociationFieldGroupModel.test.ts +86 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.title.test.ts +33 -1
- package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +11 -0
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +14 -12
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +6 -1
- package/src/flow/models/blocks/filter-manager/__tests__/getDefaultOperator.test.ts +35 -1
- package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +148 -0
- package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/defaultOperator.test.tsx +85 -0
- package/src/flow/models/blocks/filter-manager/flow-actions/connectFields.tsx +48 -6
- package/src/flow/models/blocks/filter-manager/flow-actions/customizeFilterRender.tsx +14 -44
- package/src/flow/models/blocks/filter-manager/flow-actions/defaultOperator.tsx +3 -5
- package/src/flow/models/blocks/filter-manager/flow-actions/operatorComponentProps.tsx +2 -12
- package/src/flow/models/blocks/filter-manager/utils.ts +143 -4
- 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__/FormItemModel.defineChildren.test.ts +27 -2
- 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
|
@@ -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
|
+
}
|
|
@@ -15,19 +15,66 @@ import { useTranslation } from 'react-i18next';
|
|
|
15
15
|
import { BlockItemCard } from '../BlockItemCard';
|
|
16
16
|
import { BlockModel } from '../../models/base/BlockModel';
|
|
17
17
|
|
|
18
|
+
function getResourceSettingsInitParams(model: any) {
|
|
19
|
+
if (typeof model?.getResourceSettingsInitParams === 'function') {
|
|
20
|
+
return model.getResourceSettingsInitParams();
|
|
21
|
+
}
|
|
22
|
+
if (typeof model?.getStepParams === 'function') {
|
|
23
|
+
return model.getStepParams('resourceSettings', 'init');
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getBlockResourceInfo(model: any, t: (key: string) => string) {
|
|
29
|
+
const blockModel = model?.context?.blockModel || model;
|
|
30
|
+
const params = getResourceSettingsInitParams(blockModel) || {};
|
|
31
|
+
const collection = blockModel?.context?.collection || blockModel?.collection || model?.context?.collection;
|
|
32
|
+
const dataSource =
|
|
33
|
+
collection?.dataSource || blockModel?.context?.dataSource || blockModel?.dataSource || model?.dataSource;
|
|
34
|
+
const dataSourceKey = dataSource?.key || collection?.dataSourceKey || params.dataSourceKey;
|
|
35
|
+
const collectionName =
|
|
36
|
+
collection?.title ||
|
|
37
|
+
collection?.name ||
|
|
38
|
+
collection?.tableName ||
|
|
39
|
+
model?.resource?.resourceName ||
|
|
40
|
+
model?.resource?.getResourceName?.() ||
|
|
41
|
+
params.associationName ||
|
|
42
|
+
params.collectionName;
|
|
43
|
+
const dataSourceName = dataSource ? t(dataSource.displayName || dataSource.key) : dataSourceKey;
|
|
44
|
+
const collectionLabel = collectionName ? `${t(collectionName) || collectionName}` : '';
|
|
45
|
+
const dataSourceLabel = dataSourceName ? `${t(dataSourceName)} > ` : '';
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
dataSourceName,
|
|
49
|
+
nameValue: `${dataSourceLabel}${collectionLabel}`,
|
|
50
|
+
isDataSourceUnavailable: Boolean(dataSourceKey && !dataSource),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function DataSourceUnavailablePlaceholder({ dataSourceName }: { dataSourceName?: string }) {
|
|
55
|
+
const { t } = useTranslation();
|
|
56
|
+
const subTitle = dataSourceName
|
|
57
|
+
? t(
|
|
58
|
+
'The data source "{{name}}" used by this block is disabled or unavailable. Enable the data source to display this block.',
|
|
59
|
+
{ name: dataSourceName },
|
|
60
|
+
)
|
|
61
|
+
: t('The data source used by this block is disabled or unavailable. Enable the data source to display this block.');
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<BlockItemCard>
|
|
65
|
+
<Result status="403" subTitle={subTitle}></Result>
|
|
66
|
+
</BlockItemCard>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
18
70
|
export const BlockPlaceholder = () => {
|
|
19
71
|
const { t } = useTranslation();
|
|
20
72
|
const model: BlockModel = useFlowModel();
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const nameValue = useMemo(() => {
|
|
25
|
-
const dataSourcePrefix = `${t(dataSource.displayName || dataSource.key)} > `;
|
|
26
|
-
const collectionPrefix = collection ? `${t(collection.title) || collection.name || collection.tableName} ` : '';
|
|
27
|
-
return `${dataSourcePrefix}${collectionPrefix}`;
|
|
28
|
-
}, []);
|
|
73
|
+
const { dataSourceName, isDataSourceUnavailable, nameValue } = useMemo(() => {
|
|
74
|
+
return getBlockResourceInfo(model, t);
|
|
75
|
+
}, [model, t]);
|
|
29
76
|
|
|
30
|
-
const { actionName } = model.forbidden;
|
|
77
|
+
const { actionName } = model.forbidden || {};
|
|
31
78
|
const messageValue = useMemo(() => {
|
|
32
79
|
return t(
|
|
33
80
|
`The current user only has the UI configuration permission, but don't have "{{actionName}}" permission for collection "{{name}}"`,
|
|
@@ -37,6 +84,14 @@ export const BlockPlaceholder = () => {
|
|
|
37
84
|
},
|
|
38
85
|
).replaceAll('>', '>');
|
|
39
86
|
}, [actionName, nameValue, t]);
|
|
87
|
+
|
|
88
|
+
if (isDataSourceUnavailable) {
|
|
89
|
+
if (!model.context.flowSettingsEnabled) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return <DataSourceUnavailablePlaceholder dataSourceName={dataSourceName} />;
|
|
93
|
+
}
|
|
94
|
+
|
|
40
95
|
return (
|
|
41
96
|
<BlockItemCard>
|
|
42
97
|
<Result status="403" subTitle={messageValue}></Result>
|
|
@@ -47,12 +102,10 @@ export const BlockPlaceholder = () => {
|
|
|
47
102
|
export function BlockDeletePlaceholder() {
|
|
48
103
|
const { t } = useTranslation();
|
|
49
104
|
const model: any = useFlowModel();
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return `${dataSourcePrefix}${collectionPrefix}`;
|
|
55
|
-
}, []);
|
|
105
|
+
const { dataSourceName, isDataSourceUnavailable, nameValue } = useMemo(() => {
|
|
106
|
+
return getBlockResourceInfo(model, t);
|
|
107
|
+
}, [model, t]);
|
|
108
|
+
|
|
56
109
|
const messageValue = useMemo(() => {
|
|
57
110
|
return t(`The {{type}} "{{name}}" may have been deleted. Please remove this {{blockType}}.`, {
|
|
58
111
|
type: t('Collection'),
|
|
@@ -60,6 +113,14 @@ export function BlockDeletePlaceholder() {
|
|
|
60
113
|
blockType: t('Block'),
|
|
61
114
|
}).replaceAll('>', '>');
|
|
62
115
|
}, [nameValue, t]);
|
|
116
|
+
|
|
117
|
+
if (isDataSourceUnavailable) {
|
|
118
|
+
if (!model.context.flowSettingsEnabled) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return <DataSourceUnavailablePlaceholder dataSourceName={dataSourceName} />;
|
|
122
|
+
}
|
|
123
|
+
|
|
63
124
|
return (
|
|
64
125
|
<BlockItemCard>
|
|
65
126
|
<Result status="404" subTitle={messageValue}></Result>
|
|
@@ -0,0 +1,105 @@
|
|
|
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 { FlowModelProvider } from '@nocobase/flow-engine';
|
|
11
|
+
import { render, screen } from '@testing-library/react';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { BlockDeletePlaceholder } from '../BlockPlaceholder';
|
|
14
|
+
|
|
15
|
+
vi.mock('react-i18next', () => ({
|
|
16
|
+
useTranslation: () => ({
|
|
17
|
+
t: (key: string, values?: Record<string, string>) => {
|
|
18
|
+
return key.replace(/\{\{(\w+)\}\}/g, (_, name) => values?.[name] ?? '');
|
|
19
|
+
},
|
|
20
|
+
}),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
function createModel(options: {
|
|
24
|
+
dataSourceKey?: string;
|
|
25
|
+
collectionName?: string;
|
|
26
|
+
dataSource?: { key: string; displayName?: string };
|
|
27
|
+
flowSettingsEnabled?: boolean;
|
|
28
|
+
}) {
|
|
29
|
+
const model: any = {
|
|
30
|
+
context: {
|
|
31
|
+
flowSettingsEnabled: options.flowSettingsEnabled,
|
|
32
|
+
},
|
|
33
|
+
dataSource: options.dataSource,
|
|
34
|
+
resource: {
|
|
35
|
+
resourceName: options.collectionName,
|
|
36
|
+
},
|
|
37
|
+
getResourceSettingsInitParams: () => ({
|
|
38
|
+
dataSourceKey: options.dataSourceKey,
|
|
39
|
+
collectionName: options.collectionName,
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
model.context.blockModel = model;
|
|
43
|
+
return model;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('BlockPlaceholder', () => {
|
|
47
|
+
it('should render data source unavailable placeholder in configuration mode when the configured data source is missing', () => {
|
|
48
|
+
const model = createModel({
|
|
49
|
+
dataSourceKey: 'external-mysql',
|
|
50
|
+
collectionName: 'orders',
|
|
51
|
+
flowSettingsEnabled: true,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
render(
|
|
55
|
+
<FlowModelProvider model={model}>
|
|
56
|
+
<BlockDeletePlaceholder />
|
|
57
|
+
</FlowModelProvider>,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
expect(
|
|
61
|
+
screen.getByText(
|
|
62
|
+
'The data source "external-mysql" used by this block is disabled or unavailable. Enable the data source to display this block.',
|
|
63
|
+
),
|
|
64
|
+
).toBeInTheDocument();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should hide data source unavailable block outside configuration mode', () => {
|
|
68
|
+
const model = createModel({
|
|
69
|
+
dataSourceKey: 'external-mysql',
|
|
70
|
+
collectionName: 'orders',
|
|
71
|
+
flowSettingsEnabled: false,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const { container } = render(
|
|
75
|
+
<FlowModelProvider model={model}>
|
|
76
|
+
<BlockDeletePlaceholder />
|
|
77
|
+
</FlowModelProvider>,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(
|
|
81
|
+
screen.queryByText(
|
|
82
|
+
'The data source "external-mysql" used by this block is disabled or unavailable. Enable the data source to display this block.',
|
|
83
|
+
),
|
|
84
|
+
).not.toBeInTheDocument();
|
|
85
|
+
expect(container).toBeEmptyDOMElement();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should keep collection deleted placeholder when the data source is available', () => {
|
|
89
|
+
const model = createModel({
|
|
90
|
+
dataSourceKey: 'external-mysql',
|
|
91
|
+
collectionName: 'orders',
|
|
92
|
+
dataSource: { key: 'external-mysql', displayName: 'External MySQL' },
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
render(
|
|
96
|
+
<FlowModelProvider model={model}>
|
|
97
|
+
<BlockDeletePlaceholder />
|
|
98
|
+
</FlowModelProvider>,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect(
|
|
102
|
+
screen.getByText('The Collection "External MySQL > orders" may have been deleted. Please remove this Block.'),
|
|
103
|
+
).toBeInTheDocument();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -7,24 +7,131 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import React, { type ComponentType, type CSSProperties, type ReactNode } from 'react';
|
|
10
11
|
import { DateFilterDynamicComponent } from '../../models/blocks/filter-form/fields/date-time/components/DateFilterDynamicComponent';
|
|
11
12
|
|
|
13
|
+
type OperatorMeta = {
|
|
14
|
+
value?: string;
|
|
15
|
+
schema?: {
|
|
16
|
+
'x-component'?: string;
|
|
17
|
+
'x-component-props'?: Record<string, unknown>;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type ComponentRegistry = {
|
|
22
|
+
getComponent?: (name: string) => ComponentType<Record<string, unknown>> | undefined;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type OperatorComponentFieldModel = {
|
|
26
|
+
props?: Record<string, unknown>;
|
|
27
|
+
render?: () => ReactNode;
|
|
28
|
+
__originalRender?: () => ReactNode;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type OperatorComponentRenderOptions = {
|
|
32
|
+
app?: ComponentRegistry;
|
|
33
|
+
fieldModel: unknown;
|
|
34
|
+
operator: string;
|
|
35
|
+
operators?: OperatorMeta[];
|
|
36
|
+
propsPriority?: 'field' | 'operator';
|
|
37
|
+
style?: CSSProperties;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
41
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function pickOperatorStyle(value: unknown): CSSProperties | undefined {
|
|
45
|
+
return isRecord(value) ? (value as CSSProperties) : undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getFieldModel(fieldModel: unknown) {
|
|
49
|
+
return fieldModel as OperatorComponentFieldModel & {
|
|
50
|
+
_reactiveWrapperCache?: unknown;
|
|
51
|
+
setupReactiveRender?: () => void;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function rewrapReactiveRender(fieldModel: unknown) {
|
|
56
|
+
const mutableFieldModel = getFieldModel(fieldModel);
|
|
57
|
+
mutableFieldModel._reactiveWrapperCache = undefined;
|
|
58
|
+
mutableFieldModel.setupReactiveRender?.();
|
|
59
|
+
}
|
|
60
|
+
|
|
12
61
|
/**
|
|
13
62
|
* 根据操作符的 schema 定位组件及其 props。
|
|
14
63
|
*/
|
|
15
|
-
export function resolveOperatorComponent(
|
|
64
|
+
export function resolveOperatorComponent(
|
|
65
|
+
app: ComponentRegistry | undefined,
|
|
66
|
+
operator: string,
|
|
67
|
+
operators?: OperatorMeta[],
|
|
68
|
+
) {
|
|
16
69
|
if (!operators || !Array.isArray(operators)) return null;
|
|
17
70
|
const op = operators.find((item) => item?.value === operator);
|
|
18
71
|
const schema = op?.schema;
|
|
19
72
|
const xComp = schema?.['x-component'];
|
|
20
73
|
if (!xComp) return null;
|
|
21
|
-
let Comp;
|
|
74
|
+
let Comp: ComponentType<Record<string, unknown>> | undefined;
|
|
22
75
|
if (xComp === 'DateFilterDynamicComponent') {
|
|
23
|
-
Comp = DateFilterDynamicComponent
|
|
76
|
+
Comp = DateFilterDynamicComponent as ComponentType<Record<string, unknown>>;
|
|
24
77
|
} else {
|
|
25
|
-
Comp = app?.getComponent?.(xComp
|
|
78
|
+
Comp = app?.getComponent?.(xComp);
|
|
26
79
|
}
|
|
27
80
|
if (!Comp) return null;
|
|
28
|
-
const props = (schema?.['x-component-props']
|
|
81
|
+
const props = isRecord(schema?.['x-component-props']) ? schema['x-component-props'] : {};
|
|
29
82
|
return { Comp, props };
|
|
30
83
|
}
|
|
84
|
+
|
|
85
|
+
export function restoreOperatorComponentRender(fieldModel?: unknown) {
|
|
86
|
+
const mutableFieldModel = getFieldModel(fieldModel);
|
|
87
|
+
if (!mutableFieldModel || typeof mutableFieldModel.__originalRender !== 'function') {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
mutableFieldModel.render = mutableFieldModel.__originalRender;
|
|
92
|
+
rewrapReactiveRender(mutableFieldModel);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function applyOperatorComponentRender({
|
|
97
|
+
app,
|
|
98
|
+
fieldModel,
|
|
99
|
+
operator,
|
|
100
|
+
operators,
|
|
101
|
+
propsPriority = 'field',
|
|
102
|
+
style,
|
|
103
|
+
}: OperatorComponentRenderOptions) {
|
|
104
|
+
const mutableFieldModel = getFieldModel(fieldModel);
|
|
105
|
+
const resolved = resolveOperatorComponent(app, operator, operators);
|
|
106
|
+
if (!resolved) {
|
|
107
|
+
restoreOperatorComponentRender(mutableFieldModel);
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!mutableFieldModel.__originalRender && typeof mutableFieldModel.render === 'function') {
|
|
112
|
+
mutableFieldModel.__originalRender = mutableFieldModel.render;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const { Comp, props: operatorProps } = resolved;
|
|
116
|
+
const operatorStyle = pickOperatorStyle(operatorProps.style);
|
|
117
|
+
|
|
118
|
+
mutableFieldModel.render = () => {
|
|
119
|
+
const fieldProps = mutableFieldModel.props || {};
|
|
120
|
+
const fieldStyle = pickOperatorStyle(fieldProps.style);
|
|
121
|
+
const componentProps =
|
|
122
|
+
propsPriority === 'operator' ? { ...fieldProps, ...operatorProps } : { ...operatorProps, ...fieldProps };
|
|
123
|
+
const componentStyle =
|
|
124
|
+
propsPriority === 'operator' ? { ...fieldStyle, ...operatorStyle } : { ...operatorStyle, ...fieldStyle };
|
|
125
|
+
const mergedStyle = { ...(style || {}), ...componentStyle };
|
|
126
|
+
|
|
127
|
+
return React.createElement(Comp, {
|
|
128
|
+
...componentProps,
|
|
129
|
+
...(Object.keys(mergedStyle).length > 0 ? { style: mergedStyle } : {}),
|
|
130
|
+
onCompositionStart: null,
|
|
131
|
+
onCompositionEnd: null,
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
rewrapReactiveRender(mutableFieldModel);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
@@ -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
|
},
|
|
@@ -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
|
})
|
|
@@ -541,19 +541,23 @@ export class CollectionBlockModel<T = DefaultStructure> extends DataBlockModel<T
|
|
|
541
541
|
|
|
542
542
|
protected defaultBlockTitle() {
|
|
543
543
|
const blockLabel = this.translate(this.constructor['meta']?.label || this.constructor.name);
|
|
544
|
-
const
|
|
544
|
+
const params = this.getResourceSettingsInitParams();
|
|
545
|
+
const dataSource = this.dataSource;
|
|
546
|
+
const collection = this.collection;
|
|
547
|
+
const dsName = dataSource?.displayName || dataSource?.key || params?.dataSourceKey;
|
|
545
548
|
const dsCount = this.context?.dataSourceManager?.getDataSources?.().length || 0;
|
|
546
|
-
const colTitle =
|
|
547
|
-
|
|
549
|
+
const colTitle = collection?.title || collection?.name || params?.collectionName || '';
|
|
550
|
+
const association = dataSource ? this.association : undefined;
|
|
551
|
+
if (association) {
|
|
548
552
|
const resourceName = this.resource.getResourceName();
|
|
549
|
-
const sourceCollection =
|
|
553
|
+
const sourceCollection = dataSource.getCollection(resourceName.split('.')[0]);
|
|
550
554
|
return createDefaultCollectionBlockTitle({
|
|
551
555
|
blockLabel,
|
|
552
556
|
dsName,
|
|
553
557
|
dsCount,
|
|
554
558
|
collectionTitle: colTitle,
|
|
555
|
-
sourceCollectionTitle: sourceCollection
|
|
556
|
-
associationTitle:
|
|
559
|
+
sourceCollectionTitle: sourceCollection?.title,
|
|
560
|
+
associationTitle: association.title,
|
|
557
561
|
});
|
|
558
562
|
}
|
|
559
563
|
return createDefaultCollectionBlockTitle({ blockLabel, dsName, dsCount, collectionTitle: colTitle });
|