@nocobase/client-v2 2.2.0-alpha.1 → 2.2.0-alpha.3

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 (220) hide show
  1. package/es/PluginSettingsManager.d.ts +33 -0
  2. package/es/RouteRepository.d.ts +21 -6
  3. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  4. package/es/components/category-tabs/index.d.ts +9 -0
  5. package/es/components/form/JsonTextArea.d.ts +2 -1
  6. package/es/components/form/TypedVariableInput.d.ts +22 -4
  7. package/es/components/form/VariableJsonTextArea.d.ts +19 -0
  8. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  9. package/es/components/form/filter/index.d.ts +2 -0
  10. package/es/components/form/index.d.ts +1 -0
  11. package/es/components/index.d.ts +2 -0
  12. package/es/flow/actions/afterSuccess.d.ts +8 -1
  13. package/es/flow/actions/index.d.ts +1 -1
  14. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  15. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  16. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
  17. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  18. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -0
  19. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  20. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  21. package/es/flow/components/FieldAssignValueInput.d.ts +2 -0
  22. package/es/flow/index.d.ts +1 -0
  23. package/es/flow/models/base/GridModel.d.ts +1 -1
  24. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
  25. package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
  26. package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
  27. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
  28. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +30 -0
  29. package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
  30. package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
  31. package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
  32. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  33. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
  34. package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
  35. package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
  36. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
  37. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
  38. package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
  39. package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
  40. package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
  41. package/es/flow/utils/formValueDeps.d.ts +32 -0
  42. package/es/flow-compat/FieldValidation.d.ts +2 -1
  43. package/es/index.d.ts +2 -1
  44. package/es/index.mjs +233 -137
  45. package/lib/index.js +234 -138
  46. package/package.json +7 -7
  47. package/src/PluginSettingsManager.ts +53 -0
  48. package/src/RouteRepository.ts +126 -24
  49. package/src/__tests__/PluginSettingsManager.test.ts +13 -0
  50. package/src/__tests__/RouteRepository.test.ts +216 -0
  51. package/src/__tests__/browserChecker.test.ts +61 -0
  52. package/src/__tests__/exports.test.ts +16 -0
  53. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +199 -0
  54. package/src/__tests__/plugin-manager.test.tsx +44 -2
  55. package/src/__tests__/settings-center.test.tsx +40 -1
  56. package/src/collection-manager/field-configure.ts +1 -1
  57. package/src/collection-manager/interfaces/id.ts +1 -1
  58. package/src/collection-manager/interfaces/m2m.tsx +2 -2
  59. package/src/collection-manager/interfaces/m2o.tsx +2 -2
  60. package/src/collection-manager/interfaces/nanoid.ts +1 -1
  61. package/src/collection-manager/interfaces/o2m.tsx +2 -2
  62. package/src/collection-manager/interfaces/obo.tsx +2 -2
  63. package/src/collection-manager/interfaces/oho.tsx +2 -2
  64. package/src/collection-manager/interfaces/properties/index.ts +2 -2
  65. package/src/collection-manager/interfaces/uuid.ts +1 -1
  66. package/src/components/AppComponents.tsx +19 -3
  67. package/src/components/README.md +7 -1
  68. package/src/components/README.zh-CN.md +6 -1
  69. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  70. package/src/components/category-tabs/index.ts +10 -0
  71. package/src/components/form/JsonTextArea.tsx +21 -11
  72. package/src/components/form/TypedVariableInput.tsx +416 -93
  73. package/src/components/form/VariableJsonTextArea.tsx +175 -0
  74. package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
  75. package/src/components/form/__tests__/TypedVariableInput.test.tsx +268 -9
  76. package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
  77. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  78. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  79. package/src/components/form/filter/index.ts +2 -0
  80. package/src/components/form/index.tsx +1 -0
  81. package/src/components/index.ts +2 -0
  82. package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
  83. package/src/flow/__tests__/getKey.test.ts +7 -0
  84. package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
  85. package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
  86. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +58 -0
  87. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1022 -0
  88. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
  89. package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
  90. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
  91. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
  92. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +104 -0
  93. package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
  94. package/src/flow/actions/afterSuccess.tsx +142 -3
  95. package/src/flow/actions/customVariable.tsx +1 -2
  96. package/src/flow/actions/dataScopeFilter.ts +10 -0
  97. package/src/flow/actions/dateTimeFormat.tsx +42 -28
  98. package/src/flow/actions/index.ts +1 -1
  99. package/src/flow/actions/linkageRules.tsx +123 -35
  100. package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
  101. package/src/flow/actions/openView.tsx +38 -4
  102. package/src/flow/actions/runjs.tsx +2 -14
  103. package/src/flow/actions/validation.tsx +62 -30
  104. package/src/flow/admin-shell/BaseLayoutModel.tsx +25 -3
  105. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +5 -2
  106. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +13 -0
  107. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +34 -10
  108. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
  109. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
  110. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  111. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  112. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  113. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  114. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
  115. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
  116. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
  117. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  118. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +82 -0
  119. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
  120. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  121. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  122. package/src/flow/components/DefaultValue.tsx +1 -2
  123. package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
  124. package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
  125. package/src/flow/components/FieldAssignValueInput.tsx +10 -5
  126. package/src/flow/components/FlowRoute.tsx +56 -11
  127. package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
  128. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
  129. package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
  130. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  131. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  132. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  133. package/src/flow/components/fieldAssignOptions.ts +155 -27
  134. package/src/flow/flows/editMarkdownFlow.tsx +1 -1
  135. package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
  136. package/src/flow/index.ts +2 -1
  137. package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
  138. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  139. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  140. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  141. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  142. package/src/flow/models/base/ActionModel.tsx +12 -1
  143. package/src/flow/models/base/ActionModelCore.tsx +6 -4
  144. package/src/flow/models/base/GridModel.tsx +38 -7
  145. package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
  146. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
  147. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  148. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
  149. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  150. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  151. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +31 -3
  152. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  153. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
  154. package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
  155. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
  156. package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
  157. package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
  158. package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
  159. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  160. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  161. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
  162. package/src/flow/models/blocks/form/submitHandler.ts +17 -3
  163. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +940 -75
  164. package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
  165. package/src/flow/models/blocks/form/value-runtime/runtime.ts +348 -13
  166. package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
  167. package/src/flow/models/blocks/js-block/JSBlock.tsx +225 -9
  168. package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
  169. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  170. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +36 -13
  171. package/src/flow/models/blocks/table/TableBlockModel.tsx +38 -17
  172. package/src/flow/models/blocks/table/TableColumnModel.tsx +36 -3
  173. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +111 -1
  174. package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
  175. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  176. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
  177. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +159 -1
  178. package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
  179. package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
  180. package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
  181. package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
  182. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +29 -6
  183. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +16 -4
  184. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
  185. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
  186. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -0
  187. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
  188. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
  189. package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
  190. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
  191. package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
  192. package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
  193. package/src/flow/models/fields/InputFieldModel.tsx +40 -2
  194. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  195. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  196. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  197. package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
  198. package/src/flow/models/fields/TextareaFieldModel.tsx +49 -3
  199. package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
  200. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +117 -1
  201. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
  202. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +100 -0
  203. package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
  204. package/src/flow/resolveViewParamsToViewList.tsx +11 -3
  205. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +258 -0
  206. package/src/flow/utils/dataScopeFormValueClear.ts +218 -81
  207. package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
  208. package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
  209. package/src/flow/utils/formValueDeps.ts +170 -0
  210. package/src/flow-compat/FieldValidation.tsx +122 -60
  211. package/src/flow-compat/Popover.tsx +43 -4
  212. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  213. package/src/index.ts +8 -1
  214. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  215. package/src/nocobase-buildin-plugin/index.tsx +44 -14
  216. package/src/settings-center/AdminSettingsLayout.tsx +10 -2
  217. package/src/settings-center/SystemSettingsPage.tsx +1 -2
  218. package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
  219. package/src/settings-center/plugin-manager/index.tsx +3 -0
  220. package/src/settings-center/utils.tsx +0 -6
@@ -12,6 +12,10 @@ import { act, render, screen, waitFor } from '@testing-library/react';
12
12
  import React from 'react';
13
13
  import { NocoBaseBuildInPlugin } from '../nocobase-buildin-plugin';
14
14
 
15
+ const TestingAppSpin = () => <div data-testid="app-spin">app spin</div>;
16
+
17
+ const TestingAppError = ({ error }: { error: Error }) => <div role="alert">{error.message}</div>;
18
+
15
19
  class SkippedPublicRoutePlugin extends Plugin {
16
20
  async load() {
17
21
  this.router.add('public', {
@@ -22,6 +26,31 @@ class SkippedPublicRoutePlugin extends Plugin {
22
26
  }
23
27
  }
24
28
 
29
+ class DummySigninRoutePlugin extends Plugin {
30
+ async load() {
31
+ this.router.add('signin-test', {
32
+ path: '/signin',
33
+ skipAuthCheck: true,
34
+ Component: () => <div>signin page</div>,
35
+ });
36
+ }
37
+ }
38
+
39
+ class AuthBootstrapRoutePlugin extends Plugin {
40
+ async load() {
41
+ this.router.add('secure', {
42
+ path: '/secure',
43
+ authCheck: true,
44
+ Component: () => <div>secure page</div>,
45
+ });
46
+ this.router.add('guest', {
47
+ path: '/guest',
48
+ authCheck: false,
49
+ Component: () => <div>guest page</div>,
50
+ });
51
+ }
52
+ }
53
+
25
54
  describe('nocobase buildin plugin auth redirect', () => {
26
55
  const originalLocation = globalThis.window.location;
27
56
 
@@ -155,6 +184,176 @@ describe('nocobase buildin plugin auth redirect', () => {
155
184
  });
156
185
  });
157
186
 
187
+ it('should defer data source bootstrap until auth-required route is authenticated after signin redirect', async () => {
188
+ const app = createMockClient({
189
+ publicPath: '/v2/',
190
+ plugins: [NocoBaseBuildInPlugin as any, DummySigninRoutePlugin as any, AuthBootstrapRoutePlugin as any],
191
+ components: { AppSpin: TestingAppSpin },
192
+ router: { type: 'memory', initialEntries: ['/v2/secure'] },
193
+ });
194
+ app.apiMock.onGet('app:getLang').reply(200, {
195
+ data: { lang: 'en-US', resources: { client: {} }, cron: {} },
196
+ });
197
+ const events: string[] = [];
198
+ app.apiMock.onGet('/auth:check').replyOnce(() => {
199
+ events.push('auth:first');
200
+ return [200, { data: {} }];
201
+ });
202
+ app.apiMock.onGet('/auth:check').reply(() => {
203
+ events.push('auth:second');
204
+ return [200, { data: { id: 1 } }];
205
+ });
206
+
207
+ const ensureLoaded = vi.spyOn(app.dataSourceManager, 'ensureLoaded').mockImplementation(() => {
208
+ if (!app.apiClient.auth.token) {
209
+ const pending = new Promise<void>(() => undefined);
210
+ app.dataSourceManager.loadingPromise = pending;
211
+ return pending;
212
+ }
213
+ events.push('bootstrap');
214
+ expect(events).toContain('auth:second');
215
+ return Promise.resolve();
216
+ });
217
+
218
+ const Root = app.getRootComponent();
219
+ render(<Root />);
220
+
221
+ await waitFor(() => {
222
+ expect(app.router.router.state.location.pathname).toBe('/v2/signin');
223
+ expect(app.router.router.state.location.search).toBe('?redirect=%2Fv2%2Fsecure');
224
+ });
225
+ expect(await screen.findByText('signin page')).toBeInTheDocument();
226
+ expect(screen.queryByTestId('app-spin')).not.toBeInTheDocument();
227
+ expect(ensureLoaded).not.toHaveBeenCalled();
228
+ expect(app.dataSourceManager.loadingPromise).toBeNull();
229
+ expect(events).toEqual(['auth:first']);
230
+
231
+ act(() => {
232
+ app.apiClient.auth.setToken('test-token');
233
+ });
234
+
235
+ await act(async () => {
236
+ await app.router.router.navigate('/secure');
237
+ });
238
+
239
+ expect(await screen.findByText('secure page')).toBeInTheDocument();
240
+ await waitFor(() => {
241
+ expect(document.querySelector('.ant-spin-spinning')).not.toBeInTheDocument();
242
+ });
243
+ expect(ensureLoaded).toHaveBeenCalledTimes(1);
244
+ });
245
+
246
+ it('should redirect on auth-required /auth:check 401 without bootstrapping data sources', async () => {
247
+ const app = createMockClient({
248
+ publicPath: '/v2/',
249
+ plugins: [NocoBaseBuildInPlugin as any, DummySigninRoutePlugin as any, AuthBootstrapRoutePlugin as any],
250
+ components: { AppSpin: TestingAppSpin },
251
+ router: { type: 'memory', initialEntries: ['/v2/secure'] },
252
+ });
253
+ app.apiMock.onGet('app:getLang').reply(200, {
254
+ data: { lang: 'en-US', resources: { client: {} }, cron: {} },
255
+ });
256
+ app.apiMock.onGet('/auth:check').reply(401, { errors: [{ code: 'EMPTY_TOKEN' }] });
257
+ const ensureLoaded = vi.spyOn(app.dataSourceManager, 'ensureLoaded').mockImplementation(() => {
258
+ const pending = new Promise<void>(() => undefined);
259
+ app.dataSourceManager.loadingPromise = pending;
260
+ return pending;
261
+ });
262
+
263
+ const Root = app.getRootComponent();
264
+ render(<Root />);
265
+
266
+ await waitFor(() => {
267
+ expect(app.router.router.state.location.pathname).toBe('/v2/signin');
268
+ expect(app.router.router.state.location.search).toBe('?redirect=%2Fv2%2Fsecure');
269
+ });
270
+ expect(await screen.findByText('signin page')).toBeInTheDocument();
271
+ expect(screen.queryByTestId('app-spin')).not.toBeInTheDocument();
272
+ expect(ensureLoaded).not.toHaveBeenCalled();
273
+ expect(app.dataSourceManager.loadingPromise).toBeNull();
274
+ });
275
+
276
+ it('should surface non-auth /auth:check errors instead of leaving auth-required route spinning', async () => {
277
+ const app = createMockClient({
278
+ publicPath: '/v2/',
279
+ plugins: [NocoBaseBuildInPlugin as any, AuthBootstrapRoutePlugin as any],
280
+ components: { AppSpin: TestingAppSpin, AppError: TestingAppError },
281
+ router: { type: 'memory', initialEntries: ['/v2/secure'] },
282
+ });
283
+ app.apiMock.onGet('app:getLang').reply(200, {
284
+ data: { lang: 'en-US', resources: { client: {} }, cron: {} },
285
+ });
286
+ app.apiMock.onGet('/auth:check').reply(500, { errors: [{ message: 'auth check failed' }] });
287
+ const ensureLoaded = vi.spyOn(app.dataSourceManager, 'ensureLoaded').mockResolvedValue(undefined);
288
+
289
+ const Root = app.getRootComponent();
290
+ render(<Root />);
291
+
292
+ expect(await screen.findByRole('alert')).toHaveTextContent('Request failed with status code 500');
293
+ expect(screen.queryByTestId('app-spin')).not.toBeInTheDocument();
294
+ expect(ensureLoaded).not.toHaveBeenCalled();
295
+ });
296
+
297
+ it.each(['/v2/signin', '/v2/public'])('should not bootstrap data sources on skipped auth route: %s', async (path) => {
298
+ const app = createMockClient({
299
+ publicPath: '/v2/',
300
+ plugins: [NocoBaseBuildInPlugin as any, DummySigninRoutePlugin as any, SkippedPublicRoutePlugin as any],
301
+ components: { AppSpin: TestingAppSpin },
302
+ router: { type: 'memory', initialEntries: [path] },
303
+ });
304
+ app.apiMock.onGet('app:getLang').reply(200, {
305
+ data: { lang: 'en-US', resources: { client: {} }, cron: {} },
306
+ });
307
+ const ensureLoaded = vi.spyOn(app.dataSourceManager, 'ensureLoaded').mockResolvedValue(undefined);
308
+
309
+ const Root = app.getRootComponent();
310
+ render(<Root />);
311
+
312
+ expect(await screen.findByText(path === '/v2/signin' ? 'signin page' : 'public page')).toBeInTheDocument();
313
+ expect(screen.queryByTestId('app-spin')).not.toBeInTheDocument();
314
+ expect(ensureLoaded).not.toHaveBeenCalled();
315
+ expect(app.apiMock.history.get.filter((request) => request.url === '/auth:check')).toHaveLength(0);
316
+ });
317
+
318
+ it('should bootstrap data sources for authenticated auth-required route access', async () => {
319
+ const app = createMockClient({
320
+ publicPath: '/v2/',
321
+ plugins: [NocoBaseBuildInPlugin as any, AuthBootstrapRoutePlugin as any],
322
+ router: { type: 'memory', initialEntries: ['/v2/secure'] },
323
+ });
324
+ app.apiClient.auth.setToken('test-token');
325
+ app.apiMock.onGet('app:getLang').reply(200, {
326
+ data: { lang: 'en-US', resources: { client: {} }, cron: {} },
327
+ });
328
+ app.apiMock.onGet('/auth:check').reply(200, { data: { id: 1 } });
329
+ const ensureLoaded = vi.spyOn(app.dataSourceManager, 'ensureLoaded').mockResolvedValue(undefined);
330
+
331
+ const Root = app.getRootComponent();
332
+ render(<Root />);
333
+
334
+ expect(await screen.findByText('secure page')).toBeInTheDocument();
335
+ expect(ensureLoaded).toHaveBeenCalledTimes(1);
336
+ });
337
+
338
+ it('should not auth-check or bootstrap authCheck false routes', async () => {
339
+ const app = createMockClient({
340
+ publicPath: '/v2/',
341
+ plugins: [NocoBaseBuildInPlugin as any, AuthBootstrapRoutePlugin as any],
342
+ router: { type: 'memory', initialEntries: ['/v2/guest'] },
343
+ });
344
+ app.apiMock.onGet('app:getLang').reply(200, {
345
+ data: { lang: 'en-US', resources: { client: {} }, cron: {} },
346
+ });
347
+ const ensureLoaded = vi.spyOn(app.dataSourceManager, 'ensureLoaded').mockResolvedValue(undefined);
348
+
349
+ const Root = app.getRootComponent();
350
+ render(<Root />);
351
+
352
+ expect(await screen.findByText('guest page')).toBeInTheDocument();
353
+ expect(ensureLoaded).not.toHaveBeenCalled();
354
+ expect(app.apiMock.history.get.filter((request) => request.url === '/auth:check')).toHaveLength(0);
355
+ });
356
+
158
357
  it('should render v2 admin root without redirecting away', async () => {
159
358
  const app = createMockClient({
160
359
  publicPath: '/v2/',
@@ -18,6 +18,19 @@ class TestAclPlugin extends Plugin {
18
18
  }
19
19
  }
20
20
 
21
+ class TestSettingsLinkPlugin extends Plugin {
22
+ async load() {
23
+ this.app.pluginSettingsManager.addMenuItem({ key: 'shared-settings', title: 'Shared settings' });
24
+ this.app.pluginSettingsManager.addPageTabItem({
25
+ menuKey: 'shared-settings',
26
+ key: 'target',
27
+ title: 'Target settings',
28
+ Component: () => <div>Target settings page</div>,
29
+ });
30
+ this.app.pluginSettingsManager.setPluginSettingsLink('demo-plugin', 'shared-settings.target');
31
+ }
32
+ }
33
+
21
34
  type MockClientApplication = ReturnType<typeof createMockClient>;
22
35
 
23
36
  const renderApp = (app: MockClientApplication) => {
@@ -35,9 +48,9 @@ const waitForGetRequests = async (app: MockClientApplication, urls: string[]) =>
35
48
  );
36
49
  };
37
50
 
38
- const setupApp = (pmList: any[]) => {
51
+ const setupApp = (pmList: any[], plugins: Array<typeof Plugin> = []) => {
39
52
  const app = createMockClient({
40
- plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
53
+ plugins: [NocoBaseBuildInPlugin, TestAclPlugin, ...plugins],
41
54
  router: { type: 'memory', initialEntries: ['/admin/settings/plugin-manager'] },
42
55
  });
43
56
 
@@ -174,4 +187,33 @@ describe('plugin-manager page', () => {
174
187
  expect(removeCall?.params).toMatchObject({ filterByTk: 'demo-plugin' });
175
188
  });
176
189
  });
190
+
191
+ it('shows Settings for a plugin linked to a different settings page', async () => {
192
+ const app = setupApp(
193
+ [
194
+ {
195
+ name: 'demo-plugin',
196
+ packageName: '@nocobase/demo-plugin',
197
+ displayName: 'Demo plugin',
198
+ description: 'A demo',
199
+ enabled: true,
200
+ builtIn: false,
201
+ removable: false,
202
+ version: '0.1.0',
203
+ isCompatible: true,
204
+ keywords: [],
205
+ },
206
+ ],
207
+ [TestSettingsLinkPlugin],
208
+ );
209
+
210
+ renderApp(app);
211
+ await waitForGetRequests(app, ['/auth:check', 'roles:check', 'pm:list']);
212
+
213
+ const card = await screen.findByRole('button', { name: 'Demo plugin' });
214
+ const settingsLink = within(card.closest('.ant-card') as HTMLElement).getByText('Settings');
215
+ fireEvent.click(settingsLink);
216
+
217
+ expect(await screen.findByText('Target settings page')).toBeInTheDocument();
218
+ });
177
219
  });
@@ -14,7 +14,7 @@ import { message } from 'antd';
14
14
  import { AdminSettingsLayoutModel as ClientV2AdminSettingsLayoutModel } from '../settings-center';
15
15
  import { AdminSettingsLayoutModel as ClientV1AdminSettingsLayoutModel } from '../../../client/src/pm/AdminSettingsLayoutModel';
16
16
  import { NocoBaseBuildInPlugin } from '../nocobase-buildin-plugin';
17
- import { matchSettingsRoute } from '../settings-center/utils';
17
+ import { matchSettingsRoute, sortTopLevelSettings } from '../settings-center/utils';
18
18
 
19
19
  class TestAclPlugin extends Plugin {
20
20
  async load() {
@@ -168,6 +168,12 @@ describe('settings center', () => {
168
168
  });
169
169
  });
170
170
 
171
+ it('should sort system-settings with other top-level settings by normal ordering', () => {
172
+ const settings = [{ name: 'system-settings' }, { name: 'api-keys' }, { name: 'backups' }] as any;
173
+
174
+ expect(sortTopLevelSettings(settings).map((item) => item.name)).toEqual(['api-keys', 'backups', 'system-settings']);
175
+ });
176
+
171
177
  it('should redirect /admin/settings to system-settings by default', async () => {
172
178
  const app = createMockClient({
173
179
  plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
@@ -354,6 +360,39 @@ describe('settings center', () => {
354
360
  expect(screen.getByRole('menuitem', { name: 'Menu ACL Demo' })).toBeInTheDocument();
355
361
  });
356
362
 
363
+ it('should allow the settings sidebar menu to scroll independently', async () => {
364
+ class ManySettingsPlugin extends Plugin {
365
+ async load() {
366
+ for (let index = 0; index < 30; index += 1) {
367
+ this.pluginSettingsManager.addMenuItem({
368
+ key: `scroll-demo-${index}`,
369
+ title: `Scroll demo ${index}`,
370
+ });
371
+ this.pluginSettingsManager.addPageTabItem({
372
+ menuKey: `scroll-demo-${index}`,
373
+ key: 'index',
374
+ title: `Scroll demo ${index}`,
375
+ Component: () => <div>{`Scroll demo page ${index}`}</div>,
376
+ });
377
+ }
378
+ }
379
+ }
380
+
381
+ const app = createMockClient({
382
+ plugins: [NocoBaseBuildInPlugin, TestAclPlugin, ManySettingsPlugin],
383
+ router: { type: 'memory', initialEntries: ['/admin/settings/scroll-demo-29'] },
384
+ });
385
+ mockAdminRuntime(app);
386
+
387
+ await renderApp(app);
388
+ await waitForGetRequests(app, ['/auth:check', 'roles:check']);
389
+
390
+ expect(await screen.findByText('Scroll demo page 29')).toBeInTheDocument();
391
+
392
+ const sidebar = screen.getByRole('menuitem', { name: 'Scroll demo 29' }).closest('.ant-layout-sider');
393
+ expect(sidebar).toHaveStyle({ overflowY: 'auto' });
394
+ });
395
+
357
396
  it('should save system settings through systemSettings:put', async () => {
358
397
  const app = createMockClient({
359
398
  plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
@@ -530,7 +530,7 @@ export function reverseFieldConfigureItems(): FieldConfigureItem[] {
530
530
  component: 'Input',
531
531
  required: true,
532
532
  description:
533
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
533
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
534
534
  hidden: ({ context, values }) => !context.showReverseFieldConfig && !get(values, 'autoCreateReverseField'),
535
535
  disabled: ({ context }) => !context.showReverseFieldConfig,
536
536
  },
@@ -48,7 +48,7 @@ export class IdFieldInterface extends CollectionFieldInterface {
48
48
  'x-decorator': 'FormItem',
49
49
  'x-component': 'Input',
50
50
  description:
51
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
51
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
52
52
  },
53
53
  };
54
54
  filterable = {
@@ -158,7 +158,7 @@ export class M2MFieldInterface extends CollectionFieldInterface {
158
158
  required: true,
159
159
  default: '{{ useNewId("f_") }}',
160
160
  description:
161
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
161
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
162
162
  'x-decorator': 'FormItem',
163
163
  'x-component': 'ForeignKey',
164
164
  'x-validator': 'uid',
@@ -192,7 +192,7 @@ export class M2MFieldInterface extends CollectionFieldInterface {
192
192
  required: true,
193
193
  default: '{{ useNewId("f_") }}',
194
194
  description:
195
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
195
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
196
196
  'x-decorator': 'FormItem',
197
197
  'x-component': 'ForeignKey',
198
198
  'x-validator': 'uid',
@@ -69,7 +69,7 @@ export class M2OFieldInterface extends CollectionFieldInterface {
69
69
  'x-decorator': 'FormItem',
70
70
  'x-component': 'Input',
71
71
  description:
72
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
72
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
73
73
  },
74
74
  type: relationshipType,
75
75
  grid: {
@@ -123,7 +123,7 @@ export class M2OFieldInterface extends CollectionFieldInterface {
123
123
  required: true,
124
124
  default: '{{ useNewId("f_") }}',
125
125
  description:
126
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
126
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
127
127
  'x-decorator': 'FormItem',
128
128
  'x-component': 'ForeignKey',
129
129
  'x-validator': 'uid',
@@ -42,7 +42,7 @@ export class NanoidFieldInterface extends CollectionFieldInterface {
42
42
  'x-component': 'Input',
43
43
  'x-disabled': '{{ !createOnly }}',
44
44
  description:
45
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
45
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
46
46
  },
47
47
  customAlphabet: {
48
48
  type: 'string',
@@ -68,7 +68,7 @@ export class O2MFieldInterface extends CollectionFieldInterface {
68
68
  'x-decorator': 'FormItem',
69
69
  'x-component': 'Input',
70
70
  description:
71
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
71
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
72
72
  },
73
73
  type: relationshipType,
74
74
  grid: {
@@ -135,7 +135,7 @@ export class O2MFieldInterface extends CollectionFieldInterface {
135
135
  required: true,
136
136
  default: '{{ useNewId("f_") }}',
137
137
  description:
138
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
138
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
139
139
  'x-decorator': 'FormItem',
140
140
  'x-component': 'ForeignKey',
141
141
  'x-validator': 'uid',
@@ -55,7 +55,7 @@ export class OBOFieldInterface extends CollectionFieldInterface {
55
55
  'x-decorator': 'FormItem',
56
56
  'x-component': 'Input',
57
57
  description:
58
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
58
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
59
59
  },
60
60
  type: relationshipType,
61
61
  grid: {
@@ -109,7 +109,7 @@ export class OBOFieldInterface extends CollectionFieldInterface {
109
109
  required: true,
110
110
  default: '{{ useNewId("f_") }}',
111
111
  description:
112
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
112
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
113
113
  'x-decorator': 'FormItem',
114
114
  'x-component': 'ForeignKey',
115
115
  'x-validator': 'uid',
@@ -55,7 +55,7 @@ export class OHOFieldInterface extends CollectionFieldInterface {
55
55
  'x-decorator': 'FormItem',
56
56
  'x-component': 'Input',
57
57
  description:
58
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
58
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
59
59
  },
60
60
  type: relationshipType,
61
61
  grid: {
@@ -123,7 +123,7 @@ export class OHOFieldInterface extends CollectionFieldInterface {
123
123
  required: true,
124
124
  default: '{{ useNewId("f_") }}',
125
125
  description:
126
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
126
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
127
127
  'x-decorator': 'FormItem',
128
128
  'x-component': 'ForeignKey',
129
129
  'x-validator': 'uid',
@@ -141,7 +141,7 @@ export const reverseFieldProperties: Record<string, ISchema> = {
141
141
  'x-validator': 'uid',
142
142
  'x-disabled': '{{ !showReverseFieldConfig }}',
143
143
  description:
144
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
144
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
145
145
  },
146
146
  },
147
147
  },
@@ -399,7 +399,7 @@ export const defaultProps = {
399
399
  'x-component': 'Input',
400
400
  'x-validator': 'uid',
401
401
  description:
402
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
402
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
403
403
  },
404
404
  };
405
405
 
@@ -46,7 +46,7 @@ export class UUIDFieldInterface extends CollectionFieldInterface {
46
46
  'x-component': 'Input',
47
47
  'x-disabled': '{{ !createOnly }}',
48
48
  description:
49
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
49
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
50
50
  },
51
51
  autoFill,
52
52
  layout: {
@@ -21,7 +21,13 @@ import type { Application } from '../Application';
21
21
  interface AppErrorPayload {
22
22
  code?: string;
23
23
  message?: string;
24
- command?: { name: string };
24
+ command?: {
25
+ name: string;
26
+ components?: {
27
+ maintaining?: string;
28
+ maintainingDialog?: string;
29
+ };
30
+ };
25
31
  [key: string]: any;
26
32
  }
27
33
 
@@ -188,7 +194,12 @@ export const AppError: FC<{ error: Error & { title?: string }; app: Application
188
194
  );
189
195
 
190
196
  export const AppMaintaining: FC<{ app: Application; error: Error }> = observer(
191
- ({ app }) => {
197
+ ({ app, error }) => {
198
+ const component = (error as AppErrorPayload | undefined)?.command?.components?.maintaining;
199
+ if (component) {
200
+ return app.renderComponent(component, { app, error });
201
+ }
202
+
192
203
  const { icon, status, title, subTitle } = getProps(app);
193
204
  return (
194
205
  <div>
@@ -211,7 +222,12 @@ export const AppMaintaining: FC<{ app: Application; error: Error }> = observer(
211
222
  );
212
223
 
213
224
  export const AppMaintainingDialog: FC<{ app: Application; error: Error }> = observer(
214
- ({ app }) => {
225
+ ({ app, error }) => {
226
+ const component = (error as AppErrorPayload | undefined)?.command?.components?.maintainingDialog;
227
+ if (component) {
228
+ return app.renderComponent(component, { app, error });
229
+ }
230
+
215
231
  const { icon, status, title, subTitle } = getProps(app);
216
232
  return (
217
233
  <Modal open={true} footer={null} closable={false}>
@@ -190,6 +190,10 @@ import { TypedVariableInput } from '@nocobase/client-v2';
190
190
  <Form.Item name={['options', 'secure']} label={t('Secure')} initialValue={true}>
191
191
  <TypedVariableInput types={['boolean']} namespaces={['$env']} />
192
192
  </Form.Item>
193
+
194
+ // Inject a custom variable tree (e.g. a workflow node's upstream outputs,
195
+ // which are not in the global registry)
196
+ <TypedVariableInput types={['string', 'number']} metaTree={workflowMetaTree} />
193
197
  ```
194
198
 
195
199
  Key props:
@@ -197,6 +201,7 @@ Key props:
197
201
  - `types`: allowed constant types. Shape mirrors v1 `useTypedConstant` — pass bare type names (`['number', 'boolean']`) or `[type, editorProps]` tuples (`[['number', { min, max, step }]]`) to forward props to the underlying antd editor. Defaults to `['string', 'number', 'boolean', 'date']`. **Even when only one type is allowed, the `Constant` entry still expands into a typed submenu** (Number / Boolean / Date / String) — matches v1 so users can see what type the constant is
198
202
  - `namespaces`: restrict the variable picker to specific top-level namespaces (e.g. `['$env']`). Omit to expose every namespace registered on `flowEngine.context`
199
203
  - `extraNodes`: static leaves appended after the namespace-filtered nodes
204
+ - `metaTree`: **inject the variable tree directly** instead of reading the global `flowEngine.context` meta tree. When set, `namespaces`/`extraNodes` are ignored and this tree is used verbatim — for context-scoped variable sources that are not in the global registry (typically a workflow node's upstream outputs, `$jobsMapByNodeKey`). Nodes whose `children` is a thunk (`() => Promise<MetaTreeNode[]>`) are **lazy-loaded on expand** (via flow-engine's `loadMetaTreeChildren`)
200
205
  - `nullable`: whether to expose the `Null` switcher entry. Default `true`. Combined with `Form.Item.rules={[{ required: true }]}`, the user can explicitly clear the field but submission is still blocked by validation — mirrors v1's "Null + required" pairing
201
206
  - `delimiters`: variable-token delimiters, default `['{{', '}}']` — same as `VariableInput`
202
207
  - `value` / `onChange` / `placeholder` / `disabled` / `style` / `className`: standard controlled-input props
@@ -212,9 +217,10 @@ When **not** to use it:
212
217
  - **Pure literal fields** (users will never pass a variable) → use the antd primitive directly (`InputNumber` / `Select` / `DatePicker` / `Input`) and skip the Cascader column overhead
213
218
  - **Pure variable fields** (users will never pass a literal) → use `EnvVariableInput` (`$env`-only, with optional password masking) or `VariableInput` (general-purpose)
214
219
 
220
+ Supported constant types: `string` / `number` / `boolean` / `date` / `object`. The `object` (JSON) type renders an inline monospace textarea (2 rows by default, drag-resizable) — it keeps the raw text as a draft while editing and `JSON.parse`s it back into an object on blur. On a parse failure it shows the raw `JSON.parse` message (e.g. `Expected property name or '}' in JSON at position …`, matching v1) on its own row below the input, and does not emit. Mirrors v1 `useTypedConstant`'s object form (default value `{}`).
221
+
215
222
  Capabilities skipped (present in v1, not yet ported to v2):
216
223
 
217
- - `object` constant type (JSON editor) — v2 has no inline "JSON editor + Cascader switcher" yet; add when there's a concrete caller
218
224
  - Async `loadChildren` cascading — most MetaTree namespaces are already eagerly resolved by `useFilteredMetaTree`, so this hasn't been needed
219
225
 
220
226
  #### FileSizeInput
@@ -190,6 +190,9 @@ import { TypedVariableInput } from '@nocobase/client-v2';
190
190
  <Form.Item name={['options', 'secure']} label={t('安全模式')} initialValue={true}>
191
191
  <TypedVariableInput types={['boolean']} namespaces={['$env']} />
192
192
  </Form.Item>
193
+
194
+ // 注入自定义变量树(如工作流节点的上游输出,不在全局注册表里)
195
+ <TypedVariableInput types={['string', 'number']} metaTree={workflowMetaTree} />
193
196
  ```
194
197
 
195
198
  主要属性:
@@ -197,6 +200,7 @@ import { TypedVariableInput } from '@nocobase/client-v2';
197
200
  - `types`:允许的常量类型。形态对齐 v1 `useTypedConstant`,可以传裸类型名 `['number', 'boolean']`,也可以传 `[type, editorProps]` 元组 `[['number', { min, max, step }]]` 把 props 透传给底层 antd 编辑器。默认 `['string', 'number', 'boolean', 'date']`。**即使只允许一种类型,「常量」入口也会展开二级菜单**(数字 / 逻辑值 / 日期 / 字符串)——跟 v1 一致,让用户能直观看到当前常量是什么类型
198
201
  - `namespaces`:限定变量 picker 可选的顶层命名空间(如 `['$env']`)。不传就用 `flowEngine.context` 里所有已注册命名空间
199
202
  - `extraNodes`:在命名空间过滤后追加几条静态变量节点
203
+ - `metaTree`:**直接注入变量树**,取代读取全局 `flowEngine.context` 的 MetaTree。传了它就**忽略** `namespaces`/`extraNodes`,原样使用这棵树——用于不在全局注册表里的、上下文相关的变量源(典型如工作流节点的上游节点输出 `$jobsMapByNodeKey`)。树里 `children` 为函数(`() => Promise<MetaTreeNode[]>`)的节点会在用户展开 Cascader 时**按需懒加载**(复用 flow-engine 的 `loadMetaTreeChildren`)
200
204
  - `nullable`:是否暴露「空值」入口,默认 `true`。配合 `Form.Item.rules={[{ required: true }]}` 可以让用户能手动清空、但提交时会被校验拦截——跟 v1 的「空值 + required」组合一致
201
205
  - `delimiters`:变量 token 开闭分隔符,默认 `['{{', '}}']`,跟 `VariableInput` 一致
202
206
  - `value` / `onChange` / `placeholder` / `disabled` / `style` / `className`:标准受控字段属性
@@ -212,9 +216,10 @@ import { TypedVariableInput } from '@nocobase/client-v2';
212
216
  - **纯字面量字段**(用户不会想填变量)→ 直接用 antd `InputNumber` / `Select` / `DatePicker` / `Input`,省掉 Cascader 那一格的视觉开销
213
217
  - **纯变量字段**(用户不会想填字面量)→ 用 `EnvVariableInput`(`$env` 专用,带 password mask)或 `VariableInput`(更通用)
214
218
 
219
+ 支持的常量类型:`string` / `number` / `boolean` / `date` / `object`。其中 `object`(即 JSON)渲染为一个等宽字体的内联 textarea(默认两行、可拖拽拉伸),编辑时保留原始文本草稿、失焦(blur)时 `JSON.parse` 回写为对象;解析失败则在输入框**下方单独一行**显示原生 `JSON.parse` 的错误信息(如 `Expected property name or '}' in JSON at position …`,对齐 v1)且不回写。对齐 v1 `useTypedConstant` 的 object 形态(默认值 `{}`)。
220
+
215
221
  跳过的能力(v1 有但 v2 还没补):
216
222
 
217
- - `object` 类型(JSON 编辑器)——v2 还没对应的「内联 JSON 编辑器 + Cascader 切换」组件,等真有需求再补
218
223
  - 异步 `loadChildren` 分支——大多数命名空间的 MetaTree 已经由 `useFilteredMetaTree` 提前展平,没遇到刚需
219
224
 
220
225
  #### FileSizeInput