@nocobase/client-v2 2.3.0-alpha.1 → 2.3.0-beta.11

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 (173) hide show
  1. package/es/APIClient.d.ts +12 -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/actions/linkageRules.d.ts +25 -0
  5. package/es/flow/components/FieldAssignValueInput.d.ts +6 -30
  6. package/es/flow/components/FlowRoute.d.ts +2 -2
  7. package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
  8. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +32 -0
  9. package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
  10. package/es/flow/components/field-value-variable/index.d.ts +11 -0
  11. package/es/flow/components/filter/VariableFilterItem.d.ts +7 -0
  12. package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
  13. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  14. package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
  15. package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
  16. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  17. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  18. package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
  19. package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
  20. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
  21. package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
  22. package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
  23. package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
  24. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
  25. package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
  26. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  27. package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
  28. package/es/flow/models/blocks/table/utils.d.ts +1 -0
  29. package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
  30. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
  31. package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
  32. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  33. package/es/flow-compat/passwordUtils.d.ts +1 -1
  34. package/es/index.d.ts +1 -0
  35. package/es/index.mjs +148 -145
  36. package/es/ui-operation/index.d.ts +15 -0
  37. package/es/ui-operation/ui-operation-codec.d.ts +10 -0
  38. package/lib/index.js +170 -167
  39. package/lib/locale/languageCodes.js +1 -0
  40. package/package.json +10 -8
  41. package/src/APIClient.ts +92 -0
  42. package/src/Application.tsx +3 -1
  43. package/src/__tests__/APIClient.test.tsx +58 -0
  44. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +7 -2
  45. package/src/__tests__/settings-center.test.tsx +270 -6
  46. package/src/acl/ACLProvider.tsx +2 -0
  47. package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
  48. package/src/collection-manager/__tests__/field-configure.test.ts +52 -0
  49. package/src/collection-manager/field-configure.ts +2 -2
  50. package/src/components/form/ScanInput/CodeScanner.tsx +64 -35
  51. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +107 -0
  52. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +294 -24
  53. package/src/components/form/ScanInput/__tests__/zxingWasmDecoder.test.ts +78 -0
  54. package/src/components/form/ScanInput/useCodeScanner.ts +281 -23
  55. package/src/components/form/ScanInput/zxingWasmDecoder.ts +64 -0
  56. package/src/components/form/filter/CollectionFilterItem.tsx +4 -2
  57. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +17 -0
  58. package/src/components/form/table/Table.tsx +81 -6
  59. package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
  60. package/src/flow/__tests__/FlowRoute.test.tsx +126 -8
  61. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  62. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  63. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  64. package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
  65. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
  66. package/src/flow/actions/__tests__/validation.test.ts +48 -0
  67. package/src/flow/actions/linkageRules.tsx +59 -16
  68. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
  69. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
  70. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +42 -23
  71. package/src/flow/common/Markdown/style.ts +4 -0
  72. package/src/flow/components/BlockItemCard.tsx +2 -2
  73. package/src/flow/components/FieldAssignValueInput.tsx +42 -621
  74. package/src/flow/components/FlowRoute.tsx +71 -29
  75. package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
  76. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +335 -0
  77. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +389 -0
  78. package/src/flow/components/field-value-variable/dateValue.ts +223 -0
  79. package/src/flow/components/field-value-variable/index.ts +12 -0
  80. package/src/flow/components/filter/VariableFilterItem.tsx +23 -6
  81. package/src/flow/components/filter/__tests__/VariableFilterItem.rightMetaTree.test.tsx +158 -0
  82. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +125 -0
  83. package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
  84. package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
  85. package/src/flow/index.ts +2 -0
  86. package/src/flow/internal/components/Markdown/DisplayMarkdown.tsx +14 -9
  87. package/src/flow/internal/components/Markdown/__tests__/DisplayMarkdown.test.tsx +46 -0
  88. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  89. package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
  90. package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
  91. package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
  92. package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
  93. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  94. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  95. package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
  96. package/src/flow/models/base/GridModel.tsx +0 -1
  97. package/src/flow/models/base/PageModel/PageTabModel.tsx +67 -17
  98. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +509 -12
  99. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  100. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  101. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  102. package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
  103. package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +22 -1
  104. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +36 -50
  105. package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +140 -0
  106. package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
  107. package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
  108. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
  109. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
  110. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +129 -14
  111. package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
  112. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
  113. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +29 -0
  114. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +359 -1
  115. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
  116. package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
  117. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
  118. package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +1 -1
  119. package/src/flow/models/blocks/filter-form/fields/__tests__/FieldComponentProps.options.test.tsx +61 -0
  120. package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
  121. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
  122. package/src/flow/models/blocks/form/FormBlockModel.tsx +73 -5
  123. package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
  124. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +153 -2
  125. package/src/flow/models/blocks/form/__tests__/popupLinkage.test.tsx +175 -0
  126. package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
  127. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
  128. package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
  129. package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
  130. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  131. package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
  132. package/src/flow/models/blocks/table/TableBlockModel.tsx +28 -2
  133. package/src/flow/models/blocks/table/TableColumnModel.tsx +48 -9
  134. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
  135. package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
  136. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  137. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  138. package/src/flow/models/blocks/table/utils.ts +7 -0
  139. package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
  140. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +12 -3
  141. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
  142. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
  143. package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +29 -3
  144. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
  145. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +5 -1
  146. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +191 -3
  147. package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
  148. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  149. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  150. package/src/flow/models/fields/DisplayAssociationField/DisplaySubTableFieldModel.tsx +42 -7
  151. package/src/flow/models/fields/DisplayAssociationField/__tests__/DisplaySubTableFieldModel.test.tsx +351 -0
  152. package/src/flow/models/fields/DisplayHtmlFieldModel.tsx +2 -1
  153. package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
  154. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  155. package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
  156. package/src/flow/models/fields/RichTextFieldModel/__tests__/RichTextFieldModel.test.tsx +74 -0
  157. package/src/flow/models/fields/RichTextFieldModel/index.tsx +27 -6
  158. package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
  159. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  160. package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
  161. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +6 -0
  162. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +27 -2
  163. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
  164. package/src/flow/models/topbar/TopbarActionModel.tsx +5 -5
  165. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  166. package/src/index.ts +1 -0
  167. package/src/layout-manager/LayoutContentRoute.tsx +2 -1
  168. package/src/layout-manager/LayoutRoute.tsx +2 -1
  169. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
  170. package/src/locale/languageCodes.ts +1 -0
  171. package/src/settings-center/AdminSettingsLayout.tsx +23 -2
  172. package/src/ui-operation/index.ts +33 -0
  173. package/src/ui-operation/ui-operation-codec.ts +54 -0
@@ -0,0 +1,124 @@
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 { FlowEngine, type FlowModelContext, MultiRecordResource, VIEW_ACTIVATED_EVENT } from '@nocobase/flow-engine';
11
+ import { beforeEach, describe, expect, it } from 'vitest';
12
+ import { BlockGridModel } from '../BlockGridModel';
13
+ import { CollectionBlockModel } from '../CollectionBlockModel';
14
+
15
+ class TestBlockGridModel extends BlockGridModel {
16
+ mountForTest() {
17
+ super.onMount();
18
+ }
19
+
20
+ unmountForTest() {
21
+ super.onUnmount();
22
+ }
23
+ }
24
+
25
+ class TestMultiRecordResource extends MultiRecordResource<Record<string, unknown>> {
26
+ refreshCalls = 0;
27
+
28
+ override async refresh(): Promise<void> {
29
+ this.refreshCalls += 1;
30
+ this.emit('refresh');
31
+ }
32
+ }
33
+
34
+ class TestCollectionBlockModel extends CollectionBlockModel {
35
+ createResource(ctx: FlowModelContext) {
36
+ return ctx.createResource(TestMultiRecordResource);
37
+ }
38
+
39
+ mountForTest() {
40
+ super.onMount();
41
+ }
42
+ }
43
+
44
+ describe('BlockGridModel - select scene activation', () => {
45
+ let engine: FlowEngine;
46
+
47
+ beforeEach(() => {
48
+ engine = new FlowEngine();
49
+ engine.context.defineProperty('location', { value: { search: '' } });
50
+ engine.registerModels({ TestBlockGridModel, TestCollectionBlockModel });
51
+
52
+ engine.dataSourceManager.getDataSource('main').addCollection({
53
+ name: 'roles',
54
+ filterTargetKey: 'id',
55
+ fields: [
56
+ { name: 'id', type: 'integer', interface: 'number' },
57
+ { name: 'name', type: 'string', interface: 'input' },
58
+ ],
59
+ });
60
+ });
61
+
62
+ function createModels(scene = 'select') {
63
+ engine.context.defineProperty('view', { value: { inputArgs: { scene } } });
64
+ const grid = engine.createModel<TestBlockGridModel>({ use: 'TestBlockGridModel' });
65
+ const block = grid.addSubModel<TestCollectionBlockModel>('items', {
66
+ use: 'TestCollectionBlockModel',
67
+ stepParams: {
68
+ resourceSettings: {
69
+ init: {
70
+ dataSourceKey: 'main',
71
+ collectionName: 'roles',
72
+ },
73
+ },
74
+ },
75
+ });
76
+ const resource = block.context.resource as TestMultiRecordResource;
77
+
78
+ block.mountForTest();
79
+ grid.mountForTest();
80
+ return { grid, resource };
81
+ }
82
+
83
+ it('refreshes dirty collection blocks once when a nested view closes', async () => {
84
+ const { grid, resource } = createModels();
85
+
86
+ engine.markDataSourceDirty('main', 'roles');
87
+ engine.emitter.emit(VIEW_ACTIVATED_EVENT);
88
+ await Promise.resolve();
89
+
90
+ expect(resource.refreshCalls).toBe(1);
91
+
92
+ engine.emitter.emit(VIEW_ACTIVATED_EVENT);
93
+ await Promise.resolve();
94
+
95
+ expect(resource.refreshCalls).toBe(1);
96
+ grid.unmountForTest();
97
+ });
98
+
99
+ it('does not refresh without dirty data or after the grid unmounts', async () => {
100
+ const { grid, resource } = createModels();
101
+
102
+ engine.emitter.emit(VIEW_ACTIVATED_EVENT);
103
+ await Promise.resolve();
104
+ expect(resource.refreshCalls).toBe(0);
105
+
106
+ grid.unmountForTest();
107
+ engine.markDataSourceDirty('main', 'roles');
108
+ engine.emitter.emit(VIEW_ACTIVATED_EVENT);
109
+ await Promise.resolve();
110
+
111
+ expect(resource.refreshCalls).toBe(0);
112
+ });
113
+
114
+ it('does not handle activation outside the select scene', async () => {
115
+ const { grid, resource } = createModels('list');
116
+
117
+ engine.markDataSourceDirty('main', 'roles');
118
+ engine.emitter.emit(VIEW_ACTIVATED_EVENT);
119
+ await Promise.resolve();
120
+
121
+ expect(resource.refreshCalls).toBe(0);
122
+ grid.unmountForTest();
123
+ });
124
+ });
@@ -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 { FlowEngine, FlowModelContext, MultiRecordResource } from '@nocobase/flow-engine';
11
+ import { describe, expect, it } from 'vitest';
12
+ import { CollectionBlockModel } from '../CollectionBlockModel';
13
+
14
+ class AddAppendsCollectionBlockModel extends CollectionBlockModel {
15
+ createResource(ctx: FlowModelContext) {
16
+ return ctx.createResource(MultiRecordResource);
17
+ }
18
+ }
19
+
20
+ describe('CollectionBlockModel addAppends', () => {
21
+ it('does not throw for a nested field path when the collection is unavailable', () => {
22
+ const engine = new FlowEngine();
23
+ engine.registerModels({ AddAppendsCollectionBlockModel });
24
+
25
+ const model = engine.createModel<AddAppendsCollectionBlockModel>({
26
+ uid: 'missing-collection-add-appends-block',
27
+ use: 'AddAppendsCollectionBlockModel',
28
+ stepParams: {
29
+ resourceSettings: {
30
+ init: {
31
+ dataSourceKey: 'main',
32
+ collectionName: 'missing_collection',
33
+ },
34
+ },
35
+ },
36
+ });
37
+
38
+ expect(model.collection).toBeUndefined();
39
+ expect(() => model.addAppends('createdBy.departments')).not.toThrow();
40
+ });
41
+ });
@@ -7,9 +7,10 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
11
- import { FlowEngine, MultiRecordResource } from '@nocobase/flow-engine';
10
+ import { afterEach, describe, expect, it, vi } from 'vitest';
11
+ import { FlowEngine, FlowRuntimeContext, MultiRecordResource, ResourceError } from '@nocobase/flow-engine';
12
12
  import '../../../index';
13
+ import { BlockResourceErrorPlaceholder } from '../../../components/placeholders/BlockPlaceholder';
13
14
  import { CollectionBlockModel } from '../CollectionBlockModel';
14
15
  import { dataLoadingMode } from '../../../actions/dataLoadingMode';
15
16
 
@@ -19,13 +20,16 @@ class InitialBeforeRenderTestBlockModel extends CollectionBlockModel {
19
20
  }
20
21
  }
21
22
 
22
- function setupModel() {
23
+ function setupModel(dataSourceKey = 'main') {
23
24
  const engine = new FlowEngine();
24
25
  engine.context.defineProperty('location', { value: { search: '' } as any });
25
26
  engine.registerActions({ dataLoadingMode });
26
27
  engine.registerModels({ InitialBeforeRenderTestBlockModel });
27
28
 
28
- const ds = engine.dataSourceManager.getDataSource('main');
29
+ if (!engine.dataSourceManager.getDataSource(dataSourceKey)) {
30
+ engine.dataSourceManager.addDataSource({ key: dataSourceKey });
31
+ }
32
+ const ds = engine.dataSourceManager.getDataSource(dataSourceKey);
29
33
  ds.addCollection({
30
34
  name: 'users',
31
35
  filterTargetKey: 'id',
@@ -41,7 +45,7 @@ function setupModel() {
41
45
  stepParams: {
42
46
  resourceSettings: {
43
47
  init: {
44
- dataSourceKey: 'main',
48
+ dataSourceKey,
45
49
  collectionName: 'users',
46
50
  },
47
51
  },
@@ -88,16 +92,21 @@ function setupModelWithManualMode() {
88
92
  return { model, resource: model.resource as MultiRecordResource };
89
93
  }
90
94
 
91
- describe('CollectionBlockModel initial beforeRender refresh', () => {
92
- beforeEach(() => {
93
- vi.useFakeTimers();
94
- });
95
+ async function runRefreshStep(model: InitialBeforeRenderTestBlockModel) {
96
+ const handler = model.getFlow('refreshSettings')?.getStep('refresh')?.serialize().handler;
97
+ if (!handler) {
98
+ throw new Error('refreshSettings.refresh handler is not registered');
99
+ }
100
+ await handler(new FlowRuntimeContext(model, 'refreshSettings'), {});
101
+ }
95
102
 
103
+ describe('CollectionBlockModel initial beforeRender refresh', () => {
96
104
  afterEach(() => {
97
105
  vi.useRealTimers();
98
106
  });
99
107
 
100
108
  it('only performs one actual data request on first beforeRender', async () => {
109
+ vi.useFakeTimers();
101
110
  const { model, resource } = setupModel();
102
111
  const runActionSpy = vi.spyOn(resource, 'runAction').mockResolvedValue({
103
112
  data: [{ id: 1, name: 'Alice' }],
@@ -112,7 +121,54 @@ describe('CollectionBlockModel initial beforeRender refresh', () => {
112
121
  expect(runActionSpy).toHaveBeenCalledTimes(1);
113
122
  });
114
123
 
124
+ it('keeps resource request failures in the block error state', async () => {
125
+ const { model, resource } = setupModel('external');
126
+ const resourceError = new ResourceError({
127
+ response: {
128
+ data: {
129
+ errors: [{ code: 'DATA_SOURCE_UNAVAILABLE', message: 'Data source unavailable' }],
130
+ },
131
+ },
132
+ });
133
+ vi.spyOn(resource, 'refresh').mockImplementation(async () => {
134
+ resource.setError(resourceError);
135
+ throw resourceError;
136
+ });
137
+
138
+ await runRefreshStep(model);
139
+
140
+ expect(resource.getError()).toBe(resourceError);
141
+ expect(CollectionBlockModel.prototype.render.call(model)).toMatchObject({
142
+ type: BlockResourceErrorPlaceholder,
143
+ });
144
+ });
145
+
146
+ it('continues to throw resource request failures from the main data source', async () => {
147
+ const { model, resource } = setupModel();
148
+ const resourceError = new ResourceError({
149
+ response: {
150
+ data: {
151
+ errors: [{ code: 'UNKNOWN_ERROR', message: 'Main data source failed' }],
152
+ },
153
+ },
154
+ });
155
+ vi.spyOn(resource, 'refresh').mockImplementation(async () => {
156
+ resource.setError(resourceError);
157
+ throw resourceError;
158
+ });
159
+
160
+ await expect(runRefreshStep(model)).rejects.toBe(resourceError);
161
+ });
162
+
163
+ it('continues to throw non-resource errors from beforeRender', async () => {
164
+ const { model, resource } = setupModel('external');
165
+ vi.spyOn(resource, 'refresh').mockRejectedValue(new Error('beforeRender bug'));
166
+
167
+ await expect(runRefreshStep(model)).rejects.toThrow('beforeRender bug');
168
+ });
169
+
115
170
  it('respects saved manual mode and skips initial request when filters are empty', async () => {
171
+ vi.useFakeTimers();
116
172
  const { model, resource } = setupModelWithManualMode();
117
173
  const runActionSpy = vi.spyOn(resource, 'runAction').mockResolvedValue({
118
174
  data: [{ id: 1, name: 'Alice' }],
@@ -128,4 +184,64 @@ describe('CollectionBlockModel initial beforeRender refresh', () => {
128
184
  expect(resource.getData()).toEqual([]);
129
185
  expect(resource.getMeta('count')).toBe(0);
130
186
  });
187
+
188
+ it('skips forced active refresh in manual mode when filters are empty', async () => {
189
+ const { model, resource } = setupModelWithManualMode();
190
+ const refreshSpy = vi.spyOn(resource, 'refresh');
191
+
192
+ resource.setData([{ id: 2, name: 'Stale' }]);
193
+ resource.setMeta({ count: 1, hasNext: true });
194
+ resource.setPage(2);
195
+ resource.loading = true;
196
+
197
+ model.onActive(true);
198
+ await Promise.resolve();
199
+ await Promise.resolve();
200
+
201
+ expect(refreshSpy).not.toHaveBeenCalled();
202
+ expect(resource.getData()).toEqual([]);
203
+ expect(resource.getMeta('count')).toBe(0);
204
+ expect(resource.getMeta('hasNext')).toBe(false);
205
+ expect(resource.getMeta('page')).toBe(1);
206
+ expect(resource.getRequestParameter('page')).toBe(1);
207
+ expect(resource.loading).toBe(false);
208
+ });
209
+
210
+ it('keeps forced active refresh in manual mode when filters are active', async () => {
211
+ const { model, resource } = setupModelWithManualMode();
212
+ const refreshSpy = vi.spyOn(resource, 'refresh').mockResolvedValue();
213
+
214
+ resource.setData([{ id: 2, name: 'Stale' }]);
215
+ resource.setMeta({ count: 1, hasNext: true, page: 2 });
216
+ model.setFilterActive('filter-form-item', true);
217
+
218
+ model.onActive(true);
219
+ await Promise.resolve();
220
+
221
+ expect(refreshSpy).toHaveBeenCalledTimes(1);
222
+ expect(resource.getData()).toEqual([{ id: 2, name: 'Stale' }]);
223
+ expect(resource.getMeta('count')).toBe(1);
224
+ });
225
+
226
+ it('skips forced active refresh in manual mode when only resource filters are active', async () => {
227
+ const { model, resource } = setupModelWithManualMode();
228
+ const refreshSpy = vi.spyOn(resource, 'refresh');
229
+
230
+ resource.setData([{ id: 2, name: 'Stale' }]);
231
+ resource.setMeta({ count: 1, hasNext: true });
232
+ resource.setPage(2);
233
+ resource.loading = true;
234
+ resource.addFilterGroup('data-scope', { status: { $eq: 'active' } });
235
+
236
+ model.onActive(true);
237
+ await Promise.resolve();
238
+
239
+ expect(refreshSpy).not.toHaveBeenCalled();
240
+ expect(resource.getData()).toEqual([]);
241
+ expect(resource.getMeta('count')).toBe(0);
242
+ expect(resource.getMeta('hasNext')).toBe(false);
243
+ expect(resource.getMeta('page')).toBe(1);
244
+ expect(resource.getRequestParameter('page')).toBe(1);
245
+ expect(resource.loading).toBe(false);
246
+ });
131
247
  });
@@ -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) {
@@ -11,15 +11,12 @@ import React from 'react';
11
11
  import { Input } from 'antd';
12
12
  import { define, observable } from '@formily/reactive';
13
13
  import {
14
+ EditableItemModel,
15
+ FieldDeletePlaceholder,
14
16
  FlowModelRenderer,
15
17
  FormItem,
16
- VariableInput,
17
- tExpr,
18
- isVariableExpression,
19
- parseValueToPath,
20
- isRunJSValue,
21
- EditableItemModel,
22
18
  jioToJoiSchema,
19
+ tExpr,
23
20
  } from '@nocobase/flow-engine';
24
21
  // 无需类型导入(避免未使用的类型)
25
22
  import { FormItemModel } from '../form/FormItemModel';
@@ -27,6 +24,13 @@ import { EditFormModel } from '../form/EditFormModel';
27
24
  import { customAlphabet as Alphabet } from 'nanoid';
28
25
  import { ensureOptionsFromUiSchemaEnumIfAbsent } from '../../../internal/utils/enumOptionsUtils';
29
26
  import { RunJSValueEditor } from '../../../components/RunJSValueEditor';
27
+ import {
28
+ DEFAULT_DATE_VARIABLE_COMPONENT_PROPS,
29
+ FieldValueVariableInput,
30
+ getFieldInterface,
31
+ isDateLikeField,
32
+ resolveDateVariableComponentProps,
33
+ } from '../../../components/field-value-variable';
30
34
 
31
35
  type AssignFormTempOriginField = {
32
36
  uid?: string;
@@ -163,11 +167,15 @@ export class AssignFormItemModel extends FormItemModel {
163
167
 
164
168
  getAssignedEntry(): [string, any] | null {
165
169
  const name = this.fieldPath;
166
- if (!name) return null;
170
+ if (!name || !this.collectionField) return null;
167
171
  return [name, this.assignValue];
168
172
  }
169
173
 
170
174
  render() {
175
+ if (!this.collectionField) {
176
+ return <FieldDeletePlaceholder />;
177
+ }
178
+
171
179
  // 与 FormItemModel.render 结构保持一致,仅替换内部渲染为 VariableInput + 常量编辑器
172
180
  const ctx: any = this.context;
173
181
  const collection = ctx.collection;
@@ -285,7 +293,9 @@ export class AssignFormItemModel extends FormItemModel {
285
293
  ) : null;
286
294
  };
287
295
 
288
- const NullComponent: React.FC = () => <Input placeholder={'<Null>'} readOnly style={{ width: '100%' }} />;
296
+ const NullComponent: React.FC = () => (
297
+ <Input placeholder={`<${this.context.t?.('Null') ?? 'Null'}>`} readOnly style={{ width: '100%' }} />
298
+ );
289
299
 
290
300
  const RunJSComponent: React.FC<any> = (inputProps: any) => {
291
301
  return (
@@ -298,46 +308,18 @@ export class AssignFormItemModel extends FormItemModel {
298
308
  );
299
309
  };
300
310
 
301
- const converters = {
302
- renderInputComponent: (meta: any) => {
303
- const firstPath = meta?.paths?.[0];
304
- if (firstPath === 'constant') return ConstantValueEditor as any;
305
- if (firstPath === 'null') return NullComponent as any;
306
- if (firstPath === 'runjs') return RunJSComponent as any;
307
- return undefined;
308
- },
309
- resolveValueFromPath: (item: any) => {
310
- const firstPath = item?.paths?.[0];
311
- if (firstPath === 'constant') return '';
312
- if (firstPath === 'null') return null;
313
- if (firstPath === 'runjs') return { code: '', version: 'v2' };
314
- return undefined;
315
- },
316
- resolvePathFromValue: (currentValue: any) => {
317
- if (currentValue === null) return ['null'];
318
- if (isRunJSValue(currentValue)) return ['runjs'];
319
- return isVariableExpression(currentValue) ? parseValueToPath(currentValue) : ['constant'];
320
- },
321
- } as any;
322
-
323
- // 合并变量树:在最前面追加“常量/空值”两个选项
324
- const mergedMetaTree = async () => {
311
+ const baseMetaTree = async () => {
325
312
  const getTree = (this.context as any)?.getPropertyMetaTree;
326
- const base: any[] = typeof getTree === 'function' ? await getTree() : [];
327
- return [
328
- {
329
- title: tExpr('Constant'),
330
- name: 'constant',
331
- type: 'string',
332
- paths: ['constant'],
333
- render: ConstantValueEditor,
334
- },
335
- { title: tExpr('Null'), name: 'null', type: 'object', paths: ['null'], render: NullComponent },
336
- { title: tExpr('RunJS'), name: 'runjs', type: 'object', paths: ['runjs'], render: RunJSComponent },
337
- ...base,
338
- ];
313
+ return typeof getTree === 'function' ? await getTree() : [];
339
314
  };
340
315
 
316
+ const targetCollectionField = collection?.getField?.(this.fieldPath);
317
+ const targetFieldInterface = getFieldInterface(targetCollectionField);
318
+ const dateLike = isDateLikeField(targetCollectionField, this.fieldPath?.split('.').slice(-1)[0]);
319
+ const dateComponentProps = dateLike
320
+ ? resolveDateVariableComponentProps(targetCollectionField, targetFieldInterface)
321
+ : DEFAULT_DATE_VARIABLE_COMPONENT_PROPS;
322
+
341
323
  // 计算 label:优先使用配置中的 label,其次集合字段标题,最后回退字段路径
342
324
  let labelText = this.props?.label ?? this.fieldPath ?? '';
343
325
  const cf = collection?.getField?.(this.fieldPath);
@@ -357,14 +339,18 @@ export class AssignFormItemModel extends FormItemModel {
357
339
  const formValue = formBindingProps?.__assign_value__;
358
340
  const mergedValue = typeof formValue === 'undefined' ? this.assignValue : formValue;
359
341
  return (
360
- <VariableInput
342
+ <FieldValueVariableInput
361
343
  value={mergedValue}
362
- onChange={(v: any) => {
344
+ onChange={(v) => {
363
345
  this.assignValue = v;
364
346
  formBindingProps?.__assign_trigger__?.(v);
365
347
  }}
366
- metaTree={mergedMetaTree}
367
- converters={converters}
348
+ baseMetaTree={baseMetaTree}
349
+ constantComponent={ConstantValueEditor}
350
+ nullComponent={NullComponent}
351
+ runJSComponent={RunJSComponent}
352
+ isDateLikeField={dateLike}
353
+ dateComponentProps={dateComponentProps}
368
354
  clearValue={''}
369
355
  />
370
356
  );
@@ -417,7 +403,7 @@ AssignFormItemModel.registerFlow({
417
403
  },
418
404
  defaultParams: (ctx) => {
419
405
  return {
420
- label: (ctx.model as any).collectionField.title,
406
+ label: (ctx.model as any).collectionField?.title || (ctx.model as any).fieldPath,
421
407
  };
422
408
  },
423
409
  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());