@nocobase/client-v2 2.1.0-beta.46 → 2.1.0-beta.48
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/ScanInput/CodeScanner.d.ts +18 -0
- package/es/components/form/ScanInput/ScanBox.d.ts +12 -0
- package/es/components/form/ScanInput/ScanInput.d.ts +18 -0
- package/es/components/form/ScanInput/index.d.ts +11 -0
- package/es/components/form/ScanInput/types.d.ts +10 -0
- package/es/components/form/ScanInput/useCodeScanner.d.ts +31 -0
- 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/components/form/index.d.ts +1 -0
- package/es/flow/internal/utils/enumOptionsUtils.d.ts +2 -0
- package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +14 -0
- package/es/flow/models/fields/DisplayJSONFieldModel.d.ts +2 -1
- package/es/index.mjs +101 -61
- package/lib/index.js +112 -72
- package/package.json +8 -7
- package/src/components/form/DrawerFormLayout.tsx +1 -1
- package/src/components/form/ScanInput/CodeScanner.tsx +219 -0
- package/src/components/form/ScanInput/ScanBox.tsx +30 -0
- package/src/components/form/ScanInput/ScanInput.tsx +150 -0
- package/src/components/form/ScanInput/__tests__/ScanInput.test.tsx +119 -0
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +123 -0
- package/src/components/form/ScanInput/index.ts +12 -0
- package/src/components/form/ScanInput/types.ts +12 -0
- package/src/components/form/ScanInput/useCodeScanner.ts +136 -0
- 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/components/form/index.tsx +1 -0
- package/src/flow/actions/__tests__/actionLinkageRules.race.repro.test.ts +45 -0
- package/src/flow/actions/linkageRules.tsx +18 -6
- package/src/flow/internal/utils/__tests__/enumOptionsUtils.test.ts +10 -9
- package/src/flow/internal/utils/enumOptionsUtils.ts +11 -3
- package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +111 -26
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +61 -0
- package/src/flow/models/fields/DisplayEnumFieldModel.tsx +2 -1
- package/src/flow/models/fields/DisplayJSONFieldModel.tsx +15 -4
- package/src/flow/models/fields/InputFieldModel.tsx +56 -1
- package/src/flow/models/fields/SelectFieldModel.tsx +3 -3
- package/src/flow/models/fields/__tests__/DisplayJSONFieldModel.test.tsx +36 -0
- package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +116 -0
|
@@ -31,7 +31,90 @@ import {
|
|
|
31
31
|
createRootItemChain,
|
|
32
32
|
type ItemChain,
|
|
33
33
|
} from './itemChain';
|
|
34
|
-
import { buildOpenerUids, LabelByField } from './recordSelectShared';
|
|
34
|
+
import { buildOpenerUids, LabelByField, type AssociationFieldNames } from './recordSelectShared';
|
|
35
|
+
|
|
36
|
+
const MULTIPLE_ASSOCIATION_TYPES = ['belongsToMany', 'hasMany', 'belongsToArray'];
|
|
37
|
+
|
|
38
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
39
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function canRecordPickerSelectMultiple(
|
|
43
|
+
collectionField: { type?: string } | null | undefined,
|
|
44
|
+
allowMultiple?: boolean,
|
|
45
|
+
) {
|
|
46
|
+
return (
|
|
47
|
+
!!collectionField?.type && MULTIPLE_ASSOCIATION_TYPES.includes(collectionField.type) && allowMultiple !== false
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function shouldClearRecordPickerValueOnMultipleChange(
|
|
52
|
+
collectionField: { type?: string } | null | undefined,
|
|
53
|
+
previousAllowMultiple?: boolean,
|
|
54
|
+
nextAllowMultiple?: boolean,
|
|
55
|
+
) {
|
|
56
|
+
return (
|
|
57
|
+
canRecordPickerSelectMultiple(collectionField, previousAllowMultiple) !==
|
|
58
|
+
canRecordPickerSelectMultiple(collectionField, nextAllowMultiple)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function normalizeRecordPickerSelectedRows(value: unknown, allowMultiple: boolean) {
|
|
63
|
+
if (!value) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
if (allowMultiple) {
|
|
67
|
+
return Array.isArray(value) ? value : [value];
|
|
68
|
+
}
|
|
69
|
+
return Array.isArray(value) ? value.slice(0, 1) : [value];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getRecordPickerEmptyValue(allowMultiple: boolean) {
|
|
73
|
+
return allowMultiple ? [] : undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getRecordPickerClearedValue() {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function normalizeRecordPickerValue(value: unknown, fieldNames: AssociationFieldNames, allowMultiple: boolean) {
|
|
81
|
+
if (!value) {
|
|
82
|
+
return getRecordPickerEmptyValue(allowMultiple);
|
|
83
|
+
}
|
|
84
|
+
const toSelectItem = (item: Record<string, unknown>) => ({
|
|
85
|
+
...item,
|
|
86
|
+
label: item[fieldNames.label],
|
|
87
|
+
value: item[fieldNames.value],
|
|
88
|
+
});
|
|
89
|
+
if (!allowMultiple) {
|
|
90
|
+
const item = Array.isArray(value) ? value[0] : value;
|
|
91
|
+
return isRecord(item) ? toSelectItem(item) : undefined;
|
|
92
|
+
}
|
|
93
|
+
if (!Array.isArray(value)) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
return value.filter(isRecord).map(toSelectItem);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function applyRecordPickerAllowMultiple(ctx: any, params: any, previousParams?: any) {
|
|
100
|
+
const shouldClearValue =
|
|
101
|
+
previousParams &&
|
|
102
|
+
shouldClearRecordPickerValueOnMultipleChange(
|
|
103
|
+
ctx.collectionField,
|
|
104
|
+
previousParams?.allowMultiple,
|
|
105
|
+
params?.allowMultiple,
|
|
106
|
+
);
|
|
107
|
+
const emptyValue = getRecordPickerClearedValue();
|
|
108
|
+
|
|
109
|
+
ctx.model.setProps({
|
|
110
|
+
allowMultiple: params?.allowMultiple,
|
|
111
|
+
...(shouldClearValue ? { value: emptyValue } : {}),
|
|
112
|
+
});
|
|
113
|
+
if (shouldClearValue) {
|
|
114
|
+
ctx.model.selectedRows.value = emptyValue;
|
|
115
|
+
ctx.model.props.onChange?.(emptyValue);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
35
118
|
|
|
36
119
|
export function buildRecordPickerParentItemContext(ctx: any): {
|
|
37
120
|
parentItem: ItemChain;
|
|
@@ -241,25 +324,10 @@ export function RecordPickerContent({ model, toOne = false }) {
|
|
|
241
324
|
function RecordPickerField(props) {
|
|
242
325
|
const { fieldNames, onClick, disabled } = props;
|
|
243
326
|
const ctx = useFlowContext();
|
|
244
|
-
const
|
|
327
|
+
const allowMultiple = canRecordPickerSelectMultiple(ctx.collectionField, props.allowMultiple);
|
|
245
328
|
useEffect(() => {
|
|
246
329
|
ctx.model.selectedRows.value = props.value;
|
|
247
|
-
}, [props.value]);
|
|
248
|
-
const normalizeValue = (value, fieldNames, toOne) => {
|
|
249
|
-
if (!value) return toOne ? undefined : [];
|
|
250
|
-
if (toOne) {
|
|
251
|
-
return {
|
|
252
|
-
...value,
|
|
253
|
-
label: value[fieldNames.label],
|
|
254
|
-
value: value[fieldNames.value],
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
return value.map((v) => ({
|
|
258
|
-
...v,
|
|
259
|
-
label: v[fieldNames.label],
|
|
260
|
-
value: v[fieldNames.value],
|
|
261
|
-
}));
|
|
262
|
-
};
|
|
330
|
+
}, [ctx.model.selectedRows, props.value]);
|
|
263
331
|
|
|
264
332
|
return (
|
|
265
333
|
<Select
|
|
@@ -270,12 +338,12 @@ function RecordPickerField(props) {
|
|
|
270
338
|
onClick(e);
|
|
271
339
|
}
|
|
272
340
|
}}
|
|
273
|
-
value={
|
|
341
|
+
value={normalizeRecordPickerValue(props.value, fieldNames, allowMultiple)}
|
|
274
342
|
labelRender={(item) => {
|
|
275
343
|
return <LabelByField option={item} fieldNames={fieldNames} />;
|
|
276
344
|
}}
|
|
277
345
|
labelInValue
|
|
278
|
-
mode={
|
|
346
|
+
mode={allowMultiple ? 'multiple' : undefined}
|
|
279
347
|
options={props.value}
|
|
280
348
|
allowClear
|
|
281
349
|
onChange={(newValue, option) => {
|
|
@@ -405,7 +473,7 @@ RecordPickerFieldModel.registerFlow({
|
|
|
405
473
|
},
|
|
406
474
|
handler(ctx, params) {
|
|
407
475
|
const { onChange } = ctx.inputArgs;
|
|
408
|
-
const
|
|
476
|
+
const allowMultiple = canRecordPickerSelectMultiple(ctx.collectionField, ctx.model.props.allowMultiple);
|
|
409
477
|
const sizeToWidthMap: Record<string, any> = {
|
|
410
478
|
drawer: {
|
|
411
479
|
small: '30%',
|
|
@@ -441,9 +509,9 @@ RecordPickerFieldModel.registerFlow({
|
|
|
441
509
|
currentItemValue: ctx.inputArgs.currentItemValue ?? {},
|
|
442
510
|
}),
|
|
443
511
|
rowSelectionProps: {
|
|
444
|
-
type:
|
|
512
|
+
type: allowMultiple ? 'checkbox' : 'radio',
|
|
445
513
|
defaultSelectedRows: () => {
|
|
446
|
-
return ctx.model.props.value;
|
|
514
|
+
return normalizeRecordPickerSelectedRows(ctx.model.props.value, allowMultiple);
|
|
447
515
|
},
|
|
448
516
|
renderCell: undefined,
|
|
449
517
|
selectedRowKeys: undefined,
|
|
@@ -452,8 +520,7 @@ RecordPickerFieldModel.registerFlow({
|
|
|
452
520
|
const selectTable = selectBlockModel.findSubModel('items', (m) => {
|
|
453
521
|
return m;
|
|
454
522
|
});
|
|
455
|
-
if (
|
|
456
|
-
// 单选
|
|
523
|
+
if (!allowMultiple) {
|
|
457
524
|
ctx.model.selectedRows.value = selectedRows?.[0];
|
|
458
525
|
onChange(ctx.model.selectedRows.value);
|
|
459
526
|
ctx.model._closeView?.();
|
|
@@ -475,7 +542,7 @@ RecordPickerFieldModel.registerFlow({
|
|
|
475
542
|
},
|
|
476
543
|
},
|
|
477
544
|
},
|
|
478
|
-
content: () => <RecordPickerContent model={ctx.model} toOne={
|
|
545
|
+
content: () => <RecordPickerContent model={ctx.model} toOne={!allowMultiple} />,
|
|
479
546
|
styles: {
|
|
480
547
|
content: {
|
|
481
548
|
padding: 0,
|
|
@@ -500,6 +567,24 @@ RecordPickerFieldModel.registerFlow({
|
|
|
500
567
|
fieldNames: {
|
|
501
568
|
use: 'titleField',
|
|
502
569
|
},
|
|
570
|
+
allowMultiple: {
|
|
571
|
+
title: tExpr('Multiple'),
|
|
572
|
+
uiMode: { type: 'switch', key: 'allowMultiple' },
|
|
573
|
+
hideInSettings(ctx) {
|
|
574
|
+
return !canRecordPickerSelectMultiple(ctx.collectionField, true);
|
|
575
|
+
},
|
|
576
|
+
defaultParams(ctx) {
|
|
577
|
+
return {
|
|
578
|
+
allowMultiple: canRecordPickerSelectMultiple(ctx.collectionField, true),
|
|
579
|
+
};
|
|
580
|
+
},
|
|
581
|
+
afterParamsSave(ctx, params, previousParams) {
|
|
582
|
+
applyRecordPickerAllowMultiple(ctx, params, previousParams);
|
|
583
|
+
},
|
|
584
|
+
handler(ctx, params) {
|
|
585
|
+
applyRecordPickerAllowMultiple(ctx, params);
|
|
586
|
+
},
|
|
587
|
+
},
|
|
503
588
|
},
|
|
504
589
|
});
|
|
505
590
|
|
|
@@ -20,6 +20,14 @@ import {
|
|
|
20
20
|
resolveRecordPersistenceState,
|
|
21
21
|
} from '../itemChain';
|
|
22
22
|
import { injectRecordPickerPopupContext } from '@nocobase/client-v2';
|
|
23
|
+
import {
|
|
24
|
+
canRecordPickerSelectMultiple,
|
|
25
|
+
getRecordPickerClearedValue,
|
|
26
|
+
getRecordPickerEmptyValue,
|
|
27
|
+
normalizeRecordPickerSelectedRows,
|
|
28
|
+
normalizeRecordPickerValue,
|
|
29
|
+
shouldClearRecordPickerValueOnMultipleChange,
|
|
30
|
+
} from '../RecordPickerFieldModel';
|
|
23
31
|
|
|
24
32
|
function createMockCollection() {
|
|
25
33
|
return {
|
|
@@ -32,6 +40,59 @@ function createMockCollection() {
|
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
describe('RecordPickerFieldModel item context', () => {
|
|
43
|
+
it('resolves popup select multiple mode for association fields', () => {
|
|
44
|
+
expect(canRecordPickerSelectMultiple({ type: 'belongsToMany' })).toBe(true);
|
|
45
|
+
expect(canRecordPickerSelectMultiple({ type: 'hasMany' })).toBe(true);
|
|
46
|
+
expect(canRecordPickerSelectMultiple({ type: 'belongsToArray' })).toBe(true);
|
|
47
|
+
expect(canRecordPickerSelectMultiple({ type: 'belongsToMany' }, false)).toBe(false);
|
|
48
|
+
expect(canRecordPickerSelectMultiple({ type: 'belongsTo' })).toBe(false);
|
|
49
|
+
expect(canRecordPickerSelectMultiple({ type: 'hasOne' })).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('normalizes popup select selected rows according to multiple mode', () => {
|
|
53
|
+
const rows = [
|
|
54
|
+
{ id: 1, name: 'A' },
|
|
55
|
+
{ id: 2, name: 'B' },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
expect(normalizeRecordPickerSelectedRows(rows, true)).toEqual(rows);
|
|
59
|
+
expect(normalizeRecordPickerSelectedRows(rows, false)).toEqual([{ id: 1, name: 'A' }]);
|
|
60
|
+
expect(normalizeRecordPickerSelectedRows({ id: 1, name: 'A' }, false)).toEqual([{ id: 1, name: 'A' }]);
|
|
61
|
+
expect(normalizeRecordPickerSelectedRows(undefined, true)).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('normalizes popup select display value according to multiple mode', () => {
|
|
65
|
+
const fieldNames = { label: 'name', value: 'id' };
|
|
66
|
+
const rows = [
|
|
67
|
+
{ id: 1, name: 'A' },
|
|
68
|
+
{ id: 2, name: 'B' },
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
expect(normalizeRecordPickerValue(rows, fieldNames, true)).toEqual([
|
|
72
|
+
{ id: 1, name: 'A', label: 'A', value: 1 },
|
|
73
|
+
{ id: 2, name: 'B', label: 'B', value: 2 },
|
|
74
|
+
]);
|
|
75
|
+
expect(normalizeRecordPickerValue(rows, fieldNames, false)).toEqual({ id: 1, name: 'A', label: 'A', value: 1 });
|
|
76
|
+
expect(normalizeRecordPickerValue(undefined, fieldNames, true)).toEqual([]);
|
|
77
|
+
expect(normalizeRecordPickerValue(undefined, fieldNames, false)).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('returns the empty popup select value for the current multiple mode', () => {
|
|
81
|
+
expect(getRecordPickerEmptyValue(true)).toEqual([]);
|
|
82
|
+
expect(getRecordPickerEmptyValue(false)).toBeUndefined();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('clears popup select value to undefined when multiple mode changes', () => {
|
|
86
|
+
expect(getRecordPickerClearedValue()).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('detects when changing multiple mode should clear popup select value', () => {
|
|
90
|
+
expect(shouldClearRecordPickerValueOnMultipleChange({ type: 'belongsToMany' }, true, false)).toBe(true);
|
|
91
|
+
expect(shouldClearRecordPickerValueOnMultipleChange({ type: 'belongsToMany' }, false, true)).toBe(true);
|
|
92
|
+
expect(shouldClearRecordPickerValueOnMultipleChange({ type: 'belongsToMany' }, true, true)).toBe(false);
|
|
93
|
+
expect(shouldClearRecordPickerValueOnMultipleChange({ type: 'belongsTo' }, true, false)).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
35
96
|
it('itemChain helpers: createParentItemAccessorsFromInputArgs works', () => {
|
|
36
97
|
const inputArgs = {
|
|
37
98
|
parentItem: { value: { id: 1 } },
|
|
@@ -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,10 +12,15 @@ import { Input } from 'antd';
|
|
|
12
12
|
import React from 'react';
|
|
13
13
|
import { customAlphabet as Alphabet } from 'nanoid';
|
|
14
14
|
import { FieldModel } from '../base/FieldModel';
|
|
15
|
+
import { ScanInput } from '../../../components/form/ScanInput';
|
|
15
16
|
|
|
16
17
|
export class InputFieldModel extends FieldModel {
|
|
17
18
|
render() {
|
|
18
|
-
|
|
19
|
+
if (this.props.enableScan) {
|
|
20
|
+
return <ScanInput {...this.props} />;
|
|
21
|
+
}
|
|
22
|
+
const { enableScan, disableManualInput, ...inputProps } = this.props;
|
|
23
|
+
return <Input {...inputProps} />;
|
|
19
24
|
}
|
|
20
25
|
}
|
|
21
26
|
|
|
@@ -36,6 +41,56 @@ InputFieldModel.registerFlow({
|
|
|
36
41
|
},
|
|
37
42
|
});
|
|
38
43
|
|
|
44
|
+
InputFieldModel.registerFlow({
|
|
45
|
+
key: 'scanInputSettings',
|
|
46
|
+
sort: 800,
|
|
47
|
+
title: tExpr('Scan input settings'),
|
|
48
|
+
steps: {
|
|
49
|
+
enableScan: {
|
|
50
|
+
title: tExpr('Enable Scan'),
|
|
51
|
+
uiMode: { type: 'switch', key: 'enableScan' },
|
|
52
|
+
hideInSettings(ctx) {
|
|
53
|
+
return ctx.model.getProps().pattern === 'readPretty' || ctx.model.getProps().disabled;
|
|
54
|
+
},
|
|
55
|
+
defaultParams(ctx) {
|
|
56
|
+
return {
|
|
57
|
+
enableScan: !!ctx.model.props.enableScan,
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
handler(ctx, params) {
|
|
61
|
+
ctx.model.setProps({
|
|
62
|
+
enableScan: !!params.enableScan,
|
|
63
|
+
disableManualInput: params.enableScan ? ctx.model.props.disableManualInput : false,
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
disableManualInput: {
|
|
68
|
+
title: tExpr('Disable manual input'),
|
|
69
|
+
uiMode: { type: 'switch', key: 'disableManualInput' },
|
|
70
|
+
hideInSettings(ctx) {
|
|
71
|
+
const enableScanParams = ctx.model.getStepParams?.('scanInputSettings', 'enableScan') as
|
|
72
|
+
| { enableScan?: boolean }
|
|
73
|
+
| undefined;
|
|
74
|
+
const enableScan = Object.prototype.hasOwnProperty.call(enableScanParams || {}, 'enableScan')
|
|
75
|
+
? !!enableScanParams?.enableScan
|
|
76
|
+
: !!ctx.model.props.enableScan;
|
|
77
|
+
|
|
78
|
+
return !enableScan || ctx.model.getProps().pattern === 'readPretty' || !!ctx.model.getProps().disabled;
|
|
79
|
+
},
|
|
80
|
+
defaultParams(ctx) {
|
|
81
|
+
return {
|
|
82
|
+
disableManualInput: !!ctx.model.props.disableManualInput,
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
handler(ctx, params) {
|
|
86
|
+
ctx.model.setProps({
|
|
87
|
+
disableManualInput: !!ctx.model.props.enableScan && !!params.disableManualInput,
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
39
94
|
InputFieldModel.define({
|
|
40
95
|
label: tExpr('Input'),
|
|
41
96
|
});
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
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, screen } from '@testing-library/react';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
14
|
+
import { InputFieldModel } from '../InputFieldModel';
|
|
15
|
+
|
|
16
|
+
vi.mock('react-i18next', () => ({
|
|
17
|
+
useTranslation: () => ({ t: (key: string) => key }),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
function createInputFieldModel(props?: Record<string, unknown>) {
|
|
21
|
+
const engine = new FlowEngine();
|
|
22
|
+
return new InputFieldModel({
|
|
23
|
+
uid: 'input-field-model-test',
|
|
24
|
+
flowEngine: engine,
|
|
25
|
+
props,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('InputFieldModel', () => {
|
|
30
|
+
it('renders the normal input when scan input is disabled', () => {
|
|
31
|
+
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
32
|
+
const model = createInputFieldModel({ disableManualInput: true, enableScan: false });
|
|
33
|
+
|
|
34
|
+
render(<>{model.render()}</>);
|
|
35
|
+
|
|
36
|
+
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
|
37
|
+
expect(screen.queryByRole('button', { name: 'Scan to input' })).not.toBeInTheDocument();
|
|
38
|
+
expect(consoleError).not.toHaveBeenCalledWith(
|
|
39
|
+
expect.stringContaining('React does not recognize the `disableManualInput` prop'),
|
|
40
|
+
expect.anything(),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
consoleError.mockRestore();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('renders ScanInput when scan input is enabled', () => {
|
|
47
|
+
const model = createInputFieldModel({ enableScan: true });
|
|
48
|
+
|
|
49
|
+
render(<>{model.render()}</>);
|
|
50
|
+
|
|
51
|
+
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
|
52
|
+
expect(screen.getByRole('button', { name: 'Scan to input' })).toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('stores enableScan and clears disableManualInput when scan input is turned off', () => {
|
|
56
|
+
const model = createInputFieldModel();
|
|
57
|
+
const step = model.getFlow('scanInputSettings')?.steps.enableScan;
|
|
58
|
+
const setProps = vi.fn();
|
|
59
|
+
|
|
60
|
+
step?.handler(
|
|
61
|
+
{
|
|
62
|
+
model: {
|
|
63
|
+
props: { disableManualInput: true },
|
|
64
|
+
setProps,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{ enableScan: false },
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(setProps).toHaveBeenCalledWith({
|
|
71
|
+
enableScan: false,
|
|
72
|
+
disableManualInput: false,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('does not restore manual input disabling after scan input is turned off', () => {
|
|
77
|
+
const model = createInputFieldModel({ enableScan: true, disableManualInput: true });
|
|
78
|
+
const flow = model.getFlow('scanInputSettings');
|
|
79
|
+
const ctx = { model };
|
|
80
|
+
|
|
81
|
+
flow?.steps.enableScan.handler(ctx, { enableScan: false });
|
|
82
|
+
flow?.steps.disableManualInput.handler(ctx, { disableManualInput: true });
|
|
83
|
+
|
|
84
|
+
expect(model.props).toMatchObject({
|
|
85
|
+
enableScan: false,
|
|
86
|
+
disableManualInput: false,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('hides disableManualInput until scan input is enabled', () => {
|
|
91
|
+
const model = createInputFieldModel();
|
|
92
|
+
const step = model.getFlow('scanInputSettings')?.steps.disableManualInput;
|
|
93
|
+
|
|
94
|
+
const hidden = step?.hideInSettings({
|
|
95
|
+
model: {
|
|
96
|
+
props: { enableScan: false },
|
|
97
|
+
getProps: () => ({}),
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(hidden).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('updates disableManualInput visibility from the current enableScan setting params', () => {
|
|
105
|
+
const model = createInputFieldModel({ enableScan: false });
|
|
106
|
+
const step = model.getFlow('scanInputSettings')?.steps.disableManualInput;
|
|
107
|
+
|
|
108
|
+
model.setStepParams('scanInputSettings', 'enableScan', { enableScan: true });
|
|
109
|
+
|
|
110
|
+
expect(step?.hideInSettings({ model })).toBe(false);
|
|
111
|
+
|
|
112
|
+
model.setStepParams('scanInputSettings', 'enableScan', { enableScan: false });
|
|
113
|
+
|
|
114
|
+
expect(step?.hideInSettings({ model })).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
});
|