@nocobase/client-v2 2.3.0-beta.9 → 2.4.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/RouteRepository.d.ts +6 -0
- package/es/acl/ACLProvider.d.ts +10 -0
- package/es/acl/aclCheckReadiness.d.ts +11 -0
- package/es/authRedirect.d.ts +11 -2
- package/es/components/AttachmentUpload.d.ts +51 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +10 -1
- package/es/flow/internal/utils/quickEditDataScope.d.ts +48 -0
- package/es/flow/internal/utils/quickEditViewContainer.d.ts +27 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +1 -0
- package/es/index.d.ts +4 -1
- package/es/index.mjs +129 -118
- package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
- package/es/nocobase-buildin-plugin/index.d.ts +10 -0
- package/es/settings-app/SettingsApplication.d.ts +18 -0
- package/es/settings-app/SettingsBrand.d.ts +16 -0
- package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
- package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
- package/es/settings-app/SettingsGroupNav.d.ts +18 -0
- package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
- package/es/settings-app/SettingsRouterManager.d.ts +15 -0
- package/es/settings-app/SettingsSearch.d.ts +17 -0
- package/es/settings-app/SettingsShell.d.ts +10 -0
- package/es/settings-app/isSettingsApp.d.ts +18 -0
- package/es/settings-app/runtimePaths.d.ts +18 -0
- package/es/settings-app/settingsDocumentPath.d.ts +14 -0
- package/es/settings-app/settingsTheme.d.ts +87 -0
- package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
- package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
- package/es/settings-center/groups.d.ts +66 -0
- package/es/settings-center/useSettingsGroups.d.ts +36 -0
- package/es/settings-center/useSettingsSearch.d.ts +35 -0
- package/es/settings-center/utils.d.ts +24 -0
- package/es/theme/index.d.ts +1 -0
- package/es/theme/itemActive.d.ts +24 -0
- package/es/theme/type.d.ts +9 -0
- package/lib/index.js +133 -122
- package/package.json +7 -7
- package/src/BaseApplication.tsx +2 -0
- package/src/RouteRepository.ts +14 -0
- package/src/__tests__/RouteRepository.test.ts +51 -0
- package/src/__tests__/app.test.tsx +135 -3
- package/src/__tests__/authRedirect.test.ts +154 -0
- package/src/__tests__/browserChecker.test.ts +172 -6
- package/src/__tests__/mockSettingsApplication.ts +29 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +81 -8
- package/src/__tests__/plugin-manager.test.tsx +7 -6
- package/src/__tests__/settings-application.test.ts +164 -0
- package/src/__tests__/settings-auth-routes.test.ts +71 -0
- package/src/__tests__/settings-center.test.tsx +168 -130
- package/src/__tests__/settings-layout-root.test.tsx +600 -0
- package/src/__tests__/settings-runtime-paths.test.ts +99 -0
- package/src/__tests__/settings-search-shortcut.test.tsx +321 -0
- package/src/__tests__/settings-shell.test.tsx +261 -0
- package/src/acl/ACLProvider.tsx +53 -3
- package/src/acl/aclCheckReadiness.ts +44 -0
- package/src/authRedirect.ts +85 -8
- package/src/components/AppComponents.tsx +16 -7
- package/src/components/AttachmentUpload.tsx +275 -0
- package/src/components/index.ts +1 -0
- package/src/flow/actions/__tests__/validation.test.ts +48 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -56
- package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
- package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +104 -20
- package/src/flow/components/filter/VariableFilterItem.tsx +14 -4
- package/src/flow/components/filter/__tests__/VariableFilterItem.rightMetaTree.test.tsx +158 -0
- package/src/flow/internal/utils/__tests__/quickEditDataScope.test.ts +156 -0
- package/src/flow/internal/utils/__tests__/quickEditViewContainer.test.ts +52 -0
- package/src/flow/internal/utils/quickEditDataScope.tsx +177 -0
- package/src/flow/internal/utils/quickEditViewContainer.ts +46 -0
- package/src/flow/models/actions/JSItemActionModel.tsx +19 -3
- package/src/flow/models/actions/__tests__/JSItemActionModel.test.tsx +47 -1
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +45 -11
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +120 -0
- package/src/flow/models/blocks/js-block/JSBlock.tsx +5 -1
- package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +23 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +31 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +64 -0
- package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +7 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +4 -3
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +17 -10
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +6 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableField.disabled.test.tsx +90 -0
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordSelectFieldModel.quickCreatePopup.test.tsx +183 -0
- package/src/flow/models/fields/SelectFieldModel.tsx +5 -0
- package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +29 -1
- package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
- package/src/index.ts +11 -1
- package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
- package/src/nocobase-buildin-plugin/index.tsx +112 -71
- package/src/settings-app/SettingsApplication.ts +31 -0
- package/src/settings-app/SettingsBrand.tsx +108 -0
- package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
- package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
- package/src/settings-app/SettingsGroupNav.tsx +124 -0
- package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
- package/src/settings-app/SettingsRouterManager.ts +92 -0
- package/src/settings-app/SettingsSearch.tsx +343 -0
- package/src/settings-app/SettingsShell.tsx +184 -0
- package/src/settings-app/isSettingsApp.ts +21 -0
- package/src/settings-app/runtimePaths.ts +103 -0
- package/src/settings-app/settingsDocumentPath.ts +66 -0
- package/src/settings-app/settingsTheme.ts +369 -0
- package/src/settings-app/useSettingsThemeConfig.ts +100 -0
- package/src/settings-center/AdminSettingsLayout.tsx +154 -169
- package/src/settings-center/SystemSettingsPage.tsx +4 -170
- package/src/settings-center/__tests__/groups.test.ts +25 -0
- package/src/settings-center/groups.ts +108 -0
- package/src/settings-center/useSettingsGroups.ts +105 -0
- package/src/settings-center/useSettingsSearch.ts +173 -0
- package/src/settings-center/utils.tsx +50 -1
- package/src/theme/index.tsx +1 -0
- package/src/theme/itemActive.ts +31 -0
- package/src/theme/type.ts +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0-alpha.10",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.mjs",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"@formily/antd-v5": "1.2.3",
|
|
28
28
|
"@formily/react": "^2.2.27",
|
|
29
29
|
"@formily/shared": "^2.2.27",
|
|
30
|
-
"@nocobase/evaluators": "2.
|
|
31
|
-
"@nocobase/flow-engine": "2.
|
|
32
|
-
"@nocobase/sdk": "2.
|
|
33
|
-
"@nocobase/shared": "2.
|
|
34
|
-
"@nocobase/utils": "2.
|
|
30
|
+
"@nocobase/evaluators": "2.4.0-alpha.10",
|
|
31
|
+
"@nocobase/flow-engine": "2.4.0-alpha.10",
|
|
32
|
+
"@nocobase/sdk": "2.4.0-alpha.10",
|
|
33
|
+
"@nocobase/shared": "2.4.0-alpha.10",
|
|
34
|
+
"@nocobase/utils": "2.4.0-alpha.10",
|
|
35
35
|
"ahooks": "^3.7.2",
|
|
36
36
|
"antd": "5.24.2",
|
|
37
37
|
"antd-style": "3.7.1",
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"react-router-dom": "^6.30.1",
|
|
50
50
|
"zxing-wasm": "^3.1.3"
|
|
51
51
|
},
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "c869865c0413fbe4ef85b26be1f2b8d7078c38df"
|
|
53
53
|
}
|
package/src/BaseApplication.tsx
CHANGED
|
@@ -274,6 +274,8 @@ export abstract class BaseApplication<
|
|
|
274
274
|
this.eventBus.addEventListener('auth:tokenChanged', (event: Event) => {
|
|
275
275
|
const detail = (event as CustomEvent<AuthTokenPayload>).detail;
|
|
276
276
|
if (!detail?.token) {
|
|
277
|
+
// 登录态已结束(退出登录或 token 失效),清空路由缓存,避免下一个用户沿用上一个用户的可访问路由。
|
|
278
|
+
this.context.routeRepository?.clear();
|
|
277
279
|
return;
|
|
278
280
|
}
|
|
279
281
|
this.setTokenInWebSocket(detail);
|
package/src/RouteRepository.ts
CHANGED
|
@@ -153,6 +153,20 @@ export class RouteRepository {
|
|
|
153
153
|
return loadingPromise;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/**
|
|
157
|
+
* 清空所有布局的路由缓存,并丢弃清空前发起的请求结果。
|
|
158
|
+
*
|
|
159
|
+
* 用于登录态失效(如退出登录)时,避免下一个用户沿用上一个用户的可访问路由。
|
|
160
|
+
*/
|
|
161
|
+
clear() {
|
|
162
|
+
this.routeCaches.clear();
|
|
163
|
+
this.accessibleLoadingPromises.clear();
|
|
164
|
+
this.refreshRequestIds.forEach((requestId, layoutUid) => {
|
|
165
|
+
this.refreshRequestIds.set(layoutUid, requestId + 1);
|
|
166
|
+
});
|
|
167
|
+
this.syncRoutesProperty();
|
|
168
|
+
}
|
|
169
|
+
|
|
156
170
|
/**
|
|
157
171
|
* 订阅路由缓存变化,用于驱动 React 上下文刷新。
|
|
158
172
|
*
|
|
@@ -193,6 +193,57 @@ describe('RouteRepository', () => {
|
|
|
193
193
|
unsubscribeCustom();
|
|
194
194
|
});
|
|
195
195
|
|
|
196
|
+
it('should drop cached routes of every layout when cleared', () => {
|
|
197
|
+
const { repository } = createRouteRepository();
|
|
198
|
+
|
|
199
|
+
repository.setRoutes([route('admin-page')], 'admin-layout-model');
|
|
200
|
+
repository.setRoutes([route('custom-page')], 'custom-desktop-layout-model');
|
|
201
|
+
|
|
202
|
+
repository.clear();
|
|
203
|
+
|
|
204
|
+
expect(repository.isAccessibleLoaded()).toBe(false);
|
|
205
|
+
expect(repository.listAccessible()).toEqual([]);
|
|
206
|
+
expect(repository.routes).toEqual([]);
|
|
207
|
+
const deactivateLayout = repository.activateLayout({
|
|
208
|
+
uid: 'custom-desktop-layout-model',
|
|
209
|
+
});
|
|
210
|
+
expect(repository.isAccessibleLoaded()).toBe(false);
|
|
211
|
+
expect(repository.listAccessible()).toEqual([]);
|
|
212
|
+
deactivateLayout();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should reload accessible routes after being cleared', async () => {
|
|
216
|
+
const { repository, request } = createRouteRepository();
|
|
217
|
+
request.mockResolvedValueOnce({ data: { data: [route('previous-user-page')] } });
|
|
218
|
+
await repository.ensureAccessibleLoaded();
|
|
219
|
+
|
|
220
|
+
repository.clear();
|
|
221
|
+
request.mockResolvedValueOnce({ data: { data: [route('next-user-page')] } });
|
|
222
|
+
const routes = await repository.ensureAccessibleLoaded();
|
|
223
|
+
|
|
224
|
+
expect(request).toHaveBeenCalledTimes(2);
|
|
225
|
+
expect(routes.map((item) => item.schemaUid)).toEqual(['next-user-page']);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should ignore refresh results requested before being cleared', async () => {
|
|
229
|
+
const { repository, request } = createRouteRepository();
|
|
230
|
+
let resolveStaleRequest: (value: unknown) => void = () => {};
|
|
231
|
+
request.mockImplementationOnce(
|
|
232
|
+
() =>
|
|
233
|
+
new Promise((resolve) => {
|
|
234
|
+
resolveStaleRequest = resolve;
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const staleRefresh = repository.refreshAccessible();
|
|
239
|
+
repository.clear();
|
|
240
|
+
resolveStaleRequest({ data: { data: [route('previous-user-page')] } });
|
|
241
|
+
await staleRefresh;
|
|
242
|
+
|
|
243
|
+
expect(repository.isAccessibleLoaded()).toBe(false);
|
|
244
|
+
expect(repository.listAccessible()).toEqual([]);
|
|
245
|
+
});
|
|
246
|
+
|
|
196
247
|
it('should pass the default admin layout when creating a route', async () => {
|
|
197
248
|
const { repository, create } = createRouteRepository();
|
|
198
249
|
|
|
@@ -171,6 +171,19 @@ describe('app', () => {
|
|
|
171
171
|
expect(document.documentElement.lang).toBe('ja-JP');
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
+
it('should clear cached accessible routes when the auth token is cleared', () => {
|
|
175
|
+
const app = new Application({ router });
|
|
176
|
+
const routeRepository = app.flowEngine.context.routeRepository;
|
|
177
|
+
routeRepository.setRoutes([{ schemaUid: 'previous-user-page' }]);
|
|
178
|
+
|
|
179
|
+
app.apiClient.auth.setToken('renewed-token');
|
|
180
|
+
expect(routeRepository.listAccessible().map((route) => route.schemaUid)).toEqual(['previous-user-page']);
|
|
181
|
+
|
|
182
|
+
app.apiClient.auth.setToken(null);
|
|
183
|
+
expect(routeRepository.isAccessibleLoaded()).toBe(false);
|
|
184
|
+
expect(routeRepository.listAccessible()).toEqual([]);
|
|
185
|
+
});
|
|
186
|
+
|
|
174
187
|
it('should escape app version html placeholder content', () => {
|
|
175
188
|
expect(getAppVersionHTML('<script>alert(1)</script>&"')).toBe(
|
|
176
189
|
'<span class="nb-app-version">v<script>alert(1)</script>&"</span>',
|
|
@@ -379,7 +392,11 @@ describe('app', () => {
|
|
|
379
392
|
expect(screen.getByText('maintaining dialog message')).toBeInTheDocument();
|
|
380
393
|
});
|
|
381
394
|
|
|
382
|
-
it(
|
|
395
|
+
it.each([
|
|
396
|
+
['pm.enable', 'Enabling plugin'],
|
|
397
|
+
['pm.disable', 'Disabling plugin'],
|
|
398
|
+
['future.command', 'future.command'],
|
|
399
|
+
])('should keep current content behind the default dialog while app commanding: %s', async (commandName, title) => {
|
|
383
400
|
const CurrentPage = () => {
|
|
384
401
|
const [count, setCount] = React.useState(0);
|
|
385
402
|
return <button onClick={() => setCount((value) => value + 1)}>Current page count: {count}</button>;
|
|
@@ -398,12 +415,127 @@ describe('app', () => {
|
|
|
398
415
|
act(() => {
|
|
399
416
|
app.maintained = true;
|
|
400
417
|
app.maintaining = true;
|
|
401
|
-
app.error = { code: 'APP_COMMANDING', command: { name:
|
|
418
|
+
app.error = { code: 'APP_COMMANDING', command: { name: commandName }, message: 'starting sub applications...' };
|
|
402
419
|
});
|
|
403
420
|
|
|
404
421
|
expect(screen.getByRole('button', { name: 'Current page count: 1' })).toBeInTheDocument();
|
|
405
422
|
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
|
406
|
-
expect(screen.getByText(
|
|
423
|
+
expect(screen.getByText(title)).toBeInTheDocument();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('should keep current content without a dialog while app upgrading', async () => {
|
|
427
|
+
const CurrentPage = () => <div>Current page</div>;
|
|
428
|
+
|
|
429
|
+
class PluginHelloClient extends Plugin {
|
|
430
|
+
async load() {
|
|
431
|
+
this.router.add('root', { path: '/', Component: CurrentPage });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const app = createMockClient({ plugins: [PluginHelloClient] });
|
|
436
|
+
await renderApp(app);
|
|
437
|
+
|
|
438
|
+
act(() => {
|
|
439
|
+
app.maintained = true;
|
|
440
|
+
app.maintaining = true;
|
|
441
|
+
app.error = Object.assign(new Error('Loading data sources...'), {
|
|
442
|
+
code: 'APP_COMMANDING',
|
|
443
|
+
command: { name: 'upgrade' },
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
expect(screen.getByText('Current page')).toBeInTheDocument();
|
|
448
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it.each([
|
|
452
|
+
['upgrade', 'App upgrading'],
|
|
453
|
+
['pm.enable', 'Enabling plugin'],
|
|
454
|
+
['pm.disable', 'Disabling plugin'],
|
|
455
|
+
['future.command', 'future.command'],
|
|
456
|
+
])('should not show Try again for an app commanding error state: %s', async (commandName, title) => {
|
|
457
|
+
class PluginHelloClient extends Plugin {}
|
|
458
|
+
const app = createMockClient({ plugins: [PluginHelloClient] });
|
|
459
|
+
app.error = Object.assign(new Error('Loading data sources...'), {
|
|
460
|
+
code: 'APP_COMMANDING',
|
|
461
|
+
command: { name: commandName },
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
await renderApp(app);
|
|
465
|
+
|
|
466
|
+
expect(screen.getByText(title)).toBeInTheDocument();
|
|
467
|
+
expect(screen.queryByRole('button', { name: 'Try again' })).not.toBeInTheDocument();
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('should clear upgrade feedback after an existing page error', async () => {
|
|
471
|
+
const BrokenPage = () => {
|
|
472
|
+
throw new Error('page error');
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
class PluginHelloClient extends Plugin {
|
|
476
|
+
async load() {
|
|
477
|
+
this.router.add('root', { path: '/', Component: BrokenPage });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const app = createMockClient({
|
|
482
|
+
plugins: [PluginHelloClient],
|
|
483
|
+
ws: { url: 'ws://localhost:3000/ws' },
|
|
484
|
+
});
|
|
485
|
+
app.maintained = true;
|
|
486
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
487
|
+
try {
|
|
488
|
+
await renderApp(app);
|
|
489
|
+
expect(screen.getByRole('button', { name: 'Try again' })).toBeInTheDocument();
|
|
490
|
+
|
|
491
|
+
act(() => {
|
|
492
|
+
app.ws.emit('message', {
|
|
493
|
+
type: 'maintaining',
|
|
494
|
+
payload: {
|
|
495
|
+
code: 'APP_COMMANDING',
|
|
496
|
+
command: { name: 'upgrade' },
|
|
497
|
+
message: 'Loading data sources...',
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
expect(screen.getByText('App upgrading')).toBeInTheDocument();
|
|
503
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
504
|
+
expect(screen.queryByRole('button', { name: 'Try again' })).not.toBeInTheDocument();
|
|
505
|
+
} finally {
|
|
506
|
+
errorSpy.mockRestore();
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('should keep a custom maintaining dialog while app upgrading', async () => {
|
|
511
|
+
class PluginHelloClient extends Plugin {}
|
|
512
|
+
const app = createMockClient({ plugins: [PluginHelloClient] });
|
|
513
|
+
app.addComponents({
|
|
514
|
+
CustomUpgradeStatus: () => <div>Custom upgrade status</div>,
|
|
515
|
+
});
|
|
516
|
+
app.maintained = true;
|
|
517
|
+
app.maintaining = true;
|
|
518
|
+
app.error = Object.assign(new Error('Loading data sources...'), {
|
|
519
|
+
code: 'APP_COMMANDING',
|
|
520
|
+
command: {
|
|
521
|
+
name: 'upgrade',
|
|
522
|
+
components: { maintainingDialog: 'CustomUpgradeStatus' },
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
await renderApp(app);
|
|
527
|
+
|
|
528
|
+
expect(screen.getByText('Custom upgrade status')).toBeInTheDocument();
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('should keep Try again for recoverable app errors', async () => {
|
|
532
|
+
class PluginHelloClient extends Plugin {}
|
|
533
|
+
const app = createMockClient({ plugins: [PluginHelloClient] });
|
|
534
|
+
app.error = Object.assign(new Error('load error'), { code: 'LOAD_ERROR' });
|
|
535
|
+
|
|
536
|
+
await renderApp(app);
|
|
537
|
+
|
|
538
|
+
expect(screen.getByRole('button', { name: 'Try again' })).toBeInTheDocument();
|
|
407
539
|
});
|
|
408
540
|
|
|
409
541
|
it('should handle long loading state gracefully', async () => {
|
|
@@ -17,12 +17,18 @@ import {
|
|
|
17
17
|
|
|
18
18
|
describe('auth redirect helpers', () => {
|
|
19
19
|
const originalLocation = globalThis.window.location;
|
|
20
|
+
const originalModernClientPrefix = window.__nocobase_modern_client_prefix__;
|
|
20
21
|
|
|
21
22
|
afterEach(() => {
|
|
22
23
|
Object.defineProperty(globalThis.window, 'location', {
|
|
23
24
|
configurable: true,
|
|
24
25
|
value: originalLocation,
|
|
25
26
|
});
|
|
27
|
+
if (originalModernClientPrefix === undefined) {
|
|
28
|
+
delete window.__nocobase_modern_client_prefix__;
|
|
29
|
+
} else {
|
|
30
|
+
window.__nocobase_modern_client_prefix__ = originalModernClientPrefix;
|
|
31
|
+
}
|
|
26
32
|
vi.restoreAllMocks();
|
|
27
33
|
});
|
|
28
34
|
|
|
@@ -84,6 +90,107 @@ describe('auth redirect helpers', () => {
|
|
|
84
90
|
expect(buildV2SigninHref(app, '/v2/admin/7vu4c2sdk6h')).toBe('/v2/signin?redirect=%2Fv2%2Fadmin%2F7vu4c2sdk6h');
|
|
85
91
|
});
|
|
86
92
|
|
|
93
|
+
it('should keep validated standalone settings redirects outside the v2 basename', () => {
|
|
94
|
+
const app = {
|
|
95
|
+
getPublicPath: () => '/nocobase/v2/',
|
|
96
|
+
router: {
|
|
97
|
+
getBasename: () => '/nocobase/v2',
|
|
98
|
+
},
|
|
99
|
+
} as any;
|
|
100
|
+
|
|
101
|
+
expect(normalizeV2RedirectPath(app, '/nocobase/settings/workflow?tab=list#recent')).toBe(
|
|
102
|
+
'/nocobase/settings/workflow?tab=list#recent',
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should build the standalone settings signin URL for a standalone settings app', () => {
|
|
107
|
+
const app = {
|
|
108
|
+
name: 'main',
|
|
109
|
+
getPublicPath: () => '/nocobase/',
|
|
110
|
+
router: {
|
|
111
|
+
getBasename: () => '/nocobase',
|
|
112
|
+
},
|
|
113
|
+
pluginSettingsManager: {
|
|
114
|
+
getRouteName: () => 'settings.',
|
|
115
|
+
getRoutePath: () => '/settings/',
|
|
116
|
+
},
|
|
117
|
+
} satisfies Parameters<typeof buildV2SigninHref>[0];
|
|
118
|
+
expect(buildV2SigninHref(app, '/nocobase/settings/workflow?tab=list#recent')).toBe(
|
|
119
|
+
'/nocobase/settings/signin?redirect=%2Fnocobase%2Fsettings%2Fworkflow%3Ftab%3Dlist%23recent',
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should keep an apps segment inside the main public path out of the Settings application scope', () => {
|
|
124
|
+
const app = {
|
|
125
|
+
name: 'main',
|
|
126
|
+
getPublicPath: () => '/tenant/apps/root/',
|
|
127
|
+
router: {
|
|
128
|
+
getBasename: () => '/tenant/apps/root/',
|
|
129
|
+
},
|
|
130
|
+
pluginSettingsManager: {
|
|
131
|
+
getRouteName: () => 'settings.',
|
|
132
|
+
getRoutePath: () => '/settings/',
|
|
133
|
+
},
|
|
134
|
+
} satisfies Parameters<typeof buildV2SigninHref>[0];
|
|
135
|
+
|
|
136
|
+
expect(buildV2SigninHref(app, '/tenant/apps/root/settings/workflow')).toBe(
|
|
137
|
+
'/tenant/apps/root/settings/signin?redirect=%2Ftenant%2Fapps%2Froot%2Fsettings%2Fworkflow',
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it.each([
|
|
142
|
+
['/nocobase/settings/apps/test-app/', '/nocobase/settings/apps/test-app/signin'],
|
|
143
|
+
['/nocobase/settings/_app/test-app/', '/nocobase/settings/_app/test-app/signin'],
|
|
144
|
+
])('should keep the standalone settings signin in the current sub-app scope: %s', (basename, signinPath) => {
|
|
145
|
+
const app = {
|
|
146
|
+
name: 'test-app',
|
|
147
|
+
getPublicPath: () => '/nocobase/',
|
|
148
|
+
router: {
|
|
149
|
+
getBasename: () => basename,
|
|
150
|
+
},
|
|
151
|
+
pluginSettingsManager: {
|
|
152
|
+
getRouteName: () => 'settings.',
|
|
153
|
+
getRoutePath: () => '/',
|
|
154
|
+
},
|
|
155
|
+
} as any;
|
|
156
|
+
|
|
157
|
+
expect(buildV2SigninHref(app, `${basename}workflow`)).toBe(
|
|
158
|
+
`${signinPath}?redirect=${encodeURIComponent(`${basename}workflow`)}`,
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should accept the current standalone settings signin URL only', () => {
|
|
163
|
+
Object.defineProperty(globalThis.window, 'location', {
|
|
164
|
+
configurable: true,
|
|
165
|
+
value: {
|
|
166
|
+
...originalLocation,
|
|
167
|
+
origin: 'http://localhost:20000',
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
const app = {
|
|
171
|
+
name: 'test-app',
|
|
172
|
+
getPublicPath: () => '/nocobase/',
|
|
173
|
+
router: {
|
|
174
|
+
getBasename: () => '/nocobase/settings/apps/test-app/',
|
|
175
|
+
},
|
|
176
|
+
pluginSettingsManager: {
|
|
177
|
+
getRouteName: () => 'settings.',
|
|
178
|
+
getRoutePath: () => '/',
|
|
179
|
+
},
|
|
180
|
+
} as any;
|
|
181
|
+
|
|
182
|
+
expect(
|
|
183
|
+
resolveV2SigninRedirect(
|
|
184
|
+
'/nocobase/settings/apps/test-app/signin?redirect=%2Fnocobase%2Fsettings%2Fapps%2Ftest-app',
|
|
185
|
+
app,
|
|
186
|
+
),
|
|
187
|
+
).toBe(
|
|
188
|
+
'http://localhost:20000/nocobase/settings/apps/test-app/signin?redirect=%2Fnocobase%2Fsettings%2Fapps%2Ftest-app',
|
|
189
|
+
);
|
|
190
|
+
expect(resolveV2SigninRedirect('/nocobase/v/apps/test-app/signin', app)).toBeNull();
|
|
191
|
+
expect(resolveV2SigninRedirect('/nocobase/settings/apps/other-app/signin', app)).toBeNull();
|
|
192
|
+
});
|
|
193
|
+
|
|
87
194
|
it('should redirect with window.location.replace by default', () => {
|
|
88
195
|
const replace = vi.fn();
|
|
89
196
|
Object.defineProperty(globalThis.window, 'location', {
|
|
@@ -134,6 +241,36 @@ describe('auth redirect helpers', () => {
|
|
|
134
241
|
});
|
|
135
242
|
|
|
136
243
|
describe('v2 sub-app context (router basename contains /apps/<id>/)', () => {
|
|
244
|
+
it('should keep only the current sub-app standalone settings redirect', () => {
|
|
245
|
+
const app = {
|
|
246
|
+
getPublicPath: () => '/nocobase/v2/',
|
|
247
|
+
router: {
|
|
248
|
+
getBasename: () => '/nocobase/v2/apps/test-app/',
|
|
249
|
+
},
|
|
250
|
+
} as any;
|
|
251
|
+
|
|
252
|
+
expect(normalizeV2RedirectPath(app, '/nocobase/settings/apps/test-app/workflow')).toBe(
|
|
253
|
+
'/nocobase/settings/apps/test-app/workflow',
|
|
254
|
+
);
|
|
255
|
+
expect(normalizeV2RedirectPath(app, '/nocobase/settings/apps/other-app/workflow')).toBe(
|
|
256
|
+
'/nocobase/v2/apps/test-app/admin/',
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it.each([
|
|
261
|
+
'/nocobase/settings/apps/test-app/../../other-app/settings',
|
|
262
|
+
'/nocobase/settings/apps/test-app/%2e%2e/%2E%2e/other-app/settings',
|
|
263
|
+
])('should reject a standalone settings redirect that resolves outside the current sub-app: %s', (target) => {
|
|
264
|
+
const app = {
|
|
265
|
+
getPublicPath: () => '/nocobase/v2/',
|
|
266
|
+
router: {
|
|
267
|
+
getBasename: () => '/nocobase/v2/apps/test-app/',
|
|
268
|
+
},
|
|
269
|
+
} as any;
|
|
270
|
+
|
|
271
|
+
expect(normalizeV2RedirectPath(app, target)).toBe('/nocobase/v2/apps/test-app/admin/');
|
|
272
|
+
});
|
|
273
|
+
|
|
137
274
|
it('should normalize signin redirect fallback under the current sub-app basename', () => {
|
|
138
275
|
const app = {
|
|
139
276
|
getPublicPath: () => '/v/',
|
|
@@ -210,6 +347,23 @@ describe('auth redirect helpers', () => {
|
|
|
210
347
|
);
|
|
211
348
|
});
|
|
212
349
|
|
|
350
|
+
it('should derive a scoped Portal signin href under the current sub-app basename', () => {
|
|
351
|
+
const app = {
|
|
352
|
+
getPublicPath: () => '/nocobase/v2/',
|
|
353
|
+
router: {
|
|
354
|
+
getBasename: () => '/nocobase/v2/apps/test-app/',
|
|
355
|
+
},
|
|
356
|
+
} satisfies Parameters<typeof buildV2SigninHref>[0];
|
|
357
|
+
|
|
358
|
+
expect(
|
|
359
|
+
buildV2SigninHref(app, '/nocobase/v2/apps/test-app/customer/orders', {
|
|
360
|
+
signInRoutePath: '/customer/signin',
|
|
361
|
+
}),
|
|
362
|
+
).toBe(
|
|
363
|
+
'/nocobase/v2/apps/test-app/customer/signin?redirect=%2Fnocobase%2Fv2%2Fapps%2Ftest-app%2Fcustomer%2Forders',
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
|
|
213
367
|
it('should redirect to sub-app signin with window.location.replace', () => {
|
|
214
368
|
const replace = vi.fn();
|
|
215
369
|
Object.defineProperty(globalThis.window, 'location', {
|