@nocobase/client-v2 2.1.31 → 2.2.0-alpha.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.
- package/es/APIClient.d.ts +1 -3
- package/es/BaseApplication.d.ts +2 -0
- package/es/PluginSettingsManager.d.ts +33 -0
- package/es/RouteRepository.d.ts +29 -6
- package/es/RouterManager.d.ts +15 -0
- package/es/authRedirect.d.ts +1 -0
- package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
- package/es/components/category-tabs/index.d.ts +9 -0
- package/es/components/form/TypedVariableInput.d.ts +31 -5
- package/es/components/form/filter/CollectionFilter.d.ts +2 -0
- package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
- package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
- package/es/components/form/filter/index.d.ts +2 -0
- package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
- package/es/components/index.d.ts +2 -0
- package/es/entry-actions/EntryActionManager.d.ts +24 -0
- package/es/entry-actions/index.d.ts +9 -0
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/actions/linkageRules.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +3 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
- package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -0
- package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
- package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
- package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
- package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
- package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
- package/es/flow/components/RunJSValueEditor.d.ts +1 -0
- package/es/flow/components/filter/FilterGroup.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
- package/es/flow/index.d.ts +1 -0
- package/es/flow/models/base/ActionModelCore.d.ts +2 -0
- package/es/flow/models/base/GridModel.d.ts +1 -1
- package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
- package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
- package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +9 -0
- package/es/flow-compat/routeTypes.d.ts +1 -0
- package/es/index.d.ts +7 -1
- package/es/index.mjs +223 -135
- package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
- package/es/utils/index.d.ts +1 -0
- package/lib/index.js +220 -132
- package/package.json +7 -7
- package/src/APIClient.ts +1 -10
- package/src/Application.tsx +4 -0
- package/src/BaseApplication.tsx +2 -0
- package/src/PluginSettingsManager.ts +53 -0
- package/src/RouteRepository.ts +151 -24
- package/src/RouterManager.tsx +142 -1
- package/src/__tests__/PluginSettingsManager.test.ts +13 -0
- package/src/__tests__/RouteRepository.test.ts +239 -0
- package/src/__tests__/RouterManager.test.ts +125 -0
- package/src/__tests__/app.test.tsx +18 -0
- package/src/__tests__/authRedirect.test.ts +17 -0
- package/src/__tests__/browserChecker.test.ts +61 -0
- package/src/__tests__/exports.test.ts +16 -0
- package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +28 -0
- package/src/__tests__/plugin-manager.test.tsx +44 -2
- package/src/__tests__/settings-center.test.tsx +40 -1
- package/src/authRedirect.ts +38 -0
- package/src/components/README.md +7 -1
- package/src/components/README.zh-CN.md +6 -1
- package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
- package/src/components/category-tabs/index.ts +10 -0
- package/src/components/form/JsonTextArea.tsx +4 -0
- package/src/components/form/TypedVariableInput.tsx +452 -101
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
- package/src/components/form/filter/CollectionFilter.tsx +4 -0
- package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
- package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
- package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
- package/src/components/form/filter/index.ts +2 -0
- package/src/components/form/filter/useFilterActionProps.ts +37 -6
- package/src/components/index.ts +2 -0
- package/src/entry-actions/EntryActionManager.ts +76 -0
- package/src/entry-actions/index.ts +10 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +1084 -145
- package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
- package/src/flow/actions/customVariable.tsx +1 -2
- package/src/flow/actions/index.ts +2 -0
- package/src/flow/actions/linkageRules.tsx +65 -8
- package/src/flow/actions/runjs.tsx +2 -14
- package/src/flow/admin-shell/BaseLayoutModel.tsx +13 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +46 -13
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +486 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +64 -8
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
- package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
- package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
- package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +435 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +77 -0
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +214 -2
- package/src/flow/admin-shell/admin-layout/index.ts +1 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
- package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
- package/src/flow/components/DefaultValue.tsx +1 -2
- package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
- package/src/flow/components/FieldAssignValueInput.tsx +76 -9
- package/src/flow/components/FlowRoute.tsx +185 -28
- package/src/flow/components/RunJSValueEditor.tsx +9 -1
- package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +84 -2
- package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
- package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
- package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
- package/src/flow/components/filter/FilterGroup.tsx +33 -5
- package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
- package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
- package/src/flow/index.ts +1 -0
- package/src/flow/models/actions/JSActionModel.tsx +2 -7
- package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
- package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
- package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
- package/src/flow/models/base/ActionModel.tsx +21 -8
- package/src/flow/models/base/ActionModelCore.tsx +5 -0
- package/src/flow/models/base/GridModel.tsx +38 -7
- package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
- package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
- package/src/flow/models/base/PageModel/PageTabModel.tsx +186 -3
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -14
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +586 -1
- package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
- package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
- package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +1 -2
- package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
- package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +87 -0
- package/src/flow/models/blocks/form/value-runtime/rules.ts +2 -2
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +91 -0
- package/src/flow/models/blocks/js-block/JSBlock.tsx +2 -7
- package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +1 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +13 -3
- package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +54 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
- package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
- package/src/flow/models/fields/JSFieldModel.tsx +2 -7
- package/src/flow/models/fields/JSItemModel.tsx +2 -14
- package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
- package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
- package/src/flow/models/topbar/TopbarActionModel.tsx +168 -10
- package/src/flow-compat/Popover.tsx +43 -4
- package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
- package/src/flow-compat/routeTypes.ts +1 -0
- package/src/index.ts +13 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
- package/src/nocobase-buildin-plugin/index.tsx +19 -3
- package/src/settings-center/AdminSettingsLayout.tsx +10 -2
- package/src/settings-center/SystemSettingsPage.tsx +0 -1
- package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
- package/src/settings-center/utils.tsx +0 -6
- package/src/utils/getRouteRuntimeVersion.ts +185 -0
- package/src/utils/index.tsx +1 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
|
|
11
|
+
import { act, render, screen, waitFor } from '@testing-library/react';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { createMemoryRouter, Outlet, RouterProvider } from 'react-router-dom';
|
|
14
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
15
|
+
import { NocoBaseDesktopRouteType } from '../../../flow-compat';
|
|
16
|
+
import { RouteRepository } from '../../../RouteRepository';
|
|
17
|
+
import { AdminLayoutEntryGuard } from './AdminLayoutEntryGuard';
|
|
18
|
+
import type { AdminLayoutModel } from './AdminLayoutModel';
|
|
19
|
+
|
|
20
|
+
describe('AdminLayoutEntryGuard', () => {
|
|
21
|
+
it('should ensure accessible routes for the active layout before resolving the landing route', async () => {
|
|
22
|
+
const engine = new FlowEngine();
|
|
23
|
+
const request = vi.fn().mockResolvedValue({
|
|
24
|
+
data: {
|
|
25
|
+
data: [],
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const routeRepository = new RouteRepository({
|
|
29
|
+
api: {
|
|
30
|
+
request,
|
|
31
|
+
resource: vi.fn(),
|
|
32
|
+
},
|
|
33
|
+
} as never);
|
|
34
|
+
routeRepository.setRoutes([], 'admin-layout-model');
|
|
35
|
+
const deactivateLayout = routeRepository.activateLayout({
|
|
36
|
+
uid: 'mobile-layout-model',
|
|
37
|
+
});
|
|
38
|
+
engine.context.defineProperty('routeRepository', {
|
|
39
|
+
value: routeRepository,
|
|
40
|
+
});
|
|
41
|
+
engine.context.defineProperty('app', {
|
|
42
|
+
value: {
|
|
43
|
+
router: {
|
|
44
|
+
getBasename: () => '',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const model = {
|
|
50
|
+
layout: {
|
|
51
|
+
routeName: 'mobile',
|
|
52
|
+
routePath: '/mobile',
|
|
53
|
+
uid: 'mobile-layout-model',
|
|
54
|
+
},
|
|
55
|
+
} as AdminLayoutModel;
|
|
56
|
+
const router = createMemoryRouter(
|
|
57
|
+
[
|
|
58
|
+
{
|
|
59
|
+
path: '/mobile',
|
|
60
|
+
id: 'mobile',
|
|
61
|
+
element: (
|
|
62
|
+
<FlowEngineProvider engine={engine}>
|
|
63
|
+
<AdminLayoutEntryGuard model={model}>
|
|
64
|
+
<div>Mobile layout shell</div>
|
|
65
|
+
</AdminLayoutEntryGuard>
|
|
66
|
+
</FlowEngineProvider>
|
|
67
|
+
),
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
{
|
|
71
|
+
initialEntries: ['/mobile'],
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
render(<RouterProvider router={router} />);
|
|
76
|
+
|
|
77
|
+
await waitFor(() => {
|
|
78
|
+
expect(request).toHaveBeenCalledWith({
|
|
79
|
+
url: '/desktopRoutes:listAccessible',
|
|
80
|
+
params: {
|
|
81
|
+
tree: true,
|
|
82
|
+
sort: 'sort',
|
|
83
|
+
layout: 'mobile-layout-model',
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
expect(screen.getByText('Mobile layout shell')).toBeInTheDocument();
|
|
88
|
+
deactivateLayout();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should activate the guard layout before loading accessible routes', async () => {
|
|
92
|
+
const engine = new FlowEngine();
|
|
93
|
+
const request = vi.fn().mockResolvedValue({
|
|
94
|
+
data: {
|
|
95
|
+
data: [],
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
const routeRepository = new RouteRepository({
|
|
99
|
+
api: {
|
|
100
|
+
request,
|
|
101
|
+
resource: vi.fn(),
|
|
102
|
+
},
|
|
103
|
+
} as never);
|
|
104
|
+
engine.context.defineProperty('routeRepository', {
|
|
105
|
+
value: routeRepository,
|
|
106
|
+
});
|
|
107
|
+
engine.context.defineProperty('app', {
|
|
108
|
+
value: {
|
|
109
|
+
router: {
|
|
110
|
+
getBasename: () => '',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const model = {
|
|
116
|
+
layout: {
|
|
117
|
+
routeName: 'mobile',
|
|
118
|
+
routePath: '/mobile',
|
|
119
|
+
uid: 'mobile-layout-model',
|
|
120
|
+
},
|
|
121
|
+
} as AdminLayoutModel;
|
|
122
|
+
const router = createMemoryRouter(
|
|
123
|
+
[
|
|
124
|
+
{
|
|
125
|
+
path: '/mobile',
|
|
126
|
+
id: 'mobile',
|
|
127
|
+
element: (
|
|
128
|
+
<FlowEngineProvider engine={engine}>
|
|
129
|
+
<AdminLayoutEntryGuard model={model}>
|
|
130
|
+
<div>Mobile layout shell</div>
|
|
131
|
+
</AdminLayoutEntryGuard>
|
|
132
|
+
</FlowEngineProvider>
|
|
133
|
+
),
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
{
|
|
137
|
+
initialEntries: ['/mobile'],
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
render(<RouterProvider router={router} />);
|
|
142
|
+
|
|
143
|
+
await waitFor(() => {
|
|
144
|
+
expect(request).toHaveBeenCalledWith({
|
|
145
|
+
url: '/desktopRoutes:listAccessible',
|
|
146
|
+
params: {
|
|
147
|
+
tree: true,
|
|
148
|
+
sort: 'sort',
|
|
149
|
+
layout: 'mobile-layout-model',
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
expect(screen.getByText('Mobile layout shell')).toBeInTheDocument();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should keep guard stable when layout getter returns a new object with the same fields', async () => {
|
|
157
|
+
const engine = new FlowEngine();
|
|
158
|
+
const request = vi.fn().mockResolvedValue({
|
|
159
|
+
data: {
|
|
160
|
+
data: [],
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
const routeRepository = new RouteRepository({
|
|
164
|
+
api: {
|
|
165
|
+
request,
|
|
166
|
+
resource: vi.fn(),
|
|
167
|
+
},
|
|
168
|
+
} as never);
|
|
169
|
+
const activateLayout = vi.spyOn(routeRepository, 'activateLayout');
|
|
170
|
+
engine.context.defineProperty('routeRepository', {
|
|
171
|
+
value: routeRepository,
|
|
172
|
+
});
|
|
173
|
+
engine.context.defineProperty('app', {
|
|
174
|
+
value: {
|
|
175
|
+
router: {
|
|
176
|
+
getBasename: () => '',
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const model = {
|
|
182
|
+
get layout() {
|
|
183
|
+
return {
|
|
184
|
+
authCheck: true,
|
|
185
|
+
routeName: 'admin',
|
|
186
|
+
routePath: '/admin',
|
|
187
|
+
uid: 'admin-layout-model',
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
} as AdminLayoutModel;
|
|
191
|
+
let forceRerender = () => {};
|
|
192
|
+
const GuardShell = () => {
|
|
193
|
+
const [, setVersion] = React.useState(0);
|
|
194
|
+
forceRerender = () => setVersion((version) => version + 1);
|
|
195
|
+
return (
|
|
196
|
+
<AdminLayoutEntryGuard model={model}>
|
|
197
|
+
<div>Admin layout shell</div>
|
|
198
|
+
</AdminLayoutEntryGuard>
|
|
199
|
+
);
|
|
200
|
+
};
|
|
201
|
+
const router = createMemoryRouter(
|
|
202
|
+
[
|
|
203
|
+
{
|
|
204
|
+
path: '/admin',
|
|
205
|
+
id: 'admin',
|
|
206
|
+
element: (
|
|
207
|
+
<FlowEngineProvider engine={engine}>
|
|
208
|
+
<GuardShell />
|
|
209
|
+
</FlowEngineProvider>
|
|
210
|
+
),
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
{
|
|
214
|
+
initialEntries: ['/admin'],
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
render(<RouterProvider router={router} />);
|
|
219
|
+
|
|
220
|
+
await waitFor(() => {
|
|
221
|
+
expect(screen.getByText('Admin layout shell')).toBeInTheDocument();
|
|
222
|
+
});
|
|
223
|
+
expect(activateLayout).toHaveBeenCalledTimes(1);
|
|
224
|
+
|
|
225
|
+
await act(async () => {
|
|
226
|
+
forceRerender();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(activateLayout).toHaveBeenCalledTimes(1);
|
|
230
|
+
expect(request).toHaveBeenCalledTimes(1);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should reactivate the guard when stable layout fields change', async () => {
|
|
234
|
+
const engine = new FlowEngine();
|
|
235
|
+
const request = vi.fn().mockResolvedValue({
|
|
236
|
+
data: {
|
|
237
|
+
data: [],
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
const routeRepository = new RouteRepository({
|
|
241
|
+
api: {
|
|
242
|
+
request,
|
|
243
|
+
resource: vi.fn(),
|
|
244
|
+
},
|
|
245
|
+
} as never);
|
|
246
|
+
const activateLayout = vi.spyOn(routeRepository, 'activateLayout');
|
|
247
|
+
engine.context.defineProperty('routeRepository', {
|
|
248
|
+
value: routeRepository,
|
|
249
|
+
});
|
|
250
|
+
engine.context.defineProperty('app', {
|
|
251
|
+
value: {
|
|
252
|
+
router: {
|
|
253
|
+
getBasename: () => '',
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
let layoutUid = 'admin-layout-model';
|
|
259
|
+
const model = {
|
|
260
|
+
get layout() {
|
|
261
|
+
return {
|
|
262
|
+
authCheck: true,
|
|
263
|
+
routeName: 'admin',
|
|
264
|
+
routePath: '/admin',
|
|
265
|
+
uid: layoutUid,
|
|
266
|
+
};
|
|
267
|
+
},
|
|
268
|
+
} as AdminLayoutModel;
|
|
269
|
+
let forceRerender = () => {};
|
|
270
|
+
const GuardShell = () => {
|
|
271
|
+
const [, setVersion] = React.useState(0);
|
|
272
|
+
forceRerender = () => setVersion((version) => version + 1);
|
|
273
|
+
return (
|
|
274
|
+
<AdminLayoutEntryGuard model={model}>
|
|
275
|
+
<div>Admin layout shell</div>
|
|
276
|
+
</AdminLayoutEntryGuard>
|
|
277
|
+
);
|
|
278
|
+
};
|
|
279
|
+
const router = createMemoryRouter(
|
|
280
|
+
[
|
|
281
|
+
{
|
|
282
|
+
path: '/admin',
|
|
283
|
+
id: 'admin',
|
|
284
|
+
element: (
|
|
285
|
+
<FlowEngineProvider engine={engine}>
|
|
286
|
+
<GuardShell />
|
|
287
|
+
</FlowEngineProvider>
|
|
288
|
+
),
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
{
|
|
292
|
+
initialEntries: ['/admin'],
|
|
293
|
+
},
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
render(<RouterProvider router={router} />);
|
|
297
|
+
|
|
298
|
+
await waitFor(() => {
|
|
299
|
+
expect(screen.getByText('Admin layout shell')).toBeInTheDocument();
|
|
300
|
+
});
|
|
301
|
+
expect(activateLayout).toHaveBeenCalledWith(expect.objectContaining({ uid: 'admin-layout-model' }));
|
|
302
|
+
|
|
303
|
+
await act(async () => {
|
|
304
|
+
layoutUid = 'admin-layout-next';
|
|
305
|
+
forceRerender();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(activateLayout).toHaveBeenCalledTimes(2);
|
|
309
|
+
expect(activateLayout).toHaveBeenLastCalledWith(expect.objectContaining({ uid: 'admin-layout-next' }));
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should redirect group id paths to the first v2 landing route', async () => {
|
|
313
|
+
const modernWindow = window as Window & { __nocobase_modern_client_prefix__?: string };
|
|
314
|
+
modernWindow.__nocobase_modern_client_prefix__ = 'v2';
|
|
315
|
+
const engine = new FlowEngine();
|
|
316
|
+
const request = vi.fn().mockResolvedValue({
|
|
317
|
+
data: {
|
|
318
|
+
data: [],
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
const routeRepository = new RouteRepository({
|
|
322
|
+
api: {
|
|
323
|
+
request,
|
|
324
|
+
resource: vi.fn(),
|
|
325
|
+
},
|
|
326
|
+
} as never);
|
|
327
|
+
routeRepository.setRoutes([
|
|
328
|
+
{
|
|
329
|
+
id: 1,
|
|
330
|
+
title: 'Group',
|
|
331
|
+
type: NocoBaseDesktopRouteType.group,
|
|
332
|
+
children: [
|
|
333
|
+
{
|
|
334
|
+
id: 11,
|
|
335
|
+
title: 'Flow page',
|
|
336
|
+
schemaUid: 'flow-page-1',
|
|
337
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
338
|
+
},
|
|
339
|
+
],
|
|
340
|
+
},
|
|
341
|
+
]);
|
|
342
|
+
engine.context.defineProperty('routeRepository', {
|
|
343
|
+
value: routeRepository,
|
|
344
|
+
});
|
|
345
|
+
engine.context.defineProperty('app', {
|
|
346
|
+
value: {
|
|
347
|
+
getPublicPath: () => '/apps/demo/v2/',
|
|
348
|
+
router: {
|
|
349
|
+
getBasename: () => '/apps/demo/v2',
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const model = {
|
|
355
|
+
layout: {
|
|
356
|
+
routeName: 'admin',
|
|
357
|
+
routePath: '/admin',
|
|
358
|
+
uid: 'admin-layout-model',
|
|
359
|
+
},
|
|
360
|
+
} as AdminLayoutModel;
|
|
361
|
+
const router = createMemoryRouter(
|
|
362
|
+
[
|
|
363
|
+
{
|
|
364
|
+
path: '/admin',
|
|
365
|
+
id: 'admin',
|
|
366
|
+
element: (
|
|
367
|
+
<FlowEngineProvider engine={engine}>
|
|
368
|
+
<AdminLayoutEntryGuard model={model}>
|
|
369
|
+
<Outlet />
|
|
370
|
+
</AdminLayoutEntryGuard>
|
|
371
|
+
</FlowEngineProvider>
|
|
372
|
+
),
|
|
373
|
+
children: [
|
|
374
|
+
{
|
|
375
|
+
path: ':name',
|
|
376
|
+
id: 'admin.__page',
|
|
377
|
+
element: <div>Admin page shell</div>,
|
|
378
|
+
},
|
|
379
|
+
],
|
|
380
|
+
},
|
|
381
|
+
],
|
|
382
|
+
{
|
|
383
|
+
initialEntries: ['/admin/1'],
|
|
384
|
+
},
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
render(<RouterProvider router={router} />);
|
|
389
|
+
|
|
390
|
+
await waitFor(() => {
|
|
391
|
+
expect(router.state.location.pathname).toBe('/admin/flow-page-1');
|
|
392
|
+
});
|
|
393
|
+
expect(await screen.findByText('Admin page shell')).toBeInTheDocument();
|
|
394
|
+
} finally {
|
|
395
|
+
delete modernWindow.__nocobase_modern_client_prefix__;
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('should prefer schema uid routes before resolving numeric group ids', async () => {
|
|
400
|
+
const modernWindow = window as Window & { __nocobase_modern_client_prefix__?: string };
|
|
401
|
+
modernWindow.__nocobase_modern_client_prefix__ = 'v2';
|
|
402
|
+
const engine = new FlowEngine();
|
|
403
|
+
const routeRepository = {
|
|
404
|
+
activateLayout: vi.fn(() => vi.fn()),
|
|
405
|
+
ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
|
|
406
|
+
isAccessibleLoaded: vi.fn(() => true),
|
|
407
|
+
getRouteBySchemaUid: vi.fn((pageUid: string) =>
|
|
408
|
+
pageUid === '1'
|
|
409
|
+
? {
|
|
410
|
+
schemaUid: '1',
|
|
411
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
412
|
+
}
|
|
413
|
+
: undefined,
|
|
414
|
+
),
|
|
415
|
+
getRouteById: vi.fn((routeId: string) =>
|
|
416
|
+
routeId === '1'
|
|
417
|
+
? {
|
|
418
|
+
id: 1,
|
|
419
|
+
type: NocoBaseDesktopRouteType.group,
|
|
420
|
+
children: [
|
|
421
|
+
{
|
|
422
|
+
schemaUid: 'flow-page-1',
|
|
423
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
}
|
|
427
|
+
: undefined,
|
|
428
|
+
),
|
|
429
|
+
listAccessible: vi.fn(() => []),
|
|
430
|
+
};
|
|
431
|
+
engine.context.defineProperty('routeRepository', {
|
|
432
|
+
value: routeRepository,
|
|
433
|
+
});
|
|
434
|
+
engine.context.defineProperty('app', {
|
|
435
|
+
value: {
|
|
436
|
+
getPublicPath: () => '/apps/demo/v2/',
|
|
437
|
+
router: {
|
|
438
|
+
getBasename: () => '/apps/demo/v2',
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const model = {
|
|
444
|
+
layout: {
|
|
445
|
+
routeName: 'admin',
|
|
446
|
+
routePath: '/admin',
|
|
447
|
+
uid: 'admin-layout-model',
|
|
448
|
+
},
|
|
449
|
+
} as AdminLayoutModel;
|
|
450
|
+
const router = createMemoryRouter(
|
|
451
|
+
[
|
|
452
|
+
{
|
|
453
|
+
path: '/admin',
|
|
454
|
+
id: 'admin',
|
|
455
|
+
element: (
|
|
456
|
+
<FlowEngineProvider engine={engine}>
|
|
457
|
+
<AdminLayoutEntryGuard model={model}>
|
|
458
|
+
<Outlet />
|
|
459
|
+
</AdminLayoutEntryGuard>
|
|
460
|
+
</FlowEngineProvider>
|
|
461
|
+
),
|
|
462
|
+
children: [
|
|
463
|
+
{
|
|
464
|
+
path: ':name',
|
|
465
|
+
id: 'admin.__page',
|
|
466
|
+
element: <div>Admin page shell</div>,
|
|
467
|
+
},
|
|
468
|
+
],
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
{
|
|
472
|
+
initialEntries: ['/admin/1'],
|
|
473
|
+
},
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
render(<RouterProvider router={router} />);
|
|
478
|
+
|
|
479
|
+
await screen.findByText('Admin page shell');
|
|
480
|
+
expect(router.state.location.pathname).toBe('/admin/1');
|
|
481
|
+
expect(routeRepository.getRouteById).not.toHaveBeenCalled();
|
|
482
|
+
} finally {
|
|
483
|
+
delete modernWindow.__nocobase_modern_client_prefix__;
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
});
|
|
@@ -13,13 +13,23 @@ import { useLocation, useMatches, useNavigate } from 'react-router-dom';
|
|
|
13
13
|
import { NocoBaseDesktopRouteType } from '../../../flow-compat';
|
|
14
14
|
import { useApp } from '../../../hooks/useApp';
|
|
15
15
|
import { isLayoutContentRouteName } from '../../../layout-manager/utils';
|
|
16
|
+
import type { LayoutDefinition } from '../../../layout-manager/types';
|
|
16
17
|
import {
|
|
17
18
|
findFirstV2LandingRoute,
|
|
19
|
+
getAdminLayoutRoutePath,
|
|
18
20
|
resolveAdminRouteRuntimeTarget,
|
|
21
|
+
type AdminLayoutRoutePathLike,
|
|
19
22
|
toRouterNavigationPath,
|
|
20
23
|
} from './resolveAdminRouteRuntimeTarget';
|
|
24
|
+
import type { AdminLayoutModel } from './AdminLayoutModel';
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
type StableAdminEntryLayout = AdminLayoutRoutePathLike &
|
|
27
|
+
Partial<Pick<LayoutDefinition, 'authCheck' | 'routeName' | 'uid'>>;
|
|
28
|
+
|
|
29
|
+
export const AdminLayoutEntryGuard: FC<{ children: React.ReactNode; model?: AdminLayoutModel }> = ({
|
|
30
|
+
children,
|
|
31
|
+
model,
|
|
32
|
+
}) => {
|
|
23
33
|
const flowEngine = useFlowEngine();
|
|
24
34
|
const app = useApp();
|
|
25
35
|
const navigate = useNavigate();
|
|
@@ -28,24 +38,44 @@ export const AdminLayoutEntryGuard: FC<{ children: React.ReactNode }> = ({ child
|
|
|
28
38
|
const [ready, setReady] = useState(false);
|
|
29
39
|
const replaceTriggeredRef = useRef(false);
|
|
30
40
|
const routeRepository = flowEngine.context.routeRepository;
|
|
41
|
+
const rawLayout = model?.layout;
|
|
42
|
+
const layoutUid = rawLayout?.uid;
|
|
43
|
+
const layoutRouteNameValue = rawLayout?.routeName;
|
|
44
|
+
const layoutRoutePathValue = rawLayout?.routePath;
|
|
45
|
+
const layoutAuthCheck = rawLayout?.authCheck;
|
|
46
|
+
const layout = useMemo<StableAdminEntryLayout | undefined>(() => {
|
|
47
|
+
if (!layoutUid && !layoutRouteNameValue && !layoutRoutePathValue && typeof layoutAuthCheck === 'undefined') {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
authCheck: layoutAuthCheck,
|
|
53
|
+
routeName: layoutRouteNameValue,
|
|
54
|
+
routePath: layoutRoutePathValue || '',
|
|
55
|
+
uid: layoutUid,
|
|
56
|
+
};
|
|
57
|
+
}, [layoutAuthCheck, layoutRouteNameValue, layoutRoutePathValue, layoutUid]);
|
|
58
|
+
const layoutRoutePath = getAdminLayoutRoutePath(layout);
|
|
59
|
+
const layoutRouteName = layout?.routeName || 'admin';
|
|
60
|
+
const layoutRuntime = useMemo(() => ({ routePath: layoutRoutePath }), [layoutRoutePath]);
|
|
31
61
|
const isAdminRoot = useMemo(() => {
|
|
32
62
|
const pathname = toRouterNavigationPath(location.pathname, app.router.getBasename());
|
|
33
|
-
return pathname ===
|
|
34
|
-
}, [app, location.pathname]);
|
|
63
|
+
return pathname === layoutRoutePath;
|
|
64
|
+
}, [app, layoutRoutePath, location.pathname]);
|
|
35
65
|
const pageUid = useMemo(() => {
|
|
36
66
|
const lastMatch = matches[matches.length - 1];
|
|
37
|
-
if (!isLayoutContentRouteName(
|
|
67
|
+
if (!isLayoutContentRouteName(layoutRouteName, lastMatch?.id)) {
|
|
38
68
|
return '';
|
|
39
69
|
}
|
|
40
|
-
const adminMatch = matches.find((match) => match.id ===
|
|
70
|
+
const adminMatch = matches.find((match) => match.id === layoutRouteName);
|
|
41
71
|
return (
|
|
42
72
|
(lastMatch.params?.name as string) ||
|
|
43
73
|
parsePathnameToViewParams(location.pathname, {
|
|
44
|
-
basePath: adminMatch?.pathname ||
|
|
74
|
+
basePath: adminMatch?.pathname || layoutRoutePath,
|
|
45
75
|
})[0]?.viewUid ||
|
|
46
76
|
''
|
|
47
77
|
);
|
|
48
|
-
}, [location.pathname, matches]);
|
|
78
|
+
}, [layoutRouteName, layoutRoutePath, location.pathname, matches]);
|
|
49
79
|
|
|
50
80
|
useEffect(() => {
|
|
51
81
|
replaceTriggeredRef.current = false;
|
|
@@ -53,6 +83,7 @@ export const AdminLayoutEntryGuard: FC<{ children: React.ReactNode }> = ({ child
|
|
|
53
83
|
|
|
54
84
|
useEffect(() => {
|
|
55
85
|
let active = true;
|
|
86
|
+
const deactivateLayout = routeRepository?.activateLayout?.(layout);
|
|
56
87
|
|
|
57
88
|
const run = async () => {
|
|
58
89
|
setReady(false);
|
|
@@ -85,6 +116,7 @@ export const AdminLayoutEntryGuard: FC<{ children: React.ReactNode }> = ({ child
|
|
|
85
116
|
const target = resolveAdminRouteRuntimeTarget({
|
|
86
117
|
app,
|
|
87
118
|
route: currentRoute,
|
|
119
|
+
layout: layoutRuntime,
|
|
88
120
|
location: {
|
|
89
121
|
pathname: window.location.pathname,
|
|
90
122
|
search: window.location.search,
|
|
@@ -100,6 +132,26 @@ export const AdminLayoutEntryGuard: FC<{ children: React.ReactNode }> = ({ child
|
|
|
100
132
|
}
|
|
101
133
|
}
|
|
102
134
|
|
|
135
|
+
const groupRoute = currentRoute ? undefined : routeRepository?.getRouteById?.(pageUid);
|
|
136
|
+
if (!currentRoute && groupRoute?.type === NocoBaseDesktopRouteType.group) {
|
|
137
|
+
const target = resolveAdminRouteRuntimeTarget({
|
|
138
|
+
app,
|
|
139
|
+
route: groupRoute,
|
|
140
|
+
layout: layoutRuntime,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (target.runtimePath) {
|
|
144
|
+
replaceTriggeredRef.current = true;
|
|
145
|
+
if (target.navigationMode === 'document') {
|
|
146
|
+
window.location.replace(target.runtimePath);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
navigate(toRouterNavigationPath(target.runtimePath, app.router.getBasename()), { replace: true });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
103
155
|
if (active) {
|
|
104
156
|
setReady(true);
|
|
105
157
|
}
|
|
@@ -117,6 +169,7 @@ export const AdminLayoutEntryGuard: FC<{ children: React.ReactNode }> = ({ child
|
|
|
117
169
|
const target = resolveAdminRouteRuntimeTarget({
|
|
118
170
|
app,
|
|
119
171
|
route: firstAccessibleRoute,
|
|
172
|
+
layout: layoutRuntime,
|
|
120
173
|
});
|
|
121
174
|
|
|
122
175
|
if (!target.runtimePath) {
|
|
@@ -135,15 +188,18 @@ export const AdminLayoutEntryGuard: FC<{ children: React.ReactNode }> = ({ child
|
|
|
135
188
|
navigate(toRouterNavigationPath(target.runtimePath, app.router.getBasename()), { replace: true });
|
|
136
189
|
};
|
|
137
190
|
|
|
138
|
-
|
|
191
|
+
run();
|
|
139
192
|
|
|
140
193
|
return () => {
|
|
141
194
|
active = false;
|
|
195
|
+
deactivateLayout?.();
|
|
142
196
|
};
|
|
143
197
|
}, [
|
|
144
198
|
app,
|
|
145
199
|
flowEngine,
|
|
146
200
|
isAdminRoot,
|
|
201
|
+
layout,
|
|
202
|
+
layoutRuntime,
|
|
147
203
|
location.hash,
|
|
148
204
|
location.pathname,
|
|
149
205
|
location.search,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
getAdminLayoutMenuMovePositionOptions,
|
|
18
18
|
} from './AdminLayoutMenuUtils';
|
|
19
19
|
import { getFlowPageMenuSchema, getPageMenuSchema, isVariable } from './AdminLayoutCompat';
|
|
20
|
+
import { joinAdminLayoutRoutePath, type AdminLayoutRoutePathLike } from './resolveAdminRouteRuntimeTarget';
|
|
20
21
|
|
|
21
22
|
const buildLinkSettingSchema = (t: (title: any) => any) => ({
|
|
22
23
|
href: {
|
|
@@ -298,6 +299,7 @@ export const matchesRoutePath = (
|
|
|
298
299
|
route: NocoBaseDesktopRoute | undefined,
|
|
299
300
|
pathname: string,
|
|
300
301
|
basename = '/',
|
|
302
|
+
layout?: AdminLayoutRoutePathLike | null,
|
|
301
303
|
): boolean => {
|
|
302
304
|
if (!route) {
|
|
303
305
|
return false;
|
|
@@ -310,8 +312,8 @@ export const matchesRoutePath = (
|
|
|
310
312
|
: pathname;
|
|
311
313
|
|
|
312
314
|
const candidates = [
|
|
313
|
-
route.id != null ?
|
|
314
|
-
route.schemaUid ?
|
|
315
|
+
route.id != null ? joinAdminLayoutRoutePath(layout, route.id) : null,
|
|
316
|
+
route.schemaUid ? joinAdminLayoutRoutePath(layout, route.schemaUid) : null,
|
|
315
317
|
].filter(Boolean) as string[];
|
|
316
318
|
|
|
317
319
|
if (
|
|
@@ -321,7 +323,7 @@ export const matchesRoutePath = (
|
|
|
321
323
|
}
|
|
322
324
|
|
|
323
325
|
return Array.isArray(route.children)
|
|
324
|
-
? route.children.some((child) => matchesRoutePath(child, normalizedPathname, '/'))
|
|
326
|
+
? route.children.some((child) => matchesRoutePath(child, normalizedPathname, '/', layout))
|
|
325
327
|
: false;
|
|
326
328
|
};
|
|
327
329
|
|