@nocobase/client-v2 2.1.0-beta.29 → 2.1.0-beta.30

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 (32) hide show
  1. package/es/flow/actions/index.d.ts +1 -1
  2. package/es/flow/actions/linkageRules.d.ts +2 -0
  3. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +4 -0
  4. package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -0
  5. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +5 -0
  6. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +3 -0
  7. package/es/index.mjs +78 -73
  8. package/lib/index.js +59 -54
  9. package/package.json +5 -5
  10. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +67 -46
  11. package/src/__tests__/settings-center.test.tsx +30 -0
  12. package/src/flow/__tests__/FlowRoute.test.tsx +4 -5
  13. package/src/flow/actions/__tests__/actionLinkageRules.race.repro.test.ts +199 -0
  14. package/src/flow/actions/__tests__/linkageRules.formValueDrivenRefresh.test.ts +6 -1
  15. package/src/flow/actions/__tests__/linkageRules.menu.test.ts +90 -0
  16. package/src/flow/actions/index.ts +2 -0
  17. package/src/flow/actions/linkageRules.tsx +77 -23
  18. package/src/flow/actions/linkageRulesFormValueRefresh.ts +2 -8
  19. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +8 -1
  20. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +70 -12
  21. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +26 -87
  22. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +11 -0
  23. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +5 -1
  24. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +292 -31
  25. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +50 -12
  26. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +77 -56
  27. package/src/flow/components/AdminLayout.tsx +2 -2
  28. package/src/flow/components/FlowRoute.tsx +17 -4
  29. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +4 -0
  30. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +7 -0
  31. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +4 -1
  32. package/src/flow/system-settings/useSystemSettings.tsx +36 -1
@@ -18,6 +18,7 @@ import {
18
18
  AdminLayoutMenuItemRenderer,
19
19
  AdminLayoutMenuItemModel,
20
20
  AdminLayoutModel,
21
+ ADMIN_LAYOUT_MODEL_UID,
21
22
  getAdminLayoutMenuMovePositionOptions,
22
23
  normalizeAdminLayoutMenuLegacyVariables,
23
24
  openAdminLayoutMenuLink,
@@ -211,7 +212,7 @@ describe('AdminLayoutModel menu items', () => {
211
212
  id: 11,
212
213
  title: 'Page 1',
213
214
  schemaUid: 'page-1',
214
- type: NocoBaseDesktopRouteType.page,
215
+ type: NocoBaseDesktopRouteType.flowPage,
215
216
  },
216
217
  ],
217
218
  },
@@ -264,7 +265,7 @@ describe('AdminLayoutModel menu items', () => {
264
265
  id: 11,
265
266
  title: 'Page 1',
266
267
  schemaUid: 'page-1',
267
- type: NocoBaseDesktopRouteType.page,
268
+ type: NocoBaseDesktopRouteType.flowPage,
268
269
  },
269
270
  ],
270
271
  },
@@ -285,21 +286,21 @@ describe('AdminLayoutModel menu items', () => {
285
286
  expect(route.children).toHaveLength(2);
286
287
  expect(route.children[0].path).toBe('/admin/1');
287
288
  expect(route.children[0].redirect).toBe('/admin/page-1');
288
- expect(route.children[0]._runtimePath).toBe('/apps/demo/admin/page-1');
289
- expect(route.children[0]._navigationMode).toBe('document');
290
- expect(route.children[0]._isLegacy).toBe(true);
289
+ expect(route.children[0]._runtimePath).toBe('/apps/demo/v2/admin/page-1');
290
+ expect(route.children[0]._navigationMode).toBe('spa');
291
+ expect(route.children[0]._isLegacy).toBe(false);
291
292
  expect(route.children[0]._depth).toBe(0);
292
293
  expect(route.children[0]._route).toMatchObject({ id: 1, type: NocoBaseDesktopRouteType.group });
293
294
  expect(route.children[0]._model).toBe(adminLayoutModel.subModels.menuItems?.[0]);
294
295
  expect(route.children[0].routes).toHaveLength(1);
295
296
  expect(route.children[0].routes?.[0].path).toBe('/admin/page-1');
296
297
  expect(route.children[0].routes?.[0].redirect).toBe('/admin/page-1');
297
- expect(route.children[0].routes?.[0]._runtimePath).toBe('/apps/demo/admin/page-1');
298
- expect(route.children[0].routes?.[0]._navigationMode).toBe('document');
298
+ expect(route.children[0].routes?.[0]._runtimePath).toBe('/apps/demo/v2/admin/page-1');
299
+ expect(route.children[0].routes?.[0]._navigationMode).toBe('spa');
299
300
  expect(route.children[0].routes?.[0]._depth).toBe(1);
300
301
  expect(route.children[0].routes?.[0]._route).toMatchObject({
301
302
  schemaUid: 'page-1',
302
- type: NocoBaseDesktopRouteType.page,
303
+ type: NocoBaseDesktopRouteType.flowPage,
303
304
  });
304
305
  expect(route.children[0].routes?.[0]._model).toBe(
305
306
  adminLayoutModel.subModels.menuItems?.[0].subModels.menuItems?.[0],
@@ -310,6 +311,84 @@ describe('AdminLayoutModel menu items', () => {
310
311
  expect(route.children[1]._model).toBe(adminLayoutModel.subModels.menuItems?.[1]);
311
312
  });
312
313
 
314
+ it('should filter legacy page menu routes in v2 admin layout', () => {
315
+ const adminLayoutModel = engine.createModel<AdminLayoutModel>({
316
+ uid: 'admin-layout-model',
317
+ use: AdminLayoutModel,
318
+ });
319
+
320
+ adminLayoutModel.syncMenuRoutes([
321
+ {
322
+ id: 1,
323
+ title: 'Legacy page',
324
+ schemaUid: 'legacy-page',
325
+ type: NocoBaseDesktopRouteType.page,
326
+ },
327
+ {
328
+ id: 2,
329
+ title: 'Legacy only group',
330
+ type: NocoBaseDesktopRouteType.group,
331
+ children: [
332
+ {
333
+ id: 21,
334
+ title: 'Nested legacy page',
335
+ schemaUid: 'nested-legacy-page',
336
+ type: NocoBaseDesktopRouteType.page,
337
+ },
338
+ ],
339
+ },
340
+ {
341
+ id: 3,
342
+ title: 'Mixed group',
343
+ type: NocoBaseDesktopRouteType.group,
344
+ children: [
345
+ {
346
+ id: 31,
347
+ title: 'Nested legacy page',
348
+ schemaUid: 'nested-legacy-page-2',
349
+ type: NocoBaseDesktopRouteType.page,
350
+ },
351
+ {
352
+ id: 32,
353
+ title: 'Nested flow page',
354
+ schemaUid: 'nested-flow-page',
355
+ type: NocoBaseDesktopRouteType.flowPage,
356
+ },
357
+ ],
358
+ },
359
+ {
360
+ id: 4,
361
+ title: 'Link',
362
+ type: NocoBaseDesktopRouteType.link,
363
+ },
364
+ ]);
365
+
366
+ const route = adminLayoutModel.toProLayoutRoute({
367
+ designable: false,
368
+ isMobile: false,
369
+ t: (title) => title,
370
+ });
371
+
372
+ expect(route.children).toHaveLength(2);
373
+ expect(route.children[0]).toMatchObject({
374
+ path: '/admin/3',
375
+ redirect: '/admin/nested-flow-page',
376
+ _runtimePath: '/apps/demo/v2/admin/nested-flow-page',
377
+ _navigationMode: 'spa',
378
+ _isLegacy: false,
379
+ });
380
+ expect(route.children[0].routes).toHaveLength(1);
381
+ expect(route.children[0].routes?.[0]).toMatchObject({
382
+ path: '/admin/nested-flow-page',
383
+ _runtimePath: '/apps/demo/v2/admin/nested-flow-page',
384
+ _navigationMode: 'spa',
385
+ _isLegacy: false,
386
+ });
387
+ expect(route.children[1]).toMatchObject({
388
+ path: '/admin/__admin_layout__/link/4',
389
+ });
390
+ });
391
+
313
392
  it('should resolve modern flowPage menu runtime target to v2 path', () => {
314
393
  const model = engine.createModel<AdminLayoutMenuItemModel>({
315
394
  uid: 'menu-item-flow-page',
@@ -406,7 +485,7 @@ describe('AdminLayoutModel menu items', () => {
406
485
  );
407
486
  });
408
487
 
409
- it('should render legacy menu item as native anchor and use assign on left click', () => {
488
+ it('should render document menu item as native anchor and use assign on left click', () => {
410
489
  const assign = vi.fn();
411
490
  Object.defineProperty(window, 'location', {
412
491
  configurable: true,
@@ -452,15 +531,8 @@ describe('AdminLayoutModel menu items', () => {
452
531
  fireEvent.click(link, { button: 0 });
453
532
 
454
533
  return waitFor(() => {
455
- expect(modalConfirmMock).toHaveBeenCalledWith(
456
- expect.objectContaining({
457
- title: 'Open classic page access',
458
- content: 'This page requires the classic version to open properly. Do you want to go there now?',
459
- okText: 'Yes',
460
- cancelText: 'Cancel',
461
- }),
462
- );
463
534
  expect(assign).toHaveBeenCalledWith('/apps/demo/admin/legacy-page');
535
+ expect(modalConfirmMock).not.toHaveBeenCalled();
464
536
  expect(navigateMock).not.toHaveBeenCalled();
465
537
  });
466
538
  });
@@ -518,7 +590,7 @@ describe('AdminLayoutModel menu items', () => {
518
590
  }
519
591
  });
520
592
 
521
- it('should not navigate to legacy page when user cancels confirm dialog', async () => {
593
+ it('should not ask for confirmation before document navigation', async () => {
522
594
  modalConfirmMock.mockResolvedValue(false);
523
595
  const assign = vi.fn();
524
596
  Object.defineProperty(window, 'location', {
@@ -562,9 +634,9 @@ describe('AdminLayoutModel menu items', () => {
562
634
  fireEvent.click(screen.getByRole('link', { name: 'Legacy page' }), { button: 0 });
563
635
 
564
636
  await waitFor(() => {
565
- expect(modalConfirmMock).toHaveBeenCalledTimes(1);
637
+ expect(assign).toHaveBeenCalledWith('/apps/demo/admin/legacy-page');
566
638
  });
567
- expect(assign).not.toHaveBeenCalled();
639
+ expect(modalConfirmMock).not.toHaveBeenCalled();
568
640
  expect(navigateMock).not.toHaveBeenCalled();
569
641
  });
570
642
 
@@ -684,8 +756,8 @@ describe('AdminLayoutModel menu items', () => {
684
756
 
685
757
  fireEvent.click(screen.getByRole('link', { name: 'Legacy group-landing-entry' }), { button: 0 });
686
758
  return waitFor(() => {
687
- expect(modalConfirmMock).toHaveBeenCalledTimes(1);
688
759
  expect(assign).toHaveBeenCalledWith('/apps/demo/admin/legacy-page');
760
+ expect(modalConfirmMock).not.toHaveBeenCalled();
689
761
  });
690
762
  });
691
763
 
@@ -741,7 +813,7 @@ describe('AdminLayoutModel menu items', () => {
741
813
  id: 11,
742
814
  title: 'Page 1',
743
815
  schemaUid: 'page-1',
744
- type: NocoBaseDesktopRouteType.page,
816
+ type: NocoBaseDesktopRouteType.flowPage,
745
817
  },
746
818
  ],
747
819
  },
@@ -801,7 +873,7 @@ describe('AdminLayoutModel menu items', () => {
801
873
  id: 1,
802
874
  title: 'Page 1',
803
875
  schemaUid: 'page-1',
804
- type: NocoBaseDesktopRouteType.page,
876
+ type: NocoBaseDesktopRouteType.flowPage,
805
877
  },
806
878
  ]);
807
879
  });
@@ -837,6 +909,195 @@ describe('AdminLayoutModel menu items', () => {
837
909
  });
838
910
  });
839
911
 
912
+ it('should expose menu linkage rules only for existing menu items', async () => {
913
+ const menuSettingsFlow = AdminLayoutMenuItemModel.globalFlowRegistry.getFlow('menuSettings');
914
+ expect(menuSettingsFlow?.steps?.linkageRules?.use).toBe('menuLinkageRules');
915
+
916
+ const model = engine.createModel<AdminLayoutMenuItemModel>({
917
+ uid: 'menu-item-linkage-settings',
918
+ use: AdminLayoutMenuItemModel,
919
+ props: {
920
+ route: createRoute(),
921
+ },
922
+ });
923
+ const creationModel = engine.createModel<AdminLayoutMenuItemModel>({
924
+ uid: 'menu-item-linkage-creation-settings',
925
+ use: AdminLayoutMenuItemModel,
926
+ props: {
927
+ creationMeta: {
928
+ menuType: 'link',
929
+ source: 'header',
930
+ },
931
+ },
932
+ });
933
+ const hideInSettings = menuSettingsFlow?.steps?.linkageRules?.hideInSettings as
934
+ | ((ctx: any) => Promise<boolean>)
935
+ | undefined;
936
+
937
+ await expect(hideInSettings?.({ model })).resolves.toBe(false);
938
+ await expect(hideInSettings?.({ model: creationModel })).resolves.toBe(true);
939
+ });
940
+
941
+ it('should persist menu linkage rules through flowModels and route flag', async () => {
942
+ const saveModel = vi.spyOn(engine, 'saveModel').mockResolvedValue(undefined as any);
943
+ const updateRoute = vi.fn().mockResolvedValue(undefined);
944
+ engine.context.routeRepository.updateRoute = updateRoute;
945
+
946
+ const model = engine.createModel<AdminLayoutMenuItemModel>({
947
+ uid: 'menu-item-linkage-persist',
948
+ use: AdminLayoutMenuItemModel,
949
+ props: {
950
+ route: createRoute(),
951
+ },
952
+ });
953
+
954
+ model.setStepParams('menuSettings', 'linkageRules', {
955
+ value: [
956
+ { key: 'r1', title: 'Hide menu item', enable: true, condition: { logic: '$and', items: [] }, actions: [] },
957
+ ],
958
+ });
959
+
960
+ await model.saveStepParams();
961
+
962
+ expect(saveModel).toHaveBeenCalledWith(model, { onlyStepParams: true });
963
+ expect(updateRoute).toHaveBeenCalledWith(1, {
964
+ options: {
965
+ hasPersistedMenuInstanceFlow: true,
966
+ },
967
+ });
968
+ });
969
+
970
+ it('should clear persisted menu linkage rules when no persisted state remains', async () => {
971
+ const saveModel = vi.spyOn(engine, 'saveModel').mockResolvedValue(undefined as any);
972
+ const destroy = vi.fn().mockResolvedValue(true);
973
+ const updateRoute = vi.fn().mockResolvedValue(undefined);
974
+ engine.setModelRepository({ destroy } as any);
975
+ engine.context.routeRepository.updateRoute = updateRoute;
976
+
977
+ const model = engine.createModel<AdminLayoutMenuItemModel>({
978
+ uid: 'menu-item-linkage-clear',
979
+ use: AdminLayoutMenuItemModel,
980
+ props: {
981
+ route: createRoute({
982
+ options: {
983
+ hasPersistedMenuInstanceFlow: true,
984
+ },
985
+ }),
986
+ },
987
+ });
988
+
989
+ model.setStepParams('menuSettings', 'linkageRules', { value: [] });
990
+ await model.saveStepParams();
991
+
992
+ expect(saveModel).not.toHaveBeenCalled();
993
+ expect(destroy).toHaveBeenCalledWith('menu-item-linkage-clear');
994
+ expect(updateRoute).toHaveBeenCalledWith(1, {
995
+ options: undefined,
996
+ });
997
+ });
998
+
999
+ it('should restore menu linkage rules and rerender after hydrate', async () => {
1000
+ engine.context.defineProperty('flowSettingsEnabled', {
1001
+ value: true,
1002
+ });
1003
+ engine.setModelRepository({
1004
+ findOne: vi.fn().mockResolvedValue({
1005
+ uid: 'menu-item-linkage-hydrate',
1006
+ use: 'AdminLayoutMenuItemModel',
1007
+ stepParams: {
1008
+ menuSettings: {
1009
+ linkageRules: {
1010
+ value: [
1011
+ {
1012
+ key: 'r1',
1013
+ title: 'Persisted linkage',
1014
+ enable: true,
1015
+ condition: { logic: '$and', items: [] },
1016
+ actions: [],
1017
+ },
1018
+ ],
1019
+ },
1020
+ },
1021
+ },
1022
+ }),
1023
+ } as any);
1024
+ const rerenderSpy = vi.spyOn(AdminLayoutMenuItemModel.prototype, 'rerender').mockResolvedValue(undefined as any);
1025
+
1026
+ const model = engine.createModel<AdminLayoutMenuItemModel>({
1027
+ uid: 'menu-item-linkage-hydrate',
1028
+ use: AdminLayoutMenuItemModel,
1029
+ props: {
1030
+ route: createRoute(),
1031
+ },
1032
+ });
1033
+
1034
+ await waitFor(() => {
1035
+ expect(model.getStepParams('menuSettings', 'linkageRules')).toMatchObject({
1036
+ value: [{ key: 'r1' }],
1037
+ });
1038
+ });
1039
+ expect(rerenderSpy).toHaveBeenCalledTimes(1);
1040
+ });
1041
+
1042
+ it('should hide menu route dynamically in runtime mode and refresh layout route tree', () => {
1043
+ const adminLayoutModel = engine.createModel<AdminLayoutModel>({
1044
+ uid: ADMIN_LAYOUT_MODEL_UID,
1045
+ use: AdminLayoutModel,
1046
+ });
1047
+ const model = engine.createModel<AdminLayoutMenuItemModel>({
1048
+ uid: 'menu-item-dynamic-hidden',
1049
+ use: AdminLayoutMenuItemModel,
1050
+ props: {
1051
+ route: createRoute(),
1052
+ },
1053
+ });
1054
+
1055
+ const refreshBefore = adminLayoutModel.menuRouteRefreshVersion;
1056
+
1057
+ model.setHidden(true);
1058
+ const runtimeRoute = model.toProLayoutRoute({
1059
+ designable: false,
1060
+ isMobile: false,
1061
+ t: (title) => title,
1062
+ });
1063
+ const designableRoute = model.toProLayoutRoute({
1064
+ designable: true,
1065
+ isMobile: false,
1066
+ t: (title) => title,
1067
+ });
1068
+
1069
+ expect(runtimeRoute).toBeNull();
1070
+ expect(designableRoute?.hideInMenu).toBeFalsy();
1071
+ expect(adminLayoutModel.menuRouteRefreshVersion).toBe(refreshBefore + 1);
1072
+ });
1073
+
1074
+ it('should render hidden menu item with opacity and keep original title in config mode', () => {
1075
+ const model = engine.createModel<AdminLayoutMenuItemModel>({
1076
+ uid: 'menu-item-hidden-in-config',
1077
+ use: AdminLayoutMenuItemModel,
1078
+ props: {
1079
+ route: createRoute(),
1080
+ },
1081
+ });
1082
+
1083
+ model.setProps({
1084
+ item: {
1085
+ name: 'Page 1',
1086
+ path: '/admin/page-1',
1087
+ _route: createRoute(),
1088
+ _model: model,
1089
+ },
1090
+ dom: React.createElement('span', null, 'Page 1'),
1091
+ options: { isMobile: false, collapsed: false },
1092
+ renderType: 'item',
1093
+ });
1094
+
1095
+ const rendered = (model as any).renderHiddenInConfig();
1096
+
1097
+ expect(rendered?.props?.style).toMatchObject({ opacity: 0.3 });
1098
+ expect(rendered?.props?.children?.props?.dom?.props?.children).toBe('Page 1');
1099
+ });
1100
+
840
1101
  it('should keep variable-aware editors for link menu settings', async () => {
841
1102
  const model = engine.createModel<AdminLayoutMenuItemModel>({
842
1103
  uid: 'menu-item-link-edit',
@@ -1630,7 +1891,7 @@ describe('AdminLayoutModel menu items', () => {
1630
1891
  id: 2,
1631
1892
  title: 'Next page',
1632
1893
  schemaUid: 'next-page',
1633
- type: NocoBaseDesktopRouteType.page,
1894
+ type: NocoBaseDesktopRouteType.flowPage,
1634
1895
  },
1635
1896
  ];
1636
1897
  engine.context.api.resource = vi.fn(() => ({
@@ -1655,8 +1916,8 @@ describe('AdminLayoutModel menu items', () => {
1655
1916
 
1656
1917
  expect(deleteRoute).toHaveBeenCalledWith(1);
1657
1918
  expect(removeSchema).not.toHaveBeenCalled();
1658
- expect(assign).toHaveBeenCalledWith('/apps/demo/admin/next-page');
1659
- expect(navigate).not.toHaveBeenCalled();
1919
+ expect(assign).not.toHaveBeenCalled();
1920
+ expect(navigate).toHaveBeenCalledWith('/apps/demo/v2/admin/next-page');
1660
1921
  });
1661
1922
 
1662
1923
  it('should match current route with router basename before navigating away after delete', async () => {
@@ -1684,16 +1945,16 @@ describe('AdminLayoutModel menu items', () => {
1684
1945
  id: 2,
1685
1946
  title: 'Next page',
1686
1947
  schemaUid: 'next-page',
1687
- type: NocoBaseDesktopRouteType.page,
1948
+ type: NocoBaseDesktopRouteType.flowPage,
1688
1949
  },
1689
1950
  ];
1690
1951
  engine.context.api.resource = vi.fn(() => ({
1691
1952
  'remove/current-page': removeSchema,
1692
1953
  }));
1693
- engine.context.location.pathname = '/apps/demo/admin/current-page';
1954
+ engine.context.location.pathname = '/apps/demo/v2/admin/current-page';
1694
1955
  engine.context.defineProperty('router', {
1695
1956
  value: {
1696
- basename: '/apps/demo',
1957
+ basename: '/apps/demo/v2',
1697
1958
  navigate,
1698
1959
  },
1699
1960
  });
@@ -1715,8 +1976,8 @@ describe('AdminLayoutModel menu items', () => {
1715
1976
 
1716
1977
  expect(deleteRoute).toHaveBeenCalledWith(1);
1717
1978
  expect(removeSchema).not.toHaveBeenCalled();
1718
- expect(assign).toHaveBeenCalledWith('/apps/demo/admin/next-page');
1719
- expect(navigate).not.toHaveBeenCalled();
1979
+ expect(assign).not.toHaveBeenCalled();
1980
+ expect(navigate).toHaveBeenCalledWith('/admin/next-page');
1720
1981
  });
1721
1982
 
1722
1983
  it('should reject inner move when target is not a group', async () => {
@@ -10,6 +10,8 @@
10
10
  import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../../flow-compat';
11
11
  import {
12
12
  findFirstAccessiblePageRoute,
13
+ findFirstV2LandingRoute,
14
+ isV2MenuRoute,
13
15
  resolveAdminRouteRuntimeTarget,
14
16
  toRouterNavigationPath,
15
17
  } from './resolveAdminRouteRuntimeTarget';
@@ -35,10 +37,11 @@ describe('resolveAdminRouteRuntimeTarget', () => {
35
37
  runtimePath: '/nocobase/v2/admin/flow-page-1',
36
38
  navigationMode: 'spa',
37
39
  isLegacy: false,
40
+ reason: 'ok',
38
41
  });
39
42
  });
40
43
 
41
- it('should resolve page to legacy document runtime target', () => {
44
+ it('should mark page unsupported in v2 admin runtime', () => {
42
45
  expect(
43
46
  resolveAdminRouteRuntimeTarget({
44
47
  app,
@@ -48,9 +51,10 @@ describe('resolveAdminRouteRuntimeTarget', () => {
48
51
  },
49
52
  }),
50
53
  ).toEqual({
51
- runtimePath: '/nocobase/admin/legacy-page-1',
52
- navigationMode: 'document',
53
- isLegacy: true,
54
+ runtimePath: null,
55
+ navigationMode: 'spa',
56
+ isLegacy: false,
57
+ reason: 'unsupportedV2Runtime',
54
58
  });
55
59
  });
56
60
 
@@ -74,10 +78,11 @@ describe('resolveAdminRouteRuntimeTarget', () => {
74
78
  runtimePath: '/apps/demo/admin/legacy-page-1',
75
79
  navigationMode: 'spa',
76
80
  isLegacy: false,
81
+ reason: 'ok',
77
82
  });
78
83
  });
79
84
 
80
- it('should preserve current search and hash for direct legacy correction', () => {
85
+ it('should not preserve current search and hash when direct legacy page is unsupported', () => {
81
86
  expect(
82
87
  resolveAdminRouteRuntimeTarget({
83
88
  app,
@@ -93,13 +98,14 @@ describe('resolveAdminRouteRuntimeTarget', () => {
93
98
  preserveLocationState: true,
94
99
  }),
95
100
  ).toEqual({
96
- runtimePath: '/nocobase/admin/legacy-page-1/tabs/tab-1/popups/detail?from=direct#dialog',
97
- navigationMode: 'document',
98
- isLegacy: true,
101
+ runtimePath: null,
102
+ navigationMode: 'spa',
103
+ isLegacy: false,
104
+ reason: 'unsupportedV2Runtime',
99
105
  });
100
106
  });
101
107
 
102
- it('should resolve group by DFS first accessible route', () => {
108
+ it('should resolve group by DFS first v2 landing route in v2 runtime', () => {
103
109
  const route: NocoBaseDesktopRoute = {
104
110
  type: NocoBaseDesktopRouteType.group,
105
111
  children: [
@@ -124,9 +130,10 @@ describe('resolveAdminRouteRuntimeTarget', () => {
124
130
  };
125
131
 
126
132
  expect(resolveAdminRouteRuntimeTarget({ app, route })).toEqual({
127
- runtimePath: '/nocobase/admin/legacy-page-2',
128
- navigationMode: 'document',
129
- isLegacy: true,
133
+ runtimePath: '/nocobase/v2/admin/flow-page-2',
134
+ navigationMode: 'spa',
135
+ isLegacy: false,
136
+ reason: 'ok',
130
137
  });
131
138
  });
132
139
 
@@ -158,6 +165,35 @@ describe('resolveAdminRouteRuntimeTarget', () => {
158
165
  });
159
166
  });
160
167
 
168
+ it('should find v2 landing route and skip legacy page routes', () => {
169
+ const routes = [
170
+ {
171
+ type: NocoBaseDesktopRouteType.page,
172
+ schemaUid: 'legacy-page',
173
+ },
174
+ {
175
+ type: NocoBaseDesktopRouteType.group,
176
+ children: [
177
+ {
178
+ type: NocoBaseDesktopRouteType.page,
179
+ schemaUid: 'nested-legacy-page',
180
+ },
181
+ {
182
+ type: NocoBaseDesktopRouteType.flowPage,
183
+ schemaUid: 'nested-flow-page',
184
+ },
185
+ ],
186
+ },
187
+ ];
188
+
189
+ expect(findFirstV2LandingRoute(routes)).toMatchObject({
190
+ type: NocoBaseDesktopRouteType.flowPage,
191
+ schemaUid: 'nested-flow-page',
192
+ });
193
+ expect(isV2MenuRoute(routes[0])).toBe(false);
194
+ expect(isV2MenuRoute(routes[1])).toBe(true);
195
+ });
196
+
161
197
  it('should return empty target when group has no accessible landing page', () => {
162
198
  expect(
163
199
  resolveAdminRouteRuntimeTarget({
@@ -181,6 +217,7 @@ describe('resolveAdminRouteRuntimeTarget', () => {
181
217
  runtimePath: null,
182
218
  navigationMode: 'spa',
183
219
  isLegacy: false,
220
+ reason: 'emptyGroup',
184
221
  });
185
222
  });
186
223
 
@@ -199,6 +236,7 @@ describe('resolveAdminRouteRuntimeTarget', () => {
199
236
  runtimePath: null,
200
237
  navigationMode: 'spa',
201
238
  isLegacy: false,
239
+ reason: 'missingSchemaUid',
202
240
  });
203
241
  expect(log).toHaveBeenCalledWith(
204
242
  '[NocoBase] Admin route runtime target:',