@nocobase/client-v2 2.2.0-beta.9 → 2.2.0

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 (224) hide show
  1. package/es/APIClient.d.ts +13 -3
  2. package/es/BaseApplication.d.ts +1 -0
  3. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  4. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  5. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  6. package/es/components/category-tabs/index.d.ts +9 -0
  7. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  8. package/es/components/form/TypedVariableInput.d.ts +31 -5
  9. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  10. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  11. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  12. package/es/components/form/filter/index.d.ts +2 -0
  13. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  14. package/es/components/index.d.ts +1 -0
  15. package/es/flow/actions/index.d.ts +1 -1
  16. package/es/flow/actions/linkageRules.d.ts +27 -0
  17. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  18. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  19. package/es/flow/components/FieldAssignValueInput.d.ts +7 -24
  20. package/es/flow/components/FlowRoute.d.ts +2 -2
  21. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  22. package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
  23. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
  24. package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
  25. package/es/flow/components/field-value-variable/index.d.ts +11 -0
  26. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  27. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  28. package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
  29. package/es/flow/index.d.ts +1 -0
  30. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  31. package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
  32. package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
  33. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  34. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  35. package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
  36. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  37. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  38. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  39. package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
  40. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
  41. package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
  42. package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
  43. package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
  44. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  45. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
  46. package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
  47. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  48. package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
  49. package/es/flow/models/blocks/table/utils.d.ts +1 -0
  50. package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
  51. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
  52. package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
  53. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  54. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  55. package/es/flow-compat/passwordUtils.d.ts +1 -1
  56. package/es/flow-compat/routeTypes.d.ts +1 -0
  57. package/es/index.d.ts +5 -0
  58. package/es/index.mjs +199 -161
  59. package/es/ui-operation/index.d.ts +15 -0
  60. package/es/ui-operation/ui-operation-codec.d.ts +10 -0
  61. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  62. package/es/utils/index.d.ts +1 -0
  63. package/lib/index.js +215 -177
  64. package/lib/locale/languageCodes.js +3 -1
  65. package/package.json +9 -7
  66. package/src/APIClient.ts +90 -7
  67. package/src/Application.tsx +7 -1
  68. package/src/BaseApplication.tsx +11 -6
  69. package/src/__tests__/APIClient.test.tsx +58 -0
  70. package/src/__tests__/app.test.tsx +73 -0
  71. package/src/__tests__/browserChecker.test.ts +61 -0
  72. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  73. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +35 -2
  74. package/src/__tests__/settings-center.test.tsx +270 -6
  75. package/src/acl/ACLProvider.tsx +2 -0
  76. package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
  77. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  78. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  79. package/src/collection-manager/field-validation.ts +1 -1
  80. package/src/components/README.md +7 -1
  81. package/src/components/README.zh-CN.md +6 -1
  82. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  83. package/src/components/category-tabs/index.ts +10 -0
  84. package/src/components/form/JsonTextArea.tsx +4 -0
  85. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  86. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  87. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  88. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  89. package/src/components/form/TypedVariableInput.tsx +452 -101
  90. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  91. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  92. package/src/components/form/filter/CollectionFilterItem.tsx +36 -9
  93. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  94. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +55 -0
  95. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  96. package/src/components/form/filter/index.ts +2 -0
  97. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  98. package/src/components/form/table/Table.tsx +81 -6
  99. package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
  100. package/src/components/index.ts +1 -0
  101. package/src/flow/FlowPage.tsx +9 -1
  102. package/src/flow/__tests__/FlowPage.test.tsx +50 -3
  103. package/src/flow/__tests__/FlowRoute.test.tsx +128 -10
  104. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  105. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  106. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  107. package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
  108. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
  109. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  110. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  111. package/src/flow/actions/index.ts +2 -0
  112. package/src/flow/actions/linkageRules.tsx +144 -26
  113. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  114. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +0 -1
  115. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
  116. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  117. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
  118. package/src/flow/common/Markdown/style.ts +4 -0
  119. package/src/flow/components/BlockItemCard.tsx +2 -2
  120. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  121. package/src/flow/components/FieldAssignValueInput.tsx +72 -584
  122. package/src/flow/components/FlowRoute.tsx +78 -32
  123. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  124. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  125. package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
  126. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
  127. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
  128. package/src/flow/components/field-value-variable/dateValue.ts +223 -0
  129. package/src/flow/components/field-value-variable/index.ts +12 -0
  130. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  131. package/src/flow/components/filter/VariableFilterItem.tsx +170 -13
  132. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  133. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  134. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +195 -1
  135. package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
  136. package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
  137. package/src/flow/index.ts +3 -0
  138. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  139. package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
  140. package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
  141. package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
  142. package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
  143. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  144. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  145. package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
  146. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  147. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  148. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  149. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  150. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  151. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  152. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  153. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  154. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  155. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  156. package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
  157. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
  158. package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
  159. package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
  160. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
  161. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
  162. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +155 -25
  163. package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
  164. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
  165. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +395 -1
  166. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
  167. package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
  168. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
  169. package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
  170. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
  171. package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
  172. package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
  173. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  174. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
  175. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  176. package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
  177. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
  178. package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
  179. package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
  180. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  181. package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
  182. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  183. package/src/flow/models/blocks/table/TableBlockModel.tsx +29 -3
  184. package/src/flow/models/blocks/table/TableColumnModel.tsx +48 -9
  185. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
  186. package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
  187. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  188. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  189. package/src/flow/models/blocks/table/utils.ts +7 -0
  190. package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
  191. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +10 -1
  192. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
  193. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
  194. package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +24 -1
  195. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
  196. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +167 -3
  197. package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
  198. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  199. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  200. package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
  201. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  202. package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
  203. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  204. package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
  205. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  206. package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
  207. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  208. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +26 -10
  209. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +51 -14
  210. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
  211. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  212. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  213. package/src/flow-compat/routeTypes.ts +1 -0
  214. package/src/index.ts +5 -0
  215. package/src/layout-manager/LayoutContentRoute.tsx +2 -1
  216. package/src/layout-manager/LayoutRoute.tsx +2 -1
  217. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
  218. package/src/locale/languageCodes.ts +3 -1
  219. package/src/nocobase-buildin-plugin/index.tsx +12 -0
  220. package/src/settings-center/AdminSettingsLayout.tsx +23 -2
  221. package/src/ui-operation/index.ts +33 -0
  222. package/src/ui-operation/ui-operation-codec.ts +54 -0
  223. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  224. package/src/utils/index.tsx +1 -0
@@ -1,3 +1,12 @@
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
+
1
10
  import { render, screen } from '@nocobase/test/client';
2
11
  import { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
3
12
  import { App, ConfigProvider } from 'antd';
@@ -35,3 +44,43 @@ describe('ActionModel rendering', () => {
35
44
  expect(screen.getByRole('button', { name: 'Open details' })).toBeInTheDocument();
36
45
  });
37
46
  });
47
+
48
+ describe('ActionModel disabled state persistence', () => {
49
+ it('drops disabled from persisted props during initialization', () => {
50
+ const engine = new FlowEngine();
51
+ engine.registerModels({ NoIconActionModel });
52
+ const model = engine.createModel<NoIconActionModel>({
53
+ use: 'NoIconActionModel',
54
+ props: { title: 'Open details', disabled: true },
55
+ });
56
+
57
+ expect(model.getProps()).not.toHaveProperty('disabled');
58
+ });
59
+
60
+ it('keeps runtime disabled state without serializing it', () => {
61
+ const engine = new FlowEngine();
62
+ engine.registerModels({ NoIconActionModel });
63
+ const model = engine.createModel<NoIconActionModel>({
64
+ use: 'NoIconActionModel',
65
+ props: { title: 'Open details' },
66
+ });
67
+
68
+ model.setProps('disabled', true);
69
+
70
+ expect(model.getProps().disabled).toBe(true);
71
+ expect(model.serialize().props).toEqual({ title: 'Open details' });
72
+ });
73
+
74
+ it('does not serialize fork-local disabled state', () => {
75
+ const engine = new FlowEngine();
76
+ engine.registerModels({ NoIconActionModel });
77
+ const model = engine.createModel<NoIconActionModel>({
78
+ use: 'NoIconActionModel',
79
+ props: { title: 'Open details' },
80
+ });
81
+ const fork = model.createFork({ disabled: true });
82
+
83
+ expect(fork.getProps().disabled).toBe(true);
84
+ expect(fork.serialize().props).toEqual({ title: 'Open details' });
85
+ });
86
+ });
@@ -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
  });
@@ -10,23 +10,20 @@
10
10
  import React from 'react';
11
11
  import { Input } from 'antd';
12
12
  import { define, observable } from '@formily/reactive';
13
- import {
14
- FlowModelRenderer,
15
- FormItem,
16
- VariableInput,
17
- tExpr,
18
- isVariableExpression,
19
- parseValueToPath,
20
- isRunJSValue,
21
- EditableItemModel,
22
- jioToJoiSchema,
23
- } from '@nocobase/flow-engine';
13
+ import { FlowModelRenderer, FormItem, tExpr, EditableItemModel, jioToJoiSchema } from '@nocobase/flow-engine';
24
14
  // 无需类型导入(避免未使用的类型)
25
15
  import { FormItemModel } from '../form/FormItemModel';
26
16
  import { EditFormModel } from '../form/EditFormModel';
27
17
  import { customAlphabet as Alphabet } from 'nanoid';
28
18
  import { ensureOptionsFromUiSchemaEnumIfAbsent } from '../../../internal/utils/enumOptionsUtils';
29
19
  import { RunJSValueEditor } from '../../../components/RunJSValueEditor';
20
+ import {
21
+ DEFAULT_DATE_VARIABLE_COMPONENT_PROPS,
22
+ FieldValueVariableInput,
23
+ getFieldInterface,
24
+ isDateLikeField,
25
+ resolveDateVariableComponentProps,
26
+ } from '../../../components/field-value-variable';
30
27
 
31
28
  type AssignFormTempOriginField = {
32
29
  uid?: string;
@@ -285,7 +282,9 @@ export class AssignFormItemModel extends FormItemModel {
285
282
  ) : null;
286
283
  };
287
284
 
288
- const NullComponent: React.FC = () => <Input placeholder={'<Null>'} readOnly style={{ width: '100%' }} />;
285
+ const NullComponent: React.FC = () => (
286
+ <Input placeholder={`<${this.context.t?.('Null') ?? 'Null'}>`} readOnly style={{ width: '100%' }} />
287
+ );
289
288
 
290
289
  const RunJSComponent: React.FC<any> = (inputProps: any) => {
291
290
  return (
@@ -298,46 +297,18 @@ export class AssignFormItemModel extends FormItemModel {
298
297
  );
299
298
  };
300
299
 
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 () => {
300
+ const baseMetaTree = async () => {
325
301
  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
- ];
302
+ return typeof getTree === 'function' ? await getTree() : [];
339
303
  };
340
304
 
305
+ const targetCollectionField = collection?.getField?.(this.fieldPath);
306
+ const targetFieldInterface = getFieldInterface(targetCollectionField);
307
+ const dateLike = isDateLikeField(targetCollectionField, this.fieldPath?.split('.').slice(-1)[0]);
308
+ const dateComponentProps = dateLike
309
+ ? resolveDateVariableComponentProps(targetCollectionField, targetFieldInterface)
310
+ : DEFAULT_DATE_VARIABLE_COMPONENT_PROPS;
311
+
341
312
  // 计算 label:优先使用配置中的 label,其次集合字段标题,最后回退字段路径
342
313
  let labelText = this.props?.label ?? this.fieldPath ?? '';
343
314
  const cf = collection?.getField?.(this.fieldPath);
@@ -357,14 +328,18 @@ export class AssignFormItemModel extends FormItemModel {
357
328
  const formValue = formBindingProps?.__assign_value__;
358
329
  const mergedValue = typeof formValue === 'undefined' ? this.assignValue : formValue;
359
330
  return (
360
- <VariableInput
331
+ <FieldValueVariableInput
361
332
  value={mergedValue}
362
- onChange={(v: any) => {
333
+ onChange={(v) => {
363
334
  this.assignValue = v;
364
335
  formBindingProps?.__assign_trigger__?.(v);
365
336
  }}
366
- metaTree={mergedMetaTree}
367
- converters={converters}
337
+ baseMetaTree={baseMetaTree}
338
+ constantComponent={ConstantValueEditor}
339
+ nullComponent={NullComponent}
340
+ runJSComponent={RunJSComponent}
341
+ isDateLikeField={dateLike}
342
+ dateComponentProps={dateComponentProps}
368
343
  clearValue={''}
369
344
  />
370
345
  );
@@ -20,6 +20,7 @@ import {
20
20
  SingleRecordResource,
21
21
  createCurrentRecordMetaFactory,
22
22
  createRecordResolveOnServerWithLocal,
23
+ observer,
23
24
  tExpr,
24
25
  } from '@nocobase/flow-engine';
25
26
  import { Pagination, Space } from 'antd';
@@ -154,8 +155,8 @@ export class DetailsBlockModel extends CollectionBlockModel<{
154
155
  simple
155
156
  pageSize={1}
156
157
  showSizeChanger={false}
157
- defaultCurrent={resource.getPage()}
158
- total={resource.getTotalPage()}
158
+ current={resource.getPage()}
159
+ total={resource.getCount()}
159
160
  onChange={this.handlePageChange}
160
161
  style={{ display: 'inline-block' }}
161
162
  />
@@ -226,6 +227,8 @@ export class DetailsBlockModel extends CollectionBlockModel<{
226
227
  }
227
228
  }
228
229
 
230
+ export const DetailsPagination = observer(({ model }: { model: DetailsBlockModel }) => model.renderPagination());
231
+
229
232
  const DetailsBlockContent = ({
230
233
  model,
231
234
  gridModel,
@@ -297,7 +300,9 @@ const DetailsBlockContent = ({
297
300
  model={gridModel}
298
301
  showFlowSettings={false}
299
302
  />
300
- <div ref={paginationRef}>{model.renderPagination()}</div>
303
+ <div ref={paginationRef}>
304
+ <DetailsPagination model={model} />
305
+ </div>
301
306
  </div>
302
307
  </FormComponent>
303
308
  );
@@ -7,10 +7,12 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
+ import { act, render, screen } from '@testing-library/react';
10
11
  import { beforeEach, describe, expect, it, vi } from 'vitest';
11
12
  import { FlowEngine, MultiRecordResource, SingleRecordResource } from '@nocobase/flow-engine';
13
+ import React from 'react';
12
14
  import '../../../../index';
13
- import { DetailsBlockModel } from '../DetailsBlockModel';
15
+ import { DetailsBlockModel, DetailsPagination } from '../DetailsBlockModel';
14
16
  import { DetailsGridModel } from '../DetailsGridModel';
15
17
 
16
18
  function setupDetailsBlockModel(options?: { filterByTk?: string | number }) {
@@ -93,6 +95,35 @@ describe('DetailsBlockModel initial pagination refresh', () => {
93
95
  expect(dispatchSpy.mock.calls.filter(([eventName]) => eventName === 'paginationChange')).toHaveLength(1);
94
96
  });
95
97
 
98
+ it('renders pagination from the current count and page after filtering', () => {
99
+ const { model, resource } = setupDetailsBlockModel();
100
+ resource.setData([{ id: 3, name: 'CC', title: 'filtered' }] as any);
101
+ resource.setMeta({ count: 7, page: 3, pageSize: 1, totalPage: 20 });
102
+
103
+ const paginationContainer = model.renderPagination() as any;
104
+ const pagination = paginationContainer.props.children;
105
+
106
+ expect(pagination.props.total).toBe(7);
107
+ expect(pagination.props.current).toBe(3);
108
+ expect(pagination.props.defaultCurrent).toBeUndefined();
109
+ });
110
+
111
+ it('updates the rendered pagination when filtering changes only resource metadata', () => {
112
+ const { model, resource } = setupDetailsBlockModel();
113
+ resource.setData([{ id: 1, name: 'AA', title: 'foo' }] as any);
114
+ resource.setMeta({ count: 7, page: 1, pageSize: 1, totalPage: 7 });
115
+
116
+ render(React.createElement(DetailsPagination, { model }));
117
+
118
+ expect(screen.getByRole('list')).toBeInTheDocument();
119
+
120
+ act(() => {
121
+ resource.setMeta({ count: 1, page: 1, pageSize: 1, totalPage: 1 });
122
+ });
123
+
124
+ expect(screen.queryByRole('list')).not.toBeInTheDocument();
125
+ });
126
+
96
127
  it('uses a real single-record details model and does not emit root paginationChange on initial render', async () => {
97
128
  const { model, resource } = setupDetailsBlockModel({ filterByTk: 1 });
98
129
  expect(resource).toBeInstanceOf(SingleRecordResource);