@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.
Files changed (33) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.MD +2 -0
  3. package/client/components/default-access-editor.tsx +183 -0
  4. package/client/components/permission-editor.tsx +216 -150
  5. package/client/components/role-list.tsx +61 -0
  6. package/client/lib/navigation-transform.ts +28 -1
  7. package/client/locales/system/settings-en-US.ts +73 -2
  8. package/client/locales/system/settings-zh-CN.ts +65 -2
  9. package/client/pages/settings/default-access-form.tsx +499 -0
  10. package/client/pages/settings/default-access-model.ts +154 -0
  11. package/client/pages/settings/default-access.tsx +407 -0
  12. package/client/pages/settings/permission-filters.tsx +43 -37
  13. package/client/pages/settings/permission-management-page.tsx +105 -0
  14. package/client/pages/settings/permissions-model.ts +37 -1
  15. package/client/pages/settings/role-form.tsx +46 -18
  16. package/client/pages/settings/roles.tsx +23 -43
  17. package/client/pages/settings/routes.ts +78 -19
  18. package/client/pages/settings/shared.tsx +3 -0
  19. package/client/pages/settings/users.tsx +1 -1
  20. package/client/routing/route-navigation.ts +21 -1
  21. package/package.json +19 -19
  22. package/tests/components/default-access.test.tsx +554 -0
  23. package/tests/components/page-harness.test.tsx +132 -112
  24. package/tests/components/permission-editor.test.tsx +233 -73
  25. package/tests/components/permission-management-tabs.test.tsx +128 -0
  26. package/tests/components/role-list.test.tsx +71 -0
  27. package/tests/components/roles.test.tsx +78 -0
  28. package/tests/components/user-permissions.test.tsx +12 -0
  29. package/tests/logic/client-routes.test.ts +8 -0
  30. package/tests/logic/default-access-model.test.ts +167 -0
  31. package/tests/logic/permission-management-routes.test.ts +64 -0
  32. package/tests/logic/settings-permissions.test.ts +89 -0
  33. 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('1/2');
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
- const view = render(<Draft />);
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('keeps multiple inline scopes independently named and editable', async () => {
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('keeps the category heading consistent for a single resource and labels bulk selection explicitly', async () => {
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
- ).toBeInTheDocument();
413
- expect(screen.getAllByText('1/2')).toHaveLength(1);
362
+ ).toBeVisible();
363
+ expect(batch).not.toHaveBeenCalled();
364
+ expect(screen.queryByText('1/2')).not.toBeInTheDocument();
414
365
  expect(
415
- screen.getByRole('checkbox', {
416
- name: 'Select visible permissions in Users',
417
- }),
418
- ).toHaveAttribute('aria-checked', 'mixed');
419
- await userEvent.click(screen.getByText('All', { selector: 'span' }));
420
- expect(batch).toHaveBeenCalledExactlyOnceWith([
421
- { id: 'edit', enabled: true },
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({