@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
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { AppClientRegisteredRoute } from '@nocobase/app-client/plugins';
|
|
3
|
+
import type { AuthorizationOptionsResponse } from '@nocobase/app-plugin-authorization/client';
|
|
4
|
+
import {
|
|
5
|
+
permissionWorkspaceCatalog,
|
|
6
|
+
permissionScopeVisible,
|
|
7
|
+
permissionFilterFieldLabel,
|
|
8
|
+
} from '../../client/pages/settings/permission-workspace.js';
|
|
9
|
+
import {
|
|
10
|
+
permissionSections,
|
|
11
|
+
setPermissions,
|
|
12
|
+
} from '../../client/pages/settings/permissions-model.js';
|
|
13
|
+
|
|
14
|
+
const source = {
|
|
15
|
+
sections: [
|
|
16
|
+
{
|
|
17
|
+
name: 'pages',
|
|
18
|
+
title: 'Pages',
|
|
19
|
+
order: 0,
|
|
20
|
+
subsections: [
|
|
21
|
+
{
|
|
22
|
+
name: 'page',
|
|
23
|
+
title: 'Pages',
|
|
24
|
+
recordType: {
|
|
25
|
+
type: 'page',
|
|
26
|
+
actions: [{ name: 'access', title: 'Access' }],
|
|
27
|
+
},
|
|
28
|
+
resources: [],
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'business',
|
|
34
|
+
title: 'Business operations',
|
|
35
|
+
order: 100,
|
|
36
|
+
subsections: [
|
|
37
|
+
{
|
|
38
|
+
name: 'borrowing',
|
|
39
|
+
title: 'Borrowing',
|
|
40
|
+
resources: [
|
|
41
|
+
{
|
|
42
|
+
type: 'composite',
|
|
43
|
+
id: 'borrowing',
|
|
44
|
+
title: 'Borrowing',
|
|
45
|
+
actions: [
|
|
46
|
+
{
|
|
47
|
+
name: 'approve',
|
|
48
|
+
title: 'Approve request',
|
|
49
|
+
description: 'Approve an eligible request.',
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'administration',
|
|
59
|
+
title: 'System and tools',
|
|
60
|
+
order: 200,
|
|
61
|
+
subsections: [
|
|
62
|
+
{
|
|
63
|
+
name: 'rules',
|
|
64
|
+
title: 'Rules',
|
|
65
|
+
resources: [
|
|
66
|
+
{
|
|
67
|
+
type: 'settings',
|
|
68
|
+
id: 'authorization.sharing-rules',
|
|
69
|
+
title: 'Sharing rules',
|
|
70
|
+
actions: [{ name: 'update', title: 'Edit' }],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'unused-plugin',
|
|
78
|
+
title: 'Unused optional plugin',
|
|
79
|
+
order: 300,
|
|
80
|
+
subsections: [
|
|
81
|
+
{
|
|
82
|
+
name: 'tools',
|
|
83
|
+
title: 'Tools',
|
|
84
|
+
resources: [
|
|
85
|
+
{
|
|
86
|
+
type: 'settings',
|
|
87
|
+
id: 'unused',
|
|
88
|
+
title: 'Unused tool',
|
|
89
|
+
actions: [{ name: 'read', title: 'Read' }],
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
recordAccess: [],
|
|
97
|
+
subjectTypes: [],
|
|
98
|
+
collections: [],
|
|
99
|
+
} as AuthorizationOptionsResponse;
|
|
100
|
+
|
|
101
|
+
const businessSelection = {
|
|
102
|
+
business: [
|
|
103
|
+
{ type: 'composite', id: 'borrowing', actions: ['approve', 'unsupported'] },
|
|
104
|
+
],
|
|
105
|
+
settings: [],
|
|
106
|
+
};
|
|
107
|
+
const configurationRoutes = [
|
|
108
|
+
{
|
|
109
|
+
name: 'rules',
|
|
110
|
+
path: '/rules',
|
|
111
|
+
componentLoader: async () => ({}),
|
|
112
|
+
authz: {
|
|
113
|
+
resource: { type: 'settings', id: 'authorization.sharing-rules' },
|
|
114
|
+
action: 'read',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
] as unknown as AppClientRegisteredRoute[];
|
|
118
|
+
|
|
119
|
+
describe('application-owned permission presentation', () => {
|
|
120
|
+
it('can hide a created-record selector while retaining a related-record selector and stored policy', () => {
|
|
121
|
+
const resource = {
|
|
122
|
+
type: 'composite',
|
|
123
|
+
id: 'requests',
|
|
124
|
+
title: 'Requests',
|
|
125
|
+
actions: [{ name: 'create', title: 'Submit' }],
|
|
126
|
+
dataScopes: {
|
|
127
|
+
create: [
|
|
128
|
+
{
|
|
129
|
+
key: 'requests',
|
|
130
|
+
collection: 'requests',
|
|
131
|
+
title: 'Requests',
|
|
132
|
+
fields: ['id'],
|
|
133
|
+
recordAccess: ['allRecords'],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
key: 'equipment',
|
|
137
|
+
collection: 'equipment',
|
|
138
|
+
title: 'Equipment',
|
|
139
|
+
fields: ['assetCode'],
|
|
140
|
+
recordAccess: ['allRecords'],
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
const selection = {
|
|
146
|
+
settings: [],
|
|
147
|
+
business: [
|
|
148
|
+
{
|
|
149
|
+
type: 'composite',
|
|
150
|
+
id: 'requests',
|
|
151
|
+
actions: ['create'],
|
|
152
|
+
hiddenActionScopes: { create: ['requests'] },
|
|
153
|
+
actionScopeTitles: { create: { equipment: 'Borrowable equipment' } },
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
filterFieldTitles: { equipment: { assetCode: 'Asset number' } },
|
|
157
|
+
};
|
|
158
|
+
const catalog = permissionWorkspaceCatalog(
|
|
159
|
+
{
|
|
160
|
+
...source,
|
|
161
|
+
sections: [
|
|
162
|
+
{
|
|
163
|
+
name: 'business',
|
|
164
|
+
title: 'Business',
|
|
165
|
+
order: 1,
|
|
166
|
+
subsections: [
|
|
167
|
+
{ name: 'requests', title: 'Requests', resources: [resource] },
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
} as AuthorizationOptionsResponse,
|
|
172
|
+
[],
|
|
173
|
+
[],
|
|
174
|
+
selection,
|
|
175
|
+
);
|
|
176
|
+
const grants = [
|
|
177
|
+
{
|
|
178
|
+
resource: { type: 'composite', id: 'requests' },
|
|
179
|
+
actions: [
|
|
180
|
+
{
|
|
181
|
+
action: 'create',
|
|
182
|
+
policy: {
|
|
183
|
+
type: 'composite',
|
|
184
|
+
scopes: { requests: 'allRecords', equipment: 'allRecords' },
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
const rows = permissionSections(
|
|
191
|
+
catalog,
|
|
192
|
+
grants,
|
|
193
|
+
(value) => (typeof value === 'string' ? value : value.key),
|
|
194
|
+
(item, action, scopeKey) =>
|
|
195
|
+
permissionScopeVisible(item, action, scopeKey, selection),
|
|
196
|
+
);
|
|
197
|
+
expect(
|
|
198
|
+
rows[0]?.groups[0]?.rows[0]?.scopes?.map((scope) => scope.label),
|
|
199
|
+
).toEqual(['Borrowable equipment']);
|
|
200
|
+
expect(
|
|
201
|
+
catalog.sections[0]?.subsections[0]?.resources[0]?.dataScopes?.create,
|
|
202
|
+
).toHaveLength(2);
|
|
203
|
+
expect(grants[0]?.actions[0]?.policy.scopes.requests).toBe('allRecords');
|
|
204
|
+
expect(
|
|
205
|
+
permissionFilterFieldLabel(
|
|
206
|
+
'equipment',
|
|
207
|
+
'assetCode',
|
|
208
|
+
(key) => key,
|
|
209
|
+
selection,
|
|
210
|
+
),
|
|
211
|
+
).toBe('Asset number');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('does not expose registered business or plugin capabilities without application opt-in', () => {
|
|
215
|
+
const before = structuredClone(source);
|
|
216
|
+
expect(permissionWorkspaceCatalog(source, [], []).sections).toEqual([]);
|
|
217
|
+
expect(source).toEqual(before);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('projects only the actual selected business actions and preserves their metadata', () => {
|
|
221
|
+
const catalog = permissionWorkspaceCatalog(
|
|
222
|
+
source,
|
|
223
|
+
[],
|
|
224
|
+
[],
|
|
225
|
+
businessSelection,
|
|
226
|
+
);
|
|
227
|
+
expect(catalog.sections.map((section) => section.name)).toEqual([
|
|
228
|
+
'business',
|
|
229
|
+
]);
|
|
230
|
+
const resources = catalog.sections.flatMap((section) =>
|
|
231
|
+
section.subsections.flatMap((sub) => sub.resources),
|
|
232
|
+
);
|
|
233
|
+
expect(resources).toHaveLength(1);
|
|
234
|
+
expect(resources[0]?.actions).toEqual([
|
|
235
|
+
{
|
|
236
|
+
name: 'approve',
|
|
237
|
+
title: 'Approve request',
|
|
238
|
+
description: 'Approve an eligible request.',
|
|
239
|
+
},
|
|
240
|
+
]);
|
|
241
|
+
expect(
|
|
242
|
+
permissionSections(catalog, [], (text) =>
|
|
243
|
+
typeof text === 'string' ? text : text.key,
|
|
244
|
+
).map((section) => section.title),
|
|
245
|
+
).toEqual(['Business operations']);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('fills the page section from real page resources without inventing account permissions', () => {
|
|
249
|
+
const page = {
|
|
250
|
+
type: 'page',
|
|
251
|
+
id: 'borrowings',
|
|
252
|
+
title: 'Borrowings',
|
|
253
|
+
actions: [{ name: 'access', title: 'Access' }],
|
|
254
|
+
};
|
|
255
|
+
const catalog = permissionWorkspaceCatalog(source, [], [page]);
|
|
256
|
+
expect(catalog.sections[0]?.subsections[0]?.resources).toEqual([page]);
|
|
257
|
+
expect(catalog.sections.some((section) => section.name === 'users')).toBe(
|
|
258
|
+
false,
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('includes only the account operations implemented by the registered user management page', () => {
|
|
263
|
+
const routes = [
|
|
264
|
+
{
|
|
265
|
+
name: 'settings',
|
|
266
|
+
path: '/settings',
|
|
267
|
+
children: [
|
|
268
|
+
{
|
|
269
|
+
name: 'users',
|
|
270
|
+
path: '/users',
|
|
271
|
+
componentLoader: async () => ({}),
|
|
272
|
+
authz: { resource: { type: 'user', id: '*' }, action: 'read' },
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
},
|
|
276
|
+
] as unknown as AppClientRegisteredRoute[];
|
|
277
|
+
const catalog = permissionWorkspaceCatalog(source, routes, []);
|
|
278
|
+
expect(catalog.sections[0]?.name).toBe('users');
|
|
279
|
+
const actions = catalog.sections[0]?.subsections.flatMap((sub) =>
|
|
280
|
+
sub.resources.flatMap((resource) =>
|
|
281
|
+
resource.actions.map((action) => action.name),
|
|
282
|
+
),
|
|
283
|
+
);
|
|
284
|
+
expect(actions).toEqual([
|
|
285
|
+
'read',
|
|
286
|
+
'create',
|
|
287
|
+
'update',
|
|
288
|
+
'disable',
|
|
289
|
+
'enable',
|
|
290
|
+
'revoke-sessions',
|
|
291
|
+
'assign-role',
|
|
292
|
+
]);
|
|
293
|
+
expect(
|
|
294
|
+
catalog.sections.some((section) => section.name === 'administration'),
|
|
295
|
+
).toBe(false);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('requires app opt-in, a real configuration page and a registered action together', () => {
|
|
299
|
+
const selection = {
|
|
300
|
+
business: [],
|
|
301
|
+
settings: [
|
|
302
|
+
{
|
|
303
|
+
id: 'authorization.sharing-rules',
|
|
304
|
+
actions: ['update', 'unsupported'],
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
};
|
|
308
|
+
expect(
|
|
309
|
+
permissionWorkspaceCatalog(source, configurationRoutes, [], {
|
|
310
|
+
business: [],
|
|
311
|
+
settings: [],
|
|
312
|
+
}).sections,
|
|
313
|
+
).toEqual([]);
|
|
314
|
+
expect(
|
|
315
|
+
permissionWorkspaceCatalog(source, [], [], selection).sections,
|
|
316
|
+
).toEqual([]);
|
|
317
|
+
const catalog = permissionWorkspaceCatalog(
|
|
318
|
+
source,
|
|
319
|
+
configurationRoutes,
|
|
320
|
+
[],
|
|
321
|
+
selection,
|
|
322
|
+
);
|
|
323
|
+
expect(catalog.sections.map((section) => section.name)).toEqual([
|
|
324
|
+
'configuration',
|
|
325
|
+
]);
|
|
326
|
+
const resources = catalog.sections[0]?.subsections[0]?.resources;
|
|
327
|
+
expect(resources?.map((resource) => resource.id)).toEqual([
|
|
328
|
+
'authorization.sharing-rules',
|
|
329
|
+
]);
|
|
330
|
+
expect(resources?.[0]?.actions.map((action) => action.name)).toEqual([
|
|
331
|
+
'update',
|
|
332
|
+
]);
|
|
333
|
+
expect(
|
|
334
|
+
permissionWorkspaceCatalog(
|
|
335
|
+
{ ...source, sections: [] },
|
|
336
|
+
configurationRoutes,
|
|
337
|
+
[],
|
|
338
|
+
selection,
|
|
339
|
+
).sections,
|
|
340
|
+
).toEqual([]);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('does not mistake a navigation wrapper or a different settings page for a configuration surface', () => {
|
|
344
|
+
const selection = {
|
|
345
|
+
business: [],
|
|
346
|
+
settings: [{ id: 'authorization.sharing-rules', actions: ['update'] }],
|
|
347
|
+
};
|
|
348
|
+
const wrapperOnly = [
|
|
349
|
+
{ ...configurationRoutes[0], componentLoader: undefined },
|
|
350
|
+
] as unknown as AppClientRegisteredRoute[];
|
|
351
|
+
expect(
|
|
352
|
+
permissionWorkspaceCatalog(source, wrapperOnly, [], selection).sections,
|
|
353
|
+
).toEqual([]);
|
|
354
|
+
const otherPage = [
|
|
355
|
+
{
|
|
356
|
+
...configurationRoutes[0],
|
|
357
|
+
authz: { resource: { type: 'settings', id: 'unused' }, action: 'read' },
|
|
358
|
+
},
|
|
359
|
+
] as unknown as AppClientRegisteredRoute[];
|
|
360
|
+
expect(
|
|
361
|
+
permissionWorkspaceCatalog(source, otherPage, [], selection).sections,
|
|
362
|
+
).toEqual([]);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('adapts app-owned descriptions and scope captions without changing backend identifiers or supported choices', () => {
|
|
366
|
+
const scoped = {
|
|
367
|
+
...source,
|
|
368
|
+
sections: source.sections.map((section) => ({
|
|
369
|
+
...section,
|
|
370
|
+
subsections: section.subsections.map((sub) => ({
|
|
371
|
+
...sub,
|
|
372
|
+
resources: sub.resources.map((resource) =>
|
|
373
|
+
resource.id === 'borrowing'
|
|
374
|
+
? {
|
|
375
|
+
...resource,
|
|
376
|
+
dataScopes: {
|
|
377
|
+
approve: [
|
|
378
|
+
{
|
|
379
|
+
key: 'stock',
|
|
380
|
+
title: 'warehouses',
|
|
381
|
+
collection: 'warehouses',
|
|
382
|
+
fields: ['id'],
|
|
383
|
+
recordAccess: ['own'],
|
|
384
|
+
defaultValue: 'own',
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
},
|
|
388
|
+
}
|
|
389
|
+
: resource,
|
|
390
|
+
),
|
|
391
|
+
})),
|
|
392
|
+
})),
|
|
393
|
+
};
|
|
394
|
+
const catalog = permissionWorkspaceCatalog(scoped, [], [], {
|
|
395
|
+
settings: [],
|
|
396
|
+
business: [
|
|
397
|
+
{
|
|
398
|
+
type: 'composite',
|
|
399
|
+
id: 'borrowing',
|
|
400
|
+
actions: ['approve'],
|
|
401
|
+
actionDescriptions: { approve: 'Confirm the request.' },
|
|
402
|
+
scopeTitles: { warehouses: 'Warehouse records' },
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
});
|
|
406
|
+
const resource = catalog.sections[0]?.subsections[0]?.resources[0];
|
|
407
|
+
expect(resource?.actions[0]?.description).toBe('Confirm the request.');
|
|
408
|
+
expect(resource?.dataScopes?.approve?.[0]).toEqual({
|
|
409
|
+
key: 'stock',
|
|
410
|
+
title: 'Warehouse records',
|
|
411
|
+
collection: 'warehouses',
|
|
412
|
+
fields: ['id'],
|
|
413
|
+
recordAccess: ['own'],
|
|
414
|
+
defaultValue: 'own',
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('preserves hidden grants and unknown scope parameters when a visible permission changes', () => {
|
|
419
|
+
const catalog = permissionWorkspaceCatalog(
|
|
420
|
+
source,
|
|
421
|
+
[],
|
|
422
|
+
[],
|
|
423
|
+
businessSelection,
|
|
424
|
+
);
|
|
425
|
+
const hidden = {
|
|
426
|
+
resource: { type: 'settings', id: 'authorization.sharing-rules' },
|
|
427
|
+
actions: [
|
|
428
|
+
{
|
|
429
|
+
action: 'update',
|
|
430
|
+
policy: { type: 'custom', keep: { region: 'east' } },
|
|
431
|
+
},
|
|
432
|
+
],
|
|
433
|
+
};
|
|
434
|
+
const unknown = {
|
|
435
|
+
resource: { type: 'legacy', id: 'archive' },
|
|
436
|
+
actions: [
|
|
437
|
+
{ action: 'inspect', policy: { scope: { value: 'retained' } } },
|
|
438
|
+
],
|
|
439
|
+
};
|
|
440
|
+
const grants = [hidden, unknown];
|
|
441
|
+
const next = setPermissions(grants, catalog, [
|
|
442
|
+
{
|
|
443
|
+
id: JSON.stringify(['composite', 'borrowing', 'approve']),
|
|
444
|
+
enabled: true,
|
|
445
|
+
},
|
|
446
|
+
]);
|
|
447
|
+
expect(next).toContainEqual(hidden);
|
|
448
|
+
expect(next).toContainEqual(unknown);
|
|
449
|
+
expect(next).toContainEqual({
|
|
450
|
+
resource: { type: 'composite', id: 'borrowing' },
|
|
451
|
+
actions: [{ action: 'approve' }],
|
|
452
|
+
});
|
|
453
|
+
expect(grants).toEqual([hidden, unknown]);
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it('partitions selected configuration actions by actual application pages and keeps scopes and draft ids', () => {
|
|
458
|
+
const resource = {
|
|
459
|
+
type: 'composite',
|
|
460
|
+
id: 'borrowing',
|
|
461
|
+
title: 'Borrowing',
|
|
462
|
+
actions: [
|
|
463
|
+
{ name: 'approve', title: 'Approve' },
|
|
464
|
+
{ name: 'monitor', title: 'Monitor' },
|
|
465
|
+
],
|
|
466
|
+
dataScopes: {
|
|
467
|
+
monitor: [
|
|
468
|
+
{
|
|
469
|
+
key: 'records',
|
|
470
|
+
title: 'Records',
|
|
471
|
+
collection: 'requests',
|
|
472
|
+
fields: ['id'],
|
|
473
|
+
recordAccess: ['customFilter'],
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
const catalog = {
|
|
479
|
+
...source,
|
|
480
|
+
sections: [
|
|
481
|
+
{
|
|
482
|
+
name: 'business',
|
|
483
|
+
title: 'Business',
|
|
484
|
+
subsections: [
|
|
485
|
+
{ name: 'borrowing', title: 'Borrowing', resources: [resource] },
|
|
486
|
+
],
|
|
487
|
+
},
|
|
488
|
+
],
|
|
489
|
+
} as AuthorizationOptionsResponse;
|
|
490
|
+
const selection = {
|
|
491
|
+
settings: [],
|
|
492
|
+
business: [
|
|
493
|
+
{
|
|
494
|
+
type: 'composite',
|
|
495
|
+
id: 'borrowing',
|
|
496
|
+
actions: ['approve', 'monitor', 'missing'],
|
|
497
|
+
configurationActions: [
|
|
498
|
+
{ name: 'monitor', routes: ['monitoring'] },
|
|
499
|
+
{ name: 'missing', routes: ['monitoring'] },
|
|
500
|
+
],
|
|
501
|
+
},
|
|
502
|
+
],
|
|
503
|
+
};
|
|
504
|
+
const routes = [
|
|
505
|
+
{
|
|
506
|
+
name: 'monitoring',
|
|
507
|
+
source: 'application',
|
|
508
|
+
componentLoader: async () => ({}),
|
|
509
|
+
},
|
|
510
|
+
] as unknown as AppClientRegisteredRoute[];
|
|
511
|
+
const projected = permissionWorkspaceCatalog(catalog, routes, [], selection);
|
|
512
|
+
expect(projected.sections.map((section) => section.name)).toEqual([
|
|
513
|
+
'business',
|
|
514
|
+
'configuration',
|
|
515
|
+
]);
|
|
516
|
+
expect(
|
|
517
|
+
projected.sections[0]!.subsections[0]!.resources[0]!.actions.map(
|
|
518
|
+
(action) => action.name,
|
|
519
|
+
),
|
|
520
|
+
).toEqual(['approve']);
|
|
521
|
+
const config = projected.sections[1]!.subsections[0]!.resources[0]!;
|
|
522
|
+
expect(config.actions.map((action) => action.name)).toEqual(['monitor']);
|
|
523
|
+
expect(config.dataScopes?.monitor).toEqual(resource.dataScopes.monitor);
|
|
524
|
+
const grants = [
|
|
525
|
+
{
|
|
526
|
+
resource: { type: 'composite', id: 'borrowing' },
|
|
527
|
+
actions: [
|
|
528
|
+
{
|
|
529
|
+
action: 'monitor',
|
|
530
|
+
policy: {
|
|
531
|
+
type: 'composite',
|
|
532
|
+
scopes: {
|
|
533
|
+
records: {
|
|
534
|
+
type: 'recordAccess',
|
|
535
|
+
key: 'customFilter',
|
|
536
|
+
params: { filter: { retained: true } },
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
],
|
|
542
|
+
},
|
|
543
|
+
];
|
|
544
|
+
const rows = permissionSections(projected, grants, (value) =>
|
|
545
|
+
typeof value === 'string' ? value : value.key,
|
|
546
|
+
).flatMap((section) => section.groups.flatMap((group) => group.rows));
|
|
547
|
+
expect(
|
|
548
|
+
rows.find((row) => row.id === '["composite","borrowing","monitor"]')
|
|
549
|
+
?.enabled,
|
|
550
|
+
).toBe(true);
|
|
551
|
+
for (const absent of [
|
|
552
|
+
[],
|
|
553
|
+
[{ ...routes[0], componentLoader: undefined }],
|
|
554
|
+
[{ ...routes[0], source: 'plugin' }],
|
|
555
|
+
]) {
|
|
556
|
+
const result = permissionWorkspaceCatalog(
|
|
557
|
+
catalog,
|
|
558
|
+
absent as AppClientRegisteredRoute[],
|
|
559
|
+
[],
|
|
560
|
+
selection,
|
|
561
|
+
);
|
|
562
|
+
expect(result.sections.map((section) => section.name)).toEqual([
|
|
563
|
+
'business',
|
|
564
|
+
]);
|
|
565
|
+
expect(JSON.stringify(result)).not.toContain('"name":"monitor"');
|
|
566
|
+
}
|
|
567
|
+
expect(
|
|
568
|
+
catalog.sections[0]!.subsections[0]!.resources[0]!.actions,
|
|
569
|
+
).toHaveLength(2);
|
|
570
|
+
});
|