@nocobase/client-v2 2.3.0-beta.2 → 2.3.0-beta.20
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/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 +79 -79
- package/lib/index.js +84 -84
- 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/__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 +7 -1
- 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 +9 -0
- 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/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
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
type QuickEditViewContainerLike = {
|
|
11
|
+
update?: (newConfig: { preventClose?: boolean }) => unknown;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type QuickEditFieldModelLike = {
|
|
15
|
+
parent?: {
|
|
16
|
+
use?: string;
|
|
17
|
+
viewContainer?: QuickEditViewContainerLike;
|
|
18
|
+
} | null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function getQuickEditViewContainer(model: QuickEditFieldModelLike | undefined) {
|
|
22
|
+
const parent = model?.parent;
|
|
23
|
+
if (parent?.use !== 'QuickEditFormModel') {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return parent.viewContainer;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Keeps the quick edit popover open while a nested dropdown (Select / Cascader) is open. The outside click that
|
|
31
|
+
* closes the dropdown would otherwise also close the popover and drop the unsaved edit. Releasing is deferred so the
|
|
32
|
+
* closing click itself never reaches the popover as a close request.
|
|
33
|
+
*/
|
|
34
|
+
export function syncQuickEditPreventClose(model: QuickEditFieldModelLike | undefined, dropdownOpen: boolean) {
|
|
35
|
+
const container = getQuickEditViewContainer(model);
|
|
36
|
+
if (!container?.update) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (dropdownOpen) {
|
|
40
|
+
container.update({ preventClose: true });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
container.update?.({ preventClose: false });
|
|
45
|
+
}, 0);
|
|
46
|
+
}
|
|
@@ -37,14 +37,30 @@ export class JSItemActionModel extends ActionModel {
|
|
|
37
37
|
static scene = ActionSceneEnum.all;
|
|
38
38
|
|
|
39
39
|
render() {
|
|
40
|
-
return
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
ref={this.context.ref}
|
|
43
|
+
style={{
|
|
44
|
+
display: 'inline-flex',
|
|
45
|
+
minHeight: 22,
|
|
46
|
+
minWidth: this.context.flowSettingsEnabled ? this.context.themeToken?.controlHeightLG ?? 40 : undefined,
|
|
47
|
+
alignItems: 'center',
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
renderHiddenInConfig(): React.ReactNode | undefined {
|
|
44
54
|
return (
|
|
45
55
|
<div
|
|
46
56
|
ref={this.context.ref}
|
|
47
|
-
style={{
|
|
57
|
+
style={{
|
|
58
|
+
display: 'inline-flex',
|
|
59
|
+
minHeight: 22,
|
|
60
|
+
minWidth: this.context.themeToken?.controlHeightLG ?? 40,
|
|
61
|
+
alignItems: 'center',
|
|
62
|
+
opacity: 0.3,
|
|
63
|
+
}}
|
|
48
64
|
/>
|
|
49
65
|
);
|
|
50
66
|
}
|
|
@@ -62,7 +78,7 @@ export class JSItemActionModel extends ActionModel {
|
|
|
62
78
|
}
|
|
63
79
|
|
|
64
80
|
if (this._mountedOnce && this.context.ref?.current) {
|
|
65
|
-
|
|
81
|
+
this.applyFlow('jsSettings');
|
|
66
82
|
}
|
|
67
83
|
this._mountedOnce = true;
|
|
68
84
|
}
|
|
@@ -46,21 +46,23 @@ LinkActionModel.registerFlow({
|
|
|
46
46
|
|
|
47
47
|
if (link) {
|
|
48
48
|
const isExternalLink = isURL(link);
|
|
49
|
+
// embed 是页面容器,不应销毁;仅弹窗视图在站内同窗口跳转后销毁。
|
|
50
|
+
const shouldDestroyView =
|
|
51
|
+
!!ctx.view &&
|
|
52
|
+
shouldDestroyViewAfterLinkNavigation({
|
|
53
|
+
openInNewWindow,
|
|
54
|
+
isExternalLink,
|
|
55
|
+
viewType: ctx.view.type,
|
|
56
|
+
});
|
|
49
57
|
handleLinkNavigation({
|
|
50
58
|
link,
|
|
51
59
|
openInNewWindow,
|
|
52
60
|
router: ctx.router,
|
|
53
61
|
isExternalLink,
|
|
62
|
+
// 弹窗会被销毁,用 replace 丢弃弹窗路由;页面(含移动端 embed)跳转需保留历史,以便逐级返回。
|
|
63
|
+
replace: shouldDestroyView,
|
|
54
64
|
});
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
ctx.view &&
|
|
58
|
-
shouldDestroyViewAfterLinkNavigation({
|
|
59
|
-
openInNewWindow,
|
|
60
|
-
isExternalLink,
|
|
61
|
-
viewType: ctx.view.type,
|
|
62
|
-
})
|
|
63
|
-
) {
|
|
65
|
+
if (shouldDestroyView) {
|
|
64
66
|
ctx.view.destroy();
|
|
65
67
|
}
|
|
66
68
|
} else {
|
|
@@ -25,10 +25,11 @@ export function handleLinkNavigation(options: {
|
|
|
25
25
|
openInNewWindow?: boolean;
|
|
26
26
|
router: { navigate: (to: string, options?: { replace?: boolean }) => void };
|
|
27
27
|
isExternalLink: boolean;
|
|
28
|
+
replace?: boolean;
|
|
28
29
|
setLocationHref?: (url: string) => void;
|
|
29
30
|
openWindow?: (url?: string, target?: string) => Window | null;
|
|
30
31
|
}) {
|
|
31
|
-
const { link, openInNewWindow, router, isExternalLink } = options;
|
|
32
|
+
const { link, openInNewWindow, router, isExternalLink, replace = false } = options;
|
|
32
33
|
const setLocationHref = options.setLocationHref ?? ((url: string) => (window.location.href = url));
|
|
33
34
|
const openWindow = options.openWindow ?? ((url?: string, target?: string) => window.open(url, target));
|
|
34
35
|
|
|
@@ -44,7 +45,8 @@ export function handleLinkNavigation(options: {
|
|
|
44
45
|
|
|
45
46
|
// 该辅助方法仅负责导航,是否关闭弹窗由调用方按场景决定;
|
|
46
47
|
// 在弹窗上下文中,错误的关闭时机会把新路由回滚为原页面。
|
|
47
|
-
|
|
48
|
+
// 默认 push 新的历史记录,保证浏览器/手机右滑返回能逐级回退;仅在需要丢弃当前弹窗路由时由调用方传 replace。
|
|
49
|
+
router.navigate(link, { replace });
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
/**
|
|
@@ -8,13 +8,59 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
|
-
import { render, screen, waitFor } from '@nocobase/test/client';
|
|
11
|
+
import { act, render, screen, waitFor } from '@nocobase/test/client';
|
|
12
12
|
import { App, ConfigProvider } from 'antd';
|
|
13
13
|
import { describe, expect, it } from 'vitest';
|
|
14
14
|
import { FlowEngine, FlowEngineProvider, FlowModelRenderer } from '@nocobase/flow-engine';
|
|
15
15
|
import { JSItemActionModel } from '../JSItemActionModel';
|
|
16
16
|
|
|
17
17
|
describe('JSItemActionModel', () => {
|
|
18
|
+
it('keeps empty items reachable in configuration mode without replacing rendered content', async () => {
|
|
19
|
+
const engine = new FlowEngine();
|
|
20
|
+
engine.registerModels({ JSItemActionModel });
|
|
21
|
+
const model = engine.createModel<JSItemActionModel>({ use: 'JSItemActionModel' });
|
|
22
|
+
await engine.flowSettings.disable();
|
|
23
|
+
|
|
24
|
+
render(model.render());
|
|
25
|
+
const host = model.context.ref.current;
|
|
26
|
+
expect(host).toBeInstanceOf(HTMLDivElement);
|
|
27
|
+
expect(host.style.minWidth).toBe('');
|
|
28
|
+
expect(host.textContent).toBe('');
|
|
29
|
+
|
|
30
|
+
await act(async () => {
|
|
31
|
+
await engine.flowSettings.enable();
|
|
32
|
+
});
|
|
33
|
+
await waitFor(() => expect(host.style.minWidth).toBe('40px'));
|
|
34
|
+
|
|
35
|
+
const content = document.createElement('button');
|
|
36
|
+
content.textContent = 'Custom action';
|
|
37
|
+
host.appendChild(content);
|
|
38
|
+
|
|
39
|
+
await act(async () => {
|
|
40
|
+
await engine.flowSettings.disable();
|
|
41
|
+
});
|
|
42
|
+
await waitFor(() => expect(host.style.minWidth).toBe(''));
|
|
43
|
+
expect(model.context.ref.current).toBe(host);
|
|
44
|
+
expect(host.firstElementChild).toBe(content);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('keeps hidden empty items configurable and removes them outside configuration mode', async () => {
|
|
48
|
+
const engine = new FlowEngine();
|
|
49
|
+
engine.registerModels({ JSItemActionModel });
|
|
50
|
+
const model = engine.createModel<JSItemActionModel>({ use: 'JSItemActionModel' });
|
|
51
|
+
model.setHidden(true);
|
|
52
|
+
await engine.flowSettings.enable();
|
|
53
|
+
|
|
54
|
+
const { container } = render(model.render());
|
|
55
|
+
expect(model.context.ref.current.style.minWidth).toBe('40px');
|
|
56
|
+
expect(model.context.ref.current.style.opacity).toBe('0.3');
|
|
57
|
+
|
|
58
|
+
await act(async () => {
|
|
59
|
+
await engine.flowSettings.disable();
|
|
60
|
+
});
|
|
61
|
+
await waitFor(() => expect(container).toBeEmptyDOMElement());
|
|
62
|
+
});
|
|
63
|
+
|
|
18
64
|
it('renders collection-level custom content', async () => {
|
|
19
65
|
const engine = new FlowEngine();
|
|
20
66
|
engine.registerModels({ JSItemActionModel });
|
|
@@ -22,6 +22,20 @@ describe('handleLinkNavigation', () => {
|
|
|
22
22
|
isExternalLink: false,
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
+
expect(navigate).toHaveBeenCalledWith('/target-page', { replace: false });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should replace history entry when caller requests replace', () => {
|
|
29
|
+
const navigate = vi.fn();
|
|
30
|
+
|
|
31
|
+
handleLinkNavigation({
|
|
32
|
+
link: '/target-page',
|
|
33
|
+
openInNewWindow: false,
|
|
34
|
+
router: { navigate },
|
|
35
|
+
isExternalLink: false,
|
|
36
|
+
replace: true,
|
|
37
|
+
});
|
|
38
|
+
|
|
25
39
|
expect(navigate).toHaveBeenCalledWith('/target-page', { replace: true });
|
|
26
40
|
});
|
|
27
41
|
|
|
@@ -922,7 +922,6 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
|
|
|
922
922
|
const baseLayout = this.context.isMobileLayout
|
|
923
923
|
? normalizeGridLayout({
|
|
924
924
|
rows: transformRowsToSingleColumn(projectLayoutToLegacyRows(rawLayout).rows),
|
|
925
|
-
itemUids: this.getItemUids(),
|
|
926
925
|
})
|
|
927
926
|
: rawLayout;
|
|
928
927
|
const baseProjection = projectLayoutToLegacyRows(baseLayout);
|
|
@@ -115,7 +115,28 @@ export class AssignFormGridModel extends FormGridModel {
|
|
|
115
115
|
(existing as any).assignValue = value;
|
|
116
116
|
return;
|
|
117
117
|
}
|
|
118
|
-
|
|
118
|
+
if (!collection) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const field = (collection.getFields?.() || []).find((f: any) => f.name === fieldName);
|
|
122
|
+
|
|
123
|
+
if (!field) {
|
|
124
|
+
const created = this.addSubModel('items', {
|
|
125
|
+
use: 'AssignFormItemModel',
|
|
126
|
+
stepParams: {
|
|
127
|
+
fieldSettings: {
|
|
128
|
+
init: {
|
|
129
|
+
dataSourceKey: collection?.dataSourceKey,
|
|
130
|
+
collectionName: collection?.name,
|
|
131
|
+
fieldPath: fieldName,
|
|
132
|
+
},
|
|
133
|
+
assignValue: { value },
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
created['assignValue'] = value;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
119
140
|
|
|
120
141
|
const binding = EditableItemModel.getDefaultBindingByField(this.context, field);
|
|
121
142
|
if (!binding) {
|
|
@@ -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());
|