@nocobase/client-v2 2.3.0-beta.2 → 2.3.0-beta.21
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/RouteRepository.d.ts +6 -0
- package/es/components/form/ScanInput/useCodeScanner.d.ts +12 -2
- package/es/components/form/ScanInput/zxingWasmDecoder.d.ts +9 -0
- package/es/flow/admin-shell/admin-layout/MobileTopbarActionsContext.d.ts +10 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +2 -0
- package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +10 -1
- package/es/flow/internal/utils/quickEditDataScope.d.ts +48 -0
- package/es/flow/internal/utils/quickEditViewContainer.d.ts +27 -0
- package/es/flow/models/actions/LinkActionUtils.d.ts +1 -0
- package/es/flow/models/blocks/assign-form/assignFormValue.d.ts +29 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +1 -0
- package/es/index.mjs +100 -100
- package/lib/index.js +102 -102
- package/package.json +9 -8
- package/src/BaseApplication.tsx +2 -0
- package/src/RouteRepository.ts +14 -0
- package/src/__tests__/RouteRepository.test.ts +51 -0
- package/src/__tests__/app.test.tsx +13 -0
- package/src/collection-manager/__tests__/field-configure.test.ts +52 -0
- package/src/collection-manager/field-configure.ts +2 -2
- package/src/components/form/ScanInput/CodeScanner.tsx +3 -1
- package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +7 -1
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +182 -8
- package/src/components/form/ScanInput/__tests__/zxingWasmDecoder.test.ts +78 -0
- package/src/components/form/ScanInput/useCodeScanner.ts +135 -20
- package/src/components/form/ScanInput/zxingWasmDecoder.ts +64 -0
- package/src/flow/actions/__tests__/validation.test.ts +48 -0
- package/src/flow/admin-shell/admin-layout/MobileTopbarActionsContext.ts +13 -0
- package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +13 -2
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.mobile.test.tsx +205 -0
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +42 -23
- package/src/flow/components/FieldAssignValueInput.tsx +4 -0
- package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +21 -12
- package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +9 -0
- package/src/flow/components/filter/VariableFilterItem.tsx +14 -4
- package/src/flow/components/filter/__tests__/VariableFilterItem.rightMetaTree.test.tsx +158 -0
- package/src/flow/internal/components/Markdown/DisplayMarkdown.tsx +14 -9
- package/src/flow/internal/components/Markdown/__tests__/DisplayMarkdown.test.tsx +46 -0
- package/src/flow/internal/utils/__tests__/quickEditDataScope.test.ts +156 -0
- package/src/flow/internal/utils/__tests__/quickEditViewContainer.test.ts +52 -0
- package/src/flow/internal/utils/quickEditDataScope.tsx +177 -0
- package/src/flow/internal/utils/quickEditViewContainer.ts +46 -0
- package/src/flow/models/actions/JSItemActionModel.tsx +19 -3
- package/src/flow/models/actions/LinkActionModel.tsx +11 -9
- package/src/flow/models/actions/LinkActionUtils.ts +4 -2
- package/src/flow/models/actions/__tests__/JSItemActionModel.test.tsx +47 -1
- package/src/flow/models/actions/__tests__/LinkActionModel.test.ts +14 -0
- package/src/flow/models/base/GridModel.tsx +0 -1
- package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +22 -1
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +40 -12
- package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +140 -0
- 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/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +29 -0
- package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +1 -1
- package/src/flow/models/blocks/filter-form/fields/__tests__/FieldComponentProps.options.test.tsx +61 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +26 -5
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +45 -11
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +145 -0
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +120 -0
- package/src/flow/models/blocks/form/__tests__/popupLinkage.test.tsx +175 -0
- package/src/flow/models/blocks/js-block/JSBlock.tsx +5 -1
- package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +23 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +31 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +64 -0
- package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +11 -2
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +4 -3
- package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +6 -3
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +13 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +47 -17
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +6 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.largeField.test.tsx +178 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableField.disabled.test.tsx +90 -0
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +26 -2
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordSelectFieldModel.quickCreatePopup.test.tsx +183 -0
- package/src/flow/models/fields/AssociationFieldModel/__tests__/associationSelect.search.test.ts +113 -0
- package/src/flow/models/fields/DisplayAssociationField/DisplaySubTableFieldModel.tsx +42 -7
- package/src/flow/models/fields/DisplayAssociationField/__tests__/DisplaySubTableFieldModel.test.tsx +351 -0
- package/src/flow/models/fields/DisplayHtmlFieldModel.tsx +2 -1
- package/src/flow/models/fields/InputFieldModel.tsx +1 -1
- package/src/flow/models/fields/RichTextFieldModel/__tests__/RichTextFieldModel.test.tsx +74 -0
- package/src/flow/models/fields/RichTextFieldModel/index.tsx +27 -6
- package/src/flow/models/fields/SelectFieldModel.tsx +5 -0
- package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +16 -0
- package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +29 -1
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +1 -0
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +20 -0
- package/src/flow/models/topbar/TopbarActionModel.tsx +5 -5
- package/src/flow/models/topbar/UserCenterTopbarActionModel.tsx +8 -4
|
@@ -10,7 +10,14 @@
|
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { Input } from 'antd';
|
|
12
12
|
import { define, observable } from '@formily/reactive';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
EditableItemModel,
|
|
15
|
+
FieldDeletePlaceholder,
|
|
16
|
+
FlowModelRenderer,
|
|
17
|
+
FormItem,
|
|
18
|
+
jioToJoiSchema,
|
|
19
|
+
tExpr,
|
|
20
|
+
} from '@nocobase/flow-engine';
|
|
14
21
|
// 无需类型导入(避免未使用的类型)
|
|
15
22
|
import { FormItemModel } from '../form/FormItemModel';
|
|
16
23
|
import { EditFormModel } from '../form/EditFormModel';
|
|
@@ -24,6 +31,7 @@ import {
|
|
|
24
31
|
isDateLikeField,
|
|
25
32
|
resolveDateVariableComponentProps,
|
|
26
33
|
} from '../../../components/field-value-variable';
|
|
34
|
+
import { isArrayValueField, normalizeAssignFormValue } from './assignFormValue';
|
|
27
35
|
|
|
28
36
|
type AssignFormTempOriginField = {
|
|
29
37
|
uid?: string;
|
|
@@ -105,8 +113,6 @@ export class AssignFormItemModel extends FormItemModel {
|
|
|
105
113
|
}
|
|
106
114
|
|
|
107
115
|
onInit(options: any) {
|
|
108
|
-
const initAssign = this.getStepParams('fieldSettings', 'assignValue')?.value;
|
|
109
|
-
if (typeof initAssign !== 'undefined') this.assignValue = initAssign;
|
|
110
116
|
this.context.defineProperty('collectionField', {
|
|
111
117
|
get: () => {
|
|
112
118
|
const params = this.getStepParams('fieldSettings', 'init') || {};
|
|
@@ -115,6 +121,10 @@ export class AssignFormItemModel extends FormItemModel {
|
|
|
115
121
|
return dsm?.getCollectionField?.(key);
|
|
116
122
|
},
|
|
117
123
|
});
|
|
124
|
+
const initAssign = this.getStepParams('fieldSettings', 'assignValue')?.value;
|
|
125
|
+
if (typeof initAssign !== 'undefined') {
|
|
126
|
+
this.assignValue = normalizeAssignFormValue(initAssign, isArrayValueField(this.collectionField));
|
|
127
|
+
}
|
|
118
128
|
|
|
119
129
|
// 确保存在 subModels.field,便于沿用 FormItemModel 的组件切换/设置步骤(fieldComponent 等)
|
|
120
130
|
const ctx: any = this.context;
|
|
@@ -160,11 +170,15 @@ export class AssignFormItemModel extends FormItemModel {
|
|
|
160
170
|
|
|
161
171
|
getAssignedEntry(): [string, any] | null {
|
|
162
172
|
const name = this.fieldPath;
|
|
163
|
-
if (!name) return null;
|
|
164
|
-
return [name, this.assignValue];
|
|
173
|
+
if (!name || !this.collectionField) return null;
|
|
174
|
+
return [name, normalizeAssignFormValue(this.assignValue, isArrayValueField(this.collectionField))];
|
|
165
175
|
}
|
|
166
176
|
|
|
167
177
|
render() {
|
|
178
|
+
if (!this.collectionField) {
|
|
179
|
+
return <FieldDeletePlaceholder />;
|
|
180
|
+
}
|
|
181
|
+
|
|
168
182
|
// 与 FormItemModel.render 结构保持一致,仅替换内部渲染为 VariableInput + 常量编辑器
|
|
169
183
|
const ctx: any = this.context;
|
|
170
184
|
const collection = ctx.collection;
|
|
@@ -173,6 +187,7 @@ export class AssignFormItemModel extends FormItemModel {
|
|
|
173
187
|
const fieldPath = this.fieldPath;
|
|
174
188
|
const originField = this.subModels?.field as AssignFormTempOriginField | undefined;
|
|
175
189
|
const tempFieldRefreshKey = getAssignFormTempFieldRefreshKey(originField);
|
|
190
|
+
const arrayValueField = isArrayValueField(this.collectionField);
|
|
176
191
|
|
|
177
192
|
const FieldRow: React.FC = () => {
|
|
178
193
|
const [tempRoot, setTempRoot] = React.useState<any>(null);
|
|
@@ -241,6 +256,14 @@ export class AssignFormItemModel extends FormItemModel {
|
|
|
241
256
|
fm?.dispatchEvent?.('beforeRender', undefined, { sequential: true, useCache: true });
|
|
242
257
|
// 为本地枚举型字段补全可选项(仅在未显式传入 options 时处理)
|
|
243
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
|
+
}
|
|
244
267
|
if (!fm?.props?.fieldNames && cfForMultiple?.targetCollection) {
|
|
245
268
|
const targetCol = cfForMultiple.targetCollection;
|
|
246
269
|
const valueKey = cfForMultiple?.targetKey || targetCol?.filterTargetKey || 'id';
|
|
@@ -257,16 +280,17 @@ export class AssignFormItemModel extends FormItemModel {
|
|
|
257
280
|
const ConstantValueEditor: React.FC<any> = (inputProps: any) => {
|
|
258
281
|
React.useEffect(() => {
|
|
259
282
|
if (!tempRoot) return;
|
|
283
|
+
const isArrayField = isArrayValueField(this.collectionField);
|
|
260
284
|
// 同步根 props(变量输入的受控 value/onChange)
|
|
261
285
|
tempRoot.setProps?.({ ...inputProps });
|
|
262
286
|
// 同步到具体字段模型,确保字段组件能触发 onChange
|
|
263
287
|
const fm = tempRoot?.subModels?.fields?.[0];
|
|
264
288
|
if (fm) {
|
|
265
289
|
fm.setProps?.({
|
|
266
|
-
value: inputProps?.value,
|
|
290
|
+
value: normalizeAssignFormValue(inputProps?.value, isArrayField),
|
|
267
291
|
onChange: (...args: unknown[]) => {
|
|
268
292
|
const nextArg = args && args.length ? args[0] : undefined;
|
|
269
|
-
const nextValue = normalizeAssignFormInputValue(nextArg);
|
|
293
|
+
const nextValue = normalizeAssignFormValue(normalizeAssignFormInputValue(nextArg), isArrayField);
|
|
270
294
|
fm.setProps?.({ value: nextValue });
|
|
271
295
|
this.assignValue = nextValue;
|
|
272
296
|
inputProps?.onChange?.(nextValue);
|
|
@@ -319,20 +343,24 @@ export class AssignFormItemModel extends FormItemModel {
|
|
|
319
343
|
{...this.props}
|
|
320
344
|
label={labelText}
|
|
321
345
|
name={namePath}
|
|
322
|
-
initialValue={this.assignValue}
|
|
346
|
+
initialValue={normalizeAssignFormValue(this.assignValue, arrayValueField)}
|
|
323
347
|
valuePropName="__assign_value__"
|
|
324
348
|
trigger="__assign_trigger__"
|
|
325
349
|
validateTrigger="__assign_trigger__"
|
|
326
350
|
>
|
|
327
351
|
{React.createElement((formBindingProps: any) => {
|
|
328
352
|
const formValue = formBindingProps?.__assign_value__;
|
|
329
|
-
const mergedValue =
|
|
353
|
+
const mergedValue = normalizeAssignFormValue(
|
|
354
|
+
typeof formValue === 'undefined' ? this.assignValue : formValue,
|
|
355
|
+
arrayValueField,
|
|
356
|
+
);
|
|
330
357
|
return (
|
|
331
358
|
<FieldValueVariableInput
|
|
332
359
|
value={mergedValue}
|
|
333
360
|
onChange={(v) => {
|
|
334
|
-
|
|
335
|
-
|
|
361
|
+
const nextValue = normalizeAssignFormValue(v, arrayValueField);
|
|
362
|
+
this.assignValue = nextValue;
|
|
363
|
+
formBindingProps?.__assign_trigger__?.(nextValue);
|
|
336
364
|
}}
|
|
337
365
|
baseMetaTree={baseMetaTree}
|
|
338
366
|
constantComponent={ConstantValueEditor}
|
|
@@ -392,7 +420,7 @@ AssignFormItemModel.registerFlow({
|
|
|
392
420
|
},
|
|
393
421
|
defaultParams: (ctx) => {
|
|
394
422
|
return {
|
|
395
|
-
label: (ctx.model as any).collectionField.
|
|
423
|
+
label: (ctx.model as any).collectionField?.title || (ctx.model as any).fieldPath,
|
|
396
424
|
};
|
|
397
425
|
},
|
|
398
426
|
handler(ctx, params) {
|
|
@@ -47,6 +47,146 @@ class MockFlowModelRepository implements IFlowModelRepository {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
describe('assignFieldValuesFlow (editor)', () => {
|
|
50
|
+
it('shows the deleted-field placeholder for a stale assigned value', async () => {
|
|
51
|
+
const engine = new FlowEngine();
|
|
52
|
+
engine.setModelRepository(new MockFlowModelRepository());
|
|
53
|
+
engine.registerModels({
|
|
54
|
+
AssignFormModel,
|
|
55
|
+
AssignFormGridModel,
|
|
56
|
+
AssignFormItemModel,
|
|
57
|
+
InputFieldModel,
|
|
58
|
+
VariableFieldFormModel,
|
|
59
|
+
});
|
|
60
|
+
engine.context.defineProperty('location', { value: { search: '' } });
|
|
61
|
+
engine.context.defineProperty('themeToken', { value: { marginLG: 24 } });
|
|
62
|
+
engine.context.defineProperty('flowSettingsEnabled', { value: true });
|
|
63
|
+
|
|
64
|
+
const main = engine.context.dataSourceManager.getDataSource('main');
|
|
65
|
+
main.addCollection({
|
|
66
|
+
name: 'users',
|
|
67
|
+
fields: [{ name: 'nickname', type: 'string', interface: 'input' }],
|
|
68
|
+
});
|
|
69
|
+
const users = engine.context.dataSourceManager.getCollection('main', 'users');
|
|
70
|
+
|
|
71
|
+
const action = engine.createModel({
|
|
72
|
+
use: 'FlowModel',
|
|
73
|
+
uid: 'act-assign-deleted-field',
|
|
74
|
+
});
|
|
75
|
+
action.setStepParams('assignSettings', 'assignFieldValues', {
|
|
76
|
+
assignedValues: { deletedField: 'stale value' },
|
|
77
|
+
});
|
|
78
|
+
action.context.defineProperty('blockModel', { value: { collection: users } });
|
|
79
|
+
|
|
80
|
+
const step = createAssignFieldValuesStep({ settingsFlowKey: 'assignSettings' });
|
|
81
|
+
const Editor = step.uiSchema().editor?.['x-component'] as React.ComponentType;
|
|
82
|
+
const flowSettingsCtx = new FlowRuntimeContext(action, 'assignSettings', 'settings');
|
|
83
|
+
|
|
84
|
+
render(
|
|
85
|
+
<FlowEngineProvider engine={engine}>
|
|
86
|
+
<ConfigProvider>
|
|
87
|
+
<App>
|
|
88
|
+
<FlowSettingsContextProvider value={flowSettingsCtx}>
|
|
89
|
+
<Editor />
|
|
90
|
+
</FlowSettingsContextProvider>
|
|
91
|
+
</App>
|
|
92
|
+
</ConfigProvider>
|
|
93
|
+
</FlowEngineProvider>,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
await waitFor(() => {
|
|
97
|
+
expect(screen.getByRole('button', { name: /Fields/ })).toBeInTheDocument();
|
|
98
|
+
expect(screen.getByText(/deletedField.*may have been deleted/)).toBeInTheDocument();
|
|
99
|
+
const form = engine.findModelByParentId<AssignFormModel>(action.uid, 'assignForm');
|
|
100
|
+
expect(form).toBeDefined();
|
|
101
|
+
expect(form?.subModels.grid.subModels.items || []).toHaveLength(1);
|
|
102
|
+
expect(form?.getAssignedValues()).toEqual({});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('shows the standard deleted-field placeholder for a persisted assigned field model', async () => {
|
|
107
|
+
const engine = new FlowEngine();
|
|
108
|
+
engine.setModelRepository(new MockFlowModelRepository());
|
|
109
|
+
engine.registerModels({
|
|
110
|
+
AssignFormModel,
|
|
111
|
+
AssignFormGridModel,
|
|
112
|
+
AssignFormItemModel,
|
|
113
|
+
InputFieldModel,
|
|
114
|
+
VariableFieldFormModel,
|
|
115
|
+
});
|
|
116
|
+
engine.context.defineProperty('location', { value: { search: '' } });
|
|
117
|
+
engine.context.defineProperty('themeToken', { value: { marginLG: 24 } });
|
|
118
|
+
engine.context.defineProperty('flowSettingsEnabled', { value: true });
|
|
119
|
+
|
|
120
|
+
const main = engine.context.dataSourceManager.getDataSource('main');
|
|
121
|
+
main.addCollection({
|
|
122
|
+
name: 'users',
|
|
123
|
+
fields: [{ name: 'nickname', type: 'string', interface: 'input' }],
|
|
124
|
+
});
|
|
125
|
+
const users = engine.context.dataSourceManager.getCollection('main', 'users');
|
|
126
|
+
|
|
127
|
+
const action = engine.createModel({
|
|
128
|
+
use: 'FlowModel',
|
|
129
|
+
uid: 'act-assign-persisted-deleted-field',
|
|
130
|
+
});
|
|
131
|
+
action.setStepParams('assignSettings', 'assignFieldValues', {
|
|
132
|
+
assignedValues: { deletedField: 'stale value' },
|
|
133
|
+
});
|
|
134
|
+
action.context.defineProperty('blockModel', { value: { collection: users } });
|
|
135
|
+
|
|
136
|
+
const form = engine.createModel<AssignFormModel>({
|
|
137
|
+
use: 'AssignFormModel',
|
|
138
|
+
uid: 'form-assign-persisted-deleted-field',
|
|
139
|
+
parentId: action.uid,
|
|
140
|
+
subKey: 'assignForm',
|
|
141
|
+
stepParams: {
|
|
142
|
+
resourceSettings: {
|
|
143
|
+
init: {
|
|
144
|
+
dataSourceKey: 'main',
|
|
145
|
+
collectionName: 'users',
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
action.setSubModel('assignForm', form);
|
|
151
|
+
form.subModels.grid.addSubModel('items', {
|
|
152
|
+
use: 'AssignFormItemModel',
|
|
153
|
+
uid: 'item-assign-persisted-deleted-field',
|
|
154
|
+
stepParams: {
|
|
155
|
+
fieldSettings: {
|
|
156
|
+
init: {
|
|
157
|
+
dataSourceKey: 'main',
|
|
158
|
+
collectionName: 'users',
|
|
159
|
+
fieldPath: 'deletedField',
|
|
160
|
+
},
|
|
161
|
+
assignValue: { value: 'stale value' },
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
form.subModels.grid.resetRows(true);
|
|
166
|
+
|
|
167
|
+
const step = createAssignFieldValuesStep({ settingsFlowKey: 'assignSettings' });
|
|
168
|
+
const Editor = step.uiSchema().editor?.['x-component'] as React.ComponentType;
|
|
169
|
+
const flowSettingsCtx = new FlowRuntimeContext(action, 'assignSettings', 'settings');
|
|
170
|
+
|
|
171
|
+
render(
|
|
172
|
+
<FlowEngineProvider engine={engine}>
|
|
173
|
+
<ConfigProvider>
|
|
174
|
+
<App>
|
|
175
|
+
<FlowSettingsContextProvider value={flowSettingsCtx}>
|
|
176
|
+
<Editor />
|
|
177
|
+
</FlowSettingsContextProvider>
|
|
178
|
+
</App>
|
|
179
|
+
</ConfigProvider>
|
|
180
|
+
</FlowEngineProvider>,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
await waitFor(() => {
|
|
184
|
+
expect(screen.getByText(/deletedField.*may have been deleted/)).toBeInTheDocument();
|
|
185
|
+
expect(form.subModels.grid.subModels.items).toHaveLength(1);
|
|
186
|
+
expect(form.getAssignedValues()).toEqual({});
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
50
190
|
it('repairs AssignFormModel resource init and clears cached collection', async () => {
|
|
51
191
|
const engine = new FlowEngine();
|
|
52
192
|
engine.setModelRepository(new MockFlowModelRepository());
|
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
|
+
}
|
|
@@ -21,6 +21,35 @@ describe('FilterFormGridModel.toggleFormFieldsCollapse', () => {
|
|
|
21
21
|
engine.registerModels({ FilterFormGridModel });
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
+
it.each([false, true])('preserves collapsed fields in mobile layout with settings enabled: %s', (settingsEnabled) => {
|
|
25
|
+
engine.flowSettings[settingsEnabled ? 'enable' : 'disable']();
|
|
26
|
+
const rows = {
|
|
27
|
+
first: [['field-1']],
|
|
28
|
+
second: [['field-2']],
|
|
29
|
+
third: [['field-3']],
|
|
30
|
+
};
|
|
31
|
+
const model = engine.createModel<FilterFormGridModel>({
|
|
32
|
+
uid: 'mobile-filter-grid',
|
|
33
|
+
use: 'FilterFormGridModel',
|
|
34
|
+
props: { rows },
|
|
35
|
+
subModels: {
|
|
36
|
+
items: ['field-1', 'field-2', 'field-3'].map((uid) => ({ use: 'FlowModel', uid })),
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
model.context.defineProperty('isMobileLayout', { value: true });
|
|
40
|
+
model.setStepParams(GRID_FLOW_KEY, GRID_STEP, { rows });
|
|
41
|
+
type VisibleLayoutReader = { getVisibleLayout(): { rows: Record<string, string[][]> } };
|
|
42
|
+
const renderedItems = () =>
|
|
43
|
+
Object.values((model as unknown as VisibleLayoutReader).getVisibleLayout().rows).flat(2);
|
|
44
|
+
|
|
45
|
+
expect(renderedItems()).toEqual(['field-1', 'field-2', 'field-3']);
|
|
46
|
+
model.toggleFormFieldsCollapse(true, 1);
|
|
47
|
+
expect(renderedItems()).toEqual(['field-1']);
|
|
48
|
+
model.toggleFormFieldsCollapse(false, 1);
|
|
49
|
+
expect(renderedItems()).toEqual(['field-1', 'field-2', 'field-3']);
|
|
50
|
+
expect(model.getStepParams(GRID_FLOW_KEY, GRID_STEP).rows).toEqual(rows);
|
|
51
|
+
});
|
|
52
|
+
|
|
24
53
|
it('uses rowOrder from the full layout when collapsing after reorder', () => {
|
|
25
54
|
const model = engine.createModel<FilterFormGridModel>({
|
|
26
55
|
uid: 'filter-grid-collapse-order',
|
|
@@ -351,7 +351,7 @@ export const FieldComponentProps: React.FC<{ fieldModel: string; source: string[
|
|
|
351
351
|
<FormItem label={t('Options')}>
|
|
352
352
|
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
|
353
353
|
{options.map((option: any, index: number) => (
|
|
354
|
-
<Space key={
|
|
354
|
+
<Space key={index} style={{ width: '100%' }} size={8} wrap align="start">
|
|
355
355
|
<Input
|
|
356
356
|
style={{ flex: 1, minWidth: 120 }}
|
|
357
357
|
placeholder={t('Option label')}
|