@nocobase/client-v2 3.0.0-alpha.1 → 3.0.0-alpha.11
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 +12 -0
- package/es/acl/aclCheckReadiness.d.ts +11 -0
- package/es/authRedirect.d.ts +6 -2
- package/es/flow/actions/linkageRules.d.ts +24 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +4 -30
- package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
- package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
- package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
- package/es/flow/components/field-value-variable/index.d.ts +11 -0
- package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
- package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
- package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
- package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
- package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
- package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
- package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
- package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
- package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +11 -0
- package/es/flow/models/blocks/table/utils.d.ts +1 -0
- package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
- package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
- package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/index.d.ts +2 -1
- package/es/index.mjs +140 -140
- 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 +145 -145
- package/package.json +7 -7
- package/src/APIClient.ts +92 -0
- package/src/Application.tsx +3 -1
- package/src/__tests__/APIClient.test.tsx +58 -0
- package/src/__tests__/app.test.tsx +122 -3
- package/src/__tests__/authRedirect.test.ts +17 -0
- package/src/__tests__/browserChecker.test.ts +81 -33
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +37 -0
- package/src/__tests__/settings-center.test.tsx +312 -11
- 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 +62 -1
- package/src/acl/ACLProvider.tsx +34 -3
- package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
- package/src/acl/aclCheckReadiness.ts +44 -0
- package/src/authRedirect.ts +13 -5
- package/src/components/AppComponents.tsx +16 -7
- package/src/components/form/filter/CollectionFilterItem.tsx +4 -2
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +17 -0
- package/src/components/form/table/Table.tsx +81 -6
- package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
- package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
- package/src/flow/actions/linkageRules.tsx +33 -10
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
- package/src/flow/components/FieldAssignValueInput.tsx +38 -621
- package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
- package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
- package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
- package/src/flow/components/field-value-variable/dateValue.ts +223 -0
- package/src/flow/components/field-value-variable/index.ts +12 -0
- package/src/flow/components/filter/VariableFilterItem.tsx +12 -3
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +125 -0
- package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
- package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
- package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
- package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
- package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
- package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
- package/src/flow/models/base/CollectionBlockModel.tsx +32 -1
- package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
- package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
- package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
- package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +129 -14
- package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +359 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
- package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
- package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
- package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
- package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
- package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
- package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +25 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +18 -8
- package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
- package/src/flow/models/blocks/table/utils.ts +7 -0
- package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +8 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
- package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +24 -1
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +167 -3
- package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
- package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
- package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +6 -0
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +27 -2
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
- 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 +23 -6
- package/src/settings-app/runtimePaths.ts +0 -1
- package/src/settings-app/settingsTheme.ts +82 -14
- package/src/settings-center/AdminSettingsLayout.tsx +132 -107
- package/src/settings-center/__tests__/groups.test.ts +25 -0
- package/src/settings-center/groups.ts +1 -1
- package/src/ui-operation/index.ts +33 -0
- package/src/ui-operation/ui-operation-codec.ts +54 -0
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { ACLRolesCheckProvider, Plugin } from '@nocobase/client-v2';
|
|
11
|
-
import { render, screen, waitFor,
|
|
11
|
+
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
|
12
12
|
import React from 'react';
|
|
13
13
|
import { message } from 'antd';
|
|
14
|
+
import { vi } from 'vitest';
|
|
14
15
|
import { AdminSettingsLayoutModel as ClientV2AdminSettingsLayoutModel } from '../settings-center';
|
|
15
16
|
import { AdminSettingsLayoutModel as ClientV1AdminSettingsLayoutModel } from '../../../client/src/pm/AdminSettingsLayoutModel';
|
|
17
|
+
import zhCN from '../../../client/src/locale/zh-CN.json';
|
|
16
18
|
import { SettingsBuildInPlugin } from '../settings-app/SettingsBuildInPlugin';
|
|
17
19
|
import { matchSettingsRoute, sortTopLevelSettings } from '../settings-center/utils';
|
|
18
20
|
import { createMockSettingsClient } from './mockSettingsApplication';
|
|
@@ -44,12 +46,20 @@ const waitForGetRequests = async (app: MockClientApplication, urls: string[]) =>
|
|
|
44
46
|
const mockAdminRuntime = (
|
|
45
47
|
app: MockClientApplication,
|
|
46
48
|
options: {
|
|
49
|
+
lang?: string;
|
|
47
50
|
snippets?: string[];
|
|
48
51
|
pmList?: any[];
|
|
52
|
+
resources?: Record<string, string>;
|
|
49
53
|
systemSettings?: Record<string, any>;
|
|
50
54
|
} = {},
|
|
51
55
|
) => {
|
|
52
|
-
const {
|
|
56
|
+
const {
|
|
57
|
+
lang = 'en-US',
|
|
58
|
+
snippets = ['pm', 'pm.system-settings.system-settings'],
|
|
59
|
+
pmList = [],
|
|
60
|
+
resources = {},
|
|
61
|
+
systemSettings = {},
|
|
62
|
+
} = options;
|
|
53
63
|
|
|
54
64
|
app.dataSourceManager.getCollection = ((name: string, collectionName: string) => {
|
|
55
65
|
if (name === 'main' && collectionName === 'attachments') {
|
|
@@ -74,9 +84,9 @@ const mockAdminRuntime = (
|
|
|
74
84
|
});
|
|
75
85
|
app.apiMock.onGet('app:getLang').reply(200, {
|
|
76
86
|
data: {
|
|
77
|
-
lang
|
|
87
|
+
lang,
|
|
78
88
|
resources: {
|
|
79
|
-
client:
|
|
89
|
+
client: resources,
|
|
80
90
|
},
|
|
81
91
|
cron: {},
|
|
82
92
|
},
|
|
@@ -244,6 +254,50 @@ describe('settings center', () => {
|
|
|
244
254
|
expect(await screen.findByText('Demo plugin')).toBeInTheDocument();
|
|
245
255
|
});
|
|
246
256
|
|
|
257
|
+
it('should redirect to the admin app when the role cannot access settings', async () => {
|
|
258
|
+
const originalLocation = globalThis.window.location;
|
|
259
|
+
const originalModernClientPrefix = window.__nocobase_modern_client_prefix__;
|
|
260
|
+
const replace = vi.fn();
|
|
261
|
+
Object.defineProperty(globalThis.window, 'location', {
|
|
262
|
+
configurable: true,
|
|
263
|
+
value: {
|
|
264
|
+
...originalLocation,
|
|
265
|
+
replace,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
window.__nocobase_modern_client_prefix__ = 'v';
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const app = createMockSettingsClient({
|
|
272
|
+
plugins: [SettingsBuildInPlugin, TestAclPlugin],
|
|
273
|
+
router: { type: 'memory', initialEntries: ['/settings/system-settings'] },
|
|
274
|
+
});
|
|
275
|
+
mockAdminRuntime(app, {
|
|
276
|
+
snippets: ['!pm', '!pm.system-settings.system-settings'],
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
await renderApp(app);
|
|
280
|
+
await waitForGetRequests(app, ['/auth:check', 'roles:check']);
|
|
281
|
+
|
|
282
|
+
await waitFor(() => {
|
|
283
|
+
expect(replace).toHaveBeenCalledWith('/v/admin');
|
|
284
|
+
});
|
|
285
|
+
expect(
|
|
286
|
+
screen.queryByRole('heading', { name: 'Your current role cannot access Settings' }),
|
|
287
|
+
).not.toBeInTheDocument();
|
|
288
|
+
} finally {
|
|
289
|
+
Object.defineProperty(globalThis.window, 'location', {
|
|
290
|
+
configurable: true,
|
|
291
|
+
value: originalLocation,
|
|
292
|
+
});
|
|
293
|
+
if (originalModernClientPrefix === undefined) {
|
|
294
|
+
delete window.__nocobase_modern_client_prefix__;
|
|
295
|
+
} else {
|
|
296
|
+
window.__nocobase_modern_client_prefix__ = originalModernClientPrefix;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
247
301
|
it('should hide plugin-manager menu item when pm snippet is missing', async () => {
|
|
248
302
|
const app = createMockSettingsClient({
|
|
249
303
|
plugins: [SettingsBuildInPlugin, TestAclPlugin],
|
|
@@ -265,12 +319,19 @@ describe('settings center', () => {
|
|
|
265
319
|
plugins: [SettingsBuildInPlugin, TestAclPlugin],
|
|
266
320
|
router: { type: 'memory', initialEntries: ['/settings/unknown'] },
|
|
267
321
|
});
|
|
268
|
-
mockAdminRuntime(app
|
|
322
|
+
mockAdminRuntime(app, {
|
|
323
|
+
snippets: ['!pm', '!pm.system-settings.system-settings'],
|
|
324
|
+
});
|
|
269
325
|
|
|
270
326
|
await renderApp(app);
|
|
271
327
|
await waitForGetRequests(app, ['/auth:check', 'roles:check']);
|
|
272
328
|
|
|
273
|
-
|
|
329
|
+
const result = await screen.findByRole('status');
|
|
330
|
+
expect(within(result).getByRole('heading', { name: 'Settings page not found' })).toBeInTheDocument();
|
|
331
|
+
expect(
|
|
332
|
+
within(result).getByText('The settings page you requested does not exist or has been removed.'),
|
|
333
|
+
).toBeInTheDocument();
|
|
334
|
+
expect(app.router.state.location.pathname).toBe('/settings/unknown');
|
|
274
335
|
});
|
|
275
336
|
|
|
276
337
|
it('should allow direct access to hidden page without showing menu entry', async () => {
|
|
@@ -298,6 +359,7 @@ describe('settings center', () => {
|
|
|
298
359
|
|
|
299
360
|
expect(await screen.findByText('Hidden settings page')).toBeInTheDocument();
|
|
300
361
|
expect(screen.queryByRole('menuitem', { name: 'Hidden demo' })).not.toBeInTheDocument();
|
|
362
|
+
expect(app.router.state.location.pathname).toBe('/settings/hidden-demo');
|
|
301
363
|
});
|
|
302
364
|
|
|
303
365
|
it('should show route empty state when direct access page has no permission', async () => {
|
|
@@ -319,14 +381,248 @@ describe('settings center', () => {
|
|
|
319
381
|
router: { type: 'memory', initialEntries: ['/settings/secure-demo'] },
|
|
320
382
|
});
|
|
321
383
|
mockAdminRuntime(app, {
|
|
384
|
+
lang: 'zh-CN',
|
|
385
|
+
resources: zhCN,
|
|
322
386
|
snippets: ['pm', 'pm.system-settings.system-settings', '!pm.secure-demo.index'],
|
|
323
387
|
});
|
|
324
388
|
|
|
325
389
|
await renderApp(app);
|
|
326
390
|
await waitForGetRequests(app, ['/auth:check', 'roles:check']);
|
|
327
391
|
|
|
328
|
-
|
|
392
|
+
const result = await screen.findByRole('status');
|
|
393
|
+
expect(within(result).getByRole('heading', { name: '当前角色无权访问设置中心' })).toBeInTheDocument();
|
|
394
|
+
expect(within(result).getByText('请切换至有权限的角色,或联系管理员获取访问权限。')).toBeInTheDocument();
|
|
329
395
|
expect(screen.queryByText('Secure settings page')).not.toBeInTheDocument();
|
|
396
|
+
expect(app.router.state.location.pathname).toBe('/settings/secure-demo');
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('should redirect a denied settings tab to the first accessible tab', async () => {
|
|
400
|
+
const renderDeniedTab = vi.fn(() => <div>Denied tab content</div>);
|
|
401
|
+
|
|
402
|
+
class ProtectedSettingsTabsPlugin extends Plugin {
|
|
403
|
+
async load() {
|
|
404
|
+
this.pluginSettingsManager.addMenuItem({ key: 'protected-tabs', title: 'Protected tabs' });
|
|
405
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
406
|
+
menuKey: 'protected-tabs',
|
|
407
|
+
key: 'a-second',
|
|
408
|
+
title: 'Second accessible tab',
|
|
409
|
+
aclSnippet: 'pm.protected-tabs.second',
|
|
410
|
+
sort: 20,
|
|
411
|
+
Component: () => <div>Second accessible tab content</div>,
|
|
412
|
+
});
|
|
413
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
414
|
+
menuKey: 'protected-tabs',
|
|
415
|
+
key: 'z-first',
|
|
416
|
+
title: 'First accessible tab',
|
|
417
|
+
aclSnippet: 'pm.protected-tabs.first',
|
|
418
|
+
sort: 10,
|
|
419
|
+
Component: () => <div>First accessible tab content</div>,
|
|
420
|
+
});
|
|
421
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
422
|
+
menuKey: 'protected-tabs',
|
|
423
|
+
key: 'denied',
|
|
424
|
+
title: 'Denied tab',
|
|
425
|
+
aclSnippet: 'pm.protected-tabs.denied',
|
|
426
|
+
sort: 30,
|
|
427
|
+
Component: renderDeniedTab,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const app = createMockSettingsClient({
|
|
433
|
+
plugins: [SettingsBuildInPlugin, TestAclPlugin, ProtectedSettingsTabsPlugin],
|
|
434
|
+
router: { type: 'memory', initialEntries: ['/settings/protected-tabs/denied'] },
|
|
435
|
+
});
|
|
436
|
+
mockAdminRuntime(app, {
|
|
437
|
+
snippets: ['pm', '!pm.protected-tabs.denied'],
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
await renderApp(app);
|
|
441
|
+
await waitForGetRequests(app, ['/auth:check', 'roles:check']);
|
|
442
|
+
|
|
443
|
+
await waitFor(() => {
|
|
444
|
+
expect(app.router.router.state.location.pathname).toBe('/settings/protected-tabs/z-first');
|
|
445
|
+
});
|
|
446
|
+
expect(app.router.router.state.historyAction).toBe('REPLACE');
|
|
447
|
+
expect(await screen.findByText('First accessible tab content')).toBeInTheDocument();
|
|
448
|
+
expect(screen.queryByText('Denied tab content')).not.toBeInTheDocument();
|
|
449
|
+
expect(renderDeniedTab).not.toHaveBeenCalled();
|
|
450
|
+
|
|
451
|
+
await act(async () => {
|
|
452
|
+
await app.router.router.navigate('/settings/protected-tabs/a-second');
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
expect(await screen.findByText('Second accessible tab content')).toBeInTheDocument();
|
|
456
|
+
expect(app.router.router.state.location.pathname).toBe('/settings/protected-tabs/a-second');
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it.each(['/settings/fallback-access', '/settings/fallback-access/'])(
|
|
460
|
+
'should redirect a denied index page to an accessible sibling from %s',
|
|
461
|
+
async (initialEntry) => {
|
|
462
|
+
class FallbackAccessSettingsPlugin extends Plugin {
|
|
463
|
+
async load() {
|
|
464
|
+
this.pluginSettingsManager.addMenuItem({ key: 'fallback-access', title: 'Fallback access' });
|
|
465
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
466
|
+
menuKey: 'fallback-access',
|
|
467
|
+
key: 'index',
|
|
468
|
+
title: 'Restricted index settings',
|
|
469
|
+
aclSnippet: 'pm.fallback-access.index',
|
|
470
|
+
Component: () => <div>Restricted index settings page</div>,
|
|
471
|
+
});
|
|
472
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
473
|
+
menuKey: 'fallback-access',
|
|
474
|
+
key: 'allowed',
|
|
475
|
+
title: 'Allowed fallback settings',
|
|
476
|
+
Component: () => <div>Allowed fallback settings page</div>,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const app = createMockSettingsClient({
|
|
482
|
+
plugins: [SettingsBuildInPlugin, TestAclPlugin, FallbackAccessSettingsPlugin],
|
|
483
|
+
router: { type: 'memory', initialEntries: [initialEntry] },
|
|
484
|
+
});
|
|
485
|
+
mockAdminRuntime(app, {
|
|
486
|
+
snippets: ['pm', 'pm.system-settings.system-settings', '!pm.fallback-access.index'],
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
await renderApp(app);
|
|
490
|
+
await waitForGetRequests(app, ['/auth:check', 'roles:check']);
|
|
491
|
+
|
|
492
|
+
expect(await screen.findByText('Allowed fallback settings page')).toBeInTheDocument();
|
|
493
|
+
expect(
|
|
494
|
+
screen.queryByRole('heading', { name: 'Your current role cannot access Settings' }),
|
|
495
|
+
).not.toBeInTheDocument();
|
|
496
|
+
expect(screen.queryByText('Restricted index settings page')).not.toBeInTheDocument();
|
|
497
|
+
},
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
it('should skip an accessible dynamic tab when its route params cannot be resolved', async () => {
|
|
501
|
+
class DynamicSettingsTabsPlugin extends Plugin {
|
|
502
|
+
async load() {
|
|
503
|
+
this.pluginSettingsManager.addMenuItem({ key: 'dynamic-tabs', title: 'Dynamic tabs' });
|
|
504
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
505
|
+
menuKey: 'dynamic-tabs',
|
|
506
|
+
key: ':name',
|
|
507
|
+
title: 'Dynamic tab',
|
|
508
|
+
aclSnippet: 'pm.dynamic-tabs.dynamic',
|
|
509
|
+
sort: 1,
|
|
510
|
+
Component: () => <div>Dynamic tab content</div>,
|
|
511
|
+
});
|
|
512
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
513
|
+
menuKey: 'dynamic-tabs',
|
|
514
|
+
key: 'fallback',
|
|
515
|
+
title: 'Static fallback tab',
|
|
516
|
+
aclSnippet: 'pm.dynamic-tabs.fallback',
|
|
517
|
+
sort: 2,
|
|
518
|
+
Component: () => <div>Static fallback tab content</div>,
|
|
519
|
+
});
|
|
520
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
521
|
+
menuKey: 'dynamic-tabs',
|
|
522
|
+
key: 'denied',
|
|
523
|
+
title: 'Denied tab',
|
|
524
|
+
aclSnippet: 'pm.dynamic-tabs.denied',
|
|
525
|
+
sort: 3,
|
|
526
|
+
Component: () => <div>Denied dynamic tab content</div>,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const app = createMockSettingsClient({
|
|
532
|
+
plugins: [SettingsBuildInPlugin, TestAclPlugin, DynamicSettingsTabsPlugin],
|
|
533
|
+
router: { type: 'memory', initialEntries: ['/settings/dynamic-tabs/denied'] },
|
|
534
|
+
});
|
|
535
|
+
mockAdminRuntime(app, {
|
|
536
|
+
snippets: ['pm', '!pm.dynamic-tabs.denied'],
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
await renderApp(app);
|
|
540
|
+
await waitForGetRequests(app, ['/auth:check', 'roles:check']);
|
|
541
|
+
|
|
542
|
+
await waitFor(() => {
|
|
543
|
+
expect(app.router.router.state.location.pathname).toBe('/settings/dynamic-tabs/fallback');
|
|
544
|
+
});
|
|
545
|
+
expect(await screen.findByText('Static fallback tab content')).toBeInTheDocument();
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('should preserve resolved route params when redirecting between dynamic tabs', async () => {
|
|
549
|
+
class DynamicSiblingTabsPlugin extends Plugin {
|
|
550
|
+
async load() {
|
|
551
|
+
this.pluginSettingsManager.addMenuItem({ key: 'dynamic-siblings', title: 'Dynamic sibling tabs' });
|
|
552
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
553
|
+
menuKey: 'dynamic-siblings',
|
|
554
|
+
key: ':name/channels',
|
|
555
|
+
title: 'Dynamic channels tab',
|
|
556
|
+
aclSnippet: 'pm.dynamic-siblings.channels',
|
|
557
|
+
sort: 1,
|
|
558
|
+
Component: () => <div>Dynamic channels tab content</div>,
|
|
559
|
+
});
|
|
560
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
561
|
+
menuKey: 'dynamic-siblings',
|
|
562
|
+
key: ':name/logs',
|
|
563
|
+
title: 'Dynamic logs tab',
|
|
564
|
+
aclSnippet: 'pm.dynamic-siblings.logs',
|
|
565
|
+
sort: 2,
|
|
566
|
+
Component: () => <div>Dynamic logs tab content</div>,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const app = createMockSettingsClient({
|
|
572
|
+
plugins: [SettingsBuildInPlugin, TestAclPlugin, DynamicSiblingTabsPlugin],
|
|
573
|
+
router: { type: 'memory', initialEntries: ['/settings/dynamic-siblings/email:primary/logs'] },
|
|
574
|
+
});
|
|
575
|
+
mockAdminRuntime(app, {
|
|
576
|
+
snippets: ['pm', '!pm.dynamic-siblings.logs'],
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
await renderApp(app);
|
|
580
|
+
await waitForGetRequests(app, ['/auth:check', 'roles:check']);
|
|
581
|
+
|
|
582
|
+
await waitFor(() => {
|
|
583
|
+
expect(app.router.router.state.location.pathname).toBe('/settings/dynamic-siblings/email:primary/channels');
|
|
584
|
+
});
|
|
585
|
+
expect(await screen.findByText('Dynamic channels tab content')).toBeInTheDocument();
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('should resolve hyphenated route param names when redirecting between dynamic tabs', async () => {
|
|
589
|
+
class HyphenatedParamTabsPlugin extends Plugin {
|
|
590
|
+
async load() {
|
|
591
|
+
this.pluginSettingsManager.addMenuItem({ key: 'hyphenated-params', title: 'Hyphenated param tabs' });
|
|
592
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
593
|
+
menuKey: 'hyphenated-params',
|
|
594
|
+
key: ':data-source/channels',
|
|
595
|
+
title: 'Hyphenated channels tab',
|
|
596
|
+
aclSnippet: 'pm.hyphenated-params.channels',
|
|
597
|
+
sort: 1,
|
|
598
|
+
Component: () => <div>Hyphenated channels tab content</div>,
|
|
599
|
+
});
|
|
600
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
601
|
+
menuKey: 'hyphenated-params',
|
|
602
|
+
key: ':data-source/logs',
|
|
603
|
+
title: 'Hyphenated logs tab',
|
|
604
|
+
aclSnippet: 'pm.hyphenated-params.logs',
|
|
605
|
+
sort: 2,
|
|
606
|
+
Component: () => <div>Hyphenated logs tab content</div>,
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const app = createMockSettingsClient({
|
|
612
|
+
plugins: [SettingsBuildInPlugin, TestAclPlugin, HyphenatedParamTabsPlugin],
|
|
613
|
+
router: { type: 'memory', initialEntries: ['/settings/hyphenated-params/email/logs'] },
|
|
614
|
+
});
|
|
615
|
+
mockAdminRuntime(app, {
|
|
616
|
+
snippets: ['pm', '!pm.hyphenated-params.logs'],
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
await renderApp(app);
|
|
620
|
+
await waitForGetRequests(app, ['/auth:check', 'roles:check']);
|
|
621
|
+
|
|
622
|
+
await waitFor(() => {
|
|
623
|
+
expect(app.router.router.state.location.pathname).toBe('/settings/hyphenated-params/email/channels');
|
|
624
|
+
});
|
|
625
|
+
expect(await screen.findByText('Hyphenated channels tab content')).toBeInTheDocument();
|
|
330
626
|
});
|
|
331
627
|
|
|
332
628
|
it('should keep menu visible when menu acl is denied but child page is visible', async () => {
|
|
@@ -358,10 +654,13 @@ describe('settings center', () => {
|
|
|
358
654
|
await waitForGetRequests(app, ['/auth:check', 'roles:check']);
|
|
359
655
|
|
|
360
656
|
expect(await screen.findByText('Menu ACL child page')).toBeInTheDocument();
|
|
361
|
-
|
|
657
|
+
// The sidebar is gone; this group is reached from the "other settings" hover
|
|
658
|
+
// dropdown in the top bar. The dropdown only renders on hover, so this just
|
|
659
|
+
// checks the group entry survived (ACL did not drop the whole menu).
|
|
660
|
+
expect(screen.getByRole('menuitem', { name: 'Other settings' })).toBeInTheDocument();
|
|
362
661
|
});
|
|
363
662
|
|
|
364
|
-
it('should
|
|
663
|
+
it('should keep rendering the active page when a group has many settings', async () => {
|
|
365
664
|
class ManySettingsPlugin extends Plugin {
|
|
366
665
|
async load() {
|
|
367
666
|
for (let index = 0; index < 30; index += 1) {
|
|
@@ -390,8 +689,10 @@ describe('settings center', () => {
|
|
|
390
689
|
|
|
391
690
|
expect(await screen.findByText('Scroll demo page 29')).toBeInTheDocument();
|
|
392
691
|
|
|
393
|
-
|
|
394
|
-
|
|
692
|
+
// Without the sidebar there is no separate scroll area: all 30 settings live in
|
|
693
|
+
// the top bar dropdown and the page renders only the active one, so this just
|
|
694
|
+
// checks it still holds up.
|
|
695
|
+
expect(document.querySelector('.ant-layout-sider')).toBeNull();
|
|
395
696
|
});
|
|
396
697
|
|
|
397
698
|
it('should save system settings through systemSettings:put', async () => {
|