@nocobase/client-v2 2.1.0-beta.45 → 2.1.0-beta.47
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/components/form/filter/CollectionFilter.d.ts +2 -0
- package/es/components/form/filter/CollectionFilterPanel.d.ts +38 -0
- package/es/components/form/filter/index.d.ts +2 -0
- package/es/components/form/filter/useFilterActionProps.d.ts +3 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +1 -0
- package/es/flow/internal/utils/enumOptionsUtils.d.ts +2 -0
- package/es/flow/models/fields/DisplayJSONFieldModel.d.ts +2 -1
- package/es/index.mjs +49 -49
- package/lib/index.js +34 -34
- package/package.json +7 -7
- package/src/__tests__/browserChecker.test.ts +9 -0
- package/src/components/form/DrawerFormLayout.tsx +1 -1
- package/src/components/form/filter/CollectionFilter.tsx +4 -0
- package/src/components/form/filter/CollectionFilterPanel.tsx +97 -0
- package/src/components/form/filter/__tests__/compileFilterGroup.test.ts +76 -1
- package/src/components/form/filter/index.ts +2 -0
- package/src/components/form/filter/useFilterActionProps.ts +66 -2
- package/src/flow/actions/__tests__/sortingRules.test.ts +22 -0
- package/src/flow/actions/linkageRules.tsx +2 -2
- package/src/flow/actions/sortingRules.tsx +3 -4
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +1 -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/__tests__/enumOptionsUtils.test.ts +10 -9
- package/src/flow/internal/utils/enumOptionsUtils.ts +11 -3
- package/src/flow/models/base/CollectionBlockModel.tsx +10 -6
- package/src/flow/models/base/__tests__/CollectionBlockModel.title.test.ts +33 -1
- package/src/flow/models/fields/DisplayEnumFieldModel.tsx +2 -1
- package/src/flow/models/fields/DisplayJSONFieldModel.tsx +15 -4
- package/src/flow/models/fields/SelectFieldModel.tsx +3 -3
- package/src/flow/models/fields/__tests__/DisplayJSONFieldModel.test.tsx +36 -0
|
@@ -24,27 +24,28 @@ function mockT(input: string) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
describe('enumOptions utils', () => {
|
|
27
|
-
it('enumToOptions: converts primitive enum and translates labels', () => {
|
|
27
|
+
it('enumToOptions: converts primitive enum and translates explicit template labels', () => {
|
|
28
28
|
const uiEnum = ['{{t("Yes")}}', '{{t("No")}}'];
|
|
29
|
-
const options = enumToOptions(uiEnum as any, mockT)
|
|
30
|
-
expect(options).
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
const options = enumToOptions(uiEnum as any, mockT);
|
|
30
|
+
expect(options).toEqual([
|
|
31
|
+
{ label: '是', value: '{{t("Yes")}}' },
|
|
32
|
+
{ label: '否', value: '{{t("No")}}' },
|
|
33
|
+
]);
|
|
33
34
|
});
|
|
34
35
|
|
|
35
|
-
it('enumToOptions: keeps object value and translates object label', () => {
|
|
36
|
+
it('enumToOptions: keeps object value and translates explicit template object label', () => {
|
|
36
37
|
const uiEnum = [
|
|
37
38
|
{ label: '{{t("Yes")}}', value: true },
|
|
38
39
|
{ label: '{{t("No")}}', value: false },
|
|
39
40
|
];
|
|
40
|
-
const options = enumToOptions(uiEnum as any, mockT)
|
|
41
|
+
const options = enumToOptions(uiEnum as any, mockT);
|
|
41
42
|
expect(options).toEqual([
|
|
42
43
|
{ label: '是', value: true },
|
|
43
44
|
{ label: '否', value: false },
|
|
44
45
|
]);
|
|
45
46
|
});
|
|
46
47
|
|
|
47
|
-
it('translateOptions: maps
|
|
48
|
+
it('translateOptions: maps only explicit template labels through t()', () => {
|
|
48
49
|
const options = [
|
|
49
50
|
{ label: '{{t("Draft")}}', value: 'draft' },
|
|
50
51
|
{ label: 'No', value: false },
|
|
@@ -52,7 +53,7 @@ describe('enumOptions utils', () => {
|
|
|
52
53
|
const out = translateOptions(options as any, mockT);
|
|
53
54
|
expect(out).toEqual([
|
|
54
55
|
{ label: '草稿', value: 'draft' },
|
|
55
|
-
{ label: '
|
|
56
|
+
{ label: 'No', value: false },
|
|
56
57
|
]);
|
|
57
58
|
});
|
|
58
59
|
|
|
@@ -33,13 +33,21 @@ function normalizeUiSchemaEnumToOptions(uiEnum: UiSchemaEnumItem[] = []): Option
|
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
export function isTranslationTemplate(value: unknown): value is string {
|
|
37
|
+
return typeof value === 'string' && /\{\{\s*t\s*\(/.test(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function translateOptionLabel(label: any, t: (s: string) => string): any {
|
|
41
|
+
return isTranslationTemplate(label) ? t(label) : label;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Map option labels through t() only when labels explicitly use {{t(...)}} templates.
|
|
37
45
|
export function translateOptions(options: Option[] = [], t: (s: string) => string): Option[] {
|
|
38
46
|
if (!Array.isArray(options)) return [] as Option[];
|
|
39
|
-
return options.map((opt) => (
|
|
47
|
+
return options.map((opt) => ({ ...opt, label: translateOptionLabel(opt?.label, t) }));
|
|
40
48
|
}
|
|
41
49
|
|
|
42
|
-
// Build options from uiSchema.enum with translated labels
|
|
50
|
+
// Build options from uiSchema.enum with explicitly translated labels
|
|
43
51
|
export function enumToOptions(uiEnum: UiSchemaEnumItem[] | undefined, t: (s: string) => string): Option[] | undefined {
|
|
44
52
|
if (!uiEnum || !Array.isArray(uiEnum) || uiEnum.length === 0) return undefined;
|
|
45
53
|
const normalized = normalizeUiSchemaEnumToOptions(uiEnum);
|
|
@@ -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 });
|
|
@@ -7,8 +7,17 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { DataSource, FlowEngine } from '@nocobase/flow-engine';
|
|
10
|
+
import { DataSource, FlowEngine, MultiRecordResource } from '@nocobase/flow-engine';
|
|
11
11
|
import { createDefaultCollectionBlockTitle } from '../../../utils/blockUtils';
|
|
12
|
+
import { CollectionBlockModel } from '../CollectionBlockModel';
|
|
13
|
+
|
|
14
|
+
class MissingCollectionTitleBlockModel extends CollectionBlockModel {
|
|
15
|
+
static meta = { label: 'Table' };
|
|
16
|
+
|
|
17
|
+
createResource(ctx: any, _params: any) {
|
|
18
|
+
return ctx.createResource(MultiRecordResource);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
12
21
|
|
|
13
22
|
function normalizeTitle(s: string) {
|
|
14
23
|
return (s || '').replace(/\s+/g, ' ').trim();
|
|
@@ -92,4 +101,27 @@ describe('CollectionBlockModel default title rule', () => {
|
|
|
92
101
|
expect(t).toContain('Table:');
|
|
93
102
|
expect(t).toContain('Main > Users > Tags (Tags)');
|
|
94
103
|
});
|
|
104
|
+
|
|
105
|
+
it('uses resource settings as title fallback when the configured data source is unavailable', () => {
|
|
106
|
+
const engine = new FlowEngine();
|
|
107
|
+
engine.registerModels({ MissingCollectionTitleBlockModel });
|
|
108
|
+
const model = engine.createModel<MissingCollectionTitleBlockModel>({
|
|
109
|
+
uid: 'missing-data-source-title-block',
|
|
110
|
+
use: 'MissingCollectionTitleBlockModel',
|
|
111
|
+
stepParams: {
|
|
112
|
+
resourceSettings: {
|
|
113
|
+
init: {
|
|
114
|
+
dataSourceKey: 'mysql',
|
|
115
|
+
collectionName: 'orders',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const title = normalizeTitle(model.title);
|
|
122
|
+
|
|
123
|
+
expect(title).toContain('Table:');
|
|
124
|
+
expect(title).toContain('orders');
|
|
125
|
+
expect(title).not.toContain('undefined');
|
|
126
|
+
});
|
|
95
127
|
});
|
|
@@ -13,6 +13,7 @@ import { Tag } from 'antd';
|
|
|
13
13
|
import React from 'react';
|
|
14
14
|
import { castArray } from 'lodash';
|
|
15
15
|
import { ClickableFieldModel } from './ClickableFieldModel';
|
|
16
|
+
import { translateOptionLabel } from '../../internal/utils/enumOptionsUtils';
|
|
16
17
|
|
|
17
18
|
interface FieldNames {
|
|
18
19
|
value: string;
|
|
@@ -70,7 +71,7 @@ export class DisplayEnumFieldModel extends ClickableFieldModel {
|
|
|
70
71
|
}
|
|
71
72
|
return currentOptions.map((option) => (
|
|
72
73
|
<Tag key={option[fieldNames.value]} color={option[fieldNames.color]} icon={<Icon type={option['icon']} />}>
|
|
73
|
-
{
|
|
74
|
+
{translateOptionLabel(option[fieldNames.label], this.translate)}
|
|
74
75
|
</Tag>
|
|
75
76
|
));
|
|
76
77
|
}
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import { css, cx } from '@emotion/css';
|
|
11
11
|
import { DisplayItemModel } from '@nocobase/flow-engine';
|
|
12
|
-
import React, { useRef, useState
|
|
12
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
13
|
+
import type { CSSProperties } from 'react';
|
|
13
14
|
import { Tooltip } from 'antd';
|
|
14
15
|
import { DisplayTitleFieldModel } from './DisplayTitleFieldModel';
|
|
15
16
|
|
|
@@ -21,8 +22,14 @@ const JSONClassName = css`
|
|
|
21
22
|
border: none !important;
|
|
22
23
|
`;
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
interface EllipsisJSONProps {
|
|
26
|
+
content: string;
|
|
27
|
+
style?: CSSProperties;
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const EllipsisJSON = ({ content, style, className }: EllipsisJSONProps) => {
|
|
32
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
26
33
|
const [overflow, setOverflow] = useState(false);
|
|
27
34
|
|
|
28
35
|
useEffect(() => {
|
|
@@ -75,7 +82,7 @@ const EllipsisJSON = ({ content, style, className }) => {
|
|
|
75
82
|
};
|
|
76
83
|
|
|
77
84
|
export class DisplayJSONFieldModel extends DisplayTitleFieldModel {
|
|
78
|
-
public renderComponent(value) {
|
|
85
|
+
public renderComponent(value: unknown) {
|
|
79
86
|
const { space, style, className, overflowMode } = this.props;
|
|
80
87
|
let content = '';
|
|
81
88
|
if (value !== undefined && value !== null) {
|
|
@@ -96,6 +103,10 @@ export class DisplayJSONFieldModel extends DisplayTitleFieldModel {
|
|
|
96
103
|
</pre>
|
|
97
104
|
);
|
|
98
105
|
}
|
|
106
|
+
|
|
107
|
+
public render() {
|
|
108
|
+
return this.renderComponent(this.props.value);
|
|
109
|
+
}
|
|
99
110
|
}
|
|
100
111
|
|
|
101
112
|
DisplayItemModel.bindModelToInterface('DisplayJSONFieldModel', ['json'], {
|
|
@@ -12,7 +12,7 @@ import { Select, Tag, Tooltip } from 'antd';
|
|
|
12
12
|
import React from 'react';
|
|
13
13
|
import { FieldModel } from '../base/FieldModel';
|
|
14
14
|
import { MobileSelect } from './mobile-components/MobileSelect';
|
|
15
|
-
import { enumToOptions, getSelectedEnumLabels } from '../../internal/utils/enumOptionsUtils';
|
|
15
|
+
import { enumToOptions, getSelectedEnumLabels, translateOptionLabel } from '../../internal/utils/enumOptionsUtils';
|
|
16
16
|
|
|
17
17
|
const getOriginalEnumOptions = (model: SelectFieldModel) => {
|
|
18
18
|
const fromEnum = enumToOptions(model.context.collectionField?.uiSchema?.enum, (text) => text) || [];
|
|
@@ -29,12 +29,12 @@ export class SelectFieldModel extends FieldModel {
|
|
|
29
29
|
const options = this.props.options?.map((v) => {
|
|
30
30
|
return {
|
|
31
31
|
...v,
|
|
32
|
-
label:
|
|
32
|
+
label: translateOptionLabel(v.label, this.translate),
|
|
33
33
|
};
|
|
34
34
|
});
|
|
35
35
|
const selectedLabels = getSelectedEnumLabels(this.props.value, fallbackOptions).map((item) => ({
|
|
36
36
|
...item,
|
|
37
|
-
label:
|
|
37
|
+
label: translateOptionLabel(item.label, this.translate),
|
|
38
38
|
}));
|
|
39
39
|
const value = Array.isArray(this.props.value)
|
|
40
40
|
? selectedLabels
|
|
@@ -0,0 +1,36 @@
|
|
|
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 } from '@nocobase/flow-engine';
|
|
11
|
+
import { render } from '@testing-library/react';
|
|
12
|
+
import { describe, expect, it } from 'vitest';
|
|
13
|
+
import { DisplayJSONFieldModel } from '../DisplayJSONFieldModel';
|
|
14
|
+
|
|
15
|
+
describe('DisplayJSONFieldModel', () => {
|
|
16
|
+
it('renders object field values as formatted JSON', () => {
|
|
17
|
+
const engine = new FlowEngine();
|
|
18
|
+
engine.registerModels({ DisplayJSONFieldModel });
|
|
19
|
+
|
|
20
|
+
const model = engine.createModel<DisplayJSONFieldModel>({
|
|
21
|
+
use: DisplayJSONFieldModel,
|
|
22
|
+
uid: 'display-json-object-value',
|
|
23
|
+
props: {
|
|
24
|
+
value: {
|
|
25
|
+
enabled: true,
|
|
26
|
+
name: 'NocoBase',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const { container } = render(model.render());
|
|
32
|
+
|
|
33
|
+
expect(container.textContent).toContain('"enabled": true');
|
|
34
|
+
expect(container.textContent).toContain('"name": "NocoBase"');
|
|
35
|
+
});
|
|
36
|
+
});
|