@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.
Files changed (85) hide show
  1. package/es/RouteRepository.d.ts +6 -0
  2. package/es/components/form/ScanInput/useCodeScanner.d.ts +12 -2
  3. package/es/components/form/ScanInput/zxingWasmDecoder.d.ts +9 -0
  4. package/es/flow/components/FieldAssignValueInput.d.ts +2 -0
  5. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +1 -0
  6. package/es/flow/components/filter/VariableFilterItem.d.ts +10 -1
  7. package/es/flow/internal/utils/quickEditDataScope.d.ts +48 -0
  8. package/es/flow/internal/utils/quickEditViewContainer.d.ts +27 -0
  9. package/es/flow/models/actions/LinkActionUtils.d.ts +1 -0
  10. package/es/flow/models/blocks/assign-form/assignFormValue.d.ts +29 -0
  11. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +1 -0
  12. package/es/index.mjs +79 -79
  13. package/lib/index.js +84 -84
  14. package/package.json +9 -8
  15. package/src/BaseApplication.tsx +2 -0
  16. package/src/RouteRepository.ts +14 -0
  17. package/src/__tests__/RouteRepository.test.ts +51 -0
  18. package/src/__tests__/app.test.tsx +13 -0
  19. package/src/collection-manager/__tests__/field-configure.test.ts +52 -0
  20. package/src/collection-manager/field-configure.ts +2 -2
  21. package/src/components/form/ScanInput/CodeScanner.tsx +3 -1
  22. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +7 -1
  23. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +182 -8
  24. package/src/components/form/ScanInput/__tests__/zxingWasmDecoder.test.ts +78 -0
  25. package/src/components/form/ScanInput/useCodeScanner.ts +135 -20
  26. package/src/components/form/ScanInput/zxingWasmDecoder.ts +64 -0
  27. package/src/flow/actions/__tests__/validation.test.ts +48 -0
  28. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +42 -23
  29. package/src/flow/components/FieldAssignValueInput.tsx +4 -0
  30. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +21 -12
  31. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +9 -0
  32. package/src/flow/components/filter/VariableFilterItem.tsx +14 -4
  33. package/src/flow/components/filter/__tests__/VariableFilterItem.rightMetaTree.test.tsx +158 -0
  34. package/src/flow/internal/components/Markdown/DisplayMarkdown.tsx +14 -9
  35. package/src/flow/internal/components/Markdown/__tests__/DisplayMarkdown.test.tsx +46 -0
  36. package/src/flow/internal/utils/__tests__/quickEditDataScope.test.ts +156 -0
  37. package/src/flow/internal/utils/__tests__/quickEditViewContainer.test.ts +52 -0
  38. package/src/flow/internal/utils/quickEditDataScope.tsx +177 -0
  39. package/src/flow/internal/utils/quickEditViewContainer.ts +46 -0
  40. package/src/flow/models/actions/JSItemActionModel.tsx +19 -3
  41. package/src/flow/models/actions/LinkActionModel.tsx +11 -9
  42. package/src/flow/models/actions/LinkActionUtils.ts +4 -2
  43. package/src/flow/models/actions/__tests__/JSItemActionModel.test.tsx +47 -1
  44. package/src/flow/models/actions/__tests__/LinkActionModel.test.ts +14 -0
  45. package/src/flow/models/base/GridModel.tsx +0 -1
  46. package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +22 -1
  47. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +40 -12
  48. package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +140 -0
  49. package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.multipleSelect.test.tsx +216 -0
  50. package/src/flow/models/blocks/assign-form/__tests__/assignFormValue.test.ts +41 -0
  51. package/src/flow/models/blocks/assign-form/assignFormValue.ts +64 -0
  52. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +29 -0
  53. package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +1 -1
  54. package/src/flow/models/blocks/filter-form/fields/__tests__/FieldComponentProps.options.test.tsx +61 -0
  55. package/src/flow/models/blocks/form/FormBlockModel.tsx +26 -5
  56. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +45 -11
  57. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +145 -0
  58. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +120 -0
  59. package/src/flow/models/blocks/form/__tests__/popupLinkage.test.tsx +175 -0
  60. package/src/flow/models/blocks/js-block/JSBlock.tsx +5 -1
  61. package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +23 -1
  62. package/src/flow/models/blocks/table/TableColumnModel.tsx +31 -0
  63. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +64 -0
  64. package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +7 -1
  65. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +4 -3
  66. package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +6 -3
  67. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -0
  68. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +47 -17
  69. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +6 -1
  70. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.largeField.test.tsx +178 -0
  71. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableField.disabled.test.tsx +90 -0
  72. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +26 -2
  73. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordSelectFieldModel.quickCreatePopup.test.tsx +183 -0
  74. package/src/flow/models/fields/DisplayAssociationField/DisplaySubTableFieldModel.tsx +42 -7
  75. package/src/flow/models/fields/DisplayAssociationField/__tests__/DisplaySubTableFieldModel.test.tsx +351 -0
  76. package/src/flow/models/fields/DisplayHtmlFieldModel.tsx +2 -1
  77. package/src/flow/models/fields/InputFieldModel.tsx +1 -1
  78. package/src/flow/models/fields/RichTextFieldModel/__tests__/RichTextFieldModel.test.tsx +74 -0
  79. package/src/flow/models/fields/RichTextFieldModel/index.tsx +27 -6
  80. package/src/flow/models/fields/SelectFieldModel.tsx +5 -0
  81. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +16 -0
  82. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +29 -1
  83. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +1 -0
  84. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +20 -0
  85. 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 <div ref={this.context.ref} style={{ display: 'inline-flex', minHeight: 22, alignItems: 'center' }} />;
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={{ display: 'inline-flex', minHeight: 22, alignItems: 'center', opacity: 0.3 }}
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
- void this.applyFlow('jsSettings');
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
- // embed 是页面容器,不应销毁;仅弹窗视图在站内同窗口跳转后销毁。
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
- router.navigate(link, { replace: true });
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
- const field = (collection?.getFields?.() || []).find((f: any) => f.name === fieldName);
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 { FlowModelRenderer, FormItem, tExpr, EditableItemModel, jioToJoiSchema } from '@nocobase/flow-engine';
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 = typeof formValue === 'undefined' ? this.assignValue : formValue;
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
- this.assignValue = v;
335
- formBindingProps?.__assign_trigger__?.(v);
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.title,
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());