@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.
Files changed (31) hide show
  1. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  2. package/es/components/form/filter/CollectionFilterPanel.d.ts +38 -0
  3. package/es/components/form/filter/index.d.ts +2 -0
  4. package/es/components/form/filter/useFilterActionProps.d.ts +3 -0
  5. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +1 -0
  6. package/es/flow/internal/utils/enumOptionsUtils.d.ts +2 -0
  7. package/es/flow/models/fields/DisplayJSONFieldModel.d.ts +2 -1
  8. package/es/index.mjs +49 -49
  9. package/lib/index.js +34 -34
  10. package/package.json +7 -7
  11. package/src/__tests__/browserChecker.test.ts +9 -0
  12. package/src/components/form/DrawerFormLayout.tsx +1 -1
  13. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  14. package/src/components/form/filter/CollectionFilterPanel.tsx +97 -0
  15. package/src/components/form/filter/__tests__/compileFilterGroup.test.ts +76 -1
  16. package/src/components/form/filter/index.ts +2 -0
  17. package/src/components/form/filter/useFilterActionProps.ts +66 -2
  18. package/src/flow/actions/__tests__/sortingRules.test.ts +22 -0
  19. package/src/flow/actions/linkageRules.tsx +2 -2
  20. package/src/flow/actions/sortingRules.tsx +3 -4
  21. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +1 -0
  22. package/src/flow/components/placeholders/BlockPlaceholder.tsx +76 -15
  23. package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +105 -0
  24. package/src/flow/internal/utils/__tests__/enumOptionsUtils.test.ts +10 -9
  25. package/src/flow/internal/utils/enumOptionsUtils.ts +11 -3
  26. package/src/flow/models/base/CollectionBlockModel.tsx +10 -6
  27. package/src/flow/models/base/__tests__/CollectionBlockModel.title.test.ts +33 -1
  28. package/src/flow/models/fields/DisplayEnumFieldModel.tsx +2 -1
  29. package/src/flow/models/fields/DisplayJSONFieldModel.tsx +15 -4
  30. package/src/flow/models/fields/SelectFieldModel.tsx +3 -3
  31. 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).toHaveLength(2);
31
- expect(options[0]).toEqual({ label: '是', value: '{{t("Yes")}}' });
32
- expect(options[1]).toEqual({ label: '否', value: '{{t("No")}}' });
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 given options labels through t()', () => {
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: '', value: false },
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
- // Map option labels through t() (supports {{t('key')}} template and plain strings)
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) => (typeof opt?.label === 'string' ? { ...opt, label: t(opt.label) } : 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 dsName = this.dataSource?.displayName || this.dataSource?.key;
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 = this.collection?.title;
547
- if (this.association) {
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 = this.dataSource.getCollection(resourceName.split('.')[0]);
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.title,
556
- associationTitle: this.association.title,
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
- {this.translate(option[fieldNames.label])}
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, useEffect } from 'react';
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
- const EllipsisJSON = ({ content, style, className }) => {
25
- const ref = useRef(null);
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: this.translate(v.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: this.translate(item.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
+ });