@nocobase/client-v2 2.1.10 → 2.1.12
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/flow/actions/afterSuccess.d.ts +8 -1
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
- package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
- package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
- package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
- package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
- package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
- package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
- package/es/index.mjs +100 -84
- package/lib/index.js +126 -110
- package/package.json +7 -7
- package/src/collection-manager/field-configure.ts +1 -1
- package/src/collection-manager/interfaces/id.ts +1 -1
- package/src/collection-manager/interfaces/m2m.tsx +2 -2
- package/src/collection-manager/interfaces/m2o.tsx +2 -2
- package/src/collection-manager/interfaces/nanoid.ts +1 -1
- package/src/collection-manager/interfaces/o2m.tsx +2 -2
- package/src/collection-manager/interfaces/obo.tsx +2 -2
- package/src/collection-manager/interfaces/oho.tsx +2 -2
- package/src/collection-manager/interfaces/properties/index.ts +2 -2
- package/src/collection-manager/interfaces/uuid.ts +1 -1
- package/src/flow/__tests__/getKey.test.ts +7 -0
- package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
- package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
- package/src/flow/actions/__tests__/dataScopeFilter.test.ts +58 -0
- package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +104 -0
- package/src/flow/actions/afterSuccess.tsx +142 -3
- package/src/flow/actions/dataScopeFilter.ts +10 -0
- package/src/flow/actions/index.ts +1 -1
- package/src/flow/actions/openView.tsx +38 -4
- package/src/flow/admin-shell/BaseLayoutModel.tsx +25 -3
- package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +5 -2
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +13 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +82 -0
- package/src/flow/components/FieldAssignRulesEditor.tsx +66 -9
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +427 -0
- package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
- package/src/flow/components/fieldAssignOptions.ts +155 -27
- package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
- package/src/flow/models/base/ActionModelCore.tsx +6 -4
- package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
- package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
- package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
- package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
- package/src/flow/models/blocks/form/submitHandler.ts +17 -3
- package/src/flow/models/blocks/js-block/JSBlock.tsx +223 -2
- package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +35 -12
- package/src/flow/models/blocks/table/TableBlockModel.tsx +25 -14
- package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +57 -1
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
- package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
- package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
- package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
- package/src/flow/resolveViewParamsToViewList.tsx +11 -3
- package/src/settings-center/SystemSettingsPage.tsx +1 -1
|
@@ -57,12 +57,11 @@ function getItemLabel(itemModel: any, t: (key: string) => string): string {
|
|
|
57
57
|
return '';
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
function
|
|
61
|
-
|
|
60
|
+
function splitFieldPath(path: string): string[] {
|
|
61
|
+
return String(path || '')
|
|
62
62
|
.split('.')
|
|
63
63
|
.map((s) => s.trim())
|
|
64
64
|
.filter(Boolean);
|
|
65
|
-
return segs.length ? segs[segs.length - 1] : '';
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
type CollectionLike = {
|
|
@@ -70,6 +69,15 @@ type CollectionLike = {
|
|
|
70
69
|
getFields?: () => unknown[];
|
|
71
70
|
};
|
|
72
71
|
|
|
72
|
+
type CollectionFieldLike = {
|
|
73
|
+
name?: unknown;
|
|
74
|
+
title?: unknown;
|
|
75
|
+
interface?: unknown;
|
|
76
|
+
target?: unknown;
|
|
77
|
+
isAssociationField?: () => boolean;
|
|
78
|
+
targetCollection?: CollectionLike | null;
|
|
79
|
+
};
|
|
80
|
+
|
|
73
81
|
function getCollectionFromFormBlockModel(model: any): CollectionLike | null {
|
|
74
82
|
if (!model || typeof model !== 'object') return null;
|
|
75
83
|
const collection = (model as any)?.collection || (model as any)?.context?.collection;
|
|
@@ -89,14 +97,7 @@ export function buildFieldAssignCascaderOptionsFromCollection(
|
|
|
89
97
|
const out: FieldAssignCascaderOption[] = [];
|
|
90
98
|
for (const rawField of fields) {
|
|
91
99
|
if (!rawField) continue;
|
|
92
|
-
const f = rawField as
|
|
93
|
-
name?: unknown;
|
|
94
|
-
title?: unknown;
|
|
95
|
-
interface?: unknown;
|
|
96
|
-
target?: unknown;
|
|
97
|
-
isAssociationField?: () => boolean;
|
|
98
|
-
targetCollection?: any;
|
|
99
|
-
};
|
|
100
|
+
const f = rawField as CollectionFieldLike;
|
|
100
101
|
const fieldInterface = typeof f.interface === 'string' ? f.interface : undefined;
|
|
101
102
|
if (!fieldInterface) continue;
|
|
102
103
|
if (fieldInterface === 'formula') continue;
|
|
@@ -123,23 +124,143 @@ export function buildFieldAssignCascaderOptionsFromCollection(
|
|
|
123
124
|
function mergeRootOptions(
|
|
124
125
|
configured: FieldAssignCascaderOption[],
|
|
125
126
|
allFields: FieldAssignCascaderOption[],
|
|
127
|
+
): FieldAssignCascaderOption[] {
|
|
128
|
+
return mergeCascaderOptions(configured, allFields);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function mergeCascaderOptions(
|
|
132
|
+
configured: FieldAssignCascaderOption[],
|
|
133
|
+
allFields: FieldAssignCascaderOption[],
|
|
126
134
|
): FieldAssignCascaderOption[] {
|
|
127
135
|
const configuredList = Array.isArray(configured) ? configured : [];
|
|
128
136
|
const allList = Array.isArray(allFields) ? allFields : [];
|
|
137
|
+
const result = configuredList.map((item) => cloneCascaderOption(item));
|
|
129
138
|
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
const v = it?.value ? String(it.value) : '';
|
|
133
|
-
if (v) configuredValues.add(v);
|
|
139
|
+
for (const item of allList) {
|
|
140
|
+
mergeCascaderOptionInto(result, item);
|
|
134
141
|
}
|
|
135
142
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function cloneCascaderOption(option: FieldAssignCascaderOption): FieldAssignCascaderOption {
|
|
147
|
+
return {
|
|
148
|
+
...option,
|
|
149
|
+
children: option.children?.map((child) => cloneCascaderOption(child)),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function mergeCascaderOptionInto(options: FieldAssignCascaderOption[], source: FieldAssignCascaderOption) {
|
|
154
|
+
const value = source?.value ? String(source.value) : '';
|
|
155
|
+
if (!value) return;
|
|
156
|
+
|
|
157
|
+
const existing = options.find((item) => String(item?.value || '') === value);
|
|
158
|
+
if (!existing) {
|
|
159
|
+
options.push(cloneCascaderOption(source));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!existing.label && source.label) {
|
|
164
|
+
existing.label = source.label;
|
|
165
|
+
}
|
|
166
|
+
if (source.isLeaf === false || existing.children?.length || source.children?.length) {
|
|
167
|
+
existing.isLeaf = false;
|
|
168
|
+
}
|
|
169
|
+
if (source.loading) {
|
|
170
|
+
existing.loading = source.loading;
|
|
171
|
+
}
|
|
172
|
+
if (source.children?.length) {
|
|
173
|
+
existing.children = mergeCascaderOptions(existing.children || [], source.children);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function getCollectionField(collection: CollectionLike | null, name: string): CollectionFieldLike | undefined {
|
|
178
|
+
const field = typeof collection?.getField === 'function' ? collection.getField(name) : undefined;
|
|
179
|
+
return field && typeof field === 'object' ? (field as CollectionFieldLike) : undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isAssociationFieldLike(field?: CollectionFieldLike | null) {
|
|
183
|
+
return !!(field?.isAssociationField?.() || field?.target || field?.targetCollection);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function getFieldLabelFromCollection(collection: CollectionLike | null, name: string, t: (key: string) => string) {
|
|
187
|
+
const field = getCollectionField(collection, name);
|
|
188
|
+
const title = typeof field?.title === 'string' && field.title ? field.title : name;
|
|
189
|
+
return t(title);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function getAssociationDepthFromFieldPath(
|
|
193
|
+
rootCollection: CollectionLike | null,
|
|
194
|
+
segments: string[],
|
|
195
|
+
): number | undefined {
|
|
196
|
+
if (!rootCollection) return undefined;
|
|
197
|
+
|
|
198
|
+
let collection: CollectionLike | null = rootCollection;
|
|
199
|
+
let associationDepth = 0;
|
|
200
|
+
for (let index = 0; index < segments.length; index++) {
|
|
201
|
+
const field = getCollectionField(collection, segments[index]);
|
|
202
|
+
if (!field) return undefined;
|
|
203
|
+
|
|
204
|
+
if (isAssociationFieldLike(field)) {
|
|
205
|
+
associationDepth += 1;
|
|
206
|
+
collection = field.targetCollection || null;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (index < segments.length - 1) return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return associationDepth;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function shouldIncludeConfiguredFieldPath(options: {
|
|
217
|
+
segments: string[];
|
|
218
|
+
rootCollection: CollectionLike | null;
|
|
219
|
+
leafField?: CollectionFieldLike | null;
|
|
220
|
+
maxAssociationFieldDepth: number;
|
|
221
|
+
}) {
|
|
222
|
+
const resolvedAssociationDepth = getAssociationDepthFromFieldPath(options.rootCollection, options.segments);
|
|
223
|
+
if (typeof resolvedAssociationDepth === 'number') {
|
|
224
|
+
return resolvedAssociationDepth <= options.maxAssociationFieldDepth;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const leafIsAssociation = isAssociationFieldLike(options.leafField);
|
|
228
|
+
const maxPathLength = leafIsAssociation ? options.maxAssociationFieldDepth : options.maxAssociationFieldDepth + 1;
|
|
229
|
+
return options.segments.length <= maxPathLength;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function buildNestedOptionFromFieldPath(options: {
|
|
233
|
+
targetPath: string;
|
|
234
|
+
leaf: FieldAssignCascaderOption;
|
|
235
|
+
rootCollection: CollectionLike | null;
|
|
236
|
+
t: (key: string) => string;
|
|
237
|
+
}): FieldAssignCascaderOption {
|
|
238
|
+
const segments = splitFieldPath(options.targetPath);
|
|
239
|
+
if (segments.length <= 1) {
|
|
240
|
+
return options.leaf;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const build = (index: number, collection: CollectionLike | null): FieldAssignCascaderOption => {
|
|
244
|
+
const value = segments[index];
|
|
245
|
+
const isLast = index === segments.length - 1;
|
|
246
|
+
if (isLast) {
|
|
247
|
+
return {
|
|
248
|
+
...options.leaf,
|
|
249
|
+
value,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const field = getCollectionField(collection, value);
|
|
254
|
+
const nextCollection = field?.targetCollection || null;
|
|
255
|
+
return {
|
|
256
|
+
label: getFieldLabelFromCollection(collection, value, options.t),
|
|
257
|
+
value,
|
|
258
|
+
isLeaf: false,
|
|
259
|
+
children: [build(index + 1, nextCollection)],
|
|
260
|
+
};
|
|
261
|
+
};
|
|
141
262
|
|
|
142
|
-
return
|
|
263
|
+
return build(0, options.rootCollection);
|
|
143
264
|
}
|
|
144
265
|
|
|
145
266
|
export function collectFieldAssignCascaderOptions(options: {
|
|
@@ -167,7 +288,8 @@ export function collectFieldAssignCascaderOptions(options: {
|
|
|
167
288
|
const targetPath = getItemFieldPath(item);
|
|
168
289
|
if (!targetPath) continue;
|
|
169
290
|
|
|
170
|
-
const
|
|
291
|
+
const segments = splitFieldPath(targetPath);
|
|
292
|
+
const seg = segments[segments.length - 1];
|
|
171
293
|
if (!seg) continue;
|
|
172
294
|
|
|
173
295
|
const node: FieldAssignCascaderOption = {
|
|
@@ -179,10 +301,16 @@ export function collectFieldAssignCascaderOptions(options: {
|
|
|
179
301
|
const childItems = fieldModel?.subModels?.grid?.subModels?.items;
|
|
180
302
|
const cf: (CollectionField & { target?: unknown; targetCollection?: unknown }) | undefined =
|
|
181
303
|
fieldModel?.context?.collectionField;
|
|
182
|
-
const isAssociation =
|
|
304
|
+
const isAssociation = isAssociationFieldLike(cf);
|
|
183
305
|
const hasTargetCollection = !!cf?.targetCollection;
|
|
184
|
-
|
|
185
|
-
|
|
306
|
+
if (
|
|
307
|
+
!shouldIncludeConfiguredFieldPath({
|
|
308
|
+
segments,
|
|
309
|
+
rootCollection,
|
|
310
|
+
leafField: cf,
|
|
311
|
+
maxAssociationFieldDepth,
|
|
312
|
+
})
|
|
313
|
+
) {
|
|
186
314
|
continue;
|
|
187
315
|
}
|
|
188
316
|
|
|
@@ -203,7 +331,7 @@ export function collectFieldAssignCascaderOptions(options: {
|
|
|
203
331
|
node.isLeaf = true;
|
|
204
332
|
}
|
|
205
333
|
}
|
|
206
|
-
out
|
|
334
|
+
mergeCascaderOptionInto(out, buildNestedOptionFromFieldPath({ targetPath, leaf: node, rootCollection, t }));
|
|
207
335
|
continue;
|
|
208
336
|
}
|
|
209
337
|
|
|
@@ -215,7 +343,7 @@ export function collectFieldAssignCascaderOptions(options: {
|
|
|
215
343
|
node.isLeaf = true;
|
|
216
344
|
}
|
|
217
345
|
|
|
218
|
-
out
|
|
346
|
+
mergeCascaderOptionInto(out, buildNestedOptionFromFieldPath({ targetPath, leaf: node, rootCollection, t }));
|
|
219
347
|
}
|
|
220
348
|
|
|
221
349
|
return out;
|
|
@@ -25,7 +25,7 @@ export function ActionWithoutPermission(props) {
|
|
|
25
25
|
const dataSourcePrefix = `${t(dataSource.displayName || dataSource.key)} > `;
|
|
26
26
|
const collectionPrefix = collection ? `${t(collection.title) || collection.name || collection.tableName} ` : '';
|
|
27
27
|
return `${dataSourcePrefix}${collectionPrefix}`;
|
|
28
|
-
}, []);
|
|
28
|
+
}, [collection, dataSource.displayName, dataSource.key, t]);
|
|
29
29
|
const { actionName } = props?.forbidden || model.forbidden;
|
|
30
30
|
const messageValue = useMemo(() => {
|
|
31
31
|
return t(
|
|
@@ -143,10 +143,11 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
|
|
|
143
143
|
renderButton() {
|
|
144
144
|
const { iconOnly, ...props } = this.props;
|
|
145
145
|
const icon = this.getIcon() ? <Icon type={this.getIcon() as any} /> : undefined;
|
|
146
|
+
const titleContent = iconOnly && icon ? null : props.children || this.getTitle();
|
|
146
147
|
|
|
147
148
|
return (
|
|
148
149
|
<Button {...props} onClick={this.onClick.bind(this)} icon={icon}>
|
|
149
|
-
{
|
|
150
|
+
{titleContent}
|
|
150
151
|
</Button>
|
|
151
152
|
);
|
|
152
153
|
}
|
|
@@ -162,11 +163,12 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
|
|
|
162
163
|
renderHiddenInConfig(): React.ReactNode | undefined {
|
|
163
164
|
const { iconOnly, ...props } = this.props;
|
|
164
165
|
const icon = this.getIcon() ? <Icon type={this.getIcon() as any} /> : undefined;
|
|
166
|
+
const titleContent = iconOnly && icon ? null : props.children || this.getTitle();
|
|
165
167
|
if (this.forbidden) {
|
|
166
168
|
return (
|
|
167
169
|
<ActionWithoutPermission>
|
|
168
170
|
<Button {...props} onClick={this.onClick.bind(this)} icon={icon} style={{ opacity: '0.3' }}>
|
|
169
|
-
{
|
|
171
|
+
{titleContent}
|
|
170
172
|
</Button>
|
|
171
173
|
</ActionWithoutPermission>
|
|
172
174
|
);
|
|
@@ -174,7 +176,7 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
|
|
|
174
176
|
return (
|
|
175
177
|
<Tooltip title={this.context.t('The button is hidden and only visible when the UI Editor is active')}>
|
|
176
178
|
<Button {...props} onClick={this.onClick.bind(this)} icon={icon} style={{ opacity: '0.3' }}>
|
|
177
|
-
{
|
|
179
|
+
{titleContent}
|
|
178
180
|
</Button>
|
|
179
181
|
</Tooltip>
|
|
180
182
|
);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { render, screen } from '@nocobase/test/client';
|
|
2
|
+
import { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
|
|
3
|
+
import { App, ConfigProvider } from 'antd';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import { ActionModel } from '../ActionModel';
|
|
7
|
+
|
|
8
|
+
class NoIconActionModel extends ActionModel {
|
|
9
|
+
defaultProps = {
|
|
10
|
+
type: 'link' as const,
|
|
11
|
+
title: 'Open details',
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('ActionModel rendering', () => {
|
|
16
|
+
it('shows the title when iconOnly is true but no icon is configured', () => {
|
|
17
|
+
const engine = new FlowEngine();
|
|
18
|
+
engine.registerModels({ NoIconActionModel });
|
|
19
|
+
const model = engine.createModel<NoIconActionModel>({
|
|
20
|
+
use: 'NoIconActionModel',
|
|
21
|
+
props: {
|
|
22
|
+
title: 'Open details',
|
|
23
|
+
iconOnly: true,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
render(
|
|
28
|
+
<FlowEngineProvider engine={engine}>
|
|
29
|
+
<ConfigProvider>
|
|
30
|
+
<App>{model.render()}</App>
|
|
31
|
+
</ConfigProvider>
|
|
32
|
+
</FlowEngineProvider>,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
expect(screen.getByRole('button', { name: 'Open details' })).toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -210,7 +210,7 @@ FilterFormCustomFieldModel.registerFlow({
|
|
|
210
210
|
'x-decorator': 'FormItem',
|
|
211
211
|
required: true,
|
|
212
212
|
description:
|
|
213
|
-
'{{t("Randomly generated and can be modified. Support letters, numbers and underscores, must start with
|
|
213
|
+
'{{t("Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.")}}',
|
|
214
214
|
},
|
|
215
215
|
source: {
|
|
216
216
|
type: 'array',
|
|
@@ -117,7 +117,7 @@ FormSubmitActionModel.registerFlow({
|
|
|
117
117
|
try {
|
|
118
118
|
ctx.model.setProps('loading', true);
|
|
119
119
|
const { submitHandler } = await import('./submitHandler');
|
|
120
|
-
await submitHandler(ctx, params);
|
|
120
|
+
return await submitHandler(ctx, params);
|
|
121
121
|
} catch (error) {
|
|
122
122
|
ctx.model.setProps('loading', false);
|
|
123
123
|
if (error instanceof FlowExitAllException) {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { SingleRecordResource } from '@nocobase/flow-engine';
|
|
11
11
|
import { describe, expect, it, vi } from 'vitest';
|
|
12
|
+
import { EditFormModel } from '../EditFormModel';
|
|
12
13
|
import { submitHandler } from '../submitHandler';
|
|
13
14
|
|
|
14
15
|
describe('submitHandler', () => {
|
|
@@ -52,13 +53,14 @@ describe('submitHandler', () => {
|
|
|
52
53
|
t: (value: string) => value,
|
|
53
54
|
};
|
|
54
55
|
|
|
55
|
-
await submitHandler(ctx, {
|
|
56
|
+
const responseRecord = await submitHandler(ctx, {
|
|
56
57
|
assignedValues: {
|
|
57
58
|
status: 'published',
|
|
58
59
|
reviewer: 'Alice',
|
|
59
60
|
},
|
|
60
61
|
});
|
|
61
62
|
|
|
63
|
+
expect(responseRecord).toEqual({ id: 1 });
|
|
62
64
|
expect(resource.save).toHaveBeenCalledWith(
|
|
63
65
|
{
|
|
64
66
|
title: 'Draft title',
|
|
@@ -68,4 +70,55 @@ describe('submitHandler', () => {
|
|
|
68
70
|
undefined,
|
|
69
71
|
);
|
|
70
72
|
});
|
|
73
|
+
|
|
74
|
+
it('returns the refreshed record for edit forms when the save response is empty', async () => {
|
|
75
|
+
const refreshedRecord = { id: 2, title: 'Updated title' };
|
|
76
|
+
const resource = Object.create(SingleRecordResource.prototype);
|
|
77
|
+
resource.getMeta = vi.fn((key: string) => (key === 'currentFilterByTk' ? 2 : undefined));
|
|
78
|
+
resource.setFilterByTk = vi.fn(() => resource);
|
|
79
|
+
resource.save = vi.fn(async () => undefined);
|
|
80
|
+
resource.refresh = vi.fn(async () => undefined);
|
|
81
|
+
resource.getData = vi.fn(() => refreshedRecord);
|
|
82
|
+
|
|
83
|
+
const blockModel = Object.create(EditFormModel.prototype);
|
|
84
|
+
const form = {
|
|
85
|
+
validateFields: vi.fn(async () => undefined),
|
|
86
|
+
getFieldsValue: vi.fn(() => ({
|
|
87
|
+
title: 'Updated title',
|
|
88
|
+
})),
|
|
89
|
+
};
|
|
90
|
+
Object.defineProperty(blockModel, 'context', { value: { form } });
|
|
91
|
+
Object.defineProperty(blockModel, 'collection', {
|
|
92
|
+
value: {
|
|
93
|
+
name: 'posts',
|
|
94
|
+
getFilterByTK: (record: { id?: number }) => record.id,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
blockModel.resetUserModifiedFields = vi.fn();
|
|
98
|
+
|
|
99
|
+
const responseRecord = await submitHandler(
|
|
100
|
+
{
|
|
101
|
+
resource,
|
|
102
|
+
blockModel,
|
|
103
|
+
model: {
|
|
104
|
+
getStepParams: vi.fn(),
|
|
105
|
+
},
|
|
106
|
+
message: {
|
|
107
|
+
error: vi.fn(),
|
|
108
|
+
},
|
|
109
|
+
t: (value: string) => value,
|
|
110
|
+
},
|
|
111
|
+
{},
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
expect(responseRecord).toBe(refreshedRecord);
|
|
115
|
+
expect(resource.setFilterByTk).toHaveBeenCalledWith(2);
|
|
116
|
+
expect(resource.save).toHaveBeenCalledWith(
|
|
117
|
+
{
|
|
118
|
+
title: 'Updated title',
|
|
119
|
+
},
|
|
120
|
+
undefined,
|
|
121
|
+
);
|
|
122
|
+
expect(resource.refresh).toHaveBeenCalled();
|
|
123
|
+
});
|
|
71
124
|
});
|
|
@@ -12,6 +12,16 @@ import { mergeAssignFieldValues, resolveAssignFieldValues } from '../assign-form
|
|
|
12
12
|
import type { FormBlockModel } from './FormBlockModel';
|
|
13
13
|
import { omitHiddenModelValuesFromSubmit, shouldSkipSubmitValidation, validateSubmitForm } from './submitValues';
|
|
14
14
|
|
|
15
|
+
function getResponseRecord(response: unknown) {
|
|
16
|
+
if (!response || typeof response !== 'object') {
|
|
17
|
+
return response;
|
|
18
|
+
}
|
|
19
|
+
if ('data' in response) {
|
|
20
|
+
return (response as { data?: unknown }).data;
|
|
21
|
+
}
|
|
22
|
+
return response;
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
export async function submitHandler(ctx, params, cb?: (values?: any, filterByTk?: any) => void) {
|
|
16
26
|
const resource = ctx.resource;
|
|
17
27
|
const blockModel = ctx.blockModel as FormBlockModel;
|
|
@@ -41,30 +51,34 @@ export async function submitHandler(ctx, params, cb?: (values?: any, filterByTk?
|
|
|
41
51
|
resource.setFilterByTk(currentFilterByTk);
|
|
42
52
|
}
|
|
43
53
|
}
|
|
44
|
-
const data:
|
|
54
|
+
const data: unknown = cb ? await cb(values) : await resource.save(values, params.requestConfig);
|
|
55
|
+
let responseRecord = getResponseRecord(data);
|
|
45
56
|
if (isEditFormModel) {
|
|
46
57
|
resource.isNewRecord = false;
|
|
47
58
|
// 编辑表单保存成功后,表单应回到“已同步”状态:下一次刷新应允许覆盖为服务端值
|
|
48
59
|
blockModel.resetUserModifiedFields?.();
|
|
49
60
|
await resource.refresh();
|
|
61
|
+
responseRecord = responseRecord ?? resource.getData?.();
|
|
50
62
|
} else {
|
|
51
63
|
blockModel.form.resetFields();
|
|
52
64
|
blockModel.emitter.emit('onFieldReset');
|
|
53
65
|
blockModel.resetUserModifiedFields?.();
|
|
54
66
|
blockModel.formValueRuntime?.resetAfterFormReset?.();
|
|
55
67
|
if (ctx.view.inputArgs.collectionName === blockModel.collection.name && ctx.view.inputArgs.onChange) {
|
|
56
|
-
ctx.view.inputArgs.onChange(
|
|
68
|
+
ctx.view.inputArgs.onChange(responseRecord);
|
|
57
69
|
}
|
|
58
70
|
}
|
|
71
|
+
return responseRecord;
|
|
59
72
|
} else if (resource instanceof MultiRecordResource) {
|
|
60
73
|
const currentFilterByTk = resource.getMeta('currentFilterByTk');
|
|
61
74
|
if (!currentFilterByTk) {
|
|
62
75
|
ctx.message.error(ctx.t('No filterByTk found for multi-record resource.'));
|
|
63
76
|
return;
|
|
64
77
|
}
|
|
65
|
-
|
|
78
|
+
const data = cb
|
|
66
79
|
? await cb(values, currentFilterByTk)
|
|
67
80
|
: await resource.update(currentFilterByTk, values, params.requestConfig);
|
|
68
81
|
blockModel.resetUserModifiedFields?.();
|
|
82
|
+
return getResponseRecord(data) ?? blockModel.getCurrentRecord?.();
|
|
69
83
|
}
|
|
70
84
|
}
|