@nocobase/client-v2 2.4.0-alpha.13 → 2.4.0-alpha.14
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/README.md +4 -4
- package/es/flow/models/blocks/assign-form/assignFormValue.d.ts +29 -0
- package/es/index.mjs +64 -64
- package/lib/index.js +64 -64
- package/package.json +7 -7
- package/src/flow/admin-shell/admin-layout/HelpLite.tsx +5 -1
- package/src/flow/flows/editMarkdownFlow.tsx +1 -1
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +26 -9
- package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.multipleSelect.test.tsx +216 -0
- package/src/flow/models/blocks/assign-form/__tests__/assignFormValue.test.ts +41 -0
- package/src/flow/models/blocks/assign-form/assignFormValue.ts +64 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "2.4.0-alpha.
|
|
3
|
+
"version": "2.4.0-alpha.14",
|
|
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.4.0-alpha.
|
|
31
|
-
"@nocobase/flow-engine": "2.4.0-alpha.
|
|
32
|
-
"@nocobase/sdk": "2.4.0-alpha.
|
|
33
|
-
"@nocobase/shared": "2.4.0-alpha.
|
|
34
|
-
"@nocobase/utils": "2.4.0-alpha.
|
|
30
|
+
"@nocobase/evaluators": "2.4.0-alpha.14",
|
|
31
|
+
"@nocobase/flow-engine": "2.4.0-alpha.14",
|
|
32
|
+
"@nocobase/sdk": "2.4.0-alpha.14",
|
|
33
|
+
"@nocobase/shared": "2.4.0-alpha.14",
|
|
34
|
+
"@nocobase/utils": "2.4.0-alpha.14",
|
|
35
35
|
"ahooks": "^3.7.2",
|
|
36
36
|
"antd": "5.24.2",
|
|
37
37
|
"antd-style": "3.7.1",
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"react-router-dom": "^6.30.1",
|
|
50
50
|
"zxing-wasm": "^3.1.3"
|
|
51
51
|
},
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "b276c8e47a32cb7f1f5ec2d4968892b11a14cbd5"
|
|
53
53
|
}
|
|
@@ -70,7 +70,11 @@ const SettingsMenu: React.FC = () => {
|
|
|
70
70
|
key: 'userManual',
|
|
71
71
|
label: (
|
|
72
72
|
<a
|
|
73
|
-
href={
|
|
73
|
+
href={
|
|
74
|
+
isSimplifiedChinese
|
|
75
|
+
? 'https://nocode.docs.nocobase.com/cn/guide/'
|
|
76
|
+
: 'https://nocode.docs.nocobase.com/guide/'
|
|
77
|
+
}
|
|
74
78
|
target="_blank"
|
|
75
79
|
rel="noreferrer"
|
|
76
80
|
>
|
|
@@ -70,7 +70,7 @@ export const editMarkdownFlow = defineFlow<FlowModel>({
|
|
|
70
70
|
{t('References')}:
|
|
71
71
|
</span>
|
|
72
72
|
<a
|
|
73
|
-
href={`https://docs.nocobase.com/interface-builder/blocks/other-blocks/markdown`}
|
|
73
|
+
href={`https://nocode.docs.nocobase.com/interface-builder/blocks/other-blocks/markdown`}
|
|
74
74
|
target="_blank"
|
|
75
75
|
rel="noreferrer"
|
|
76
76
|
>
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
isDateLikeField,
|
|
32
32
|
resolveDateVariableComponentProps,
|
|
33
33
|
} from '../../../components/field-value-variable';
|
|
34
|
+
import { isArrayValueField, normalizeAssignFormValue } from './assignFormValue';
|
|
34
35
|
|
|
35
36
|
type AssignFormTempOriginField = {
|
|
36
37
|
uid?: string;
|
|
@@ -112,8 +113,6 @@ export class AssignFormItemModel extends FormItemModel {
|
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
onInit(options: any) {
|
|
115
|
-
const initAssign = this.getStepParams('fieldSettings', 'assignValue')?.value;
|
|
116
|
-
if (typeof initAssign !== 'undefined') this.assignValue = initAssign;
|
|
117
116
|
this.context.defineProperty('collectionField', {
|
|
118
117
|
get: () => {
|
|
119
118
|
const params = this.getStepParams('fieldSettings', 'init') || {};
|
|
@@ -122,6 +121,10 @@ export class AssignFormItemModel extends FormItemModel {
|
|
|
122
121
|
return dsm?.getCollectionField?.(key);
|
|
123
122
|
},
|
|
124
123
|
});
|
|
124
|
+
const initAssign = this.getStepParams('fieldSettings', 'assignValue')?.value;
|
|
125
|
+
if (typeof initAssign !== 'undefined') {
|
|
126
|
+
this.assignValue = normalizeAssignFormValue(initAssign, isArrayValueField(this.collectionField));
|
|
127
|
+
}
|
|
125
128
|
|
|
126
129
|
// 确保存在 subModels.field,便于沿用 FormItemModel 的组件切换/设置步骤(fieldComponent 等)
|
|
127
130
|
const ctx: any = this.context;
|
|
@@ -168,7 +171,7 @@ export class AssignFormItemModel extends FormItemModel {
|
|
|
168
171
|
getAssignedEntry(): [string, any] | null {
|
|
169
172
|
const name = this.fieldPath;
|
|
170
173
|
if (!name || !this.collectionField) return null;
|
|
171
|
-
return [name, this.assignValue];
|
|
174
|
+
return [name, normalizeAssignFormValue(this.assignValue, isArrayValueField(this.collectionField))];
|
|
172
175
|
}
|
|
173
176
|
|
|
174
177
|
render() {
|
|
@@ -184,6 +187,7 @@ export class AssignFormItemModel extends FormItemModel {
|
|
|
184
187
|
const fieldPath = this.fieldPath;
|
|
185
188
|
const originField = this.subModels?.field as AssignFormTempOriginField | undefined;
|
|
186
189
|
const tempFieldRefreshKey = getAssignFormTempFieldRefreshKey(originField);
|
|
190
|
+
const arrayValueField = isArrayValueField(this.collectionField);
|
|
187
191
|
|
|
188
192
|
const FieldRow: React.FC = () => {
|
|
189
193
|
const [tempRoot, setTempRoot] = React.useState<any>(null);
|
|
@@ -252,6 +256,14 @@ export class AssignFormItemModel extends FormItemModel {
|
|
|
252
256
|
fm?.dispatchEvent?.('beforeRender', undefined, { sequential: true, useCache: true });
|
|
253
257
|
// 为本地枚举型字段补全可选项(仅在未显式传入 options 时处理)
|
|
254
258
|
ensureOptionsFromUiSchemaEnumIfAbsent(fm as any, cfForMultiple as any);
|
|
259
|
+
// `mode` is the antd Select switch for multi-select. It is normally injected by
|
|
260
|
+
// `collectionField.getComponentProps()`, which the temp field does not apply.
|
|
261
|
+
// Apply the fallback after `beforeRender` so field flows keep precedence; a
|
|
262
|
+
// schema-provided mode is used when present, otherwise default to `multiple`.
|
|
263
|
+
if (cfForMultiple?.interface === 'multipleSelect' && fm?.props?.mode === undefined) {
|
|
264
|
+
const schemaMode = cfForMultiple?.uiSchema?.['x-component-props']?.mode as string | undefined;
|
|
265
|
+
fm?.setProps?.({ mode: schemaMode ?? 'multiple' });
|
|
266
|
+
}
|
|
255
267
|
if (!fm?.props?.fieldNames && cfForMultiple?.targetCollection) {
|
|
256
268
|
const targetCol = cfForMultiple.targetCollection;
|
|
257
269
|
const valueKey = cfForMultiple?.targetKey || targetCol?.filterTargetKey || 'id';
|
|
@@ -268,16 +280,17 @@ export class AssignFormItemModel extends FormItemModel {
|
|
|
268
280
|
const ConstantValueEditor: React.FC<any> = (inputProps: any) => {
|
|
269
281
|
React.useEffect(() => {
|
|
270
282
|
if (!tempRoot) return;
|
|
283
|
+
const isArrayField = isArrayValueField(this.collectionField);
|
|
271
284
|
// 同步根 props(变量输入的受控 value/onChange)
|
|
272
285
|
tempRoot.setProps?.({ ...inputProps });
|
|
273
286
|
// 同步到具体字段模型,确保字段组件能触发 onChange
|
|
274
287
|
const fm = tempRoot?.subModels?.fields?.[0];
|
|
275
288
|
if (fm) {
|
|
276
289
|
fm.setProps?.({
|
|
277
|
-
value: inputProps?.value,
|
|
290
|
+
value: normalizeAssignFormValue(inputProps?.value, isArrayField),
|
|
278
291
|
onChange: (...args: unknown[]) => {
|
|
279
292
|
const nextArg = args && args.length ? args[0] : undefined;
|
|
280
|
-
const nextValue = normalizeAssignFormInputValue(nextArg);
|
|
293
|
+
const nextValue = normalizeAssignFormValue(normalizeAssignFormInputValue(nextArg), isArrayField);
|
|
281
294
|
fm.setProps?.({ value: nextValue });
|
|
282
295
|
this.assignValue = nextValue;
|
|
283
296
|
inputProps?.onChange?.(nextValue);
|
|
@@ -330,20 +343,24 @@ export class AssignFormItemModel extends FormItemModel {
|
|
|
330
343
|
{...this.props}
|
|
331
344
|
label={labelText}
|
|
332
345
|
name={namePath}
|
|
333
|
-
initialValue={this.assignValue}
|
|
346
|
+
initialValue={normalizeAssignFormValue(this.assignValue, arrayValueField)}
|
|
334
347
|
valuePropName="__assign_value__"
|
|
335
348
|
trigger="__assign_trigger__"
|
|
336
349
|
validateTrigger="__assign_trigger__"
|
|
337
350
|
>
|
|
338
351
|
{React.createElement((formBindingProps: any) => {
|
|
339
352
|
const formValue = formBindingProps?.__assign_value__;
|
|
340
|
-
const mergedValue =
|
|
353
|
+
const mergedValue = normalizeAssignFormValue(
|
|
354
|
+
typeof formValue === 'undefined' ? this.assignValue : formValue,
|
|
355
|
+
arrayValueField,
|
|
356
|
+
);
|
|
341
357
|
return (
|
|
342
358
|
<FieldValueVariableInput
|
|
343
359
|
value={mergedValue}
|
|
344
360
|
onChange={(v) => {
|
|
345
|
-
|
|
346
|
-
|
|
361
|
+
const nextValue = normalizeAssignFormValue(v, arrayValueField);
|
|
362
|
+
this.assignValue = nextValue;
|
|
363
|
+
formBindingProps?.__assign_trigger__?.(nextValue);
|
|
347
364
|
}}
|
|
348
365
|
baseMetaTree={baseMetaTree}
|
|
349
366
|
constantComponent={ConstantValueEditor}
|
package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.multipleSelect.test.tsx
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
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 React from 'react';
|
|
11
|
+
import { App, ConfigProvider } from 'antd';
|
|
12
|
+
import { describe, expect, it } from 'vitest';
|
|
13
|
+
import { render, waitFor } from '@nocobase/test/client';
|
|
14
|
+
import {
|
|
15
|
+
FlowEngine,
|
|
16
|
+
FlowEngineProvider,
|
|
17
|
+
FlowRuntimeContext,
|
|
18
|
+
FlowSettingsContextProvider,
|
|
19
|
+
type FlowModel,
|
|
20
|
+
type IFlowModelRepository,
|
|
21
|
+
} from '@nocobase/flow-engine';
|
|
22
|
+
import { AssignFormGridModel } from '../AssignFormGridModel';
|
|
23
|
+
import { AssignFormItemModel } from '../AssignFormItemModel';
|
|
24
|
+
import { AssignFormModel } from '../AssignFormModel';
|
|
25
|
+
import { createAssignFieldValuesStep } from '../assignFieldValuesFlow';
|
|
26
|
+
import { InputFieldModel } from '../../../fields/InputFieldModel';
|
|
27
|
+
import { SelectFieldModel } from '../../../fields/SelectFieldModel';
|
|
28
|
+
import { VariableFieldFormModel } from '../../../fields/VariableFieldFormModel';
|
|
29
|
+
|
|
30
|
+
class MockFlowModelRepository implements IFlowModelRepository {
|
|
31
|
+
async findOne(): Promise<Record<string, any> | null> {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async save(model: FlowModel): Promise<Record<string, any>> {
|
|
36
|
+
return { uid: model.uid };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async destroy(): Promise<boolean> {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async move(): Promise<void> {}
|
|
44
|
+
|
|
45
|
+
async duplicate(): Promise<Record<string, any> | null> {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createEngine() {
|
|
51
|
+
const engine = new FlowEngine();
|
|
52
|
+
engine.setModelRepository(new MockFlowModelRepository());
|
|
53
|
+
engine.registerModels({
|
|
54
|
+
AssignFormModel,
|
|
55
|
+
AssignFormGridModel,
|
|
56
|
+
AssignFormItemModel,
|
|
57
|
+
InputFieldModel,
|
|
58
|
+
SelectFieldModel,
|
|
59
|
+
VariableFieldFormModel,
|
|
60
|
+
});
|
|
61
|
+
engine.context.defineProperty('location', { value: { search: '' } });
|
|
62
|
+
engine.context.defineProperty('themeToken', { value: { marginLG: 24 } });
|
|
63
|
+
return engine;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function addMultipleSelectCollection(engine: FlowEngine) {
|
|
67
|
+
const dsm = engine.context.dataSourceManager;
|
|
68
|
+
const main = dsm.getDataSource('main');
|
|
69
|
+
main.addCollection({
|
|
70
|
+
name: 'assign_multiple_select',
|
|
71
|
+
fields: [
|
|
72
|
+
{
|
|
73
|
+
name: 'tags',
|
|
74
|
+
type: 'array',
|
|
75
|
+
interface: 'multipleSelect',
|
|
76
|
+
uiSchema: {
|
|
77
|
+
type: 'array',
|
|
78
|
+
'x-component': 'Select',
|
|
79
|
+
'x-component-props': { mode: 'multiple' },
|
|
80
|
+
enum: [
|
|
81
|
+
{ value: 'a', label: 'A' },
|
|
82
|
+
{ value: 'b', label: 'B' },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
// Fields created before the interface default was persisted may miss `mode`.
|
|
88
|
+
name: 'legacyTags',
|
|
89
|
+
type: 'array',
|
|
90
|
+
interface: 'multipleSelect',
|
|
91
|
+
uiSchema: {
|
|
92
|
+
type: 'array',
|
|
93
|
+
'x-component': 'Select',
|
|
94
|
+
enum: [{ value: 'c', label: 'C' }],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'tagMode',
|
|
99
|
+
type: 'array',
|
|
100
|
+
interface: 'multipleSelect',
|
|
101
|
+
uiSchema: {
|
|
102
|
+
type: 'array',
|
|
103
|
+
'x-component': 'Select',
|
|
104
|
+
'x-component-props': { mode: 'tags' },
|
|
105
|
+
enum: [{ value: 't', label: 'T' }],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'single',
|
|
110
|
+
type: 'string',
|
|
111
|
+
interface: 'select',
|
|
112
|
+
uiSchema: {
|
|
113
|
+
type: 'string',
|
|
114
|
+
'x-component': 'Select',
|
|
115
|
+
enum: [{ value: 'x', label: 'X' }],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{ name: 'nickname', type: 'string', interface: 'input' },
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
return dsm.getCollection('main', 'assign_multiple_select');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function renderAssignEditor(engine: FlowEngine, action: FlowModel) {
|
|
125
|
+
const step = createAssignFieldValuesStep({ settingsFlowKey: 'assignSettings' });
|
|
126
|
+
const Editor = step.uiSchema().editor?.['x-component'] as React.ComponentType;
|
|
127
|
+
const flowSettingsCtx = new FlowRuntimeContext(action as any, 'assignSettings', 'settings');
|
|
128
|
+
|
|
129
|
+
render(
|
|
130
|
+
<FlowEngineProvider engine={engine}>
|
|
131
|
+
<ConfigProvider>
|
|
132
|
+
<App>
|
|
133
|
+
<FlowSettingsContextProvider value={flowSettingsCtx}>
|
|
134
|
+
<Editor />
|
|
135
|
+
</FlowSettingsContextProvider>
|
|
136
|
+
</App>
|
|
137
|
+
</ConfigProvider>
|
|
138
|
+
</FlowEngineProvider>,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
describe('assignFieldValuesFlow multipleSelect', () => {
|
|
143
|
+
it('renders multipleSelect as multi select and normalizes historical scalar values', async () => {
|
|
144
|
+
const engine = createEngine();
|
|
145
|
+
const collection = addMultipleSelectCollection(engine);
|
|
146
|
+
|
|
147
|
+
const action = engine.createModel({ use: 'FlowModel', uid: 'act-assign-multiple-select' });
|
|
148
|
+
action.setStepParams('assignSettings', 'assignFieldValues', {
|
|
149
|
+
assignedValues: {
|
|
150
|
+
tags: 'a',
|
|
151
|
+
legacyTags: 'c',
|
|
152
|
+
tagMode: 't',
|
|
153
|
+
single: 'x',
|
|
154
|
+
nickname: 'hello',
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
action.context.defineProperty('blockModel', { value: { collection } });
|
|
158
|
+
|
|
159
|
+
const tempFields: any[] = [];
|
|
160
|
+
const originalCreateModel = engine.createModel.bind(engine);
|
|
161
|
+
(engine as any).createModel = (options: any) => {
|
|
162
|
+
const model = originalCreateModel(options);
|
|
163
|
+
if (options?.use === 'VariableFieldFormModel') {
|
|
164
|
+
tempFields.push(model);
|
|
165
|
+
}
|
|
166
|
+
return model;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
await renderAssignEditor(engine, action);
|
|
170
|
+
|
|
171
|
+
await waitFor(() => {
|
|
172
|
+
const form = engine.findModelByParentId<AssignFormModel>(action.uid, 'assignForm');
|
|
173
|
+
expect(form?.subModels.grid.subModels.items || []).toHaveLength(5);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const form = engine.findModelByParentId<AssignFormModel>(action.uid, 'assignForm');
|
|
177
|
+
await waitFor(() => {
|
|
178
|
+
expect(form?.getAssignedValues()).toEqual({
|
|
179
|
+
tags: ['a'],
|
|
180
|
+
legacyTags: ['c'],
|
|
181
|
+
tagMode: ['t'],
|
|
182
|
+
single: 'x',
|
|
183
|
+
nickname: 'hello',
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await waitFor(() => {
|
|
188
|
+
// All three multipleSelect fields must render as antd multi select, including the
|
|
189
|
+
// field without schema `mode`; the normal select stays single.
|
|
190
|
+
expect(document.querySelectorAll('.ant-select-multiple')).toHaveLength(3);
|
|
191
|
+
expect(document.querySelectorAll('.ant-select-single').length).toBeGreaterThanOrEqual(1);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const findTempField = (fieldPath: string) =>
|
|
195
|
+
tempFields
|
|
196
|
+
.map((root) => root?.subModels?.fields?.[0])
|
|
197
|
+
.find((field) => field?.getStepParams?.('fieldSettings', 'init')?.fieldPath === fieldPath);
|
|
198
|
+
await waitFor(() => {
|
|
199
|
+
expect(findTempField('tags')?.props?.onChange).toBeTypeOf('function');
|
|
200
|
+
expect(findTempField('tags')?.props?.mode).toBe('multiple');
|
|
201
|
+
expect(findTempField('legacyTags')?.props?.mode).toBe('multiple');
|
|
202
|
+
// An explicit schema mode must be preserved by the fallback.
|
|
203
|
+
expect(findTempField('tagMode')?.props?.mode).toBe('tags');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
findTempField('tags').props.onChange(['a', 'b']);
|
|
207
|
+
await waitFor(() => {
|
|
208
|
+
expect(form?.getAssignedValues()).toMatchObject({ tags: ['a', 'b'], single: 'x' });
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
findTempField('tags').props.onChange('');
|
|
212
|
+
await waitFor(() => {
|
|
213
|
+
expect(form?.getAssignedValues()).toMatchObject({ tags: [] });
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
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 { isArrayValueField, normalizeAssignFormValue } from '../assignFormValue';
|
|
12
|
+
|
|
13
|
+
describe('assignFormValue', () => {
|
|
14
|
+
it('detects array-valued fields', () => {
|
|
15
|
+
expect(isArrayValueField({ interface: 'multipleSelect' })).toBe(true);
|
|
16
|
+
expect(isArrayValueField({ interface: 'checkboxGroup' })).toBe(true);
|
|
17
|
+
expect(isArrayValueField({ uiSchema: { type: 'array' } })).toBe(true);
|
|
18
|
+
expect(isArrayValueField({ interface: 'select', uiSchema: { type: 'string' } })).toBe(false);
|
|
19
|
+
expect(isArrayValueField(undefined)).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('wraps scalar constants for array fields', () => {
|
|
23
|
+
expect(normalizeAssignFormValue('a', true)).toEqual(['a']);
|
|
24
|
+
expect(normalizeAssignFormValue(0, true)).toEqual([0]);
|
|
25
|
+
expect(normalizeAssignFormValue(false, true)).toEqual([false]);
|
|
26
|
+
expect(normalizeAssignFormValue(['a', 'b'], true)).toEqual(['a', 'b']);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('keeps non-constant and empty values unchanged', () => {
|
|
30
|
+
expect(normalizeAssignFormValue('', true)).toEqual([]);
|
|
31
|
+
expect(normalizeAssignFormValue(null, true)).toBeNull();
|
|
32
|
+
expect(normalizeAssignFormValue(undefined, true)).toBeUndefined();
|
|
33
|
+
expect(normalizeAssignFormValue('{{ ctx.record.name }}', true)).toBe('{{ ctx.record.name }}');
|
|
34
|
+
expect(normalizeAssignFormValue({ code: 'return 1;' }, true)).toEqual({ code: 'return 1;' });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('does not touch non-array fields', () => {
|
|
38
|
+
expect(normalizeAssignFormValue('a', false)).toBe('a');
|
|
39
|
+
expect(normalizeAssignFormValue(['a'], false)).toEqual(['a']);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
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 { isRunJSValue, isVariableExpression } from '@nocobase/flow-engine';
|
|
11
|
+
|
|
12
|
+
type ArrayValueCollectionField =
|
|
13
|
+
| {
|
|
14
|
+
interface?: string;
|
|
15
|
+
uiSchema?: {
|
|
16
|
+
type?: string;
|
|
17
|
+
} | null;
|
|
18
|
+
}
|
|
19
|
+
| null
|
|
20
|
+
| undefined;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Fields whose stored value is an array. Besides `multipleSelect`/`checkboxGroup`,
|
|
24
|
+
* some legacy fields declare `uiSchema.type === 'array'` explicitly.
|
|
25
|
+
*/
|
|
26
|
+
export function isArrayValueField(collectionField: ArrayValueCollectionField): boolean {
|
|
27
|
+
if (!collectionField) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
if (collectionField.interface === 'multipleSelect' || collectionField.interface === 'checkboxGroup') {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return collectionField.uiSchema?.type === 'array';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Normalize a constant assignment value for array-valued fields.
|
|
38
|
+
*
|
|
39
|
+
* Historical configurations may store a scalar (before the multi-select control
|
|
40
|
+
* was rendered correctly), so wrap it into an array instead of submitting a
|
|
41
|
+
* string to the server. Variable expressions, RunJS values, Null and empty
|
|
42
|
+
* values are kept as-is so the assignment mode is not changed.
|
|
43
|
+
*/
|
|
44
|
+
export function normalizeAssignFormValue(value: unknown, arrayValueField: boolean): unknown {
|
|
45
|
+
if (!arrayValueField) {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
if (isVariableExpression(value) || isRunJSValue(value)) {
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
if (value === '') {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(value)) {
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
if (value === null || typeof value === 'undefined') {
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
61
|
+
return [value];
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|