@nocobase/client-v2 2.2.0-beta.1 → 2.2.0-beta.10

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 (247) hide show
  1. package/es/BaseApplication.d.ts +3 -0
  2. package/es/PluginSettingsManager.d.ts +33 -0
  3. package/es/RouteRepository.d.ts +29 -6
  4. package/es/RouterManager.d.ts +15 -0
  5. package/es/authRedirect.d.ts +1 -0
  6. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  7. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  8. package/es/components/form/JsonTextArea.d.ts +2 -1
  9. package/es/components/form/VariableJsonTextArea.d.ts +19 -0
  10. package/es/components/form/index.d.ts +1 -0
  11. package/es/components/index.d.ts +1 -0
  12. package/es/entry-actions/EntryActionManager.d.ts +24 -0
  13. package/es/entry-actions/index.d.ts +9 -0
  14. package/es/flow/actions/afterSuccess.d.ts +8 -1
  15. package/es/flow/actions/index.d.ts +1 -1
  16. package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
  17. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
  18. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  19. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  20. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
  21. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  22. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -0
  23. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
  24. package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
  25. package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
  26. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  27. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
  28. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  29. package/es/flow/models/base/ActionModelCore.d.ts +2 -0
  30. package/es/flow/models/base/GridModel.d.ts +1 -1
  31. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
  32. package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
  33. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  34. package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
  35. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
  36. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +30 -0
  37. package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
  38. package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
  39. package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
  40. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  41. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
  42. package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
  43. package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
  44. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
  45. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
  46. package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
  47. package/es/flow/routeTransientInputArgs.d.ts +14 -0
  48. package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
  49. package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
  50. package/es/flow/utils/formValueDeps.d.ts +32 -0
  51. package/es/flow-compat/FieldValidation.d.ts +2 -1
  52. package/es/index.d.ts +4 -1
  53. package/es/index.mjs +305 -157
  54. package/es/utils/markdownSanitize.d.ts +11 -0
  55. package/lib/index.js +305 -157
  56. package/package.json +7 -7
  57. package/src/BaseApplication.tsx +6 -0
  58. package/src/PluginSettingsManager.ts +53 -0
  59. package/src/RouteRepository.ts +151 -24
  60. package/src/RouterManager.tsx +142 -1
  61. package/src/__tests__/PluginSettingsManager.test.ts +13 -0
  62. package/src/__tests__/RouteRepository.test.ts +239 -0
  63. package/src/__tests__/RouterManager.test.ts +125 -0
  64. package/src/__tests__/authRedirect.test.ts +17 -0
  65. package/src/__tests__/exports.test.ts +16 -0
  66. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +199 -0
  67. package/src/__tests__/plugin-manager.test.tsx +44 -2
  68. package/src/__tests__/settings-center.test.tsx +40 -1
  69. package/src/authRedirect.ts +38 -0
  70. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  71. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  72. package/src/collection-manager/field-configure.ts +1 -1
  73. package/src/collection-manager/interfaces/id.ts +1 -1
  74. package/src/collection-manager/interfaces/m2m.tsx +2 -2
  75. package/src/collection-manager/interfaces/m2o.tsx +2 -2
  76. package/src/collection-manager/interfaces/nanoid.ts +1 -1
  77. package/src/collection-manager/interfaces/o2m.tsx +2 -2
  78. package/src/collection-manager/interfaces/obo.tsx +2 -2
  79. package/src/collection-manager/interfaces/oho.tsx +2 -2
  80. package/src/collection-manager/interfaces/properties/index.ts +2 -2
  81. package/src/collection-manager/interfaces/uuid.ts +1 -1
  82. package/src/components/AppComponents.tsx +19 -3
  83. package/src/components/form/JsonTextArea.tsx +21 -11
  84. package/src/components/form/VariableJsonTextArea.tsx +175 -0
  85. package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
  86. package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
  87. package/src/components/form/index.tsx +1 -0
  88. package/src/components/index.ts +1 -0
  89. package/src/entry-actions/EntryActionManager.ts +76 -0
  90. package/src/entry-actions/index.ts +10 -0
  91. package/src/flow/FlowPage.tsx +38 -3
  92. package/src/flow/__tests__/FlowPage.test.tsx +201 -27
  93. package/src/flow/__tests__/FlowRoute.test.tsx +1088 -105
  94. package/src/flow/__tests__/getKey.test.ts +7 -0
  95. package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
  96. package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
  97. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +120 -0
  98. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1022 -0
  99. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
  100. package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
  101. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
  102. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
  103. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +164 -0
  104. package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
  105. package/src/flow/actions/afterSuccess.tsx +142 -3
  106. package/src/flow/actions/customVariable.tsx +1 -2
  107. package/src/flow/actions/dataScopeFilter.ts +20 -1
  108. package/src/flow/actions/dateTimeFormat.tsx +42 -28
  109. package/src/flow/actions/index.ts +1 -1
  110. package/src/flow/actions/linkageRules.tsx +123 -35
  111. package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
  112. package/src/flow/actions/openView.tsx +59 -5
  113. package/src/flow/actions/runjs.tsx +2 -14
  114. package/src/flow/actions/validation.tsx +62 -30
  115. package/src/flow/admin-shell/BaseLayoutModel.tsx +149 -3
  116. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +187 -42
  117. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1020 -110
  118. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +74 -13
  119. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +486 -0
  120. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +64 -8
  121. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  122. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  123. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  124. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  125. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
  126. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
  127. package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
  128. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
  129. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
  130. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  131. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +518 -2
  132. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +214 -2
  133. package/src/flow/admin-shell/admin-layout/index.ts +1 -0
  134. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  135. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  136. package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
  137. package/src/flow/common/Markdown/Display.tsx +14 -3
  138. package/src/flow/common/Markdown/Edit.tsx +19 -7
  139. package/src/flow/components/DefaultValue.tsx +1 -2
  140. package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
  141. package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
  142. package/src/flow/components/FieldAssignValueInput.tsx +15 -11
  143. package/src/flow/components/FlowRoute.tsx +186 -25
  144. package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
  145. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
  146. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +134 -0
  147. package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
  148. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  149. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  150. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  151. package/src/flow/components/fieldAssignOptions.ts +155 -27
  152. package/src/flow/flows/editMarkdownFlow.tsx +1 -1
  153. package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
  154. package/src/flow/index.ts +1 -1
  155. package/src/flow/internal/components/Markdown/util.ts +2 -1
  156. package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
  157. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  158. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  159. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  160. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  161. package/src/flow/models/base/ActionModel.tsx +21 -8
  162. package/src/flow/models/base/ActionModelCore.tsx +11 -4
  163. package/src/flow/models/base/GridModel.tsx +38 -7
  164. package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
  165. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
  166. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  167. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
  168. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  169. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  170. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +32 -3
  171. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
  172. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  173. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
  174. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +111 -0
  175. package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
  176. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
  177. package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
  178. package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
  179. package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
  180. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  181. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  182. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  183. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  184. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
  185. package/src/flow/models/blocks/form/submitHandler.ts +17 -3
  186. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +940 -75
  187. package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
  188. package/src/flow/models/blocks/form/value-runtime/runtime.ts +348 -13
  189. package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
  190. package/src/flow/models/blocks/js-block/JSBlock.tsx +225 -9
  191. package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
  192. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  193. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +36 -13
  194. package/src/flow/models/blocks/table/TableBlockModel.tsx +38 -17
  195. package/src/flow/models/blocks/table/TableColumnModel.tsx +42 -5
  196. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +111 -1
  197. package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
  198. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  199. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
  200. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +210 -1
  201. package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
  202. package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
  203. package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
  204. package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
  205. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +30 -6
  206. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +61 -17
  207. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
  208. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
  209. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -0
  210. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
  211. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
  212. package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
  213. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
  214. package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
  215. package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
  216. package/src/flow/models/fields/InputFieldModel.tsx +40 -2
  217. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  218. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  219. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  220. package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
  221. package/src/flow/models/fields/TextareaFieldModel.tsx +72 -3
  222. package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
  223. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +117 -1
  224. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
  225. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +131 -0
  226. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
  227. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
  228. package/src/flow/models/topbar/TopbarActionModel.tsx +168 -10
  229. package/src/flow/resolveViewParamsToViewList.tsx +11 -3
  230. package/src/flow/routeTransientInputArgs.ts +27 -0
  231. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +300 -0
  232. package/src/flow/utils/dataScopeFormValueClear.ts +218 -81
  233. package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
  234. package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
  235. package/src/flow/utils/formValueDeps.ts +170 -0
  236. package/src/flow-compat/FieldValidation.tsx +122 -60
  237. package/src/flow-compat/Popover.tsx +43 -4
  238. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  239. package/src/index.ts +10 -1
  240. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  241. package/src/nocobase-buildin-plugin/index.tsx +51 -15
  242. package/src/settings-center/AdminSettingsLayout.tsx +10 -2
  243. package/src/settings-center/SystemSettingsPage.tsx +1 -2
  244. package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
  245. package/src/settings-center/plugin-manager/index.tsx +3 -0
  246. package/src/settings-center/utils.tsx +0 -6
  247. package/src/utils/markdownSanitize.ts +88 -0
@@ -0,0 +1,239 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { describe, expect, it, vi } from 'vitest';
11
+ import type { NocoBaseDesktopRoute } from '../flow-compat';
12
+ import { RouteRepository } from '../RouteRepository';
13
+
14
+ function createRouteRepository() {
15
+ const request = vi.fn().mockResolvedValue({
16
+ data: {
17
+ data: [],
18
+ },
19
+ });
20
+ const create = vi.fn().mockResolvedValue({
21
+ data: {
22
+ data: {},
23
+ },
24
+ });
25
+ const resource = vi.fn(() => ({
26
+ create,
27
+ }));
28
+ const repository = new RouteRepository({
29
+ api: {
30
+ request,
31
+ resource,
32
+ },
33
+ } as never);
34
+
35
+ return {
36
+ create,
37
+ repository,
38
+ request,
39
+ resource,
40
+ };
41
+ }
42
+
43
+ function route(schemaUid: string): NocoBaseDesktopRoute {
44
+ return { schemaUid } as NocoBaseDesktopRoute;
45
+ }
46
+
47
+ describe('RouteRepository', () => {
48
+ it('should pass the default admin layout when loading accessible routes', async () => {
49
+ const { repository, request } = createRouteRepository();
50
+
51
+ await repository.refreshAccessible();
52
+
53
+ expect(request).toHaveBeenCalledWith({
54
+ url: '/desktopRoutes:listAccessible',
55
+ params: {
56
+ tree: true,
57
+ sort: 'sort',
58
+ layout: 'admin-layout-model',
59
+ },
60
+ });
61
+ });
62
+
63
+ it('should pass the active client-v2 layout when loading accessible routes', async () => {
64
+ const { repository, request } = createRouteRepository();
65
+ const deactivateLayout = repository.activateLayout({
66
+ uid: 'custom-desktop-layout-model',
67
+ });
68
+
69
+ await repository.refreshAccessible();
70
+ deactivateLayout();
71
+
72
+ expect(request).toHaveBeenCalledWith({
73
+ url: '/desktopRoutes:listAccessible',
74
+ params: {
75
+ tree: true,
76
+ sort: 'sort',
77
+ layout: 'custom-desktop-layout-model',
78
+ },
79
+ });
80
+ });
81
+
82
+ it('should reload accessible routes when the active client-v2 layout changes', async () => {
83
+ const { repository, request } = createRouteRepository();
84
+ const deactivateFirstLayout = repository.activateLayout({
85
+ uid: 'first-layout-model',
86
+ });
87
+ await repository.ensureAccessibleLoaded();
88
+ deactivateFirstLayout();
89
+
90
+ const deactivateSecondLayout = repository.activateLayout({
91
+ uid: 'second-layout-model',
92
+ });
93
+ await repository.ensureAccessibleLoaded();
94
+ deactivateSecondLayout();
95
+
96
+ expect(request).toHaveBeenNthCalledWith(1, {
97
+ url: '/desktopRoutes:listAccessible',
98
+ params: {
99
+ tree: true,
100
+ sort: 'sort',
101
+ layout: 'first-layout-model',
102
+ },
103
+ });
104
+ expect(request).toHaveBeenNthCalledWith(2, {
105
+ url: '/desktopRoutes:listAccessible',
106
+ params: {
107
+ tree: true,
108
+ sort: 'sort',
109
+ layout: 'second-layout-model',
110
+ },
111
+ });
112
+ });
113
+
114
+ it('should report accessible routes as unloaded when the active layout changes', () => {
115
+ const { repository } = createRouteRepository();
116
+
117
+ repository.setRoutes([], 'first-layout-model');
118
+ const deactivateLayout = repository.activateLayout({
119
+ uid: 'second-layout-model',
120
+ });
121
+
122
+ expect(repository.isAccessibleLoaded()).toBe(false);
123
+ deactivateLayout();
124
+ });
125
+
126
+ it('should keep accessible route caches isolated by active layout', () => {
127
+ const { repository } = createRouteRepository();
128
+
129
+ repository.setRoutes([route('admin-page')], 'admin-layout-model');
130
+ const deactivateLayout = repository.activateLayout({
131
+ uid: 'custom-desktop-layout-model',
132
+ });
133
+ repository.setRoutes([route('custom-page')], 'custom-desktop-layout-model');
134
+
135
+ expect(repository.listAccessible().map((route) => route.schemaUid)).toEqual(['custom-page']);
136
+ expect(repository.getRouteBySchemaUid('admin-page')).toBeUndefined();
137
+ deactivateLayout();
138
+
139
+ expect(repository.listAccessible().map((route) => route.schemaUid)).toEqual(['admin-page']);
140
+ expect(repository.getRouteBySchemaUid('custom-page')).toBeUndefined();
141
+ });
142
+
143
+ it('should find nested routes by id', () => {
144
+ const { repository } = createRouteRepository();
145
+
146
+ repository.setRoutes([
147
+ {
148
+ id: 1,
149
+ schemaUid: 'group-1',
150
+ children: [
151
+ {
152
+ id: 11,
153
+ schemaUid: 'page-1',
154
+ },
155
+ ],
156
+ },
157
+ ] as NocoBaseDesktopRoute[]);
158
+
159
+ expect(repository.getRouteById('11')).toMatchObject({
160
+ id: 11,
161
+ schemaUid: 'page-1',
162
+ });
163
+ expect(repository.getRouteById('missing')).toBeUndefined();
164
+ });
165
+
166
+ it('should notify only subscribers for the changed layout cache', () => {
167
+ const { repository } = createRouteRepository();
168
+ const adminEvents: string[][] = [];
169
+ const customEvents: string[][] = [];
170
+
171
+ const unsubscribeAdmin = repository.subscribe(() => {
172
+ adminEvents.push(repository.listAccessible().map((route) => route.schemaUid || ''));
173
+ });
174
+ const deactivateLayout = repository.activateLayout({
175
+ uid: 'custom-desktop-layout-model',
176
+ });
177
+ const unsubscribeCustom = repository.subscribe(() => {
178
+ customEvents.push(repository.listAccessible().map((route) => route.schemaUid || ''));
179
+ });
180
+
181
+ repository.setRoutes([route('custom-page')], 'custom-desktop-layout-model');
182
+
183
+ expect(adminEvents).toEqual([]);
184
+ expect(customEvents).toEqual([['custom-page']]);
185
+
186
+ deactivateLayout();
187
+ repository.setRoutes([route('admin-page')], 'admin-layout-model');
188
+
189
+ expect(adminEvents).toEqual([['admin-page']]);
190
+ expect(customEvents).toEqual([['custom-page']]);
191
+
192
+ unsubscribeAdmin();
193
+ unsubscribeCustom();
194
+ });
195
+
196
+ it('should pass the default admin layout when creating a route', async () => {
197
+ const { repository, create } = createRouteRepository();
198
+
199
+ await repository.createRoute(
200
+ {
201
+ title: 'Admin page',
202
+ },
203
+ {
204
+ refreshAfterMutation: false,
205
+ },
206
+ );
207
+
208
+ expect(create).toHaveBeenCalledWith({
209
+ values: {
210
+ title: 'Admin page',
211
+ },
212
+ layout: 'admin-layout-model',
213
+ });
214
+ });
215
+
216
+ it('should pass the active client-v2 layout when creating a route', async () => {
217
+ const { repository, create } = createRouteRepository();
218
+ const deactivateLayout = repository.activateLayout({
219
+ uid: 'custom-desktop-layout-model',
220
+ });
221
+
222
+ await repository.createRoute(
223
+ {
224
+ title: 'Custom page',
225
+ },
226
+ {
227
+ refreshAfterMutation: false,
228
+ },
229
+ );
230
+ deactivateLayout();
231
+
232
+ expect(create).toHaveBeenCalledWith({
233
+ values: {
234
+ title: 'Custom page',
235
+ },
236
+ layout: 'custom-desktop-layout-model',
237
+ });
238
+ });
239
+ });
@@ -0,0 +1,125 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import type { BaseApplication } from '../BaseApplication';
11
+ import { RouterManager, shouldOpenAdminRouteInNewWindow } from '../RouterManager';
12
+
13
+ describe('RouterManager', () => {
14
+ describe('shouldOpenAdminRouteInNewWindow', () => {
15
+ it('opens admin route in a new window from a non-admin portal', () => {
16
+ expect(
17
+ shouldOpenAdminRouteInNewWindow({
18
+ currentPathname: '/v/test',
19
+ targetPathname: '/admin/settings',
20
+ basePath: '/v',
21
+ adminRoutePath: '/admin',
22
+ }),
23
+ ).toBe(true);
24
+ });
25
+
26
+ it('supports target pathname with the v2 base path', () => {
27
+ expect(
28
+ shouldOpenAdminRouteInNewWindow({
29
+ currentPathname: '/nocobase/v/test',
30
+ targetPathname: '/nocobase/v/admin/settings',
31
+ basePath: '/nocobase/v',
32
+ adminRoutePath: '/admin',
33
+ }),
34
+ ).toBe(true);
35
+ });
36
+
37
+ it('supports custom portal routes in sub-apps', () => {
38
+ expect(
39
+ shouldOpenAdminRouteInNewWindow({
40
+ currentPathname: '/v/apps/a_9xlild35jir/crm-amd/ekeisumx1zu',
41
+ targetPathname: '/v/apps/a_9xlild35jir/admin/settings',
42
+ basePath: '/v',
43
+ adminRoutePath: '/admin',
44
+ }),
45
+ ).toBe(true);
46
+ });
47
+
48
+ it('keeps admin route navigation in the current window when already in sub-app admin', () => {
49
+ expect(
50
+ shouldOpenAdminRouteInNewWindow({
51
+ currentPathname: '/v/apps/a_9xlild35jir/admin',
52
+ targetPathname: '/v/apps/a_9xlild35jir/admin/settings',
53
+ basePath: '/v',
54
+ adminRoutePath: '/admin',
55
+ }),
56
+ ).toBe(false);
57
+ });
58
+
59
+ it('keeps admin route navigation in the current window when already in admin', () => {
60
+ expect(
61
+ shouldOpenAdminRouteInNewWindow({
62
+ currentPathname: '/v/admin',
63
+ targetPathname: '/admin/settings',
64
+ basePath: '/v',
65
+ adminRoutePath: '/admin',
66
+ }),
67
+ ).toBe(false);
68
+ });
69
+
70
+ it('keeps non-admin portal navigation in the current window', () => {
71
+ expect(
72
+ shouldOpenAdminRouteInNewWindow({
73
+ currentPathname: '/v/test',
74
+ targetPathname: '/test/page',
75
+ basePath: '/v',
76
+ adminRoutePath: '/admin',
77
+ }),
78
+ ).toBe(false);
79
+ });
80
+
81
+ it('does not intercept replace navigation or relative paths', () => {
82
+ expect(
83
+ shouldOpenAdminRouteInNewWindow({
84
+ currentPathname: '/v/test',
85
+ targetPathname: '/admin/settings',
86
+ basePath: '/v',
87
+ adminRoutePath: '/admin',
88
+ replace: true,
89
+ }),
90
+ ).toBe(false);
91
+ expect(
92
+ shouldOpenAdminRouteInNewWindow({
93
+ currentPathname: '/v/test',
94
+ targetPathname: 'admin/settings',
95
+ basePath: '/v',
96
+ adminRoutePath: '/admin',
97
+ }),
98
+ ).toBe(false);
99
+ });
100
+ });
101
+
102
+ it('opens admin route in a new window through the underlying router navigate from a sub-app portal', async () => {
103
+ const app = {
104
+ name: 'a_9xlild35jir',
105
+ getPublicPath: () => '/v/',
106
+ getHref: (pathname: string) => `/v/apps/a_9xlild35jir/${pathname.replace(/^\/+/, '')}`,
107
+ layoutManager: {
108
+ getLayout: () => ({ routePath: '/admin' }),
109
+ },
110
+ renderComponent: () => null,
111
+ } as unknown as BaseApplication<unknown>;
112
+ const manager = new RouterManager({ type: 'browser' }, app);
113
+ const open = vi.spyOn(window, 'open').mockImplementation(() => null);
114
+
115
+ window.history.pushState({}, '', '/v/apps/a_9xlild35jir/crm-amd/ekeisumx1zu');
116
+ manager.getRouterComponent();
117
+ await manager.router.navigate('/admin/settings/version-control/list');
118
+
119
+ expect(open).toHaveBeenCalledWith(
120
+ '/v/apps/a_9xlild35jir/admin/settings/version-control/list',
121
+ '_blank',
122
+ 'noopener,noreferrer',
123
+ );
124
+ });
125
+ });
@@ -10,6 +10,7 @@
10
10
  import {
11
11
  buildV2SigninHref,
12
12
  getCurrentV2RedirectPath,
13
+ normalizeV2RedirectPath,
13
14
  redirectToV2Signin,
14
15
  resolveV2SigninRedirect,
15
16
  } from '../authRedirect';
@@ -133,6 +134,22 @@ describe('auth redirect helpers', () => {
133
134
  });
134
135
 
135
136
  describe('v2 sub-app context (router basename contains /apps/<id>/)', () => {
137
+ it('should normalize signin redirect fallback under the current sub-app basename', () => {
138
+ const app = {
139
+ getPublicPath: () => '/v/',
140
+ router: {
141
+ getBasename: () => '/v/apps/test-app/',
142
+ },
143
+ } as any;
144
+
145
+ expect(normalizeV2RedirectPath(app, '')).toBe('/v/apps/test-app/admin/');
146
+ expect(normalizeV2RedirectPath(app, '/admin/?tab=overview#panel')).toBe(
147
+ '/v/apps/test-app/admin/?tab=overview#panel',
148
+ );
149
+ expect(normalizeV2RedirectPath(app, '/v/apps/test-app/admin/')).toBe('/v/apps/test-app/admin/');
150
+ expect(normalizeV2RedirectPath(app, '/v/admin/')).toBe('/v/apps/test-app/admin/');
151
+ });
152
+
136
153
  it('should preserve sub-app segment when building current redirect path under simple public path', () => {
137
154
  const app = {
138
155
  getPublicPath: () => '/v2/',
@@ -0,0 +1,16 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { IconPicker } from '..';
11
+
12
+ describe('@nocobase/client-v2 exports', () => {
13
+ it('should export IconPicker from the root entry', () => {
14
+ expect(IconPicker).toBeDefined();
15
+ });
16
+ });
@@ -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
  });