@nocobase/client-v2 3.0.0-alpha.3 → 3.0.0-alpha.5
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/acl/aclCheckReadiness.d.ts +11 -0
- package/es/authRedirect.d.ts +6 -2
- package/es/index.d.ts +2 -1
- package/es/index.mjs +60 -60
- package/es/nocobase-buildin-plugin/index.d.ts +1 -0
- package/es/settings-app/SettingsGroupNav.d.ts +3 -2
- package/es/settings-app/SettingsSearch.d.ts +1 -1
- package/es/ui-operation/index.d.ts +15 -0
- package/es/ui-operation/ui-operation-codec.d.ts +10 -0
- package/lib/index.js +65 -65
- package/package.json +7 -7
- package/src/__tests__/authRedirect.test.ts +17 -0
- package/src/__tests__/browserChecker.test.ts +44 -33
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +37 -0
- package/src/__tests__/settings-center.test.tsx +110 -10
- package/src/__tests__/settings-layout-root.test.tsx +346 -6
- package/src/__tests__/settings-runtime-paths.test.ts +1 -1
- package/src/__tests__/settings-search-shortcut.test.tsx +321 -0
- package/src/__tests__/settings-shell.test.tsx +35 -0
- package/src/acl/ACLProvider.tsx +32 -3
- package/src/acl/aclCheckReadiness.ts +44 -0
- package/src/authRedirect.ts +13 -5
- package/src/flow/models/base/CollectionBlockModel.tsx +9 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +60 -0
- package/src/index.ts +2 -0
- package/src/layout-manager/LayoutContentRoute.tsx +2 -1
- package/src/layout-manager/LayoutRoute.tsx +2 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
- package/src/nocobase-buildin-plugin/index.tsx +14 -1
- package/src/settings-app/SettingsGroupNav.tsx +11 -11
- package/src/settings-app/SettingsSearch.tsx +115 -10
- package/src/settings-app/SettingsShell.tsx +4 -2
- package/src/settings-app/runtimePaths.ts +0 -1
- package/src/settings-app/settingsTheme.ts +82 -14
- package/src/settings-center/AdminSettingsLayout.tsx +96 -106
- package/src/ui-operation/index.ts +33 -0
- package/src/ui-operation/ui-operation-codec.ts +54 -0
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
|
10
|
+
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
|
11
11
|
import MockAdapter from 'axios-mock-adapter';
|
|
12
12
|
import React from 'react';
|
|
13
13
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
14
|
-
import { ACLRolesCheckProvider } from '../acl';
|
|
14
|
+
import { ACLRolesCheckProvider, useRoleRecheck } from '../acl';
|
|
15
15
|
import { Plugin } from '../Plugin';
|
|
16
16
|
import { SettingsApplication } from '../settings-app/SettingsApplication';
|
|
17
17
|
import { SettingsBuildInPlugin } from '../settings-app/SettingsBuildInPlugin';
|
|
@@ -69,11 +69,32 @@ class PrimarySettingsPlugin extends Plugin {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
let recheckRole: (() => Promise<void>) | null = null;
|
|
73
|
+
|
|
74
|
+
const RoleRecheckPage = () => {
|
|
75
|
+
recheckRole = useRoleRecheck();
|
|
76
|
+
return <div>Role recheck page</div>;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
class RoleRecheckSettingsPlugin extends Plugin {
|
|
80
|
+
async load() {
|
|
81
|
+
this.pluginSettingsManager.addMenuItem({ key: 'role-recheck', title: 'Role recheck' });
|
|
82
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
83
|
+
menuKey: 'role-recheck',
|
|
84
|
+
key: 'index',
|
|
85
|
+
title: 'Role recheck',
|
|
86
|
+
Component: RoleRecheckPage,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
72
91
|
describe('standalone settings layout root', () => {
|
|
73
92
|
const originalLocation = window.location;
|
|
74
93
|
const originalModernClientPrefix = window.__nocobase_modern_client_prefix__;
|
|
75
94
|
|
|
76
95
|
afterEach(() => {
|
|
96
|
+
recheckRole = null;
|
|
97
|
+
vi.restoreAllMocks();
|
|
77
98
|
Object.defineProperty(window, 'location', { configurable: true, value: originalLocation });
|
|
78
99
|
if (originalModernClientPrefix === undefined) {
|
|
79
100
|
delete window.__nocobase_modern_client_prefix__;
|
|
@@ -163,7 +184,7 @@ describe('standalone settings layout root', () => {
|
|
|
163
184
|
});
|
|
164
185
|
});
|
|
165
186
|
|
|
166
|
-
it('keeps ordinary negative-sort settings in the active Settings group
|
|
187
|
+
it('keeps ordinary negative-sort settings reachable in the active Settings group', async () => {
|
|
167
188
|
const app = new SettingsApplication({
|
|
168
189
|
plugins: [SettingsBuildInPlugin, TestAclPlugin, PrimarySettingsPlugin],
|
|
169
190
|
router: { type: 'memory', initialEntries: ['/settings/portal-manager'] },
|
|
@@ -188,9 +209,18 @@ describe('standalone settings layout root', () => {
|
|
|
188
209
|
|
|
189
210
|
expect(await screen.findByText('Portal manager page')).toBeInTheDocument();
|
|
190
211
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
212
|
+
// The sidebar is gone: a negative-sort setting no longer gets its own column and
|
|
213
|
+
// follows whichever group it belongs to. This checks it did not drift into a
|
|
214
|
+
// different group.
|
|
215
|
+
expect(document.querySelector('.ant-layout-sider')).toBeNull();
|
|
216
|
+
const groupNav = document.querySelector('.ant-layout-header .ant-menu') as HTMLElement;
|
|
217
|
+
expect(groupNav).toBeInTheDocument();
|
|
218
|
+
// Being an ordinary negative-sort setting, it lands in the catch-all "other
|
|
219
|
+
// settings" group: never promoted to a top-level entry, never folded into the
|
|
220
|
+
// plugin manager group.
|
|
221
|
+
expect(within(groupNav).getByRole('menuitem', { name: 'Other settings' })).toBeInTheDocument();
|
|
222
|
+
expect(within(groupNav).getByRole('menuitem', { name: /Plugin manager$/ })).toBeInTheDocument();
|
|
223
|
+
expect(within(groupNav).queryByRole('menuitem', { name: 'Portal manager' })).not.toBeInTheDocument();
|
|
194
224
|
});
|
|
195
225
|
|
|
196
226
|
it('uses document navigation to the standalone Settings signin page when unauthenticated', async () => {
|
|
@@ -257,4 +287,314 @@ describe('standalone settings layout root', () => {
|
|
|
257
287
|
resolveAuthCheck([200, { data: { id: 1 } }]);
|
|
258
288
|
expect(await screen.findByRole('banner')).toBeInTheDocument();
|
|
259
289
|
});
|
|
290
|
+
|
|
291
|
+
it('does not render Settings navigation or search before the initial role check completes', async () => {
|
|
292
|
+
let resolveRoleCheck: (response: [number, { data: { role: string; snippets: string[] } }]) => void = () => {
|
|
293
|
+
throw new Error('Role check resolver is not initialized');
|
|
294
|
+
};
|
|
295
|
+
const roleCheckResponse = new Promise<[number, { data: { role: string; snippets: string[] } }]>((resolve) => {
|
|
296
|
+
resolveRoleCheck = resolve;
|
|
297
|
+
});
|
|
298
|
+
const app = new SettingsApplication({
|
|
299
|
+
plugins: [SettingsBuildInPlugin, TestAclPlugin, MultiPortalSettingsPlugin],
|
|
300
|
+
router: { type: 'memory', initialEntries: ['/settings/system-settings'] },
|
|
301
|
+
ws: false,
|
|
302
|
+
});
|
|
303
|
+
const apiMock = new MockAdapter(app.apiClient.axios);
|
|
304
|
+
app.dataSourceManager.ensureLoaded = async () => {};
|
|
305
|
+
apiMock.onGet('app:getLang').reply(200, {
|
|
306
|
+
data: { lang: 'en-US', resources: { client: {} }, cron: {} },
|
|
307
|
+
});
|
|
308
|
+
apiMock.onGet('/auth:check').reply(200, { data: { id: 1, nickname: 'Admin' } });
|
|
309
|
+
apiMock.onGet('app:getInfo').reply(200, { data: { id: 'mock-app', version: 'test' } });
|
|
310
|
+
apiMock.onGet('roles:check').reply(() => roleCheckResponse);
|
|
311
|
+
apiMock.onGet('systemSettings:get').reply(200, {
|
|
312
|
+
data: { id: 1, title: 'NocoBase', raw_title: 'NocoBase', logo: null },
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const Root = app.getRootComponent();
|
|
316
|
+
render(<Root />);
|
|
317
|
+
|
|
318
|
+
await waitFor(() => {
|
|
319
|
+
expect(apiMock.history.get.some((request) => request.url === 'roles:check')).toBe(true);
|
|
320
|
+
});
|
|
321
|
+
const header = await screen.findByRole('banner');
|
|
322
|
+
expect(within(header).queryByRole('menu')).not.toBeInTheDocument();
|
|
323
|
+
expect(within(header).queryByTitle('Search settings')).not.toBeInTheDocument();
|
|
324
|
+
|
|
325
|
+
resolveRoleCheck([200, { data: { role: 'root', snippets: ['pm'] } }]);
|
|
326
|
+
await waitFor(() => {
|
|
327
|
+
expect(within(header).getByRole('menu')).toBeInTheDocument();
|
|
328
|
+
expect(within(header).getByTitle('Search settings')).toBeInTheDocument();
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('keeps Settings navigation hidden until the latest overlapping role check completes', async () => {
|
|
333
|
+
let resolveOlderCheck: (response: [number, { data: { role: string; snippets: string[] } }]) => void = () => {
|
|
334
|
+
throw new Error('Older role check resolver is not initialized');
|
|
335
|
+
};
|
|
336
|
+
let resolveLatestCheck: (response: [number, { data: { role: string; snippets: string[] } }]) => void = () => {
|
|
337
|
+
throw new Error('Latest role check resolver is not initialized');
|
|
338
|
+
};
|
|
339
|
+
const olderCheckResponse = new Promise<[number, { data: { role: string; snippets: string[] } }]>((resolve) => {
|
|
340
|
+
resolveOlderCheck = resolve;
|
|
341
|
+
});
|
|
342
|
+
const latestCheckResponse = new Promise<[number, { data: { role: string; snippets: string[] } }]>((resolve) => {
|
|
343
|
+
resolveLatestCheck = resolve;
|
|
344
|
+
});
|
|
345
|
+
const app = new SettingsApplication({
|
|
346
|
+
plugins: [SettingsBuildInPlugin, TestAclPlugin, MultiPortalSettingsPlugin, RoleRecheckSettingsPlugin],
|
|
347
|
+
router: { type: 'memory', initialEntries: ['/settings/role-recheck'] },
|
|
348
|
+
ws: false,
|
|
349
|
+
});
|
|
350
|
+
const apiMock = new MockAdapter(app.apiClient.axios);
|
|
351
|
+
app.dataSourceManager.ensureLoaded = async () => {};
|
|
352
|
+
apiMock.onGet('app:getLang').reply(200, {
|
|
353
|
+
data: { lang: 'en-US', resources: { client: {} }, cron: {} },
|
|
354
|
+
});
|
|
355
|
+
apiMock.onGet('/auth:check').reply(200, { data: { id: 1, nickname: 'Admin' } });
|
|
356
|
+
apiMock.onGet('app:getInfo').reply(200, { data: { id: 'mock-app', version: 'test' } });
|
|
357
|
+
apiMock.onGet('roles:check').replyOnce(200, { data: { role: 'root', snippets: ['pm'] } });
|
|
358
|
+
apiMock.onGet('roles:check').replyOnce(() => olderCheckResponse);
|
|
359
|
+
apiMock.onGet('roles:check').replyOnce(() => latestCheckResponse);
|
|
360
|
+
apiMock.onGet('systemSettings:get').reply(200, {
|
|
361
|
+
data: { id: 1, title: 'NocoBase', raw_title: 'NocoBase', logo: null },
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const Root = app.getRootComponent();
|
|
365
|
+
render(<Root />);
|
|
366
|
+
|
|
367
|
+
expect(await screen.findByText('Role recheck page')).toBeInTheDocument();
|
|
368
|
+
const header = await screen.findByRole('banner');
|
|
369
|
+
expect(within(header).getByRole('menu')).toBeInTheDocument();
|
|
370
|
+
if (!recheckRole) {
|
|
371
|
+
throw new Error('Expected the role recheck callback to be available');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let olderCheck: Promise<void> = Promise.resolve();
|
|
375
|
+
let latestCheck: Promise<void> = Promise.resolve();
|
|
376
|
+
act(() => {
|
|
377
|
+
olderCheck = recheckRole?.() || Promise.resolve();
|
|
378
|
+
latestCheck = recheckRole?.() || Promise.resolve();
|
|
379
|
+
});
|
|
380
|
+
await waitFor(() => {
|
|
381
|
+
expect(apiMock.history.get.filter((request) => request.url === 'roles:check')).toHaveLength(3);
|
|
382
|
+
});
|
|
383
|
+
expect(within(header).queryByRole('menu')).not.toBeInTheDocument();
|
|
384
|
+
|
|
385
|
+
await act(async () => {
|
|
386
|
+
resolveOlderCheck([200, { data: { role: 'root', snippets: ['pm'] } }]);
|
|
387
|
+
await olderCheck;
|
|
388
|
+
});
|
|
389
|
+
expect(within(header).queryByRole('menu')).not.toBeInTheDocument();
|
|
390
|
+
|
|
391
|
+
await act(async () => {
|
|
392
|
+
resolveLatestCheck([200, { data: { role: 'member', snippets: ['!pm.*'] } }]);
|
|
393
|
+
await latestCheck;
|
|
394
|
+
});
|
|
395
|
+
expect(within(header).queryByRole('menu')).not.toBeInTheDocument();
|
|
396
|
+
expect(within(header).getByTitle('Search settings')).toBeInTheDocument();
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('ignores a pending role check from an unmounted provider', async () => {
|
|
400
|
+
let resolveUnmountedCheck: (response: [number, { data: { role: string; snippets: string[] } }]) => void = () => {
|
|
401
|
+
throw new Error('Unmounted role check resolver is not initialized');
|
|
402
|
+
};
|
|
403
|
+
let resolveRemountedCheck: (response: [number, { data: { role: string; snippets: string[] } }]) => void = () => {
|
|
404
|
+
throw new Error('Remounted role check resolver is not initialized');
|
|
405
|
+
};
|
|
406
|
+
const unmountedCheckResponse = new Promise<[number, { data: { role: string; snippets: string[] } }]>((resolve) => {
|
|
407
|
+
resolveUnmountedCheck = resolve;
|
|
408
|
+
});
|
|
409
|
+
const remountedCheckResponse = new Promise<[number, { data: { role: string; snippets: string[] } }]>((resolve) => {
|
|
410
|
+
resolveRemountedCheck = resolve;
|
|
411
|
+
});
|
|
412
|
+
const app = new SettingsApplication({
|
|
413
|
+
plugins: [SettingsBuildInPlugin, TestAclPlugin, MultiPortalSettingsPlugin, RoleRecheckSettingsPlugin],
|
|
414
|
+
router: { type: 'memory', initialEntries: ['/settings/role-recheck'] },
|
|
415
|
+
ws: false,
|
|
416
|
+
});
|
|
417
|
+
const apiMock = new MockAdapter(app.apiClient.axios);
|
|
418
|
+
app.dataSourceManager.ensureLoaded = async () => {};
|
|
419
|
+
apiMock.onGet('app:getLang').reply(200, {
|
|
420
|
+
data: { lang: 'en-US', resources: { client: {} }, cron: {} },
|
|
421
|
+
});
|
|
422
|
+
apiMock.onGet('/auth:check').reply(200, { data: { id: 1, nickname: 'Admin' } });
|
|
423
|
+
apiMock.onGet('app:getInfo').reply(200, { data: { id: 'mock-app', version: 'test' } });
|
|
424
|
+
apiMock.onGet('roles:check').replyOnce(200, { data: { role: 'root', snippets: ['pm'] } });
|
|
425
|
+
apiMock.onGet('roles:check').replyOnce(() => unmountedCheckResponse);
|
|
426
|
+
apiMock.onGet('roles:check').replyOnce(() => remountedCheckResponse);
|
|
427
|
+
apiMock.onGet('systemSettings:get').reply(200, {
|
|
428
|
+
data: { id: 1, title: 'NocoBase', raw_title: 'NocoBase', logo: null },
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const Root = app.getRootComponent();
|
|
432
|
+
const firstRender = render(<Root />);
|
|
433
|
+
|
|
434
|
+
expect(await screen.findByText('Role recheck page')).toBeInTheDocument();
|
|
435
|
+
if (!recheckRole) {
|
|
436
|
+
throw new Error('Expected the role recheck callback to be available');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
let unmountedCheck: Promise<void> = Promise.resolve();
|
|
440
|
+
act(() => {
|
|
441
|
+
unmountedCheck = recheckRole?.() || Promise.resolve();
|
|
442
|
+
});
|
|
443
|
+
await waitFor(() => {
|
|
444
|
+
expect(apiMock.history.get.filter((request) => request.url === 'roles:check')).toHaveLength(2);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
firstRender.unmount();
|
|
448
|
+
render(<Root />);
|
|
449
|
+
|
|
450
|
+
await waitFor(() => {
|
|
451
|
+
expect(apiMock.history.get.filter((request) => request.url === 'roles:check')).toHaveLength(3);
|
|
452
|
+
});
|
|
453
|
+
const remountedHeader = await screen.findByRole('banner');
|
|
454
|
+
expect(within(remountedHeader).queryByRole('menu')).not.toBeInTheDocument();
|
|
455
|
+
|
|
456
|
+
await act(async () => {
|
|
457
|
+
resolveUnmountedCheck([200, { data: { role: 'root', snippets: ['pm'] } }]);
|
|
458
|
+
await unmountedCheck;
|
|
459
|
+
});
|
|
460
|
+
expect(within(remountedHeader).queryByRole('menu')).not.toBeInTheDocument();
|
|
461
|
+
|
|
462
|
+
await act(async () => {
|
|
463
|
+
resolveRemountedCheck([200, { data: { role: 'member', snippets: ['!pm.*'] } }]);
|
|
464
|
+
});
|
|
465
|
+
await waitFor(() => {
|
|
466
|
+
expect(within(remountedHeader).getByTitle('Search settings')).toBeInTheDocument();
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('does not reopen Settings search after a role recheck', async () => {
|
|
471
|
+
let resolveRoleCheck: (response: [number, { data: { role: string; snippets: string[] } }]) => void = () => {
|
|
472
|
+
throw new Error('Role check resolver is not initialized');
|
|
473
|
+
};
|
|
474
|
+
const roleCheckResponse = new Promise<[number, { data: { role: string; snippets: string[] } }]>((resolve) => {
|
|
475
|
+
resolveRoleCheck = resolve;
|
|
476
|
+
});
|
|
477
|
+
const app = new SettingsApplication({
|
|
478
|
+
plugins: [SettingsBuildInPlugin, TestAclPlugin, MultiPortalSettingsPlugin, RoleRecheckSettingsPlugin],
|
|
479
|
+
router: { type: 'memory', initialEntries: ['/settings/role-recheck'] },
|
|
480
|
+
ws: false,
|
|
481
|
+
});
|
|
482
|
+
const apiMock = new MockAdapter(app.apiClient.axios);
|
|
483
|
+
app.dataSourceManager.ensureLoaded = async () => {};
|
|
484
|
+
apiMock.onGet('app:getLang').reply(200, {
|
|
485
|
+
data: { lang: 'en-US', resources: { client: {} }, cron: {} },
|
|
486
|
+
});
|
|
487
|
+
apiMock.onGet('/auth:check').reply(200, { data: { id: 1, nickname: 'Admin' } });
|
|
488
|
+
apiMock.onGet('app:getInfo').reply(200, { data: { id: 'mock-app', version: 'test' } });
|
|
489
|
+
apiMock.onGet('roles:check').replyOnce(200, { data: { role: 'root', snippets: ['pm'] } });
|
|
490
|
+
apiMock.onGet('roles:check').replyOnce(() => roleCheckResponse);
|
|
491
|
+
apiMock.onGet('systemSettings:get').reply(200, {
|
|
492
|
+
data: { id: 1, title: 'NocoBase', raw_title: 'NocoBase', logo: null },
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const Root = app.getRootComponent();
|
|
496
|
+
render(<Root />);
|
|
497
|
+
|
|
498
|
+
expect(await screen.findByText('Role recheck page')).toBeInTheDocument();
|
|
499
|
+
const header = await screen.findByRole('banner');
|
|
500
|
+
fireEvent.click(within(header).getByTitle('Search settings'));
|
|
501
|
+
expect(await screen.findByRole('dialog')).toBeInTheDocument();
|
|
502
|
+
if (!recheckRole) {
|
|
503
|
+
throw new Error('Expected the role recheck callback to be available');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
let roleCheck: Promise<void> = Promise.resolve();
|
|
507
|
+
act(() => {
|
|
508
|
+
roleCheck = recheckRole?.() || Promise.resolve();
|
|
509
|
+
});
|
|
510
|
+
await waitFor(() => {
|
|
511
|
+
expect(apiMock.history.get.filter((request) => request.url === 'roles:check')).toHaveLength(2);
|
|
512
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
await act(async () => {
|
|
516
|
+
resolveRoleCheck([200, { data: { role: 'member', snippets: ['!pm.*'] } }]);
|
|
517
|
+
await roleCheck;
|
|
518
|
+
});
|
|
519
|
+
expect(within(header).getByTitle('Search settings')).toBeInTheDocument();
|
|
520
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('keeps Settings navigation and search unavailable when the initial role check fails', async () => {
|
|
524
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
525
|
+
const app = new SettingsApplication({
|
|
526
|
+
plugins: [SettingsBuildInPlugin, TestAclPlugin, MultiPortalSettingsPlugin],
|
|
527
|
+
router: { type: 'memory', initialEntries: ['/settings/system-settings'] },
|
|
528
|
+
ws: false,
|
|
529
|
+
});
|
|
530
|
+
const apiMock = new MockAdapter(app.apiClient.axios);
|
|
531
|
+
app.dataSourceManager.ensureLoaded = async () => {};
|
|
532
|
+
apiMock.onGet('app:getLang').reply(200, {
|
|
533
|
+
data: { lang: 'en-US', resources: { client: {} }, cron: {} },
|
|
534
|
+
});
|
|
535
|
+
apiMock.onGet('/auth:check').reply(200, { data: { id: 1, nickname: 'Admin' } });
|
|
536
|
+
apiMock.onGet('app:getInfo').reply(200, { data: { id: 'mock-app', version: 'test' } });
|
|
537
|
+
apiMock.onGet('roles:check').reply(500, { errors: [{ message: 'Temporary failure' }] });
|
|
538
|
+
apiMock.onGet('systemSettings:get').reply(200, {
|
|
539
|
+
data: { id: 1, title: 'NocoBase', raw_title: 'NocoBase', logo: null },
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
const Root = app.getRootComponent();
|
|
543
|
+
render(<Root />);
|
|
544
|
+
|
|
545
|
+
await waitFor(() => {
|
|
546
|
+
expect(apiMock.history.get.some((request) => request.url === 'roles:check')).toBe(true);
|
|
547
|
+
});
|
|
548
|
+
const header = await screen.findByRole('banner');
|
|
549
|
+
expect(within(header).queryByRole('menu')).not.toBeInTheDocument();
|
|
550
|
+
expect(within(header).queryByTitle('Search settings')).not.toBeInTheDocument();
|
|
551
|
+
|
|
552
|
+
const isMac = /Mac|iPhone|iPad/.test(window.navigator.platform || '');
|
|
553
|
+
const shortcutEvent = new KeyboardEvent('keydown', {
|
|
554
|
+
key: 'f',
|
|
555
|
+
ctrlKey: !isMac,
|
|
556
|
+
metaKey: isMac,
|
|
557
|
+
bubbles: true,
|
|
558
|
+
cancelable: true,
|
|
559
|
+
});
|
|
560
|
+
fireEvent(window, shortcutEvent);
|
|
561
|
+
expect(shortcutEvent.defaultPrevented).toBe(false);
|
|
562
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it('restores the last successful Settings readiness after a recheck error', async () => {
|
|
566
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
567
|
+
const app = new SettingsApplication({
|
|
568
|
+
plugins: [SettingsBuildInPlugin, TestAclPlugin, MultiPortalSettingsPlugin, RoleRecheckSettingsPlugin],
|
|
569
|
+
router: { type: 'memory', initialEntries: ['/settings/role-recheck'] },
|
|
570
|
+
ws: false,
|
|
571
|
+
});
|
|
572
|
+
const apiMock = new MockAdapter(app.apiClient.axios);
|
|
573
|
+
app.dataSourceManager.ensureLoaded = async () => {};
|
|
574
|
+
apiMock.onGet('app:getLang').reply(200, {
|
|
575
|
+
data: { lang: 'en-US', resources: { client: {} }, cron: {} },
|
|
576
|
+
});
|
|
577
|
+
apiMock.onGet('/auth:check').reply(200, { data: { id: 1, nickname: 'Admin' } });
|
|
578
|
+
apiMock.onGet('app:getInfo').reply(200, { data: { id: 'mock-app', version: 'test' } });
|
|
579
|
+
apiMock.onGet('roles:check').replyOnce(200, { data: { role: 'root', snippets: ['pm'] } });
|
|
580
|
+
apiMock.onGet('roles:check').replyOnce(500, { errors: [{ message: 'Temporary failure' }] });
|
|
581
|
+
apiMock.onGet('systemSettings:get').reply(200, {
|
|
582
|
+
data: { id: 1, title: 'NocoBase', raw_title: 'NocoBase', logo: null },
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
const Root = app.getRootComponent();
|
|
586
|
+
render(<Root />);
|
|
587
|
+
|
|
588
|
+
expect(await screen.findByText('Role recheck page')).toBeInTheDocument();
|
|
589
|
+
const header = await screen.findByRole('banner');
|
|
590
|
+
expect(within(header).getByRole('menu')).toBeInTheDocument();
|
|
591
|
+
if (!recheckRole) {
|
|
592
|
+
throw new Error('Expected the role recheck callback to be available');
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
await act(async () => {
|
|
596
|
+
await recheckRole?.();
|
|
597
|
+
});
|
|
598
|
+
expect(within(header).getByRole('menu')).toBeInTheDocument();
|
|
599
|
+
});
|
|
260
600
|
});
|
|
@@ -38,7 +38,7 @@ describe('standalone settings runtime paths', () => {
|
|
|
38
38
|
expect(resolveStandaloneSettingsPath(app, '/nocobase/v/admin/workflow/executions/88')).toBe(
|
|
39
39
|
'/nocobase/settings/workflow/executions/88',
|
|
40
40
|
);
|
|
41
|
-
expect(resolveStandaloneSettingsPath(app, '/nocobase/v/admin/ai/knowledge-base/detail/k1/documents')).toBe(
|
|
41
|
+
expect(resolveStandaloneSettingsPath(app, '/nocobase/v/admin/settings/ai/knowledge-base/detail/k1/documents')).toBe(
|
|
42
42
|
'/nocobase/settings/ai/knowledge-base/detail/k1/documents',
|
|
43
43
|
);
|
|
44
44
|
});
|