@nocobase/app-template-default 1.0.0-beta.83 → 1.0.0-beta.85
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/rules/build-and-deploy.md +1 -1
- package/.agents/rules/shell.md +4 -0
- package/AGENTS.md +8 -2
- package/README.MD +10 -2
- package/client/components/back-button.tsx +3 -5
- package/client/components/data-table/data-table.tsx +86 -32
- package/client/components/default-access-editor.tsx +13 -2
- package/client/components/feedback-form.tsx +474 -0
- package/client/components/lifecycle-diagram.tsx +10 -10
- package/client/components/list-feedback.tsx +174 -0
- package/client/components/permission-editor.tsx +419 -226
- package/client/extensions/nocobase-auth-forms/password-login-form.tsx +17 -6
- package/client/extensions/nocobase-auth-forms/password-registration-form.tsx +24 -7
- package/client/extensions/nocobase-auth-forms/password-reset-form.tsx +19 -7
- package/client/extensions/nocobase-auth-forms/password-reset-request-form.tsx +15 -7
- package/client/extensions/nocobase-device-approval/device-approval.tsx +7 -2
- package/client/extensions/nocobase-device-approval/locales/en-US.ts +16 -0
- package/client/extensions/nocobase-device-approval/locales/zh-CN.ts +7 -0
- package/client/extensions/nocobase-file-component-ui/components/file-preview-dialog.tsx +20 -17
- package/client/extensions/nocobase-workflow-management/README.md +3 -1
- package/client/extensions/nocobase-workflow-management/definition-list.tsx +19 -7
- package/client/extensions/nocobase-workflow-management/execution-list.tsx +61 -48
- package/client/hooks/use-list-data.ts +76 -0
- package/client/hooks/use-list-location.ts +23 -0
- package/client/hooks/use-list-tabs.ts +42 -0
- package/client/hooks/use-table-url-state.ts +143 -30
- package/client/locales/system/en-US.ts +24 -1
- package/client/locales/system/settings-en-US.ts +8 -10
- package/client/locales/system/settings-zh-CN.ts +8 -8
- package/client/locales/system/zh-CN.ts +22 -1
- package/client/pages/settings/default-access-form.tsx +44 -26
- package/client/pages/settings/default-access.tsx +100 -93
- package/client/pages/settings/hooks.tsx +4 -59
- package/client/pages/settings/page-permissions.ts +17 -0
- package/client/pages/settings/permissions-model.ts +54 -59
- package/client/pages/settings/role-form.tsx +98 -56
- package/client/pages/settings/roles.tsx +69 -95
- package/client/pages/settings/shared.tsx +43 -33
- package/client/pages/settings/user-form.tsx +51 -30
- package/client/pages/settings/users.tsx +105 -91
- package/client/routing/session-preparation.tsx +10 -3
- package/client/runtime.ts +0 -4
- package/config.example.yml +24 -0
- package/database/main/migrations/202611020002_create_mutation_receipts.ts +19 -0
- package/package.json +24 -21
- package/server/app.ts +0 -2
- package/server/config/auth.ts +2 -0
- package/server/config/concurrency.ts +3 -0
- package/server/config/index.ts +3 -0
- package/server/config/jobs.ts +1 -13
- package/server/config/notification.ts +1 -1
- package/server/config/queue.ts +1 -7
- package/server/plugins.ts +2 -0
- package/server/providers/index.ts +4 -1
- package/server/providers/submission.ts +61 -0
- package/tests/client/auth-i18n.test.tsx +9 -0
- package/tests/client/back-button.test.tsx +3 -3
- package/tests/client/data-table.test.tsx +9 -4
- package/tests/client/default-access.test.tsx +99 -5
- package/tests/client/form-feedback.test.tsx +589 -0
- package/tests/client/list-feedback.test.tsx +301 -0
- package/tests/client/list-recovery.test.tsx +373 -0
- package/tests/client/list-tabs.test.tsx +121 -0
- package/tests/client/manual-list-retry.test.tsx +190 -0
- package/tests/client/page-permission-navigation.test.ts +8 -0
- package/tests/client/permission-editor.test.tsx +191 -21
- package/tests/client/permission-filter-binding.test.tsx +1 -1
- package/tests/client/permission-workspace.test.ts +13 -0
- package/tests/client/role-return.test.tsx +2 -1
- package/tests/client/session-preparation.test.tsx +24 -1
- package/tests/client/settings-continuity.test.tsx +85 -5
- package/tests/client/settings-feedback.test.tsx +32 -15
- package/tests/client/settings-permissions.test.ts +35 -0
- package/tests/client/settings.test.tsx +2 -1
- package/tests/client/table-url-state.test.tsx +117 -3
- package/tests/client/user-drawer.test.tsx +2 -1
- package/tests/client/user-permissions.test.tsx +30 -5
- package/tests/database/mutation-receipts.test.ts +45 -0
- package/tests/fixtures/form-feedback/README.md +33 -0
- package/tests/fixtures/form-feedback/equipment-detail.tsx +11 -0
- package/tests/fixtures/form-feedback/equipment-layout.tsx +15 -0
- package/tests/fixtures/form-feedback/equipment-migration.ts +27 -0
- package/tests/fixtures/form-feedback/equipment-server.ts +197 -0
- package/tests/fixtures/form-feedback/equipment-validation.tsx +94 -0
- package/tests/fixtures/form-feedback/equipment.tsx +369 -0
- package/tests/fixtures/form-feedback/verify-inline.mjs +174 -0
- package/tests/fixtures/form-feedback/verify.mjs +373 -0
- package/tests/fixtures/permission-alignment.html +16 -0
- package/tests/fixtures/permission-alignment.tsx +124 -0
- package/tests/fixtures/submission/README.md +35 -0
- package/tests/fixtures/submission/browser.test.ts +478 -0
- package/tests/fixtures/submission/migration.ts +61 -0
- package/tests/fixtures/submission/page.tsx +138 -0
- package/tests/fixtures/submission/playwright.config.ts +11 -0
- package/tests/fixtures/submission/routes.ts +208 -0
- package/tests/helpers/settings-client.tsx +31 -0
- package/tests/playwright/permission-alignment.test.ts +109 -0
- package/tests/server/auth-email-config.test.ts +79 -0
- package/tests/server/concurrency.test.ts +61 -0
- package/tests/server/config.test.ts +34 -14
- package/tests/server/member-role.test.ts +7 -1
- package/tests/server/submission-settings.test.ts +241 -0
- package/client/route-overrides.ts +0 -9
- package/client/source-extensions.ts +0 -19
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
import { useListLocation } from '#hooks/use-list-location';
|
|
1
2
|
import { RecordLink } from '#components/record-link';
|
|
2
3
|
import { useTableUrlState } from '#hooks/use-table-url-state';
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ApiClientError,
|
|
6
|
+
resolveListState,
|
|
7
|
+
useApiClient,
|
|
8
|
+
useSubmission,
|
|
9
|
+
useToaster,
|
|
10
|
+
} from '@nocobase/app-client';
|
|
4
11
|
import {
|
|
5
12
|
useCan,
|
|
6
13
|
type AuthorizationOptionsResponse,
|
|
@@ -8,13 +15,14 @@ import {
|
|
|
8
15
|
} from '@nocobase/app-plugin-authorization/client';
|
|
9
16
|
import { useTranslation } from '@nocobase/i18n/client';
|
|
10
17
|
import { useRef, useState, type ReactElement } from 'react';
|
|
11
|
-
import { Link, Outlet, useLocation
|
|
12
|
-
import {
|
|
18
|
+
import { Link, Outlet, useLocation } from 'react-router';
|
|
19
|
+
import { MoreHorizontalIcon, PlusIcon } from 'lucide-react';
|
|
13
20
|
import type { ColumnDef } from '@tanstack/react-table';
|
|
14
21
|
import { Alert, AlertDescription } from '#components/ui/alert';
|
|
15
22
|
import { PermissionManagementPage } from './permission-management-page.js';
|
|
16
23
|
import { DataTable, DataTableColumnHeader } from '#components/data-table';
|
|
17
24
|
import { Button } from '#components/ui/button';
|
|
25
|
+
import { Spinner } from '#components/ui/spinner';
|
|
18
26
|
import { Input } from '#components/ui/input';
|
|
19
27
|
import {
|
|
20
28
|
DropdownMenu,
|
|
@@ -23,14 +31,7 @@ import {
|
|
|
23
31
|
DropdownMenuSeparator,
|
|
24
32
|
DropdownMenuTrigger,
|
|
25
33
|
} from '#components/ui/dropdown-menu';
|
|
26
|
-
import {
|
|
27
|
-
Empty,
|
|
28
|
-
EmptyMedia,
|
|
29
|
-
EmptyTitle,
|
|
30
|
-
EmptyDescription,
|
|
31
|
-
EmptyContent,
|
|
32
|
-
} from '#components/ui/empty';
|
|
33
|
-
import { ConfirmAction, SettingsError, SettingsLoading } from './shared.js';
|
|
34
|
+
import { ConfirmAction, SettingsError } from './shared.js';
|
|
34
35
|
import { useSettingsData } from './hooks.js';
|
|
35
36
|
import {
|
|
36
37
|
actionKey,
|
|
@@ -45,19 +46,21 @@ export default function DefaultAccessPage(): ReactElement {
|
|
|
45
46
|
const api = useApiClient();
|
|
46
47
|
const toaster = useToaster();
|
|
47
48
|
const location = useLocation();
|
|
48
|
-
const
|
|
49
|
+
const { params, setParams, active } = useListLocation();
|
|
49
50
|
const search = params.get('q') ?? '';
|
|
50
51
|
const searchRef = useRef<HTMLInputElement>(null);
|
|
51
52
|
const rules = useSettingsData<{ data: readonly DefaultAccessRule[] }>(
|
|
52
53
|
'authorization/defaultAccess',
|
|
54
|
+
{ active },
|
|
53
55
|
);
|
|
54
56
|
const catalog = useSettingsData<{ data: AuthorizationOptionsResponse }>(
|
|
55
57
|
'authorization/defaultAccess/options',
|
|
56
58
|
);
|
|
57
|
-
const
|
|
59
|
+
const createPermission = useCan({
|
|
58
60
|
resource: { type: 'settings', id: 'authorization.default-access' },
|
|
59
61
|
action: 'create',
|
|
60
|
-
})
|
|
62
|
+
});
|
|
63
|
+
const canCreate = createPermission.can && !createPermission.isPending;
|
|
61
64
|
const canDelete = useCan({
|
|
62
65
|
resource: { type: 'settings', id: 'authorization.default-access' },
|
|
63
66
|
action: 'delete',
|
|
@@ -212,6 +215,8 @@ export default function DefaultAccessPage(): ReactElement {
|
|
|
212
215
|
)
|
|
213
216
|
.sort((a, b) => name(a).localeCompare(name(b), i18n.language));
|
|
214
217
|
const tableState = useTableUrlState({
|
|
218
|
+
active,
|
|
219
|
+
revision: rules.revision,
|
|
215
220
|
defaultPageSize: 20,
|
|
216
221
|
sortColumns: ['resource'],
|
|
217
222
|
rowCount:
|
|
@@ -224,6 +229,30 @@ export default function DefaultAccessPage(): ReactElement {
|
|
|
224
229
|
? rows.length
|
|
225
230
|
: undefined,
|
|
226
231
|
});
|
|
232
|
+
const listError =
|
|
233
|
+
[rules.error, catalog.error].find(
|
|
234
|
+
(error) =>
|
|
235
|
+
error instanceof ApiClientError && [401, 403].includes(error.status),
|
|
236
|
+
) ??
|
|
237
|
+
tableState.error ??
|
|
238
|
+
rules.error ??
|
|
239
|
+
catalog.error;
|
|
240
|
+
const listData = rules.data && catalog.data ? rows : undefined;
|
|
241
|
+
const listState = resolveListState(
|
|
242
|
+
{
|
|
243
|
+
data: listData,
|
|
244
|
+
error: listError,
|
|
245
|
+
isPending: !listData && (rules.loading || catalog.loading),
|
|
246
|
+
},
|
|
247
|
+
rows.length,
|
|
248
|
+
Boolean(search.trim()),
|
|
249
|
+
);
|
|
250
|
+
const canShowCreate =
|
|
251
|
+
canCreate &&
|
|
252
|
+
(listState === 'empty' ||
|
|
253
|
+
listState === 'ready' ||
|
|
254
|
+
listState === 'no-results' ||
|
|
255
|
+
listState === 'refresh-failed');
|
|
227
256
|
const clearSearch = () =>
|
|
228
257
|
setParams(
|
|
229
258
|
(old) => {
|
|
@@ -262,43 +291,58 @@ export default function DefaultAccessPage(): ReactElement {
|
|
|
262
291
|
</div>
|
|
263
292
|
)
|
|
264
293
|
) : null;
|
|
294
|
+
const submission = useSubmission();
|
|
265
295
|
const remove = async () => {
|
|
266
296
|
if (!removing || busy || !canDelete) return;
|
|
267
297
|
setBusy(true);
|
|
268
298
|
setError(undefined);
|
|
299
|
+
let submitted = false;
|
|
269
300
|
try {
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
301
|
+
const outcome = await submission.run(
|
|
302
|
+
{ key: removing.key },
|
|
303
|
+
async (idempotencyKey, _signal, _snapshot, recovering) => {
|
|
304
|
+
if (!recovering) {
|
|
305
|
+
const latest = await api.request<{
|
|
306
|
+
data: readonly DefaultAccessRule[];
|
|
307
|
+
}>({ path: 'authorization/defaultAccess' });
|
|
308
|
+
if (
|
|
309
|
+
JSON.stringify(
|
|
310
|
+
latest.data.find((rule) => rule.key === removing.key),
|
|
311
|
+
) !== JSON.stringify(removing)
|
|
312
|
+
) {
|
|
313
|
+
rules.reload();
|
|
314
|
+
setRemoving(undefined);
|
|
315
|
+
setConflict(true);
|
|
316
|
+
requestAnimationFrame(() => searchRef.current?.focus());
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
submitted = true;
|
|
321
|
+
await api.request({
|
|
322
|
+
path: `authorization/defaultAccess/${encodeURIComponent(removing.key)}`,
|
|
323
|
+
method: 'DELETE',
|
|
324
|
+
headers: { 'Idempotency-Key': idempotencyKey },
|
|
325
|
+
});
|
|
326
|
+
return true;
|
|
327
|
+
},
|
|
328
|
+
);
|
|
329
|
+
if (!outcome) return;
|
|
288
330
|
toaster.show({
|
|
289
331
|
type: 'success',
|
|
290
332
|
title: t('settings.defaultAccess.deleted', { name: name(removing) }),
|
|
291
333
|
});
|
|
292
|
-
rules.replaceData({
|
|
293
|
-
data: (rules.data?.data ?? []).filter(
|
|
294
|
-
(rule) => rule.key !== removing.key,
|
|
295
|
-
),
|
|
296
|
-
});
|
|
297
334
|
setRemoving(undefined);
|
|
298
335
|
rules.reload();
|
|
299
336
|
requestAnimationFrame(() => searchRef.current?.focus());
|
|
300
337
|
} catch (next) {
|
|
301
338
|
setError(next);
|
|
339
|
+
if (
|
|
340
|
+
submitted &&
|
|
341
|
+
(!(next instanceof ApiClientError) || next.status >= 500)
|
|
342
|
+
) {
|
|
343
|
+
// The write may have committed before the response was lost. Verify without replaying it.
|
|
344
|
+
rules.reload();
|
|
345
|
+
}
|
|
302
346
|
} finally {
|
|
303
347
|
setBusy(false);
|
|
304
348
|
}
|
|
@@ -307,9 +351,7 @@ export default function DefaultAccessPage(): ReactElement {
|
|
|
307
351
|
<>
|
|
308
352
|
<PermissionManagementPage
|
|
309
353
|
tab='default-access'
|
|
310
|
-
actions={
|
|
311
|
-
rules.data && (rules.data.data.length || search) ? create : undefined
|
|
312
|
-
}
|
|
354
|
+
actions={canShowCreate ? create : undefined}
|
|
313
355
|
>
|
|
314
356
|
<div className='flex flex-wrap items-center gap-2'>
|
|
315
357
|
<Input
|
|
@@ -331,11 +373,16 @@ export default function DefaultAccessPage(): ReactElement {
|
|
|
331
373
|
)
|
|
332
374
|
}
|
|
333
375
|
/>
|
|
334
|
-
{search && (
|
|
376
|
+
{search && listState !== 'no-results' && (
|
|
335
377
|
<Button variant='ghost' onClick={clearSearch}>
|
|
336
378
|
{t('settings.clear')}
|
|
337
379
|
</Button>
|
|
338
380
|
)}
|
|
381
|
+
{active &&
|
|
382
|
+
(rules.loading || catalog.loading) &&
|
|
383
|
+
(rules.data || catalog.data) ? (
|
|
384
|
+
<Spinner className='text-muted-foreground' />
|
|
385
|
+
) : null}
|
|
339
386
|
</div>
|
|
340
387
|
{conflict && (
|
|
341
388
|
<Alert variant='destructive'>
|
|
@@ -344,62 +391,22 @@ export default function DefaultAccessPage(): ReactElement {
|
|
|
344
391
|
</AlertDescription>
|
|
345
392
|
</Alert>
|
|
346
393
|
)}
|
|
347
|
-
<SettingsError
|
|
348
|
-
|
|
349
|
-
|
|
394
|
+
<SettingsError error={!removing && error} />
|
|
395
|
+
<DataTable
|
|
396
|
+
{...tableState}
|
|
397
|
+
columns={columns}
|
|
398
|
+
data={rows}
|
|
399
|
+
getRowId={(rule) => rule.key}
|
|
400
|
+
showSelectedCount={false}
|
|
401
|
+
pageSize={20}
|
|
402
|
+
loading={rules.loading || catalog.loading}
|
|
403
|
+
listState={listState}
|
|
404
|
+
onRetry={() => {
|
|
350
405
|
rules.reload();
|
|
351
406
|
catalog.reload();
|
|
352
|
-
setError(undefined);
|
|
353
407
|
}}
|
|
408
|
+
onClearFilters={clearSearch}
|
|
354
409
|
/>
|
|
355
|
-
{(rules.loading && !rules.data) ||
|
|
356
|
-
(catalog.loading && !catalog.data) ? (
|
|
357
|
-
<SettingsLoading />
|
|
358
|
-
) : (
|
|
359
|
-
!rules.error &&
|
|
360
|
-
!catalog.error &&
|
|
361
|
-
(rows.length ? (
|
|
362
|
-
<DataTable
|
|
363
|
-
{...tableState}
|
|
364
|
-
columns={columns}
|
|
365
|
-
data={rows}
|
|
366
|
-
getRowId={(rule) => rule.key}
|
|
367
|
-
showSelectedCount={false}
|
|
368
|
-
pageSize={20}
|
|
369
|
-
/>
|
|
370
|
-
) : (
|
|
371
|
-
<Empty className='min-h-48 border border-dashed'>
|
|
372
|
-
<EmptyMedia variant='icon'>
|
|
373
|
-
<ListFilterIcon />
|
|
374
|
-
</EmptyMedia>
|
|
375
|
-
<EmptyTitle>
|
|
376
|
-
{t(
|
|
377
|
-
search
|
|
378
|
-
? 'settings.noResults'
|
|
379
|
-
: 'settings.defaultAccess.empty',
|
|
380
|
-
)}
|
|
381
|
-
</EmptyTitle>
|
|
382
|
-
<EmptyDescription>
|
|
383
|
-
{t(
|
|
384
|
-
search
|
|
385
|
-
? 'settings.clearHint'
|
|
386
|
-
: canCreate
|
|
387
|
-
? 'settings.defaultAccess.emptyHint'
|
|
388
|
-
: 'settings.defaultAccess.readOnlyEmpty',
|
|
389
|
-
)}
|
|
390
|
-
</EmptyDescription>
|
|
391
|
-
<EmptyContent>
|
|
392
|
-
{search ? (
|
|
393
|
-
<Button variant='outline' onClick={clearSearch}>
|
|
394
|
-
{t('settings.clear')}
|
|
395
|
-
</Button>
|
|
396
|
-
) : (
|
|
397
|
-
create
|
|
398
|
-
)}
|
|
399
|
-
</EmptyContent>
|
|
400
|
-
</Empty>
|
|
401
|
-
))
|
|
402
|
-
)}
|
|
403
410
|
<ConfirmAction
|
|
404
411
|
error={<SettingsError error={error} />}
|
|
405
412
|
open={Boolean(removing)}
|
|
@@ -1,67 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ApiClientError,
|
|
3
|
-
useApiClient,
|
|
4
|
-
useNavigationGuard,
|
|
5
|
-
} from '@nocobase/app-client';
|
|
1
|
+
import { useNavigationGuard } from '@nocobase/app-client';
|
|
6
2
|
import { useCan } from '@nocobase/app-plugin-authorization/client';
|
|
7
3
|
import { useTranslation } from '@nocobase/i18n/client';
|
|
8
|
-
import {
|
|
4
|
+
import { useEffect, useRef, useState } from 'react';
|
|
9
5
|
import { useLocation } from 'react-router';
|
|
10
6
|
import { ConfirmAction } from './shared.js';
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const [revision, setRevision] = useState(0);
|
|
15
|
-
const [result, setResult] = useState<{
|
|
16
|
-
path: string;
|
|
17
|
-
revision: number;
|
|
18
|
-
data?: T;
|
|
19
|
-
error?: unknown;
|
|
20
|
-
loading: boolean;
|
|
21
|
-
}>({ path, revision: -1, loading: true });
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
let active = true;
|
|
24
|
-
void api.request<T>({ path }).then(
|
|
25
|
-
(data) => {
|
|
26
|
-
if (active) setResult({ path, revision, data, loading: false });
|
|
27
|
-
},
|
|
28
|
-
(error: unknown) => {
|
|
29
|
-
if (active)
|
|
30
|
-
setResult((old) => ({
|
|
31
|
-
...(old.path === path &&
|
|
32
|
-
!(
|
|
33
|
-
error instanceof ApiClientError &&
|
|
34
|
-
[401, 403, 404].includes(error.status)
|
|
35
|
-
)
|
|
36
|
-
? old
|
|
37
|
-
: {}),
|
|
38
|
-
path,
|
|
39
|
-
revision,
|
|
40
|
-
error,
|
|
41
|
-
loading: false,
|
|
42
|
-
}));
|
|
43
|
-
},
|
|
44
|
-
);
|
|
45
|
-
return () => {
|
|
46
|
-
active = false;
|
|
47
|
-
};
|
|
48
|
-
}, [api, path, revision]);
|
|
49
|
-
return {
|
|
50
|
-
...(result.path === path || keepPreviousData
|
|
51
|
-
? result
|
|
52
|
-
: { path, loading: true, data: undefined, error: undefined }),
|
|
53
|
-
loading: result.path !== path || result.revision !== revision,
|
|
54
|
-
error:
|
|
55
|
-
result.path === path && result.revision === revision
|
|
56
|
-
? result.error
|
|
57
|
-
: undefined,
|
|
58
|
-
reload: useCallback(() => setRevision((value) => value + 1), []),
|
|
59
|
-
replaceData: useCallback(
|
|
60
|
-
(data: T) => setResult({ path, revision, data, loading: false }),
|
|
61
|
-
[path, revision],
|
|
62
|
-
),
|
|
63
|
-
};
|
|
64
|
-
}
|
|
8
|
+
/** Compatibility entry for prepared settings pages and application-owned consumers. */
|
|
9
|
+
export { useListData as useSettingsData } from '#hooks/use-list-data';
|
|
65
10
|
|
|
66
11
|
export function useUserPermission(action: string, id = '*') {
|
|
67
12
|
return useCan({ resource: { type: 'user', id }, action }).can;
|
|
@@ -101,6 +101,21 @@ export function pagePermissionNavigation(
|
|
|
101
101
|
({ type, id }) => !configuration.has(JSON.stringify([type, id])),
|
|
102
102
|
);
|
|
103
103
|
const resources = new Map(pages.map((page) => [page.id, page]));
|
|
104
|
+
const paths = new Map<string, string>();
|
|
105
|
+
const collectPaths = (items: readonly AppClientRegisteredRoute[]): void => {
|
|
106
|
+
for (const route of items) {
|
|
107
|
+
if (
|
|
108
|
+
route.componentLoader &&
|
|
109
|
+
typeof route.authz === 'object' &&
|
|
110
|
+
route.authz.resource.type === 'page' &&
|
|
111
|
+
!paths.has(route.authz.resource.id)
|
|
112
|
+
) {
|
|
113
|
+
paths.set(route.authz.resource.id, route.path);
|
|
114
|
+
}
|
|
115
|
+
collectPaths(route.children ?? []);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
collectPaths(routes);
|
|
104
119
|
const seen = new Set<string>();
|
|
105
120
|
const visit = (
|
|
106
121
|
items: readonly RouteNavigationItem[],
|
|
@@ -130,6 +145,7 @@ export function pagePermissionNavigation(
|
|
|
130
145
|
: unnamed(route.path),
|
|
131
146
|
),
|
|
132
147
|
rowId,
|
|
148
|
+
path: route.path,
|
|
133
149
|
children: descendants,
|
|
134
150
|
},
|
|
135
151
|
];
|
|
@@ -142,6 +158,7 @@ export function pagePermissionNavigation(
|
|
|
142
158
|
id: `page:${page.id}`,
|
|
143
159
|
label: text(page.title),
|
|
144
160
|
rowId: permissionRowId(page, 'access'),
|
|
161
|
+
path: paths.get(page.id),
|
|
145
162
|
}));
|
|
146
163
|
return [
|
|
147
164
|
...navigation,
|
|
@@ -61,7 +61,6 @@ export function setPermission(
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
const sectionDescriptions: Readonly<Record<string, string>> = {
|
|
64
|
-
pages: 'pageAccessHint',
|
|
65
64
|
business: 'businessOperationsHint',
|
|
66
65
|
users: 'userAccountsHint',
|
|
67
66
|
configuration: 'configurationPermissionsHint',
|
|
@@ -126,68 +125,64 @@ export function permissionSections(
|
|
|
126
125
|
actionKey: action.name,
|
|
127
126
|
enabled: Boolean(granted),
|
|
128
127
|
};
|
|
129
|
-
//
|
|
128
|
+
// Keep scope metadata stable for layout and search; only enabled actions render controls.
|
|
130
129
|
return {
|
|
131
130
|
...row,
|
|
132
|
-
scopes:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
131
|
+
scopes: (resource.dataScopes?.[action.name] ?? [])
|
|
132
|
+
.filter((scope) => showScope(resource, action.name, scope.key))
|
|
133
|
+
.map((scope) => {
|
|
134
|
+
const value = scopeSelectionKey(
|
|
135
|
+
granted?.policy?.scopes &&
|
|
136
|
+
typeof granted.policy.scopes === 'object'
|
|
137
|
+
? Reflect.get(granted.policy.scopes, scope.key)
|
|
138
|
+
: undefined,
|
|
139
|
+
);
|
|
140
|
+
const preserved =
|
|
141
|
+
value !== '' && !scope.recordAccess.includes(value);
|
|
142
|
+
return {
|
|
143
|
+
id: JSON.stringify([
|
|
144
|
+
resource.type,
|
|
145
|
+
resource.id,
|
|
146
|
+
action.name,
|
|
147
|
+
scope.key,
|
|
148
|
+
]),
|
|
149
|
+
label: text(
|
|
150
|
+
typeof scope.title === 'string' &&
|
|
151
|
+
scope.title === scope.collection
|
|
152
|
+
? { key: 'settings.recordScope' }
|
|
153
|
+
: scope.title,
|
|
154
|
+
),
|
|
155
|
+
levels: [
|
|
156
|
+
...(preserved
|
|
157
|
+
? [
|
|
158
|
+
{
|
|
159
|
+
value,
|
|
160
|
+
label: text({
|
|
161
|
+
key: 'settings.preservedRecordScope',
|
|
162
|
+
}),
|
|
163
|
+
},
|
|
164
|
+
]
|
|
165
|
+
: []),
|
|
166
|
+
{
|
|
167
|
+
value: '',
|
|
168
|
+
label: text({ key: 'settings.noRecordScope' }),
|
|
169
|
+
},
|
|
170
|
+
...scope.recordAccess.map((key) => ({
|
|
171
|
+
value: key,
|
|
153
172
|
label: text(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
: scope.title,
|
|
173
|
+
catalog.recordAccess.find(
|
|
174
|
+
(entry) => entry.key === key,
|
|
175
|
+
)?.title ?? key,
|
|
158
176
|
),
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
]
|
|
169
|
-
: []),
|
|
170
|
-
{
|
|
171
|
-
value: '',
|
|
172
|
-
label: text({ key: 'settings.noRecordScope' }),
|
|
173
|
-
},
|
|
174
|
-
...scope.recordAccess.map((key) => ({
|
|
175
|
-
value: key,
|
|
176
|
-
label: text(
|
|
177
|
-
catalog.recordAccess.find(
|
|
178
|
-
(entry) => entry.key === key,
|
|
179
|
-
)?.title ?? key,
|
|
180
|
-
),
|
|
181
|
-
})),
|
|
182
|
-
],
|
|
183
|
-
level: value,
|
|
184
|
-
hint:
|
|
185
|
-
value === ''
|
|
186
|
-
? text({ key: 'settings.noAdditionalScopeHint' })
|
|
187
|
-
: undefined,
|
|
188
|
-
};
|
|
189
|
-
})
|
|
190
|
-
: [],
|
|
177
|
+
})),
|
|
178
|
+
],
|
|
179
|
+
level: value,
|
|
180
|
+
hint:
|
|
181
|
+
value === ''
|
|
182
|
+
? text({ key: 'settings.noAdditionalScopeHint' })
|
|
183
|
+
: undefined,
|
|
184
|
+
};
|
|
185
|
+
}),
|
|
191
186
|
};
|
|
192
187
|
}),
|
|
193
188
|
})),
|