@nocobase/app-template-default 1.0.0-beta.73 → 1.0.0-beta.74
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/AGENTS.md +1 -1
- package/README.MD +2 -0
- package/client/components/default-access-editor.tsx +183 -0
- package/client/components/permission-editor.tsx +216 -150
- package/client/components/role-list.tsx +61 -0
- package/client/lib/navigation-transform.ts +28 -1
- package/client/locales/system/settings-en-US.ts +73 -2
- package/client/locales/system/settings-zh-CN.ts +65 -2
- package/client/pages/settings/default-access-form.tsx +499 -0
- package/client/pages/settings/default-access-model.ts +154 -0
- package/client/pages/settings/default-access.tsx +407 -0
- package/client/pages/settings/permission-filters.tsx +43 -37
- package/client/pages/settings/permission-management-page.tsx +105 -0
- package/client/pages/settings/permissions-model.ts +37 -1
- package/client/pages/settings/role-form.tsx +46 -18
- package/client/pages/settings/roles.tsx +23 -43
- package/client/pages/settings/routes.ts +78 -19
- package/client/pages/settings/shared.tsx +3 -0
- package/client/pages/settings/users.tsx +1 -1
- package/client/routing/route-navigation.ts +21 -1
- package/package.json +19 -19
- package/tests/components/default-access.test.tsx +554 -0
- package/tests/components/page-harness.test.tsx +132 -112
- package/tests/components/permission-editor.test.tsx +233 -73
- package/tests/components/permission-management-tabs.test.tsx +128 -0
- package/tests/components/role-list.test.tsx +71 -0
- package/tests/components/roles.test.tsx +78 -0
- package/tests/components/user-permissions.test.tsx +12 -0
- package/tests/logic/client-routes.test.ts +8 -0
- package/tests/logic/default-access-model.test.ts +167 -0
- package/tests/logic/permission-management-routes.test.ts +64 -0
- package/tests/logic/settings-permissions.test.ts +89 -0
- package/vitest.config.ts +2 -2
|
@@ -82,7 +82,8 @@ describe('permission workspace', () => {
|
|
|
82
82
|
);
|
|
83
83
|
expect(
|
|
84
84
|
screen.getByRole('button', { name: /Business actions/ }),
|
|
85
|
-
).toHaveTextContent('
|
|
85
|
+
).toHaveTextContent('Business actions');
|
|
86
|
+
expect(screen.queryByText('1/2')).not.toBeInTheDocument();
|
|
86
87
|
await user.clear(screen.getByRole('textbox'));
|
|
87
88
|
expect(
|
|
88
89
|
screen.getByRole('checkbox', { name: 'Read orders' }),
|
|
@@ -93,57 +94,6 @@ describe('permission workspace', () => {
|
|
|
93
94
|
});
|
|
94
95
|
|
|
95
96
|
describe('compact resource actions', () => {
|
|
96
|
-
it('batches only visible editable changes and shows a mixed group checkbox', async () => {
|
|
97
|
-
const user = userEvent.setup();
|
|
98
|
-
const batch = vi.fn();
|
|
99
|
-
render(
|
|
100
|
-
<PermissionEditor
|
|
101
|
-
sections={[
|
|
102
|
-
{
|
|
103
|
-
...sections[0]!,
|
|
104
|
-
groups: [
|
|
105
|
-
{
|
|
106
|
-
id: 'orders',
|
|
107
|
-
title: 'Orders',
|
|
108
|
-
rows: [
|
|
109
|
-
...sections[0]!.groups[0]!.rows,
|
|
110
|
-
{
|
|
111
|
-
id: 'locked',
|
|
112
|
-
label: 'Protected',
|
|
113
|
-
enabled: false,
|
|
114
|
-
toggleable: false,
|
|
115
|
-
},
|
|
116
|
-
],
|
|
117
|
-
},
|
|
118
|
-
],
|
|
119
|
-
},
|
|
120
|
-
]}
|
|
121
|
-
onEnabledChange={vi.fn()}
|
|
122
|
-
onEnabledBatchChange={batch}
|
|
123
|
-
onLevelChange={vi.fn()}
|
|
124
|
-
/>,
|
|
125
|
-
);
|
|
126
|
-
const group = screen.getByRole('checkbox', {
|
|
127
|
-
name: 'Select visible permissions in Orders',
|
|
128
|
-
});
|
|
129
|
-
expect(group).toHaveAttribute('aria-checked', 'mixed');
|
|
130
|
-
await user.click(group);
|
|
131
|
-
expect(batch).toHaveBeenLastCalledWith([
|
|
132
|
-
{ id: 'edit-orders', enabled: true },
|
|
133
|
-
]);
|
|
134
|
-
await user.type(screen.getByRole('textbox'), 'owner');
|
|
135
|
-
await user.click(
|
|
136
|
-
screen.getByRole('checkbox', {
|
|
137
|
-
name: 'Select visible permissions in Orders',
|
|
138
|
-
}),
|
|
139
|
-
);
|
|
140
|
-
expect(batch).toHaveBeenLastCalledWith([
|
|
141
|
-
{ id: 'edit-orders', enabled: true },
|
|
142
|
-
]);
|
|
143
|
-
expect(
|
|
144
|
-
screen.queryByRole('checkbox', { name: 'Protected' }),
|
|
145
|
-
).not.toBeInTheDocument();
|
|
146
|
-
});
|
|
147
97
|
it('restores stable focus when enabled filtering removes the action or group', async () => {
|
|
148
98
|
const user = userEvent.setup();
|
|
149
99
|
function Draft() {
|
|
@@ -177,21 +127,13 @@ describe('compact resource actions', () => {
|
|
|
177
127
|
/>
|
|
178
128
|
);
|
|
179
129
|
}
|
|
180
|
-
|
|
130
|
+
render(<Draft />);
|
|
181
131
|
await user.click(screen.getByRole('switch', { name: 'Enabled only' }));
|
|
182
132
|
await user.click(screen.getByRole('checkbox', { name: 'Action' }));
|
|
183
133
|
expect(screen.getByRole('textbox')).toHaveFocus();
|
|
184
134
|
expect(screen.getByRole('status')).toHaveTextContent(
|
|
185
135
|
'No matching permissions',
|
|
186
136
|
);
|
|
187
|
-
view.unmount();
|
|
188
|
-
render(<Draft />);
|
|
189
|
-
await user.click(screen.getByRole('switch', { name: 'Enabled only' }));
|
|
190
|
-
screen
|
|
191
|
-
.getByRole('checkbox', { name: 'Select visible permissions in Group' })
|
|
192
|
-
.focus();
|
|
193
|
-
await user.keyboard('[Space]');
|
|
194
|
-
expect(screen.getByRole('textbox')).toHaveFocus();
|
|
195
137
|
});
|
|
196
138
|
it('keeps scopes attached to actions, readonly scopes disabled and control ids instance-local', async () => {
|
|
197
139
|
const scoped = [
|
|
@@ -325,7 +267,7 @@ it('shows a single page access checkbox without a duplicate access action or res
|
|
|
325
267
|
expect(onEnabledBatchChange).not.toHaveBeenCalled();
|
|
326
268
|
});
|
|
327
269
|
|
|
328
|
-
it('
|
|
270
|
+
it('reveals multiple scopes without changing their independent saved selections', async () => {
|
|
329
271
|
const change = vi.fn();
|
|
330
272
|
render(
|
|
331
273
|
<PermissionEditor
|
|
@@ -361,6 +303,9 @@ it('keeps multiple inline scopes independently named and editable', async () =>
|
|
|
361
303
|
onLevelChange={change}
|
|
362
304
|
/>,
|
|
363
305
|
);
|
|
306
|
+
expect(screen.getByText('Records')).not.toBeVisible();
|
|
307
|
+
await userEvent.click(screen.getByText('Data ranges (2)'));
|
|
308
|
+
expect(change).not.toHaveBeenCalled();
|
|
364
309
|
expect(screen.getByText('Records')).toBeVisible();
|
|
365
310
|
expect(screen.getByText('Attachments')).toBeVisible();
|
|
366
311
|
await userEvent.click(
|
|
@@ -372,7 +317,7 @@ it('keeps multiple inline scopes independently named and editable', async () =>
|
|
|
372
317
|
expect(change).toHaveBeenCalledExactlyOnceWith('attachments', 'all');
|
|
373
318
|
});
|
|
374
319
|
|
|
375
|
-
it('
|
|
320
|
+
it('uses the category navigation without a duplicate heading and identifies filtered results', async () => {
|
|
376
321
|
const batch = vi.fn();
|
|
377
322
|
render(
|
|
378
323
|
<PermissionEditor
|
|
@@ -407,19 +352,24 @@ it('keeps the category heading consistent for a single resource and labels bulk
|
|
|
407
352
|
onLevelChange={vi.fn()}
|
|
408
353
|
/>,
|
|
409
354
|
);
|
|
355
|
+
expect(
|
|
356
|
+
screen.queryByRole('heading', { level: 2, name: 'Users' }),
|
|
357
|
+
).not.toBeInTheDocument();
|
|
358
|
+
expect(screen.getByRole('region', { name: 'Users' })).toBeInTheDocument();
|
|
359
|
+
await userEvent.type(screen.getByRole('textbox'), 'users');
|
|
410
360
|
expect(
|
|
411
361
|
screen.getByRole('heading', { level: 2, name: 'Users' }),
|
|
412
|
-
).
|
|
413
|
-
expect(
|
|
362
|
+
).toBeVisible();
|
|
363
|
+
expect(batch).not.toHaveBeenCalled();
|
|
364
|
+
expect(screen.queryByText('1/2')).not.toBeInTheDocument();
|
|
414
365
|
expect(
|
|
415
|
-
screen.
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
]);
|
|
366
|
+
screen.queryByRole('checkbox', { name: /Select visible/ }),
|
|
367
|
+
).not.toBeInTheDocument();
|
|
368
|
+
expect(
|
|
369
|
+
screen.queryByText('All', { selector: 'span' }),
|
|
370
|
+
).not.toBeInTheDocument();
|
|
371
|
+
await userEvent.click(screen.getByRole('checkbox', { name: 'Edit users' }));
|
|
372
|
+
expect(batch).not.toHaveBeenCalled();
|
|
423
373
|
});
|
|
424
374
|
|
|
425
375
|
it('aligns semantic actions across resources without dropping special or filtered actions', async () => {
|
|
@@ -531,9 +481,219 @@ it('keeps scope captions consistent in a category with multiple data ranges, inc
|
|
|
531
481
|
/>,
|
|
532
482
|
);
|
|
533
483
|
expect(screen.getByText('Visible quotes')).toBeVisible();
|
|
484
|
+
expect(
|
|
485
|
+
screen.getByText(
|
|
486
|
+
'Linked projects: All records; Allowed quotes: All records',
|
|
487
|
+
),
|
|
488
|
+
).toBeVisible();
|
|
489
|
+
await userEvent.click(screen.getByText('Data ranges (2)'));
|
|
534
490
|
expect(screen.getByText('Linked projects')).toBeVisible();
|
|
535
491
|
expect(screen.getByText('Allowed quotes')).toBeVisible();
|
|
536
492
|
await userEvent.type(screen.getByRole('textbox'), 'View quotes');
|
|
537
493
|
expect(screen.queryByText('Linked projects')).not.toBeInTheDocument();
|
|
538
494
|
expect(screen.getByText('Visible quotes')).toBeVisible();
|
|
539
495
|
});
|
|
496
|
+
|
|
497
|
+
it('keeps administration actions ordered and independently editable with unrelated special actions', async () => {
|
|
498
|
+
const enabled = vi.fn();
|
|
499
|
+
render(
|
|
500
|
+
<PermissionEditor
|
|
501
|
+
sections={[
|
|
502
|
+
{
|
|
503
|
+
id: 'admin',
|
|
504
|
+
title: 'Administration',
|
|
505
|
+
groups: [
|
|
506
|
+
{
|
|
507
|
+
id: 'roles',
|
|
508
|
+
title: 'Roles',
|
|
509
|
+
rows: [
|
|
510
|
+
{
|
|
511
|
+
id: 'grants',
|
|
512
|
+
actionKey: 'grants',
|
|
513
|
+
label: 'Manage grants',
|
|
514
|
+
enabled: false,
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
id: 'delete',
|
|
518
|
+
actionKey: 'delete',
|
|
519
|
+
label: 'Delete',
|
|
520
|
+
enabled: false,
|
|
521
|
+
},
|
|
522
|
+
{ id: 'read', actionKey: 'read', label: 'View', enabled: true },
|
|
523
|
+
{
|
|
524
|
+
id: 'update',
|
|
525
|
+
actionKey: 'update',
|
|
526
|
+
label: 'Edit',
|
|
527
|
+
enabled: false,
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
id: 'create',
|
|
531
|
+
actionKey: 'create',
|
|
532
|
+
label: 'Create',
|
|
533
|
+
enabled: false,
|
|
534
|
+
},
|
|
535
|
+
],
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
id: 'checker',
|
|
539
|
+
title: 'Checker',
|
|
540
|
+
rows: [
|
|
541
|
+
{
|
|
542
|
+
id: 'check',
|
|
543
|
+
actionKey: 'check',
|
|
544
|
+
label: 'Check permissions',
|
|
545
|
+
enabled: false,
|
|
546
|
+
},
|
|
547
|
+
],
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
id: 'scheduler',
|
|
551
|
+
title: 'Scheduler',
|
|
552
|
+
rows: [
|
|
553
|
+
{
|
|
554
|
+
id: 'run',
|
|
555
|
+
actionKey: 'run',
|
|
556
|
+
label: 'Start and stop rules',
|
|
557
|
+
enabled: false,
|
|
558
|
+
},
|
|
559
|
+
],
|
|
560
|
+
},
|
|
561
|
+
],
|
|
562
|
+
},
|
|
563
|
+
]}
|
|
564
|
+
onEnabledChange={enabled}
|
|
565
|
+
onLevelChange={vi.fn()}
|
|
566
|
+
/>,
|
|
567
|
+
);
|
|
568
|
+
const controls = screen.getAllByRole('checkbox');
|
|
569
|
+
for (const [index, name] of [
|
|
570
|
+
'View',
|
|
571
|
+
'Create',
|
|
572
|
+
'Edit',
|
|
573
|
+
'Delete',
|
|
574
|
+
'Manage grants',
|
|
575
|
+
'Check permissions',
|
|
576
|
+
'Start and stop rules',
|
|
577
|
+
].entries()) {
|
|
578
|
+
expect(controls[index]).toHaveAccessibleName(name);
|
|
579
|
+
}
|
|
580
|
+
expect(
|
|
581
|
+
screen.getByRole('checkbox', { name: 'View', exact: true }),
|
|
582
|
+
).toBeChecked();
|
|
583
|
+
expect(enabled).not.toHaveBeenCalled();
|
|
584
|
+
await userEvent.type(screen.getByRole('textbox'), 'Manage grants');
|
|
585
|
+
expect(screen.getAllByRole('checkbox')).toHaveLength(1);
|
|
586
|
+
await userEvent.click(
|
|
587
|
+
screen.getByRole('checkbox', { name: 'Manage grants' }),
|
|
588
|
+
);
|
|
589
|
+
expect(enabled).toHaveBeenCalledExactlyOnceWith('grants', true);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it('explains categories and allows individual review without bulk selection', async () => {
|
|
593
|
+
const change = vi.fn();
|
|
594
|
+
const batch = vi.fn();
|
|
595
|
+
render(
|
|
596
|
+
<PermissionEditor
|
|
597
|
+
sections={[
|
|
598
|
+
{
|
|
599
|
+
id: 'accounts',
|
|
600
|
+
title: 'Users and accounts',
|
|
601
|
+
description: 'Review account permissions individually.',
|
|
602
|
+
groups: [
|
|
603
|
+
{
|
|
604
|
+
id: 'security',
|
|
605
|
+
title: 'Account security',
|
|
606
|
+
rows: [
|
|
607
|
+
{
|
|
608
|
+
id: 'disable',
|
|
609
|
+
label: 'Disable account',
|
|
610
|
+
enabled: false,
|
|
611
|
+
hint: 'Prevent further sign-in.',
|
|
612
|
+
},
|
|
613
|
+
{ id: 'reset', label: 'Reset password', enabled: false },
|
|
614
|
+
],
|
|
615
|
+
},
|
|
616
|
+
],
|
|
617
|
+
},
|
|
618
|
+
]}
|
|
619
|
+
onEnabledChange={change}
|
|
620
|
+
onEnabledBatchChange={batch}
|
|
621
|
+
onLevelChange={vi.fn()}
|
|
622
|
+
/>,
|
|
623
|
+
);
|
|
624
|
+
expect(
|
|
625
|
+
screen.getByText('Review account permissions individually.'),
|
|
626
|
+
).toBeVisible();
|
|
627
|
+
expect(screen.getAllByRole('checkbox')).toHaveLength(2);
|
|
628
|
+
expect(
|
|
629
|
+
screen.queryByText('All', { selector: 'span' }),
|
|
630
|
+
).not.toBeInTheDocument();
|
|
631
|
+
expect(screen.getByText('Prevent further sign-in.')).toBeVisible();
|
|
632
|
+
expect(change).not.toHaveBeenCalled();
|
|
633
|
+
expect(batch).not.toHaveBeenCalled();
|
|
634
|
+
await userEvent.click(
|
|
635
|
+
screen.getByRole('checkbox', { name: 'Disable account' }),
|
|
636
|
+
);
|
|
637
|
+
expect(change).toHaveBeenCalledExactlyOnceWith('disable', true);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('shows scope explanations on focus or tap without changing the permission draft', async () => {
|
|
641
|
+
const user = userEvent.setup();
|
|
642
|
+
const enabled = vi.fn();
|
|
643
|
+
const changed = vi.fn();
|
|
644
|
+
render(
|
|
645
|
+
<PermissionEditor
|
|
646
|
+
sections={[
|
|
647
|
+
{
|
|
648
|
+
id: 'orders',
|
|
649
|
+
title: 'Orders',
|
|
650
|
+
groups: [
|
|
651
|
+
{
|
|
652
|
+
id: 'orders',
|
|
653
|
+
title: 'Orders',
|
|
654
|
+
rows: [
|
|
655
|
+
{
|
|
656
|
+
id: 'view',
|
|
657
|
+
label: 'View orders',
|
|
658
|
+
enabled: true,
|
|
659
|
+
scopes: [
|
|
660
|
+
{
|
|
661
|
+
id: 'scope',
|
|
662
|
+
label: 'Orders',
|
|
663
|
+
level: '',
|
|
664
|
+
hint: 'Default access and sharing still apply.',
|
|
665
|
+
levels: [
|
|
666
|
+
{ value: '', label: 'No additional record access' },
|
|
667
|
+
{ value: 'all', label: 'All records' },
|
|
668
|
+
],
|
|
669
|
+
},
|
|
670
|
+
],
|
|
671
|
+
},
|
|
672
|
+
],
|
|
673
|
+
},
|
|
674
|
+
],
|
|
675
|
+
},
|
|
676
|
+
]}
|
|
677
|
+
onEnabledChange={enabled}
|
|
678
|
+
onLevelChange={changed}
|
|
679
|
+
/>,
|
|
680
|
+
);
|
|
681
|
+
const help = screen.getByRole('button', {
|
|
682
|
+
name: 'About the data range for View orders · Orders',
|
|
683
|
+
});
|
|
684
|
+
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
|
|
685
|
+
await user.click(help);
|
|
686
|
+
expect(await screen.findByRole('tooltip')).toHaveTextContent(
|
|
687
|
+
'Default access and sharing still apply.',
|
|
688
|
+
);
|
|
689
|
+
await user.keyboard('{Escape}');
|
|
690
|
+
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
|
|
691
|
+
await user.tab({ shift: true });
|
|
692
|
+
await user.tab();
|
|
693
|
+
expect(help).toHaveFocus();
|
|
694
|
+
expect(await screen.findByRole('tooltip')).toHaveTextContent(
|
|
695
|
+
'Default access and sharing still apply.',
|
|
696
|
+
);
|
|
697
|
+
expect(enabled).not.toHaveBeenCalled();
|
|
698
|
+
expect(changed).not.toHaveBeenCalled();
|
|
699
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { renderWithApp } from '@nocobase/app-testing/client';
|
|
2
|
+
import { screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { Route, Routes, useLocation } from 'react-router';
|
|
5
|
+
import { beforeEach, expect, it, vi } from 'vitest';
|
|
6
|
+
import { PermissionManagementPage } from '../../client/pages/settings/permission-management-page.js';
|
|
7
|
+
import enUS from '../../client/locales/en-US.js';
|
|
8
|
+
import zhCN from '../../client/locales/zh-CN.js';
|
|
9
|
+
const access = vi.hoisted(() => ({ roles: true, defaults: true }));
|
|
10
|
+
vi.mock('@nocobase/app-plugin-authorization/client', async (original) => ({
|
|
11
|
+
...(await original<
|
|
12
|
+
typeof import('@nocobase/app-plugin-authorization/client')
|
|
13
|
+
>()),
|
|
14
|
+
useCan: ({ resource }: { resource: { id: string } }) => ({
|
|
15
|
+
can:
|
|
16
|
+
resource.id === 'authorization.permission-sets'
|
|
17
|
+
? access.roles
|
|
18
|
+
: access.defaults,
|
|
19
|
+
}),
|
|
20
|
+
}));
|
|
21
|
+
const namespace = '@nocobase/permission-tabs-test';
|
|
22
|
+
const namespaces = {
|
|
23
|
+
[namespace]: {
|
|
24
|
+
'en-US': async () => ({ default: enUS }),
|
|
25
|
+
'zh-CN': async () => ({ default: zhCN }),
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
access.roles = true;
|
|
30
|
+
access.defaults = true;
|
|
31
|
+
});
|
|
32
|
+
function Content({ tab }: { tab: 'roles' | 'default-access' }) {
|
|
33
|
+
const location = useLocation();
|
|
34
|
+
return (
|
|
35
|
+
<PermissionManagementPage tab={tab}>
|
|
36
|
+
<output aria-label='Route'>{location.pathname}</output>
|
|
37
|
+
</PermissionManagementPage>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
function tree(prefix: string) {
|
|
41
|
+
return (
|
|
42
|
+
<Routes>
|
|
43
|
+
<Route
|
|
44
|
+
path={`${prefix}/roles`}
|
|
45
|
+
element={<Content key='roles' tab='roles' />}
|
|
46
|
+
/>
|
|
47
|
+
<Route
|
|
48
|
+
path={`${prefix}/default-access`}
|
|
49
|
+
element={<Content key='default-access' tab='default-access' />}
|
|
50
|
+
/>
|
|
51
|
+
</Routes>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
it.each(['', '/settings'])(
|
|
55
|
+
'uses two URL-backed tabs under one heading, preserves the existing %s route prefix and keyboard focus',
|
|
56
|
+
async (prefix) => {
|
|
57
|
+
const user = userEvent.setup();
|
|
58
|
+
await renderWithApp(tree(prefix), {
|
|
59
|
+
route: `${prefix}/roles`,
|
|
60
|
+
namespace,
|
|
61
|
+
namespaces,
|
|
62
|
+
});
|
|
63
|
+
expect(
|
|
64
|
+
screen.getByRole('heading', { name: 'Permission management' }),
|
|
65
|
+
).toBeVisible();
|
|
66
|
+
expect(screen.getAllByRole('tab')).toHaveLength(2);
|
|
67
|
+
await user.click(screen.getByRole('tab', { name: 'Default data scopes' }));
|
|
68
|
+
await waitFor(() =>
|
|
69
|
+
expect(screen.getByRole('status', { name: 'Route' })).toHaveTextContent(
|
|
70
|
+
`${prefix}/default-access`,
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
expect(
|
|
74
|
+
screen.getByRole('tab', { name: 'Default data scopes' }),
|
|
75
|
+
).toHaveAttribute('aria-selected', 'true');
|
|
76
|
+
expect(
|
|
77
|
+
screen.getByRole('tabpanel', { name: 'Default data scopes' }),
|
|
78
|
+
).toBeVisible();
|
|
79
|
+
await waitFor(() =>
|
|
80
|
+
expect(
|
|
81
|
+
screen.getByRole('tab', { name: 'Default data scopes' }),
|
|
82
|
+
).toHaveFocus(),
|
|
83
|
+
);
|
|
84
|
+
await user.keyboard('{ArrowLeft}');
|
|
85
|
+
expect(
|
|
86
|
+
screen.getByRole('tab', { name: 'Roles and permissions' }),
|
|
87
|
+
).toHaveFocus();
|
|
88
|
+
await user.keyboard('{Enter}');
|
|
89
|
+
await waitFor(() =>
|
|
90
|
+
expect(
|
|
91
|
+
screen.getByRole('tab', { name: 'Roles and permissions' }),
|
|
92
|
+
).toHaveAttribute('aria-selected', 'true'),
|
|
93
|
+
);
|
|
94
|
+
await waitFor(() =>
|
|
95
|
+
expect(
|
|
96
|
+
screen.getByRole('tab', { name: 'Roles and permissions' }),
|
|
97
|
+
).toHaveFocus(),
|
|
98
|
+
);
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
it('hides an unreadable tab while keeping direct navigation on the readable default scope page', async () => {
|
|
102
|
+
access.roles = false;
|
|
103
|
+
await renderWithApp(tree(''), {
|
|
104
|
+
route: '/default-access',
|
|
105
|
+
namespace,
|
|
106
|
+
namespaces,
|
|
107
|
+
});
|
|
108
|
+
expect(
|
|
109
|
+
screen.queryByRole('tab', { name: 'Roles and permissions' }),
|
|
110
|
+
).not.toBeInTheDocument();
|
|
111
|
+
expect(
|
|
112
|
+
screen.getByRole('tab', { name: 'Default data scopes' }),
|
|
113
|
+
).toHaveAttribute('aria-selected', 'true');
|
|
114
|
+
expect(screen.getByRole('status', { name: 'Route' })).toHaveTextContent(
|
|
115
|
+
'/default-access',
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
it('uses the agreed Chinese menu and tab names', async () => {
|
|
119
|
+
await renderWithApp(tree(''), {
|
|
120
|
+
route: '/roles',
|
|
121
|
+
namespace,
|
|
122
|
+
namespaces,
|
|
123
|
+
locale: 'zh-CN',
|
|
124
|
+
});
|
|
125
|
+
expect(screen.getByRole('heading', { name: '权限管理' })).toBeVisible();
|
|
126
|
+
expect(screen.getByRole('tab', { name: '角色与权限' })).toBeVisible();
|
|
127
|
+
expect(screen.getByRole('tab', { name: '默认数据范围' })).toBeVisible();
|
|
128
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { MemoryRouter, Route, Routes } from 'react-router';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { RoleList } from '../../client/components/role-list.js';
|
|
6
|
+
|
|
7
|
+
const roles = [
|
|
8
|
+
{
|
|
9
|
+
key: 'member',
|
|
10
|
+
title: 'Member',
|
|
11
|
+
to: { pathname: '/roles/member/edit', search: '?q=team' },
|
|
12
|
+
protected: true,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
key: 'team',
|
|
16
|
+
title: 'A very long role name that remains available in full',
|
|
17
|
+
to: '/roles/team/edit',
|
|
18
|
+
protected: false,
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
describe('compact role list', () => {
|
|
22
|
+
it('renders ordinary list content with full names, identifiers and translated state labels', () => {
|
|
23
|
+
render(
|
|
24
|
+
<MemoryRouter>
|
|
25
|
+
<RoleList
|
|
26
|
+
roles={roles}
|
|
27
|
+
ariaLabel='角色与权限'
|
|
28
|
+
labels={{ protected: '受保护', custom: '自定义' }}
|
|
29
|
+
/>
|
|
30
|
+
</MemoryRouter>,
|
|
31
|
+
);
|
|
32
|
+
expect(screen.getByRole('list', { name: '角色与权限' })).toBeVisible();
|
|
33
|
+
expect(screen.getAllByRole('listitem')).toHaveLength(2);
|
|
34
|
+
expect(
|
|
35
|
+
screen.queryByRole('heading', { name: 'Member' }),
|
|
36
|
+
).not.toBeInTheDocument();
|
|
37
|
+
expect(screen.getByTitle(roles[1]!.title)).toHaveTextContent(
|
|
38
|
+
roles[1]!.title,
|
|
39
|
+
);
|
|
40
|
+
expect(screen.getByTitle('team')).toHaveTextContent('team');
|
|
41
|
+
expect(screen.getByText('受保护')).toBeVisible();
|
|
42
|
+
expect(screen.getByText('自定义')).toBeVisible();
|
|
43
|
+
});
|
|
44
|
+
it('keeps keyboard navigation and the supplied edit path and query', async () => {
|
|
45
|
+
const user = userEvent.setup();
|
|
46
|
+
render(
|
|
47
|
+
<MemoryRouter>
|
|
48
|
+
<Routes>
|
|
49
|
+
<Route
|
|
50
|
+
path='/'
|
|
51
|
+
element={
|
|
52
|
+
<RoleList
|
|
53
|
+
roles={roles}
|
|
54
|
+
labels={{ protected: 'Protected', custom: 'Custom' }}
|
|
55
|
+
/>
|
|
56
|
+
}
|
|
57
|
+
/>
|
|
58
|
+
<Route path='/roles/member/edit' element={<p>Edit member</p>} />
|
|
59
|
+
</Routes>
|
|
60
|
+
</MemoryRouter>,
|
|
61
|
+
);
|
|
62
|
+
const link = screen.getByRole('link', {
|
|
63
|
+
name: 'Member',
|
|
64
|
+
});
|
|
65
|
+
expect(link).toHaveAttribute('href', '/roles/member/edit?q=team');
|
|
66
|
+
await user.tab();
|
|
67
|
+
expect(link).toHaveFocus();
|
|
68
|
+
await user.keyboard('{Enter}');
|
|
69
|
+
expect(screen.getByText('Edit member')).toBeVisible();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { answerApi, renderWithApp } from '@nocobase/app-testing/client';
|
|
2
|
+
import { apiClientToken, type ClientApplication } from '@nocobase/app-client';
|
|
3
|
+
import {
|
|
4
|
+
AuthorizationClient,
|
|
5
|
+
authorizationClientToken,
|
|
6
|
+
} from '@nocobase/app-plugin-authorization/client';
|
|
7
|
+
import { screen, waitFor } from '@testing-library/react';
|
|
8
|
+
import userEvent from '@testing-library/user-event';
|
|
9
|
+
import { Routes, Route } from 'react-router';
|
|
10
|
+
import { expect, it } from 'vitest';
|
|
11
|
+
import RolesPage from '../../client/pages/settings/roles.js';
|
|
12
|
+
import enUS from '../../client/locales/en-US.js';
|
|
13
|
+
import zhCN from '../../client/locales/zh-CN.js';
|
|
14
|
+
const settingsNamespace = '@nocobase/compact-role-test';
|
|
15
|
+
const services = (app: ClientApplication) =>
|
|
16
|
+
app.container.instance(
|
|
17
|
+
authorizationClientToken,
|
|
18
|
+
new AuthorizationClient(app.container.resolve(apiClientToken)),
|
|
19
|
+
);
|
|
20
|
+
const namespaces = {
|
|
21
|
+
[settingsNamespace]: {
|
|
22
|
+
'en-US': async () => ({ default: enUS }),
|
|
23
|
+
'zh-CN': async () => ({ default: zhCN }),
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
it('keeps the compact role list sorted, searchable and linked to the current query', async () => {
|
|
28
|
+
const user = userEvent.setup();
|
|
29
|
+
await renderWithApp(
|
|
30
|
+
<Routes>
|
|
31
|
+
<Route path='/settings/roles' element={<RolesPage />} />
|
|
32
|
+
</Routes>,
|
|
33
|
+
{
|
|
34
|
+
route: '/settings/roles',
|
|
35
|
+
namespace: settingsNamespace,
|
|
36
|
+
namespaces,
|
|
37
|
+
services,
|
|
38
|
+
fetch: answerApi(({ path }) =>
|
|
39
|
+
path === 'authorization/permissions'
|
|
40
|
+
? { data: { unrestricted: false, permissions: [] } }
|
|
41
|
+
: {
|
|
42
|
+
data: [
|
|
43
|
+
{ key: 'team', title: 'NocoBase team', grants: [] },
|
|
44
|
+
{ key: 'cp-customer', title: 'Customer', grants: [] },
|
|
45
|
+
{
|
|
46
|
+
key: 'member',
|
|
47
|
+
title: 'Member',
|
|
48
|
+
grants: [],
|
|
49
|
+
protection: { allow: ['update'] },
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
),
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
await screen.findByTitle('Customer');
|
|
57
|
+
const list = screen.getByRole('list', { name: 'Roles and permissions' });
|
|
58
|
+
expect(
|
|
59
|
+
Array.from(list.querySelectorAll('li')).map((item) =>
|
|
60
|
+
item.querySelector('[title]')?.getAttribute('title'),
|
|
61
|
+
),
|
|
62
|
+
).toEqual(['Customer', 'Member', 'NocoBase team']);
|
|
63
|
+
expect(
|
|
64
|
+
screen.queryByRole('heading', { name: 'Customer' }),
|
|
65
|
+
).not.toBeInTheDocument();
|
|
66
|
+
expect(
|
|
67
|
+
screen.queryByRole('button', { name: 'New role' }),
|
|
68
|
+
).not.toBeInTheDocument();
|
|
69
|
+
await user.type(
|
|
70
|
+
screen.getByRole('textbox', { name: 'Search role names' }),
|
|
71
|
+
'Customer',
|
|
72
|
+
);
|
|
73
|
+
await waitFor(() => expect(list.querySelectorAll('li')).toHaveLength(1));
|
|
74
|
+
expect(screen.getByTitle('Customer').closest('a')).toHaveAttribute(
|
|
75
|
+
'href',
|
|
76
|
+
'/settings/roles/cp-customer/edit?q=Customer',
|
|
77
|
+
);
|
|
78
|
+
});
|
|
@@ -105,6 +105,18 @@ describe('user permission configuration', () => {
|
|
|
105
105
|
expect(checkbox).not.toBeChecked();
|
|
106
106
|
await user.click(checkbox);
|
|
107
107
|
}
|
|
108
|
+
expect(
|
|
109
|
+
screen.getByRole('heading', { name: words.userInformation }),
|
|
110
|
+
).toBeVisible();
|
|
111
|
+
expect(
|
|
112
|
+
screen.getByRole('heading', { name: words.accountSecurity }),
|
|
113
|
+
).toBeVisible();
|
|
114
|
+
expect(
|
|
115
|
+
screen.getByRole('heading', { name: words.roleAssignment }),
|
|
116
|
+
).toBeVisible();
|
|
117
|
+
expect(screen.getAllByRole('checkbox')).toHaveLength(10);
|
|
118
|
+
const rowIds = screen.getAllByRole('checkbox').map((node) => node.id);
|
|
119
|
+
expect(new Set(rowIds).size).toBe(10);
|
|
108
120
|
await user.click(
|
|
109
121
|
screen.getByRole('button', { name: words.save, exact: true }),
|
|
110
122
|
);
|
|
@@ -90,6 +90,14 @@ describe('app client routes', () => {
|
|
|
90
90
|
name: 'settingsRoleEdit',
|
|
91
91
|
authorizedAs: 'settings:authorization.permission-sets',
|
|
92
92
|
},
|
|
93
|
+
...[
|
|
94
|
+
'settingsDefaultAccess',
|
|
95
|
+
'settingsDefaultAccessCreate',
|
|
96
|
+
'settingsDefaultAccessEdit',
|
|
97
|
+
].map((name) => ({
|
|
98
|
+
name,
|
|
99
|
+
authorizedAs: 'settings:authorization.default-access',
|
|
100
|
+
})),
|
|
93
101
|
]);
|
|
94
102
|
for (const loader of componentLoadersIn(built.routes))
|
|
95
103
|
await expect(loader()).resolves.toMatchObject({
|