@nocobase/client-v2 2.1.0-beta.46 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/client-v2",
3
- "version": "2.1.0-beta.46",
3
+ "version": "2.1.0-beta.47",
4
4
  "license": "Apache-2.0",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.mjs",
@@ -27,11 +27,11 @@
27
27
  "@formily/antd-v5": "1.2.3",
28
28
  "@formily/react": "^2.2.27",
29
29
  "@formily/shared": "^2.2.27",
30
- "@nocobase/evaluators": "2.1.0-beta.46",
31
- "@nocobase/flow-engine": "2.1.0-beta.46",
32
- "@nocobase/sdk": "2.1.0-beta.46",
33
- "@nocobase/shared": "2.1.0-beta.46",
34
- "@nocobase/utils": "2.1.0-beta.46",
30
+ "@nocobase/evaluators": "2.1.0-beta.47",
31
+ "@nocobase/flow-engine": "2.1.0-beta.47",
32
+ "@nocobase/sdk": "2.1.0-beta.47",
33
+ "@nocobase/shared": "2.1.0-beta.47",
34
+ "@nocobase/utils": "2.1.0-beta.47",
35
35
  "ahooks": "^3.7.2",
36
36
  "antd": "5.24.2",
37
37
  "antd-style": "3.7.1",
@@ -45,5 +45,5 @@
45
45
  "react-i18next": "^11.15.1",
46
46
  "react-router-dom": "^6.30.1"
47
47
  },
48
- "gitHead": "91fd3ab238a5ff58735bbfca04a8cb05d233b0af"
48
+ "gitHead": "bf8fc3790e3494c901b4d22861c5471a0d27c464"
49
49
  }
@@ -64,7 +64,7 @@ export function DrawerFormLayout(props: DrawerFormLayoutProps) {
64
64
  }, [props, view]);
65
65
 
66
66
  return (
67
- <div>
67
+ <div style={{ height: '100%', minHeight: 0, display: 'flex', flexDirection: 'column' }}>
68
68
  {view.Header ? <view.Header title={props.title} /> : null}
69
69
  {props.children}
70
70
  {view.Footer ? (
@@ -19,6 +19,8 @@ const identity = (s: string) => s;
19
19
  export interface CollectionFilterProps {
20
20
  /** Collection whose fields drive the filter row's field picker. */
21
21
  collection: Collection | undefined;
22
+ /** Previously compiled filter param used to seed the editable filter group. */
23
+ initialValue?: CompiledFilter;
22
24
  /** Called on Submit or Reset with the compiled NocoBase filter param (`undefined` when cleared). */
23
25
  onChange: (filter: CompiledFilter) => void;
24
26
  /** Translator. Defaults to identity. */
@@ -55,6 +57,7 @@ export interface CollectionFilterProps {
55
57
  export const CollectionFilter: FC<CollectionFilterProps> = (props) => {
56
58
  const {
57
59
  collection,
60
+ initialValue,
58
61
  onChange,
59
62
  t = identity,
60
63
  filterableFieldNames,
@@ -71,6 +74,7 @@ export const CollectionFilter: FC<CollectionFilterProps> = (props) => {
71
74
 
72
75
  const filterAction = useFilterActionProps({
73
76
  collection,
77
+ initialValue,
74
78
  filterableFieldNames,
75
79
  nonfilterableFieldNames,
76
80
  noIgnore,
@@ -0,0 +1,97 @@
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 { Collection } from '@nocobase/flow-engine';
11
+ import { Empty } from 'antd';
12
+ import React, { forwardRef, useImperativeHandle } from 'react';
13
+ import { FilterGroup } from '../../../flow/components/filter';
14
+ import {
15
+ compileFilterGroup,
16
+ type CompiledFilter,
17
+ type FilterApplyAction,
18
+ useFilterActionProps,
19
+ } from './useFilterActionProps';
20
+
21
+ const identity = (s: string) => s;
22
+
23
+ export interface CollectionFilterPanelRef {
24
+ getFilter: () => CompiledFilter;
25
+ reset: () => void;
26
+ }
27
+
28
+ export interface CollectionFilterPanelProps {
29
+ /** Collection whose fields drive the filter row's field picker. */
30
+ collection: Collection | undefined;
31
+ /** Previously compiled filter param used to seed the editable filter group. */
32
+ initialValue?: CompiledFilter;
33
+ /** Called when the condition group structure changes or `reset()` is invoked. */
34
+ onChange?: (filter: CompiledFilter) => void;
35
+ /** Translator. Defaults to identity. */
36
+ t?: (key: string, options?: Record<string, unknown>) => string;
37
+ /** Whitelist of root-level field names to expose. */
38
+ filterableFieldNames?: string[];
39
+ /**
40
+ * Blacklist of root-level field names to drop. Mirrors v1's `nonfilterable: [...]` on `Filter.Action`.
41
+ */
42
+ nonfilterableFieldNames?: string[];
43
+ /** Bypass the `filterableFieldNames` whitelist. */
44
+ noIgnore?: boolean;
45
+ }
46
+
47
+ /**
48
+ * Inline collection filter editor for forms and drawers. Unlike `CollectionFilter`, this does not render a trigger button, popover, or nested `<form>`, so it can safely live inside an antd form and be submitted by the outer drawer action.
49
+ */
50
+ export const CollectionFilterPanel = forwardRef<CollectionFilterPanelRef, CollectionFilterPanelProps>((props, ref) => {
51
+ const {
52
+ collection,
53
+ initialValue,
54
+ onChange,
55
+ t = identity,
56
+ filterableFieldNames,
57
+ nonfilterableFieldNames,
58
+ noIgnore,
59
+ } = props;
60
+
61
+ const filterAction = useFilterActionProps({
62
+ collection,
63
+ initialValue,
64
+ filterableFieldNames,
65
+ nonfilterableFieldNames,
66
+ noIgnore,
67
+ t,
68
+ onApply: (filter: CompiledFilter, _action: FilterApplyAction) => {
69
+ onChange?.(filter);
70
+ },
71
+ });
72
+
73
+ useImperativeHandle(
74
+ ref,
75
+ () => ({
76
+ getFilter: () => compileFilterGroup(filterAction.value),
77
+ reset: filterAction.onReset,
78
+ }),
79
+ [filterAction.onReset, filterAction.value],
80
+ );
81
+
82
+ if (!collection || !filterAction.FilterItem) {
83
+ return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('No data')} />;
84
+ }
85
+
86
+ return (
87
+ <FilterGroup
88
+ value={filterAction.value}
89
+ FilterItem={filterAction.FilterItem}
90
+ onChange={() => onChange?.(compileFilterGroup(filterAction.value))}
91
+ />
92
+ );
93
+ });
94
+
95
+ CollectionFilterPanel.displayName = 'CollectionFilterPanel';
96
+
97
+ export default CollectionFilterPanel;
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { describe, expect, it } from 'vitest';
11
- import { compileFilterGroup } from '../useFilterActionProps';
11
+ import { compileFilterGroup, decompileFilterGroup } from '../useFilterActionProps';
12
12
 
13
13
  describe('compileFilterGroup', () => {
14
14
  it('returns undefined for an empty group so callers can drop the filter param', () => {
@@ -144,3 +144,78 @@ describe('compileFilterGroup', () => {
144
144
  });
145
145
  });
146
146
  });
147
+
148
+ describe('decompileFilterGroup', () => {
149
+ it('returns undefined for an empty compiled filter', () => {
150
+ expect(decompileFilterGroup(undefined)).toBeUndefined();
151
+ expect(decompileFilterGroup({})).toBeUndefined();
152
+ });
153
+
154
+ it('restores a compiled condition into an editable filter row', () => {
155
+ expect(decompileFilterGroup({ $and: [{ lockReason: { $includes: 'abuse' } }] })).toEqual({
156
+ logic: '$and',
157
+ items: [{ path: 'lockReason', operator: '$includes', value: 'abuse' }],
158
+ });
159
+ });
160
+
161
+ it('restores a direct condition object into an editable filter row', () => {
162
+ expect(decompileFilterGroup({ title: { $includes: 'test' } })).toEqual({
163
+ logic: '$and',
164
+ items: [{ path: 'title', operator: '$includes', value: 'test' }],
165
+ });
166
+ });
167
+
168
+ it('restores legacy direct field values as equality rows', () => {
169
+ expect(decompileFilterGroup({ createdById: '{{ ctx.state.currentUser.id }}' })).toEqual({
170
+ logic: '$and',
171
+ items: [{ path: 'createdById', operator: '$eq', value: '{{ ctx.state.currentUser.id }}' }],
172
+ });
173
+ });
174
+
175
+ it('restores multiple direct condition fields', () => {
176
+ expect(
177
+ decompileFilterGroup({
178
+ title: { $includes: 'test' },
179
+ status: 'published',
180
+ }),
181
+ ).toEqual({
182
+ logic: '$and',
183
+ items: [
184
+ { path: 'title', operator: '$includes', value: 'test' },
185
+ { path: 'status', operator: '$eq', value: 'published' },
186
+ ],
187
+ });
188
+ });
189
+
190
+ it('restores nested association paths', () => {
191
+ expect(decompileFilterGroup({ $and: [{ user: { createdBy: { password: { $includes: '123' } } } }] })).toEqual({
192
+ logic: '$and',
193
+ items: [{ path: 'user.createdBy.password', operator: '$includes', value: '123' }],
194
+ });
195
+ });
196
+
197
+ it('restores nested filter groups', () => {
198
+ expect(
199
+ decompileFilterGroup({
200
+ $and: [
201
+ { lockReason: { $eq: 'abuse' } },
202
+ {
203
+ $or: [{ lockedTs: { $dateAfter: '2026-01-01' } }, { lockedTs: { $dateBefore: '2026-12-31' } }],
204
+ },
205
+ ],
206
+ }),
207
+ ).toEqual({
208
+ logic: '$and',
209
+ items: [
210
+ { path: 'lockReason', operator: '$eq', value: 'abuse' },
211
+ {
212
+ logic: '$or',
213
+ items: [
214
+ { path: 'lockedTs', operator: '$dateAfter', value: '2026-01-01' },
215
+ { path: 'lockedTs', operator: '$dateBefore', value: '2026-12-31' },
216
+ ],
217
+ },
218
+ ],
219
+ });
220
+ });
221
+ });
@@ -10,4 +10,6 @@
10
10
  // Higher-level filter compositions for non-schema surfaces (settings pages, panels, side drawers). The low-level primitives — `FilterContainer`, `FilterGroup`, `FilterItem`, `fieldsToOptions`, `useFilterOptions` — live under `src/flow/components/filter/`; this layer composes them with a `Collection` binding and exposes the hook/component pair callers actually reach for. The dependency direction is form/filter → flow/components/filter only.
11
11
  export { CollectionFilter } from './CollectionFilter';
12
12
  export type { CollectionFilterProps } from './CollectionFilter';
13
+ export { CollectionFilterPanel } from './CollectionFilterPanel';
14
+ export type { CollectionFilterPanelProps, CollectionFilterPanelRef } from './CollectionFilterPanel';
13
15
  export type { CompiledFilter } from './useFilterActionProps';
@@ -68,6 +68,68 @@ const nestPath = (path: string, leaf: unknown): Record<string, unknown> => {
68
68
  return result as Record<string, unknown>;
69
69
  };
70
70
 
71
+ const decompileConditions = (value: unknown, path: string[] = []): CollectionFilterItemValue[] => {
72
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
73
+ return path.length ? [{ path: path.join('.'), operator: '$eq', value }] : [];
74
+ }
75
+
76
+ const entries = Object.entries(value as Record<string, unknown>);
77
+ const operatorEntries = entries.filter(([key]) => key.startsWith('$'));
78
+ if (operatorEntries.length) {
79
+ if (!path.length) {
80
+ return [];
81
+ }
82
+ return operatorEntries.map(([operator, operatorValue]) => ({
83
+ path: path.join('.'),
84
+ operator,
85
+ value: operatorValue,
86
+ }));
87
+ }
88
+
89
+ return entries.flatMap(([fieldName, nextValue]) => decompileConditions(nextValue, [...path, fieldName]));
90
+ };
91
+
92
+ const getGroupLogic = (record: Record<string, unknown>): FilterGroupValue['logic'] | undefined => {
93
+ if (Array.isArray(record.$or)) {
94
+ return '$or';
95
+ }
96
+ if (Array.isArray(record.$and)) {
97
+ return '$and';
98
+ }
99
+ return undefined;
100
+ };
101
+
102
+ const decompileFilterItem = (item: unknown): FilterGroupItem[] => {
103
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
104
+ return [];
105
+ }
106
+ const record = item as Record<string, unknown>;
107
+ if (getGroupLogic(record)) {
108
+ const group = decompileFilterGroup(record);
109
+ return group ? [group] : [];
110
+ }
111
+ return decompileConditions(record);
112
+ };
113
+
114
+ export function decompileFilterGroup(filter: CompiledFilter): FilterGroupValue | undefined {
115
+ if (!filter || typeof filter !== 'object' || Array.isArray(filter)) {
116
+ return undefined;
117
+ }
118
+
119
+ const record = filter as Record<string, unknown>;
120
+ const logic = getGroupLogic(record);
121
+ if (!logic) {
122
+ const items = decompileConditions(record);
123
+ return items.length ? { logic: '$and', items } : undefined;
124
+ }
125
+ const sourceItems = record[logic] as unknown[];
126
+ const items = sourceItems
127
+ .flatMap((item) => decompileFilterItem(item))
128
+ .filter((item): item is FilterGroupItem => Boolean(item));
129
+
130
+ return items.length ? { logic, items } : undefined;
131
+ }
132
+
71
133
  /**
72
134
  * Compile a reactive filter group into the `{ $and: [{ path: { op: val } }] }` envelope accepted by NocoBase's resource `list` filter param. Returns `undefined` when the group is empty so callers can drop the param.
73
135
  *
@@ -100,6 +162,8 @@ export type FilterApplyAction = 'submit' | 'reset';
100
162
  export interface UseFilterActionPropsArgs extends UseFilterOptionsArgs {
101
163
  /** Collection whose fields populate the filter row's field picker. */
102
164
  collection: Collection | undefined;
165
+ /** Previously compiled filter param used to seed the editable filter group. */
166
+ initialValue?: CompiledFilter;
103
167
  /**
104
168
  * Called when the user submits or resets the filter popover. Receives the compiled filter param (`undefined` when cleared) and which footer button triggered the call. Typical implementation: `(filter, action) => { listRequest.run(filter); if (action === 'submit') closePopover(); }`.
105
169
  */
@@ -152,12 +216,12 @@ export interface UseFilterActionPropsResult {
152
216
  * ```
153
217
  */
154
218
  export function useFilterActionProps(args: UseFilterActionPropsArgs): UseFilterActionPropsResult {
155
- const { collection, onApply, filterableFieldNames, nonfilterableFieldNames, noIgnore, t } = args;
219
+ const { collection, initialValue, onApply, filterableFieldNames, nonfilterableFieldNames, noIgnore, t } = args;
156
220
 
157
221
  // Held in a ref so the group object identity is stable for the lifetime of the host component — `<FilterContent>` mutates this object directly (push/splice on `items`, swap `logic`), and a fresh observable on every render would reset that internal state.
158
222
  const valueRef = useRef<FilterGroupValue>();
159
223
  if (!valueRef.current) {
160
- valueRef.current = observable(createEmptyGroup()) as FilterGroupValue;
224
+ valueRef.current = observable(decompileFilterGroup(initialValue) || createEmptyGroup()) as FilterGroupValue;
161
225
  }
162
226
  const value = valueRef.current;
163
227
 
@@ -46,7 +46,7 @@ import { useAssociationTitleFieldSync } from '../components/useAssociationTitleF
46
46
  import _ from 'lodash';
47
47
  import { SubFormFieldModel, SubFormListFieldModel } from '../models';
48
48
  import { coerceForToOneField } from '../internal/utils/associationValueCoercion';
49
- import { enumToOptions } from '../internal/utils/enumOptionsUtils';
49
+ import { enumToOptions, translateOptionLabel } from '../internal/utils/enumOptionsUtils';
50
50
  import {
51
51
  findFormItemModelByFieldPath,
52
52
  getCollectionFromModel,
@@ -156,7 +156,7 @@ const getFieldModelOptions = (fieldModel: any, t: (s: string) => string) => {
156
156
  if (originalOptions) {
157
157
  return originalOptions.map((option: any) =>
158
158
  option && typeof option === 'object' && typeof option.label === 'string'
159
- ? { ...option, label: t(option.label) }
159
+ ? { ...option, label: translateOptionLabel(option.label, t) }
160
160
  : option,
161
161
  );
162
162
  }
@@ -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);
@@ -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
+ });