@nocobase/app-template-default 1.0.0-beta.76 → 1.0.0-beta.78
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/client/components/default-access-editor.tsx +79 -52
- package/client/components/long-text.tsx +200 -0
- package/client/components/permission-editor.tsx +534 -341
- package/client/locales/system/settings-en-US.ts +14 -3
- package/client/locales/system/settings-zh-CN.ts +13 -3
- package/client/pages/home/use-copy.ts +6 -3
- package/client/pages/settings/default-access-form.tsx +10 -1
- package/client/pages/settings/default-access-model.ts +6 -2
- package/client/pages/settings/page-permissions.ts +166 -1
- package/client/pages/settings/permission-filters.tsx +9 -2
- package/client/pages/settings/permission-workspace.ts +330 -0
- package/client/pages/settings/permissions-model.ts +62 -53
- package/client/pages/settings/role-form.tsx +30 -94
- package/package.json +16 -16
- package/server/config/app.ts +1 -1
- package/server/config/session.ts +7 -5
- package/tests/components/default-access-editor.test.tsx +130 -0
- package/tests/components/permission-editor.test.tsx +613 -572
- package/tests/components/permission-filter-binding.test.tsx +20 -0
- package/tests/components/settings.test.tsx +33 -8
- package/tests/components/user-drawer.test.tsx +14 -3
- package/tests/components/user-permissions.test.tsx +47 -13
- package/tests/logic/config.test.ts +3 -1
- package/tests/logic/page-permission-navigation.test.ts +394 -0
- package/tests/logic/permission-workspace.test.ts +570 -0
- package/tests/logic/session-isolation.test.ts +250 -0
|
@@ -18,6 +18,25 @@ const catalog = {
|
|
|
18
18
|
{
|
|
19
19
|
name: 'business',
|
|
20
20
|
title: 'Business',
|
|
21
|
+
subsections: [
|
|
22
|
+
{
|
|
23
|
+
name: 'other',
|
|
24
|
+
title: 'Other',
|
|
25
|
+
resources: [
|
|
26
|
+
{
|
|
27
|
+
type: 'composite',
|
|
28
|
+
id: 'expenses.records',
|
|
29
|
+
title: 'Expenses',
|
|
30
|
+
actions: [{ name: 'other', title: 'Other' }],
|
|
31
|
+
dataScopes: {},
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'configuration',
|
|
39
|
+
title: 'Configuration',
|
|
21
40
|
subsections: [
|
|
22
41
|
{
|
|
23
42
|
name: 'expenses',
|
|
@@ -108,6 +127,7 @@ it('collects native custom filter parameters before changing the draft, then sav
|
|
|
108
127
|
);
|
|
109
128
|
}
|
|
110
129
|
render(<Role />);
|
|
130
|
+
await user.click(screen.getByRole('button', { name: 'Configuration' }));
|
|
111
131
|
const scope = screen.getByRole('combobox', {
|
|
112
132
|
name: 'How far View · Records reaches',
|
|
113
133
|
});
|
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
AuthorizationClient,
|
|
5
5
|
authorizationClientToken,
|
|
6
6
|
} from '@nocobase/app-plugin-authorization/client';
|
|
7
|
+
import {
|
|
8
|
+
defineAppRoutes,
|
|
9
|
+
defineClientPlugin,
|
|
10
|
+
} from '@nocobase/app-client/plugins';
|
|
7
11
|
import { act, fireEvent, screen, waitFor } from '@testing-library/react';
|
|
8
12
|
import userEvent from '@testing-library/user-event';
|
|
9
13
|
import { Routes, Route, useNavigate } from 'react-router';
|
|
@@ -197,8 +201,8 @@ describe('template system settings', () => {
|
|
|
197
201
|
data: {
|
|
198
202
|
sections: [
|
|
199
203
|
{
|
|
200
|
-
name: '
|
|
201
|
-
title: '
|
|
204
|
+
name: 'administration',
|
|
205
|
+
title: 'Business operations',
|
|
202
206
|
order: 0,
|
|
203
207
|
subsections: [
|
|
204
208
|
{
|
|
@@ -207,9 +211,9 @@ describe('template system settings', () => {
|
|
|
207
211
|
resources: [
|
|
208
212
|
{
|
|
209
213
|
type: 'settings',
|
|
210
|
-
id: '
|
|
211
|
-
title: '
|
|
212
|
-
actions: [{ name: 'read', title: 'Read
|
|
214
|
+
id: 'authorization.permission-sets',
|
|
215
|
+
title: 'Roles',
|
|
216
|
+
actions: [{ name: 'read', title: 'Read roles' }],
|
|
213
217
|
},
|
|
214
218
|
],
|
|
215
219
|
},
|
|
@@ -261,14 +265,32 @@ describe('template system settings', () => {
|
|
|
261
265
|
</Routes>,
|
|
262
266
|
{
|
|
263
267
|
route: '/settings/roles/sales/edit',
|
|
268
|
+
plugins: [
|
|
269
|
+
defineClientPlugin({
|
|
270
|
+
packageName: settingsNamespace,
|
|
271
|
+
routes: defineAppRoutes([
|
|
272
|
+
{
|
|
273
|
+
name: 'roles',
|
|
274
|
+
path: '/role-config',
|
|
275
|
+
componentLoader: async () => ({ default: () => null }),
|
|
276
|
+
authz: {
|
|
277
|
+
resource: {
|
|
278
|
+
type: 'settings',
|
|
279
|
+
id: 'authorization.permission-sets',
|
|
280
|
+
},
|
|
281
|
+
action: 'read',
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
]),
|
|
285
|
+
})(),
|
|
286
|
+
],
|
|
264
287
|
namespace: settingsNamespace,
|
|
265
288
|
namespaces,
|
|
266
289
|
services,
|
|
267
290
|
fetch: answerApi(calls),
|
|
268
291
|
},
|
|
269
292
|
);
|
|
270
|
-
|
|
271
|
-
const toggle = await screen.findByRole('checkbox', { name: 'Read files' });
|
|
293
|
+
const toggle = await screen.findByRole('checkbox', { name: 'Read roles' });
|
|
272
294
|
await waitFor(() =>
|
|
273
295
|
expect(toggle).not.toHaveAttribute('aria-disabled', 'true'),
|
|
274
296
|
);
|
|
@@ -283,7 +305,10 @@ describe('template system settings', () => {
|
|
|
283
305
|
expectedRevision: 'version-1',
|
|
284
306
|
grants: [
|
|
285
307
|
{
|
|
286
|
-
resource: {
|
|
308
|
+
resource: {
|
|
309
|
+
type: 'settings',
|
|
310
|
+
id: 'authorization.permission-sets',
|
|
311
|
+
},
|
|
287
312
|
actions: [{ action: 'read' }],
|
|
288
313
|
},
|
|
289
314
|
],
|
|
@@ -254,9 +254,20 @@ it.each(['/users', '/settings/users'])(
|
|
|
254
254
|
},
|
|
255
255
|
}),
|
|
256
256
|
);
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
257
|
+
// Refreshing remounts cells; permission loading can replace the name span with a link.
|
|
258
|
+
await waitFor(() => {
|
|
259
|
+
const table = screen.getByRole('table');
|
|
260
|
+
expect(table).toHaveAttribute('aria-busy', 'false');
|
|
261
|
+
const row = within(table).getByRole('row', {
|
|
262
|
+
name: /alice@example\.test/u,
|
|
263
|
+
});
|
|
264
|
+
const updated = within(row).getByRole('link', {
|
|
265
|
+
name: 'Alice updated',
|
|
266
|
+
exact: true,
|
|
267
|
+
});
|
|
268
|
+
expect(updated).toBeVisible();
|
|
269
|
+
expect(updated).toHaveAttribute('href', `${base}/alice/edit${search}`);
|
|
270
|
+
});
|
|
260
271
|
expect(screen.getByTestId('location')).toHaveTextContent(
|
|
261
272
|
`${base}${search}`,
|
|
262
273
|
);
|
|
@@ -5,6 +5,10 @@ import {
|
|
|
5
5
|
authorizationClientToken,
|
|
6
6
|
type PermissionGrant,
|
|
7
7
|
} from '@nocobase/app-plugin-authorization/client';
|
|
8
|
+
import {
|
|
9
|
+
defineAppRoutes,
|
|
10
|
+
defineClientPlugin,
|
|
11
|
+
} from '@nocobase/app-client/plugins';
|
|
8
12
|
import { screen, waitFor } from '@testing-library/react';
|
|
9
13
|
import userEvent from '@testing-library/user-event';
|
|
10
14
|
import { Routes, Route } from 'react-router';
|
|
@@ -35,7 +39,11 @@ describe('user permission configuration', () => {
|
|
|
35
39
|
grants: [
|
|
36
40
|
{
|
|
37
41
|
resource: { type: 'user', id: '*' },
|
|
38
|
-
actions: [
|
|
42
|
+
actions: [
|
|
43
|
+
{ action: 'read' },
|
|
44
|
+
{ action: 'disable' },
|
|
45
|
+
{ action: 'invite' },
|
|
46
|
+
],
|
|
39
47
|
},
|
|
40
48
|
] as readonly PermissionGrant[],
|
|
41
49
|
};
|
|
@@ -83,6 +91,22 @@ describe('user permission configuration', () => {
|
|
|
83
91
|
namespace,
|
|
84
92
|
namespaces,
|
|
85
93
|
locale,
|
|
94
|
+
plugins: [
|
|
95
|
+
defineClientPlugin({
|
|
96
|
+
packageName: namespace,
|
|
97
|
+
routes: defineAppRoutes([
|
|
98
|
+
{
|
|
99
|
+
name: 'users',
|
|
100
|
+
path: '/users',
|
|
101
|
+
authz: {
|
|
102
|
+
resource: { type: 'user', id: '*' },
|
|
103
|
+
action: 'read',
|
|
104
|
+
},
|
|
105
|
+
componentLoader: async () => ({ default: () => null }),
|
|
106
|
+
},
|
|
107
|
+
]),
|
|
108
|
+
})(),
|
|
109
|
+
],
|
|
86
110
|
services: (app: ClientApplication) =>
|
|
87
111
|
app.container.instance(
|
|
88
112
|
authorizationClientToken,
|
|
@@ -93,17 +117,17 @@ describe('user permission configuration', () => {
|
|
|
93
117
|
);
|
|
94
118
|
const first = await mount();
|
|
95
119
|
const labels = [
|
|
96
|
-
words.
|
|
97
|
-
words.
|
|
98
|
-
words.
|
|
120
|
+
words.createUsers,
|
|
121
|
+
words.updateUsers,
|
|
122
|
+
words.revokeSessions,
|
|
99
123
|
];
|
|
100
124
|
for (const label of labels) {
|
|
101
|
-
const
|
|
125
|
+
const toggle = await screen.findByRole('checkbox', { name: label });
|
|
102
126
|
await waitFor(() =>
|
|
103
|
-
expect(
|
|
127
|
+
expect(toggle).not.toHaveAttribute('aria-disabled', 'true'),
|
|
104
128
|
);
|
|
105
|
-
expect(
|
|
106
|
-
await user.click(
|
|
129
|
+
expect(toggle).not.toBeChecked();
|
|
130
|
+
await user.click(toggle);
|
|
107
131
|
}
|
|
108
132
|
expect(
|
|
109
133
|
screen.getByRole('heading', { name: words.userInformation }),
|
|
@@ -114,9 +138,12 @@ describe('user permission configuration', () => {
|
|
|
114
138
|
expect(
|
|
115
139
|
screen.getByRole('heading', { name: words.roleAssignment }),
|
|
116
140
|
).toBeVisible();
|
|
117
|
-
expect(screen.getAllByRole('checkbox')).toHaveLength(
|
|
141
|
+
expect(screen.getAllByRole('checkbox')).toHaveLength(8);
|
|
142
|
+
expect(
|
|
143
|
+
screen.queryByRole('checkbox', { name: words.inviteUsers }),
|
|
144
|
+
).not.toBeInTheDocument();
|
|
118
145
|
const rowIds = screen.getAllByRole('checkbox').map((node) => node.id);
|
|
119
|
-
expect(new Set(rowIds).size).toBe(
|
|
146
|
+
expect(new Set(rowIds).size).toBe(8);
|
|
120
147
|
await user.click(
|
|
121
148
|
screen.getByRole('button', { name: words.save, exact: true }),
|
|
122
149
|
);
|
|
@@ -128,7 +155,14 @@ describe('user permission configuration', () => {
|
|
|
128
155
|
}),
|
|
129
156
|
),
|
|
130
157
|
);
|
|
131
|
-
const all = [
|
|
158
|
+
const all = [
|
|
159
|
+
'read',
|
|
160
|
+
'disable',
|
|
161
|
+
'invite',
|
|
162
|
+
'create',
|
|
163
|
+
'update',
|
|
164
|
+
'revoke-sessions',
|
|
165
|
+
];
|
|
132
166
|
expect(role.grants[0]?.actions.map((action) => action.action)).toEqual(
|
|
133
167
|
all,
|
|
134
168
|
);
|
|
@@ -139,14 +173,14 @@ describe('user permission configuration', () => {
|
|
|
139
173
|
await screen.findByRole('checkbox', { name: label }),
|
|
140
174
|
).toBeChecked();
|
|
141
175
|
await user.click(
|
|
142
|
-
screen.getByRole('checkbox', { name: words.
|
|
176
|
+
screen.getByRole('checkbox', { name: words.updateUsers }),
|
|
143
177
|
);
|
|
144
178
|
await user.click(
|
|
145
179
|
screen.getByRole('button', { name: words.save, exact: true }),
|
|
146
180
|
);
|
|
147
181
|
await waitFor(() =>
|
|
148
182
|
expect(role.grants[0]?.actions.map((action) => action.action)).toEqual(
|
|
149
|
-
all.filter((action) => action !== '
|
|
183
|
+
all.filter((action) => action !== 'update'),
|
|
150
184
|
),
|
|
151
185
|
);
|
|
152
186
|
},
|
|
@@ -167,7 +167,9 @@ describe('application config', () => {
|
|
|
167
167
|
env: { APP_BASE_PATH: '/crm/' },
|
|
168
168
|
});
|
|
169
169
|
expect(mounted.config.get('session.cookie.path')).toBe('/crm');
|
|
170
|
-
expect(mounted.config.get('session.cookie.name')).toBe(
|
|
170
|
+
expect(mounted.config.get('session.cookie.name')).toBe(
|
|
171
|
+
'nocobase_session_%2Fcrm',
|
|
172
|
+
);
|
|
171
173
|
});
|
|
172
174
|
|
|
173
175
|
it('loads only explicit env overrides and restores defaults on reload', async () => {
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
defineAppRoutes,
|
|
4
|
+
resolveAppClientContributions,
|
|
5
|
+
} from '@nocobase/app-client/plugins';
|
|
6
|
+
import type { LocalizedText } from '@nocobase/app-plugin-authorization/client';
|
|
7
|
+
import type {
|
|
8
|
+
PermissionEditorNavigationNode,
|
|
9
|
+
PermissionEditorSection,
|
|
10
|
+
} from '../../client/components/permission-editor.js';
|
|
11
|
+
import {
|
|
12
|
+
pagePermissionNavigation,
|
|
13
|
+
pagePermissionResources,
|
|
14
|
+
permissionNavigationSections,
|
|
15
|
+
} from '../../client/pages/settings/page-permissions.js';
|
|
16
|
+
import enUS from '../../client/locales/en-US.js';
|
|
17
|
+
import zhCN from '../../client/locales/zh-CN.js';
|
|
18
|
+
|
|
19
|
+
const componentLoader = async () => ({ default: () => null });
|
|
20
|
+
const text = (value: LocalizedText): string =>
|
|
21
|
+
typeof value === 'string' ? value : value.key;
|
|
22
|
+
const resource = (id: string) => ({
|
|
23
|
+
resource: { type: 'page' as const, id },
|
|
24
|
+
action: 'access',
|
|
25
|
+
});
|
|
26
|
+
const flatten = (
|
|
27
|
+
nodes: readonly PermissionEditorNavigationNode[],
|
|
28
|
+
): PermissionEditorNavigationNode[] =>
|
|
29
|
+
nodes.flatMap((node) => [node, ...flatten(node.children ?? [])]);
|
|
30
|
+
const fixture = () =>
|
|
31
|
+
resolveAppClientContributions([
|
|
32
|
+
{
|
|
33
|
+
packageName: 'private-app',
|
|
34
|
+
source: 'application',
|
|
35
|
+
routes: defineAppRoutes([
|
|
36
|
+
{
|
|
37
|
+
name: 'basic',
|
|
38
|
+
navigation: { title: 'Basic data' },
|
|
39
|
+
children: [
|
|
40
|
+
{
|
|
41
|
+
name: 'materials',
|
|
42
|
+
path: '/materials',
|
|
43
|
+
navigation: { title: 'Materials' },
|
|
44
|
+
authz: resource('legacy-materials'),
|
|
45
|
+
componentLoader,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'locations',
|
|
49
|
+
navigation: { title: 'Locations' },
|
|
50
|
+
children: [
|
|
51
|
+
{
|
|
52
|
+
name: 'warehouses',
|
|
53
|
+
path: '/warehouses',
|
|
54
|
+
navigation: { title: 'Warehouses' },
|
|
55
|
+
authz: resource('legacy-warehouses'),
|
|
56
|
+
componentLoader,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'loans',
|
|
64
|
+
path: '/loans',
|
|
65
|
+
navigation: { title: 'Loans' },
|
|
66
|
+
authz: resource('legacy-loans'),
|
|
67
|
+
componentLoader,
|
|
68
|
+
children: [
|
|
69
|
+
{ name: 'loanDetail', path: ':id', componentLoader },
|
|
70
|
+
{
|
|
71
|
+
name: 'reviews',
|
|
72
|
+
path: 'reviews',
|
|
73
|
+
navigation: { title: 'Reviews' },
|
|
74
|
+
authz: resource('legacy-reviews'),
|
|
75
|
+
componentLoader,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'audit',
|
|
79
|
+
path: 'audit',
|
|
80
|
+
authz: resource('legacy-audit'),
|
|
81
|
+
componentLoader,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'alias',
|
|
87
|
+
path: '/loan-alias',
|
|
88
|
+
navigation: { title: 'Loan alias' },
|
|
89
|
+
authz: resource('legacy-loans'),
|
|
90
|
+
componentLoader,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'settings',
|
|
94
|
+
path: '/settings',
|
|
95
|
+
navigation: { title: 'Settings' },
|
|
96
|
+
authz: {
|
|
97
|
+
resource: { type: 'settings', id: 'rules' },
|
|
98
|
+
action: 'read',
|
|
99
|
+
},
|
|
100
|
+
componentLoader,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'landing',
|
|
104
|
+
path: '/',
|
|
105
|
+
navigation: { title: 'Landing' },
|
|
106
|
+
authz: 'skip',
|
|
107
|
+
componentLoader,
|
|
108
|
+
},
|
|
109
|
+
]),
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
describe('page permission navigation projection', () => {
|
|
114
|
+
it('supplies named disclosure and non-menu labels in both supported languages', () => {
|
|
115
|
+
for (const locale of [enUS, zhCN]) {
|
|
116
|
+
const settings = Reflect.get(locale, 'settings') as Record<
|
|
117
|
+
string,
|
|
118
|
+
unknown
|
|
119
|
+
>;
|
|
120
|
+
for (const key of [
|
|
121
|
+
'expandPermissionGroup',
|
|
122
|
+
'collapsePermissionGroup',
|
|
123
|
+
'pagesOutsideNavigation',
|
|
124
|
+
]) {
|
|
125
|
+
const value = settings[key];
|
|
126
|
+
expect(typeof value).toBe('string');
|
|
127
|
+
expect(String(value).trim()).not.toBe('');
|
|
128
|
+
expect(value).not.toBe(key);
|
|
129
|
+
if (key !== 'pagesOutsideNavigation')
|
|
130
|
+
expect(value).toContain('{{name}}');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
it('preserves real groups, deeper menus and independent parent/child identities', () => {
|
|
135
|
+
const { routes } = fixture();
|
|
136
|
+
const result = pagePermissionNavigation(
|
|
137
|
+
routes,
|
|
138
|
+
{ audit: 'Audit details' },
|
|
139
|
+
(path) => `Unnamed (${path})`,
|
|
140
|
+
text,
|
|
141
|
+
);
|
|
142
|
+
expect(result.map((node) => node.label)).toEqual([
|
|
143
|
+
'Basic data',
|
|
144
|
+
'Loans',
|
|
145
|
+
'settings.pagesOutsideNavigation',
|
|
146
|
+
]);
|
|
147
|
+
expect(result[0]?.rowId).toBeUndefined();
|
|
148
|
+
expect(result[0]?.children?.map((node) => node.label)).toEqual([
|
|
149
|
+
'Materials',
|
|
150
|
+
'Locations',
|
|
151
|
+
]);
|
|
152
|
+
expect(result[0]?.children?.[1]?.children?.[0]?.label).toBe('Warehouses');
|
|
153
|
+
expect(result[1]?.rowId).toBe('["page","legacy-loans","access"]');
|
|
154
|
+
expect(result[1]?.children?.[0]?.rowId).toBe(
|
|
155
|
+
'["page","legacy-reviews","access"]',
|
|
156
|
+
);
|
|
157
|
+
expect(
|
|
158
|
+
flatten(result)
|
|
159
|
+
.filter((node) => node.rowId)
|
|
160
|
+
.map((node) => node.rowId),
|
|
161
|
+
).toEqual([
|
|
162
|
+
'["page","legacy-materials","access"]',
|
|
163
|
+
'["page","legacy-warehouses","access"]',
|
|
164
|
+
'["page","legacy-loans","access"]',
|
|
165
|
+
'["page","legacy-reviews","access"]',
|
|
166
|
+
'["page","legacy-audit","access"]',
|
|
167
|
+
]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('keeps actual non-menu independent pages and deduplicates inherited and aliased grants', () => {
|
|
171
|
+
const { routes } = fixture();
|
|
172
|
+
const before = JSON.stringify(routes);
|
|
173
|
+
const pages = pagePermissionResources(
|
|
174
|
+
routes,
|
|
175
|
+
{ audit: 'Audit details' },
|
|
176
|
+
(path) => `Unnamed (${path})`,
|
|
177
|
+
);
|
|
178
|
+
const result = pagePermissionNavigation(
|
|
179
|
+
routes,
|
|
180
|
+
{ audit: 'Audit details' },
|
|
181
|
+
(path) => `Unnamed (${path})`,
|
|
182
|
+
text,
|
|
183
|
+
);
|
|
184
|
+
expect(result.at(-1)?.children).toEqual([
|
|
185
|
+
{
|
|
186
|
+
id: 'page:legacy-audit',
|
|
187
|
+
label: 'Audit details',
|
|
188
|
+
rowId: '["page","legacy-audit","access"]',
|
|
189
|
+
},
|
|
190
|
+
]);
|
|
191
|
+
expect(flatten(result).filter((node) => node.rowId)).toHaveLength(
|
|
192
|
+
pages.length,
|
|
193
|
+
);
|
|
194
|
+
expect(flatten(result).map((node) => node.label)).not.toContain(
|
|
195
|
+
'Loan alias',
|
|
196
|
+
);
|
|
197
|
+
expect(flatten(result).map((node) => node.label)).not.toContain('Landing');
|
|
198
|
+
expect(flatten(result).map((node) => node.label)).not.toContain('Settings');
|
|
199
|
+
expect(JSON.stringify(routes)).toBe(before);
|
|
200
|
+
expect(
|
|
201
|
+
pagePermissionNavigation(
|
|
202
|
+
routes,
|
|
203
|
+
{ audit: 'Audit details' },
|
|
204
|
+
(path) => `Unnamed (${path})`,
|
|
205
|
+
text,
|
|
206
|
+
),
|
|
207
|
+
).toEqual(result);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('renders single-level menus at a single level without inventing groups', () => {
|
|
211
|
+
const { routes } = resolveAppClientContributions([
|
|
212
|
+
{
|
|
213
|
+
packageName: 'flat-app',
|
|
214
|
+
source: 'application',
|
|
215
|
+
routes: defineAppRoutes([
|
|
216
|
+
{
|
|
217
|
+
name: 'one',
|
|
218
|
+
path: '/one',
|
|
219
|
+
navigation: { title: 'One' },
|
|
220
|
+
authz: resource('one'),
|
|
221
|
+
componentLoader,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'two',
|
|
225
|
+
path: '/two',
|
|
226
|
+
navigation: { title: 'Two' },
|
|
227
|
+
authz: resource('two'),
|
|
228
|
+
componentLoader,
|
|
229
|
+
},
|
|
230
|
+
]),
|
|
231
|
+
},
|
|
232
|
+
]);
|
|
233
|
+
const result = pagePermissionNavigation(routes, {}, () => 'Unnamed', text);
|
|
234
|
+
expect(result.map((node) => node.label)).toEqual(['One', 'Two']);
|
|
235
|
+
expect(result.every((node) => !node.children?.length)).toBe(true);
|
|
236
|
+
expect(result.every((node) => node.rowId)).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('omits menus governed by settings or account permissions instead of mirroring them as page controls or links', () => {
|
|
241
|
+
const { routes } = resolveAppClientContributions([
|
|
242
|
+
{
|
|
243
|
+
packageName: 'app',
|
|
244
|
+
source: 'application',
|
|
245
|
+
routes: defineAppRoutes([
|
|
246
|
+
{
|
|
247
|
+
name: 'settings',
|
|
248
|
+
path: '/settings',
|
|
249
|
+
navigation: { title: 'System settings' },
|
|
250
|
+
children: [
|
|
251
|
+
{
|
|
252
|
+
name: 'users',
|
|
253
|
+
path: 'users',
|
|
254
|
+
navigation: { title: 'User management' },
|
|
255
|
+
authz: { resource: { type: 'user', id: '*' }, action: 'read' },
|
|
256
|
+
componentLoader,
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: 'roles',
|
|
260
|
+
path: 'roles',
|
|
261
|
+
navigation: { title: 'Permission management' },
|
|
262
|
+
authz: {
|
|
263
|
+
resource: {
|
|
264
|
+
type: 'settings',
|
|
265
|
+
id: 'authorization.permission-sets',
|
|
266
|
+
},
|
|
267
|
+
action: 'read',
|
|
268
|
+
},
|
|
269
|
+
componentLoader,
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: 'logs',
|
|
273
|
+
path: 'logs',
|
|
274
|
+
navigation: { title: 'Notification logs' },
|
|
275
|
+
authz: resource('logs'),
|
|
276
|
+
componentLoader,
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
]),
|
|
281
|
+
},
|
|
282
|
+
]);
|
|
283
|
+
const result = pagePermissionNavigation(routes, {}, (path) => path, text);
|
|
284
|
+
expect(result).toEqual([]);
|
|
285
|
+
const withoutPage = routes.map((route) => ({
|
|
286
|
+
...route,
|
|
287
|
+
children: route.children?.filter(({ name }) => name !== 'logs'),
|
|
288
|
+
}));
|
|
289
|
+
expect(
|
|
290
|
+
pagePermissionNavigation(withoutPage, {}, (path) => path, text),
|
|
291
|
+
).toEqual([]);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it.each(['settings', 'systemManagement'])(
|
|
295
|
+
'moves all %s controls into configuration without changing or duplicating grants',
|
|
296
|
+
(rootName) => {
|
|
297
|
+
const { routes } = resolveAppClientContributions([
|
|
298
|
+
{
|
|
299
|
+
packageName: 'app',
|
|
300
|
+
source: 'application',
|
|
301
|
+
routes: defineAppRoutes([
|
|
302
|
+
{
|
|
303
|
+
name: 'orders',
|
|
304
|
+
path: '/orders',
|
|
305
|
+
navigation: { title: 'Orders' },
|
|
306
|
+
authz: resource('orders'),
|
|
307
|
+
componentLoader,
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: rootName,
|
|
311
|
+
path: '/settings',
|
|
312
|
+
navigation: { title: 'Settings' },
|
|
313
|
+
children: [
|
|
314
|
+
{
|
|
315
|
+
name: 'users',
|
|
316
|
+
path: 'users',
|
|
317
|
+
navigation: { title: 'Users' },
|
|
318
|
+
authz: { resource: { type: 'user', id: '*' }, action: 'read' },
|
|
319
|
+
componentLoader,
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: 'logs',
|
|
323
|
+
path: 'logs',
|
|
324
|
+
navigation: { title: 'Logs' },
|
|
325
|
+
authz: resource('logs'),
|
|
326
|
+
componentLoader,
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
},
|
|
330
|
+
]),
|
|
331
|
+
},
|
|
332
|
+
]);
|
|
333
|
+
const user = {
|
|
334
|
+
id: '["user","*","read"]',
|
|
335
|
+
label: 'View users',
|
|
336
|
+
enabled: true,
|
|
337
|
+
};
|
|
338
|
+
const accountAction = {
|
|
339
|
+
id: '["user","*","disable"]',
|
|
340
|
+
label: 'Disable account',
|
|
341
|
+
enabled: false,
|
|
342
|
+
};
|
|
343
|
+
const order = {
|
|
344
|
+
id: '["page","orders","access"]',
|
|
345
|
+
label: 'Orders',
|
|
346
|
+
enabled: true,
|
|
347
|
+
};
|
|
348
|
+
const logs = {
|
|
349
|
+
id: '["page","logs","access"]',
|
|
350
|
+
label: 'Logs',
|
|
351
|
+
enabled: false,
|
|
352
|
+
};
|
|
353
|
+
const sections: PermissionEditorSection[] = [
|
|
354
|
+
{
|
|
355
|
+
id: 'users',
|
|
356
|
+
title: 'Users and accounts',
|
|
357
|
+
groups: [{ id: 'users', rows: [user, accountAction] }],
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
id: 'pages',
|
|
361
|
+
title: 'Page access',
|
|
362
|
+
groups: [
|
|
363
|
+
{ id: 'orders', accessOnly: true, rows: [order] },
|
|
364
|
+
{ id: 'logs', accessOnly: true, rows: [logs] },
|
|
365
|
+
],
|
|
366
|
+
},
|
|
367
|
+
];
|
|
368
|
+
const before = JSON.stringify(sections);
|
|
369
|
+
const result = permissionNavigationSections(
|
|
370
|
+
sections,
|
|
371
|
+
routes,
|
|
372
|
+
{},
|
|
373
|
+
(path) => path,
|
|
374
|
+
text,
|
|
375
|
+
);
|
|
376
|
+
expect(result.map(({ id }) => id)).toEqual(['pages', 'configuration']);
|
|
377
|
+
expect(result[0]?.groups.flatMap(({ rows }) => rows)).toEqual([order]);
|
|
378
|
+
expect(
|
|
379
|
+
flatten(result[0]?.navigation ?? []).flatMap(({ rowId }) =>
|
|
380
|
+
rowId ? [rowId] : [],
|
|
381
|
+
),
|
|
382
|
+
).toEqual([order.id]);
|
|
383
|
+
expect(result[1]?.groups.flatMap(({ rows }) => rows)).toEqual([
|
|
384
|
+
user,
|
|
385
|
+
accountAction,
|
|
386
|
+
logs,
|
|
387
|
+
]);
|
|
388
|
+
expect(result[1]?.groups[0]?.rows[0]).toBe(user);
|
|
389
|
+
expect(JSON.stringify(sections)).toBe(before);
|
|
390
|
+
expect(
|
|
391
|
+
permissionNavigationSections(result, routes, {}, (path) => path, text),
|
|
392
|
+
).toEqual(result);
|
|
393
|
+
},
|
|
394
|
+
);
|