@nocobase/client-v2 2.2.0-beta.9 → 3.0.0-alpha.1

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 (208) hide show
  1. package/es/APIClient.d.ts +1 -3
  2. package/es/BaseApplication.d.ts +1 -0
  3. package/es/acl/ACLProvider.d.ts +10 -0
  4. package/es/authRedirect.d.ts +5 -0
  5. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  6. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  7. package/es/components/AttachmentUpload.d.ts +51 -0
  8. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  9. package/es/components/category-tabs/index.d.ts +9 -0
  10. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  11. package/es/components/form/TypedVariableInput.d.ts +31 -5
  12. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  13. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  14. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  15. package/es/components/form/filter/index.d.ts +2 -0
  16. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  17. package/es/components/index.d.ts +2 -0
  18. package/es/flow/actions/index.d.ts +1 -1
  19. package/es/flow/actions/linkageRules.d.ts +3 -0
  20. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  21. package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
  22. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  23. package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
  24. package/es/flow/components/FlowRoute.d.ts +2 -2
  25. package/es/flow/components/RunJSValueEditor.d.ts +1 -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/index.d.ts +1 -0
  29. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  30. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  31. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  32. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  33. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  34. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  35. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  36. package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
  37. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  38. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  39. package/es/flow-compat/routeTypes.d.ts +1 -0
  40. package/es/index.d.ts +8 -1
  41. package/es/index.mjs +190 -141
  42. package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
  43. package/es/nocobase-buildin-plugin/index.d.ts +9 -0
  44. package/es/settings-app/SettingsApplication.d.ts +18 -0
  45. package/es/settings-app/SettingsBrand.d.ts +16 -0
  46. package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
  47. package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
  48. package/es/settings-app/SettingsGroupNav.d.ts +17 -0
  49. package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
  50. package/es/settings-app/SettingsRouterManager.d.ts +15 -0
  51. package/es/settings-app/SettingsSearch.d.ts +17 -0
  52. package/es/settings-app/SettingsShell.d.ts +10 -0
  53. package/es/settings-app/isSettingsApp.d.ts +18 -0
  54. package/es/settings-app/runtimePaths.d.ts +18 -0
  55. package/es/settings-app/settingsDocumentPath.d.ts +14 -0
  56. package/es/settings-app/settingsTheme.d.ts +87 -0
  57. package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
  58. package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
  59. package/es/settings-center/groups.d.ts +66 -0
  60. package/es/settings-center/useSettingsGroups.d.ts +36 -0
  61. package/es/settings-center/useSettingsSearch.d.ts +35 -0
  62. package/es/settings-center/utils.d.ts +24 -0
  63. package/es/theme/index.d.ts +1 -0
  64. package/es/theme/itemActive.d.ts +24 -0
  65. package/es/theme/type.d.ts +9 -0
  66. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  67. package/es/utils/index.d.ts +1 -0
  68. package/lib/index.js +195 -146
  69. package/lib/locale/languageCodes.js +3 -1
  70. package/package.json +9 -7
  71. package/src/APIClient.ts +1 -10
  72. package/src/Application.tsx +4 -0
  73. package/src/BaseApplication.tsx +11 -6
  74. package/src/__tests__/app.test.tsx +73 -0
  75. package/src/__tests__/authRedirect.test.ts +137 -0
  76. package/src/__tests__/browserChecker.test.ts +185 -6
  77. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  78. package/src/__tests__/mockSettingsApplication.ts +29 -0
  79. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +79 -10
  80. package/src/__tests__/plugin-manager.test.tsx +7 -6
  81. package/src/__tests__/settings-application.test.ts +164 -0
  82. package/src/__tests__/settings-auth-routes.test.ts +71 -0
  83. package/src/__tests__/settings-center.test.tsx +38 -37
  84. package/src/__tests__/settings-layout-root.test.tsx +260 -0
  85. package/src/__tests__/settings-runtime-paths.test.ts +99 -0
  86. package/src/__tests__/settings-shell.test.tsx +200 -0
  87. package/src/acl/ACLProvider.tsx +21 -0
  88. package/src/authRedirect.ts +72 -3
  89. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  90. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  91. package/src/collection-manager/field-validation.ts +1 -1
  92. package/src/components/AttachmentUpload.tsx +275 -0
  93. package/src/components/README.md +7 -1
  94. package/src/components/README.zh-CN.md +6 -1
  95. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  96. package/src/components/category-tabs/index.ts +10 -0
  97. package/src/components/form/JsonTextArea.tsx +4 -0
  98. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  99. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  100. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  101. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  102. package/src/components/form/TypedVariableInput.tsx +452 -101
  103. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  104. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  105. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  106. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  107. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  108. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  109. package/src/components/form/filter/index.ts +2 -0
  110. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  111. package/src/components/index.ts +2 -0
  112. package/src/flow/FlowPage.tsx +9 -1
  113. package/src/flow/__tests__/FlowPage.test.tsx +50 -3
  114. package/src/flow/__tests__/FlowRoute.test.tsx +128 -10
  115. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  116. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  117. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  118. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  119. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  120. package/src/flow/actions/index.ts +2 -0
  121. package/src/flow/actions/linkageRules.tsx +111 -16
  122. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  123. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -57
  124. package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
  125. package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
  126. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  127. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +77 -13
  128. package/src/flow/common/Markdown/style.ts +4 -0
  129. package/src/flow/components/BlockItemCard.tsx +2 -2
  130. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  131. package/src/flow/components/FieldAssignValueInput.tsx +91 -20
  132. package/src/flow/components/FlowRoute.tsx +78 -32
  133. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  134. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  135. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  136. package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
  137. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  138. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  139. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
  140. package/src/flow/index.ts +3 -0
  141. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  142. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  143. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  144. package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
  145. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  146. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  147. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  148. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  149. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  150. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  151. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  152. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  153. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  154. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  155. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
  156. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
  157. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
  158. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
  159. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  160. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  161. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  162. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  163. package/src/flow/models/blocks/table/TableBlockModel.tsx +4 -2
  164. package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
  165. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  166. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  167. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +2 -1
  168. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  169. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  170. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  171. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  172. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  173. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  174. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
  175. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
  176. package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
  177. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  178. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  179. package/src/flow-compat/routeTypes.ts +1 -0
  180. package/src/index.ts +14 -1
  181. package/src/locale/languageCodes.ts +3 -1
  182. package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
  183. package/src/nocobase-buildin-plugin/index.tsx +110 -70
  184. package/src/settings-app/SettingsApplication.ts +31 -0
  185. package/src/settings-app/SettingsBrand.tsx +108 -0
  186. package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
  187. package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
  188. package/src/settings-app/SettingsGroupNav.tsx +124 -0
  189. package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
  190. package/src/settings-app/SettingsRouterManager.ts +92 -0
  191. package/src/settings-app/SettingsSearch.tsx +238 -0
  192. package/src/settings-app/SettingsShell.tsx +167 -0
  193. package/src/settings-app/isSettingsApp.ts +21 -0
  194. package/src/settings-app/runtimePaths.ts +104 -0
  195. package/src/settings-app/settingsDocumentPath.ts +66 -0
  196. package/src/settings-app/settingsTheme.ts +301 -0
  197. package/src/settings-app/useSettingsThemeConfig.ts +100 -0
  198. package/src/settings-center/AdminSettingsLayout.tsx +122 -141
  199. package/src/settings-center/SystemSettingsPage.tsx +4 -170
  200. package/src/settings-center/groups.ts +108 -0
  201. package/src/settings-center/useSettingsGroups.ts +105 -0
  202. package/src/settings-center/useSettingsSearch.ts +173 -0
  203. package/src/settings-center/utils.tsx +50 -1
  204. package/src/theme/index.tsx +1 -0
  205. package/src/theme/itemActive.ts +31 -0
  206. package/src/theme/type.ts +10 -0
  207. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  208. package/src/utils/index.tsx +1 -0
@@ -11,23 +11,58 @@ import { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
11
11
  import { act, render, screen, waitFor } from '@testing-library/react';
12
12
  import React, { useEffect } from 'react';
13
13
  import { createMemoryRouter, RouterProvider, useParams } from 'react-router-dom';
14
- import { describe, expect, it, vi } from 'vitest';
14
+ import { afterEach, describe, expect, it, vi } from 'vitest';
15
+ import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../../../flow-compat';
15
16
  import type { LayoutDefinition } from '../../../../layout-manager/types';
16
17
  import { AdminLayoutComponent } from '../AdminLayoutComponent';
17
18
  import { AdminLayoutModel } from '../AdminLayoutModel';
19
+ import { AdminLayoutMenuItemModel } from '../AdminLayoutMenuModels';
18
20
  import { AdminLayoutContent } from '../AdminLayoutSlotModels';
19
21
 
22
+ const proLayoutLifecycle = vi.hoisted(() => ({
23
+ events: [] as string[],
24
+ routes: [] as Array<{ children?: Array<{ name?: React.ReactNode }> }>,
25
+ }));
26
+
20
27
  vi.mock('@ant-design/pro-layout', async () => {
21
28
  const ReactModule = await import('react');
22
29
  const RouteContext = ReactModule.createContext({ isMobile: false });
30
+ const ProLayoutMock = (props: {
31
+ children?: React.ReactNode;
32
+ route?: { children?: Array<{ name?: React.ReactNode }> };
33
+ }) => {
34
+ ReactModule.useEffect(() => {
35
+ proLayoutLifecycle.events.push('pro-layout:mount');
36
+ return () => {
37
+ proLayoutLifecycle.events.push('pro-layout:unmount');
38
+ };
39
+ }, []);
40
+ proLayoutLifecycle.routes.push(props.route || {});
23
41
 
24
- return {
25
- default: (props: { children?: React.ReactNode }) =>
42
+ return ReactModule.createElement(
43
+ RouteContext.Provider,
44
+ { value: { isMobile: false } },
26
45
  ReactModule.createElement(
27
- RouteContext.Provider,
28
- { value: { isMobile: false } },
29
- ReactModule.createElement('div', { 'data-testid': 'pro-layout' }, props.children),
46
+ 'div',
47
+ { 'data-testid': 'pro-layout' },
48
+ ReactModule.createElement(
49
+ 'nav',
50
+ { 'data-testid': 'pro-layout-menu' },
51
+ (props.route?.children || []).map((item) =>
52
+ ReactModule.createElement(
53
+ 'span',
54
+ { key: String(item.name), 'data-testid': 'pro-layout-menu-item' },
55
+ item.name,
56
+ ),
57
+ ),
58
+ ),
59
+ props.children,
30
60
  ),
61
+ );
62
+ };
63
+
64
+ return {
65
+ default: ProLayoutMock,
31
66
  RouteContext,
32
67
  };
33
68
  });
@@ -53,6 +88,10 @@ vi.mock('../AppListRender', () => ({
53
88
  useAppListRender: () => undefined,
54
89
  }));
55
90
 
91
+ afterEach(() => {
92
+ delete (window as Window & { __nocobase_modern_client_prefix__?: string }).__nocobase_modern_client_prefix__;
93
+ });
94
+
56
95
  describe('AdminLayoutComponent', () => {
57
96
  it('keeps custom admin layout pages alive by layout route name', async () => {
58
97
  const layout: LayoutDefinition = {
@@ -185,4 +224,212 @@ describe('AdminLayoutComponent', () => {
185
224
 
186
225
  expect(deactivateLayout).toHaveBeenCalledTimes(1);
187
226
  });
227
+
228
+ it('keeps layout content mounted when the menu route tree refreshes', async () => {
229
+ proLayoutLifecycle.events = [];
230
+ proLayoutLifecycle.routes = [];
231
+ const layout: LayoutDefinition = {
232
+ routeName: 'admin',
233
+ routePath: '/admin',
234
+ rootRouteName: 'admin',
235
+ uid: 'admin-layout-model',
236
+ layoutModelClass: 'AdminLayoutModel',
237
+ rootPageModelClass: 'RootPageModel',
238
+ childPageModelClass: 'ChildPageModel',
239
+ authCheck: true,
240
+ };
241
+ const events: string[] = [];
242
+ const engine = new FlowEngine();
243
+ engine.context.defineProperty('routeRepository', {
244
+ value: {
245
+ activateLayout: vi.fn(() => vi.fn()),
246
+ ensureAccessibleLoaded: vi.fn(async () => []),
247
+ listAccessible: () => [],
248
+ subscribe: vi.fn(),
249
+ unsubscribe: vi.fn(),
250
+ },
251
+ });
252
+ engine.context.defineProperty('t', {
253
+ value: (key: string) => key,
254
+ });
255
+ const model = engine.createModel<AdminLayoutModel>({
256
+ uid: layout.uid,
257
+ use: AdminLayoutModel,
258
+ props: {
259
+ layout,
260
+ },
261
+ });
262
+
263
+ const Page = () => {
264
+ useEffect(() => {
265
+ events.push('page:mount');
266
+ return () => {
267
+ events.push('page:unmount');
268
+ };
269
+ }, []);
270
+
271
+ return <div data-testid="custom-admin-route">Workflow tasks</div>;
272
+ };
273
+
274
+ const router = createMemoryRouter(
275
+ [
276
+ {
277
+ id: layout.routeName,
278
+ path: layout.routePath,
279
+ element: <AdminLayoutComponent model={model} />,
280
+ children: [
281
+ {
282
+ id: 'admin.workflow.tasks',
283
+ path: 'workflow/tasks/:taskType?/:status?/:popupId?',
284
+ element: <Page />,
285
+ },
286
+ ],
287
+ },
288
+ ],
289
+ {
290
+ initialEntries: ['/admin/workflow/tasks/approval-apply/pending'],
291
+ },
292
+ );
293
+
294
+ render(
295
+ <FlowEngineProvider engine={engine}>
296
+ <RouterProvider router={router} />
297
+ </FlowEngineProvider>,
298
+ );
299
+
300
+ expect(await screen.findByTestId('custom-admin-route')).toBeInTheDocument();
301
+ expect(proLayoutLifecycle.events).toEqual(['pro-layout:mount']);
302
+
303
+ act(() => {
304
+ model.refreshMenuRouteTree();
305
+ });
306
+
307
+ await act(async () => {
308
+ await router.navigate('/admin/workflow/tasks/approval-apply/pending/1');
309
+ });
310
+
311
+ expect(screen.getByTestId('custom-admin-route')).toBeInTheDocument();
312
+ expect(proLayoutLifecycle.events).toEqual(['pro-layout:mount']);
313
+ expect(events).toEqual(['page:mount']);
314
+ });
315
+
316
+ it('updates ProLayout menu routes after accessible routes change without remounting the layout', async () => {
317
+ proLayoutLifecycle.events = [];
318
+ proLayoutLifecycle.routes = [];
319
+ (window as Window & { __nocobase_modern_client_prefix__?: string }).__nocobase_modern_client_prefix__ = 'v2';
320
+ const layout: LayoutDefinition = {
321
+ routeName: 'admin',
322
+ routePath: '/admin',
323
+ rootRouteName: 'admin',
324
+ uid: 'admin-layout-model',
325
+ layoutModelClass: 'AdminLayoutModel',
326
+ rootPageModelClass: 'RootPageModel',
327
+ childPageModelClass: 'ChildPageModel',
328
+ authCheck: true,
329
+ };
330
+ let accessibleRoutes: NocoBaseDesktopRoute[] = [
331
+ {
332
+ id: 1,
333
+ title: 'Visible page',
334
+ schemaUid: 'visible-page',
335
+ type: NocoBaseDesktopRouteType.flowPage,
336
+ },
337
+ ];
338
+ let routeRepositorySubscriber: (() => void) | undefined;
339
+ const events: string[] = [];
340
+ const engine = new FlowEngine();
341
+ engine.registerModels({
342
+ AdminLayoutMenuItemModel,
343
+ });
344
+ engine.context.defineProperty('routeRepository', {
345
+ value: {
346
+ activateLayout: vi.fn(() => vi.fn()),
347
+ ensureAccessibleLoaded: vi.fn(async () => accessibleRoutes),
348
+ listAccessible: () => accessibleRoutes,
349
+ subscribe: vi.fn((subscriber: () => void) => {
350
+ routeRepositorySubscriber = subscriber;
351
+ }),
352
+ unsubscribe: vi.fn(),
353
+ },
354
+ });
355
+ engine.context.defineProperty('app', {
356
+ value: {
357
+ getPublicPath: () => '/v/',
358
+ router: {
359
+ getBasename: () => '/v',
360
+ },
361
+ },
362
+ });
363
+ engine.context.defineProperty('t', {
364
+ value: (key: string) => key,
365
+ });
366
+ const model = engine.createModel<AdminLayoutModel>({
367
+ uid: layout.uid,
368
+ use: AdminLayoutModel,
369
+ props: {
370
+ layout,
371
+ },
372
+ });
373
+
374
+ const Page = () => {
375
+ useEffect(() => {
376
+ events.push('page:mount');
377
+ return () => {
378
+ events.push('page:unmount');
379
+ };
380
+ }, []);
381
+
382
+ return <div data-testid="custom-admin-route">Workflow tasks</div>;
383
+ };
384
+
385
+ const router = createMemoryRouter(
386
+ [
387
+ {
388
+ id: layout.routeName,
389
+ path: layout.routePath,
390
+ element: <AdminLayoutComponent model={model} />,
391
+ children: [
392
+ {
393
+ id: 'admin.workflow.tasks',
394
+ path: 'workflow/tasks/:taskType?/:status?/:popupId?',
395
+ element: <Page />,
396
+ },
397
+ ],
398
+ },
399
+ ],
400
+ {
401
+ initialEntries: ['/admin/workflow/tasks/approval-apply/pending'],
402
+ },
403
+ );
404
+
405
+ render(
406
+ <FlowEngineProvider engine={engine}>
407
+ <RouterProvider router={router} />
408
+ </FlowEngineProvider>,
409
+ );
410
+
411
+ expect(await screen.findByText('Visible page')).toBeInTheDocument();
412
+ expect(proLayoutLifecycle.events).toEqual(['pro-layout:mount']);
413
+
414
+ act(() => {
415
+ accessibleRoutes = [];
416
+ routeRepositorySubscriber?.();
417
+ });
418
+
419
+ await waitFor(() => {
420
+ expect(screen.queryByText('Visible page')).not.toBeInTheDocument();
421
+ });
422
+
423
+ await act(async () => {
424
+ await router.navigate('/admin/workflow/tasks/approval-apply/pending/1');
425
+ });
426
+
427
+ expect(screen.getByTestId('custom-admin-route')).toBeInTheDocument();
428
+ expect(proLayoutLifecycle.events).toEqual(['pro-layout:mount']);
429
+ expect(events).toEqual(['page:mount']);
430
+ expect(
431
+ proLayoutLifecycle.routes.some((route) => route.children?.some((item) => item.name === 'Visible page')),
432
+ ).toBe(true);
433
+ expect(proLayoutLifecycle.routes.at(-1)?.children?.some((item) => item.name === 'Visible page')).toBe(false);
434
+ });
188
435
  });
@@ -136,6 +136,16 @@ describe('TopbarActionsBar helpers', () => {
136
136
  title: 'Plugin manager',
137
137
  path: '/admin/settings/plugin-manager',
138
138
  icon: null,
139
+ sort: -200,
140
+ componentLoader: async () => null,
141
+ },
142
+ {
143
+ key: 'multi-portal',
144
+ name: 'multi-portal',
145
+ title: 'Portal manager',
146
+ path: '/admin/settings/multi-portal',
147
+ icon: null,
148
+ sort: -300,
139
149
  componentLoader: async () => null,
140
150
  },
141
151
  {
@@ -159,13 +169,14 @@ describe('TopbarActionsBar helpers', () => {
159
169
  });
160
170
 
161
171
  expect((items as any[]).map((item) => item.type || item.key)).toEqual([
172
+ 'multi-portal',
162
173
  'plugin-manager',
163
174
  'divider',
164
175
  'system-settings',
165
176
  'divider',
166
177
  'security',
167
178
  ]);
168
- expect((items as any[])[2]).toMatchObject({
179
+ expect((items as any[]).find((item) => item.key === 'system-settings')).toMatchObject({
169
180
  key: 'system-settings',
170
181
  name: 'system-settings',
171
182
  path: '/admin/settings/system-settings',
@@ -191,7 +202,7 @@ describe('TopbarActionsBar helpers', () => {
191
202
  renderSettingsLabel((items as any[])[0].label, '/sales/p1');
192
203
 
193
204
  const link = screen.getByRole('link', { name: 'System settings' });
194
- expect(link).toHaveAttribute('href', '/nocobase/v/admin/settings/system-settings');
205
+ expect(link).toHaveAttribute('href', '/nocobase/settings/system-settings');
195
206
  expect(link).toHaveAttribute('target', '_blank');
196
207
  expect(link).toHaveAttribute('rel', expect.stringContaining('noopener'));
197
208
  expect(link).toHaveAttribute('rel', expect.stringContaining('noreferrer'));
@@ -216,7 +227,7 @@ describe('TopbarActionsBar helpers', () => {
216
227
  renderSettingsLabel((items as any[])[0].label, '/sales/p1');
217
228
 
218
229
  const link = screen.getByRole('link', { name: 'Plugin manager' });
219
- expect(link).toHaveAttribute('href', '/nocobase/v/admin/settings/plugin-manager');
230
+ expect(link).toHaveAttribute('href', '/nocobase/settings/plugin-manager');
220
231
  expect(link).toHaveAttribute('target', '_blank');
221
232
  expect(link).toHaveAttribute('rel', expect.stringContaining('noopener'));
222
233
  expect(link).toHaveAttribute('rel', expect.stringContaining('noreferrer'));
@@ -241,13 +252,13 @@ describe('TopbarActionsBar helpers', () => {
241
252
  renderSettingsLabel((items as any[])[0].label, '/apps/a_9xlild35jir/crm-amd/ekeisumx1zu');
242
253
 
243
254
  const link = screen.getByRole('link', { name: 'System settings' });
244
- expect(link).toHaveAttribute('href', '/nocobase/v/apps/a_9xlild35jir/admin/settings/system-settings');
255
+ expect(link).toHaveAttribute('href', '/nocobase/settings/apps/a_9xlild35jir/system-settings');
245
256
  expect(link).toHaveAttribute('target', '_blank');
246
257
  expect(link).toHaveAttribute('rel', expect.stringContaining('noopener'));
247
258
  expect(link).toHaveAttribute('rel', expect.stringContaining('noreferrer'));
248
259
  });
249
260
 
250
- it('should keep regular admin settings as SPA links inside admin runtime', () => {
261
+ it('should open regular admin settings in the standalone SPA from admin runtime', () => {
251
262
  const items = getTopbarPluginSettingsItems({
252
263
  canManagePlugins: false,
253
264
  t: (key) => key,
@@ -266,11 +277,12 @@ describe('TopbarActionsBar helpers', () => {
266
277
  renderSettingsLabel((items as any[])[0].label, '/admin/settings/routes');
267
278
 
268
279
  const link = screen.getByRole('link', { name: 'Routes' });
269
- expect(link).toHaveAttribute('href', '/admin/settings/routes');
270
- expect(link).not.toHaveAttribute('target', '_blank');
280
+ expect(link).toHaveAttribute('href', '/nocobase/settings/routes');
281
+ expect(link).toHaveAttribute('target', '_blank');
282
+ expect(link).toHaveAttribute('rel', expect.stringContaining('noopener'));
271
283
  });
272
284
 
273
- it('should keep sub-app admin settings in the current window inside sub-app admin runtime', () => {
285
+ it('should open sub-app settings in the standalone SPA from sub-app admin runtime', () => {
274
286
  const items = getTopbarPluginSettingsItems({
275
287
  canManagePlugins: false,
276
288
  t: (key) => key,
@@ -289,8 +301,9 @@ describe('TopbarActionsBar helpers', () => {
289
301
  renderSettingsLabel((items as any[])[0].label, '/apps/a_9xlild35jir/admin/settings/routes');
290
302
 
291
303
  const link = screen.getByRole('link', { name: 'Routes' });
292
- expect(link).toHaveAttribute('href', '/nocobase/v/apps/a_9xlild35jir/admin/settings/routes');
293
- expect(link).not.toHaveAttribute('target', '_blank');
304
+ expect(link).toHaveAttribute('href', '/nocobase/settings/apps/a_9xlild35jir/routes');
305
+ expect(link).toHaveAttribute('target', '_blank');
306
+ expect(link).toHaveAttribute('rel', expect.stringContaining('noopener'));
294
307
  });
295
308
 
296
309
  it('should not treat admin-like paths as admin runtime', () => {
@@ -312,11 +325,11 @@ describe('TopbarActionsBar helpers', () => {
312
325
  renderSettingsLabel((items as any[])[0].label, '/admin2/foo');
313
326
 
314
327
  const link = screen.getByRole('link', { name: 'System settings' });
315
- expect(link).toHaveAttribute('href', '/nocobase/v/admin/settings/system-settings');
328
+ expect(link).toHaveAttribute('href', '/nocobase/settings/system-settings');
316
329
  expect(link).toHaveAttribute('target', '_blank');
317
330
  });
318
331
 
319
- it('should not duplicate the basename when building new-tab settings hrefs', () => {
332
+ it('should remove the modern basename when building standalone settings hrefs', () => {
320
333
  const items = getTopbarPluginSettingsItems({
321
334
  canManagePlugins: false,
322
335
  t: (key) => key,
@@ -335,7 +348,30 @@ describe('TopbarActionsBar helpers', () => {
335
348
  renderSettingsLabel((items as any[])[0].label, '/sales/p1');
336
349
 
337
350
  const link = screen.getByRole('link', { name: 'System settings' });
338
- expect(link).toHaveAttribute('href', '/nocobase/v/admin/settings/system-settings');
351
+ expect(link).toHaveAttribute('href', '/nocobase/settings/system-settings');
352
+ });
353
+
354
+ it('should preserve the new sub-app scope in standalone settings hrefs', () => {
355
+ const items = getTopbarPluginSettingsItems({
356
+ canManagePlugins: false,
357
+ t: (key) => key,
358
+ settings: [
359
+ {
360
+ key: 'system-settings',
361
+ name: 'system-settings',
362
+ title: 'System settings',
363
+ path: '/admin/settings/system-settings',
364
+ icon: null,
365
+ componentLoader: async () => null,
366
+ },
367
+ ] as any,
368
+ });
369
+
370
+ renderSettingsLabel((items as any[])[0].label, '/_app/a_new/admin/settings/system-settings');
371
+
372
+ const link = screen.getByRole('link', { name: 'System settings' });
373
+ expect(link).toHaveAttribute('href', '/nocobase/settings/_app/a_new/system-settings');
374
+ expect(link).toHaveAttribute('target', '_blank');
339
375
  });
340
376
 
341
377
  it('should keep external settings opening in a new tab', () => {
@@ -402,6 +438,34 @@ describe('TopbarActionsBar', () => {
402
438
  vi.restoreAllMocks();
403
439
  });
404
440
 
441
+ it('should group plugin settings with Help and user center', () => {
442
+ render(
443
+ <TopbarActionsBar
444
+ actions={[
445
+ createAction({ uid: 'notification', actionId: 'notification' }),
446
+ createAction({ uid: 'plugin-settings', actionId: 'plugin-settings' }),
447
+ createAction({ uid: 'user-center', actionId: 'user-center' }),
448
+ ]}
449
+ />,
450
+ );
451
+
452
+ const notification = screen.getByTestId('flow-model-notification');
453
+ const pluginSettings = screen.getByTestId('flow-model-plugin-settings');
454
+ const help = screen.getByTestId('help-lite');
455
+ const userCenter = screen.getByTestId('flow-model-user-center');
456
+ const mainGroup = notification.closest('.nb-topbar-actions-list');
457
+ const utilityGroup = pluginSettings.closest('.nb-topbar-utility-actions-list');
458
+
459
+ expect(mainGroup).toContainElement(notification);
460
+ expect(mainGroup).not.toContainElement(pluginSettings);
461
+ expect(utilityGroup).not.toBeNull();
462
+ expect(utilityGroup).toContainElement(pluginSettings);
463
+ expect(utilityGroup).toContainElement(help);
464
+ expect(utilityGroup).toContainElement(userCenter);
465
+ expect(pluginSettings.compareDocumentPosition(help) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
466
+ expect(help.compareDocumentPosition(userCenter) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
467
+ });
468
+
405
469
  it('should keep HelpLite rendered when one action fails', () => {
406
470
  vi.spyOn(console, 'error').mockImplementation(() => {});
407
471
 
@@ -26,6 +26,10 @@ export default function useStyle() {
26
26
  () => ({
27
27
  [`.${COMPONENT_CLS}`]: {
28
28
  '.vditor-reset': { fontSize: `${token.fontSize}px !important`, color: 'unset' },
29
+ '.vditor-reset h2': {
30
+ borderBottom: 'none !important',
31
+ boxShadow: 'none !important',
32
+ },
29
33
  '.vditor': {
30
34
  borderRadius: 8,
31
35
  },
@@ -150,7 +150,7 @@ export const BlockItemCard = React.forwardRef(
150
150
  ) => {
151
151
  const { t } = useTranslation();
152
152
  const { token } = theme.useToken();
153
- const { title: blockTitle, description, children, className, heightMode, ...rest } = props;
153
+ const { title: blockTitle, description, children, className, heightMode, style, ...rest } = props;
154
154
  const cardRef = useRef<HTMLDivElement | null>(null);
155
155
  const setCardRef = useCallback(
156
156
  (node: HTMLDivElement | null) => {
@@ -185,7 +185,7 @@ export const BlockItemCard = React.forwardRef(
185
185
  <Card
186
186
  ref={setCardRef as any}
187
187
  title={title}
188
- style={{ display: 'flex', flexDirection: 'column', height: height }}
188
+ style={{ display: 'flex', flexDirection: 'column', ...style, height: height ?? style?.height }}
189
189
  styles={{
190
190
  body: { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'auto' },
191
191
  header: {
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import React, { useMemo, useState } from 'react';
10
+ import React, { useCallback, useMemo, useState } from 'react';
11
11
  import { DatePicker, Select, Space } from 'antd';
12
12
  import { inferPickerType } from '../../flow-compat';
13
13
  import { dayjs, getDateTimeFormat, getPickerFormat } from '@nocobase/utils/client';
@@ -29,6 +29,7 @@ export interface FieldAssignExactDatePickerProps {
29
29
  isRange?: boolean;
30
30
  onChange?: (value: ExactDatePickerValue) => void;
31
31
  style?: React.CSSProperties;
32
+ disabled?: boolean;
32
33
  }
33
34
 
34
35
  function getRawSingleValue(value: unknown, isRange: boolean): unknown {
@@ -114,7 +115,7 @@ function parseRangeValue(value: unknown, format: string): [dayjs.Dayjs, dayjs.Da
114
115
  }
115
116
 
116
117
  export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProps> = (props) => {
117
- const { picker = 'date', format, showTime, timeFormat, value, isRange = false, onChange, style } = props;
118
+ const { picker = 'date', format, showTime, timeFormat, value, isRange = false, onChange, style, disabled } = props;
118
119
  const flowCtx = useFlowContext();
119
120
  const t = flowCtx.model.translate.bind(flowCtx.model);
120
121
 
@@ -124,17 +125,17 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
124
125
  return inferPickerFromRawValue(rawSingleValue, picker);
125
126
  });
126
127
 
127
- const getResolvedFormat = (nextPicker: ExactDatePickerMode) => {
128
- const baseFormat = nextPicker === picker && format ? format : getPickerFormat(nextPicker);
129
- const dateFormat = nextPicker === 'date' ? stripTimeFromFormat(baseFormat) : baseFormat;
130
- return getDateTimeFormat(nextPicker, dateFormat, showTime, timeFormat);
131
- };
132
-
133
- const resolvedFormat = useMemo(
134
- () => getResolvedFormat(targetPicker),
135
- [targetPicker, format, picker, showTime, timeFormat],
128
+ const getResolvedFormat = useCallback(
129
+ (nextPicker: ExactDatePickerMode) => {
130
+ const baseFormat = nextPicker === picker && format ? format : getPickerFormat(nextPicker);
131
+ const dateFormat = nextPicker === 'date' ? stripTimeFromFormat(baseFormat) : baseFormat;
132
+ return getDateTimeFormat(nextPicker, dateFormat, showTime, timeFormat);
133
+ },
134
+ [format, picker, showTime, timeFormat],
136
135
  );
137
136
 
137
+ const resolvedFormat = useMemo(() => getResolvedFormat(targetPicker), [getResolvedFormat, targetPicker]);
138
+
138
139
  const singleValue = useMemo(() => {
139
140
  if (isRange) return null;
140
141
  return parseDateFromRawValue(value, resolvedFormat);
@@ -156,6 +157,10 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
156
157
  );
157
158
 
158
159
  const handlePickerTypeChange = (nextPicker: ExactDatePickerMode) => {
160
+ if (disabled) {
161
+ return;
162
+ }
163
+
159
164
  setTargetPicker(nextPicker);
160
165
 
161
166
  if (!onChange) {
@@ -182,7 +187,11 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
182
187
  inputReadOnly: flowCtx.isMobileLayout,
183
188
  showTime: showTime ? { defaultValue: dayjs('00:00:00', 'HH:mm:ss') } : false,
184
189
  value: singleValue,
190
+ disabled,
185
191
  onChange: (nextDate: dayjs.Dayjs | null) => {
192
+ if (disabled) {
193
+ return;
194
+ }
186
195
  if (nextDate && dayjs.isDayjs(nextDate)) {
187
196
  onChange?.(nextDate);
188
197
  return;
@@ -200,7 +209,11 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
200
209
  inputReadOnly: flowCtx.isMobileLayout,
201
210
  showTime: showTime ? { defaultValue: [dayjs('00:00:00', 'HH:mm:ss'), dayjs('23:59:59', 'HH:mm:ss')] } : false,
202
211
  value: rangeValue,
212
+ disabled,
203
213
  onChange: (nextDates: [dayjs.Dayjs | null, dayjs.Dayjs | null] | null) => {
214
+ if (disabled) {
215
+ return;
216
+ }
204
217
  if (Array.isArray(nextDates) && nextDates[0] && nextDates[1]) {
205
218
  onChange?.([nextDates[0], nextDates[1]]);
206
219
  return;
@@ -218,6 +231,7 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
218
231
  value={targetPicker}
219
232
  options={pickerOptions}
220
233
  onChange={handlePickerTypeChange}
234
+ disabled={disabled}
221
235
  />
222
236
  {isRange ? <DatePicker.RangePicker {...rangePickerProps} /> : <DatePicker {...singlePickerProps} />}
223
237
  </Space.Compact>