@nocobase/app-template-default 1.0.0-beta.87 → 1.0.0-beta.89
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/components.md +3 -1
- package/.agents/rules/notification-templates.md +5 -0
- package/.agents/rules/shell.md +9 -3
- package/AGENTS.md +4 -2
- package/README.MD +11 -5
- package/client/components/back-button.tsx +37 -12
- package/client/components/body-portal.tsx +25 -0
- package/client/components/brand-loading-mark.tsx +5 -2
- package/client/components/brand-mark-geometry.ts +3 -3
- package/client/components/breadcrumbs.tsx +60 -3
- package/client/components/data-table/data-table.tsx +90 -19
- package/client/components/data-table/pagination.tsx +17 -2
- package/client/components/flow-diagram.tsx +2 -2
- package/client/components/list-feedback.tsx +31 -22
- package/client/components/loading.tsx +127 -16
- package/client/components/pending-button.tsx +111 -0
- package/client/components/role-list.tsx +7 -0
- package/client/components/route-child-page.tsx +72 -1
- package/client/components/route-overlay.tsx +149 -15
- package/client/components/task-progress.tsx +155 -0
- package/client/components/use-route-overlay.ts +8 -11
- package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +62 -32
- package/client/extensions/nocobase-file-component-ui/locales/de-DE.ts +2 -0
- package/client/extensions/nocobase-file-component-ui/locales/en-US.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/es-ES.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/fr-FR.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/id-ID.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/ja-JP.ts +2 -0
- package/client/extensions/nocobase-file-component-ui/locales/pt-BR.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/vi-VN.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/zh-CN.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/zh-TW.ts +1 -0
- package/client/extensions/nocobase-workflow-management/README.md +5 -5
- package/client/extensions/nocobase-workflow-management/definition-list.tsx +2 -9
- package/client/extensions/nocobase-workflow-management/execution-list.tsx +2 -4
- package/client/layouts/app-layout.tsx +8 -8
- package/client/layouts/components/sidebar-footer.tsx +18 -25
- package/client/lib/navigation-transform.ts +156 -22
- package/client/locales/system/en-US.ts +26 -5
- package/client/locales/system/settings-en-US.ts +17 -0
- package/client/locales/system/settings-zh-CN.ts +15 -0
- package/client/locales/system/zh-CN.ts +26 -5
- package/client/pages/auth/register.tsx +39 -22
- package/client/pages/auth/shared.tsx +1 -3
- package/client/pages/settings/default-access.tsx +1 -1
- package/client/pages/settings/hooks.tsx +3 -68
- package/client/pages/settings/notification-routes.ts +109 -0
- package/client/pages/settings/notification-templates.tsx +26 -0
- package/client/pages/settings/permission-management-page.tsx +15 -90
- package/client/pages/settings/roles.tsx +27 -27
- package/client/pages/settings/routes.ts +107 -101
- package/client/pages/settings/settings-home.tsx +109 -0
- package/client/pages/settings/settings-module-page.tsx +158 -0
- package/client/pages/settings/shell-routes.ts +53 -0
- package/client/pages/settings/user-detail.tsx +3 -17
- package/client/pages/settings/user-form.tsx +9 -15
- package/client/pages/settings/users.tsx +11 -27
- package/client/routes.ts +17 -1
- package/client/routing/app-router.tsx +1 -14
- package/client/routing/client-route.tsx +199 -30
- package/client/routing/navigation-destination.ts +42 -0
- package/client/routing/route-context.tsx +30 -3
- package/client/routing/route-leave-boundary.tsx +261 -0
- package/client/routing/route-navigation.ts +72 -8
- package/client/routing/route-tree.tsx +7 -12
- package/client/runtime.ts +2 -2
- package/client/styles.css +18 -1
- package/config.example.yml +1 -1
- package/package.json +13 -13
- package/server/config/auth.ts +1 -1
- package/server/providers/index.ts +2 -0
- package/server/providers/notification-management.ts +27 -0
- package/tests/client/account-permissions.test.tsx +1 -1
- package/tests/client/auth-i18n.test.tsx +1 -1
- package/tests/client/authenticated-client-fixture.test.tsx +37 -0
- package/tests/client/back-button.test.tsx +34 -0
- package/tests/client/client-auth.test.tsx +10 -1
- package/tests/client/client-route-recovery.test.tsx +93 -1
- package/tests/client/client-routes.test.ts +137 -4
- package/tests/client/client-shell.test.tsx +23 -8
- package/tests/client/foundation-integration.test.ts +38 -17
- package/tests/client/layout-i18n.test.tsx +9 -4
- package/tests/client/list-feedback.test.tsx +5 -5
- package/tests/client/list-loading.test.tsx +397 -0
- package/tests/client/loading.test.tsx +20 -11
- package/tests/client/manual-list-retry.test.tsx +2 -2
- package/tests/client/navigation-parent.test.tsx +172 -0
- package/tests/client/pending-button.test.tsx +106 -0
- package/tests/client/permission-management-tabs.test.tsx +458 -17
- package/tests/client/primitive-labels.test.tsx +2 -2
- package/tests/client/registration-feedback.test.tsx +143 -0
- package/tests/client/role-return.test.tsx +21 -3
- package/tests/client/roles.test.tsx +84 -3
- package/tests/client/route-leave-recheck.test.tsx +74 -0
- package/tests/client/route-presentation.test.tsx +557 -0
- package/tests/client/session-preparation.test.tsx +20 -17
- package/tests/client/settings-continuity.test.tsx +35 -11
- package/tests/client/settings-home.test.tsx +112 -0
- package/tests/client/settings-idle-list.test.tsx +124 -0
- package/tests/client/settings-navigation.test.ts +230 -0
- package/tests/client/user-drawer.test.tsx +3 -4
- package/tests/fixtures/dialog-footer.html +12 -0
- package/tests/fixtures/dialog-footer.tsx +215 -0
- package/tests/helpers/authenticated-client.tsx +70 -22
- package/tests/playwright/dialog-footer.test.ts +227 -0
- package/tests/server/notification-templates.test.ts +144 -0
- package/tests/client/permission-management-routes.test.ts +0 -64
|
@@ -5,6 +5,7 @@ import type { ReactElement } from 'react';
|
|
|
5
5
|
import { Link, Navigate } from 'react-router';
|
|
6
6
|
|
|
7
7
|
import { PasswordRegistrationForm } from '#extensions/nocobase-auth-forms/password-registration-form';
|
|
8
|
+
import { AuthFormStatus } from '#extensions/nocobase-auth-forms/form-parts';
|
|
8
9
|
|
|
9
10
|
import { AuthPage } from './shared.js';
|
|
10
11
|
|
|
@@ -17,30 +18,46 @@ export default function RegisterPage(): ReactElement {
|
|
|
17
18
|
|
|
18
19
|
return (
|
|
19
20
|
<AuthPage
|
|
20
|
-
description={t('auth.registerDescription')}
|
|
21
|
-
title={t(
|
|
21
|
+
description={registration.isSuccess ? '' : t('auth.registerDescription')}
|
|
22
|
+
title={t(
|
|
23
|
+
registration.isSuccess
|
|
24
|
+
? 'auth.registrationAccepted'
|
|
25
|
+
: 'auth.registerTitle',
|
|
26
|
+
)}
|
|
22
27
|
>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
{registration.isSuccess ? (
|
|
29
|
+
<div className='space-y-4'>
|
|
30
|
+
<AuthFormStatus type='success'>
|
|
31
|
+
{t('auth.registrationGuidance')}
|
|
32
|
+
</AuthFormStatus>
|
|
33
|
+
<p className='flex justify-center gap-4 text-sm'>
|
|
28
34
|
<Link to='/login'>{t('auth.signIn')}</Link>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
<Link to='/forgot-password'>{t('auth.forgotLink')}</Link>
|
|
36
|
+
</p>
|
|
37
|
+
</div>
|
|
38
|
+
) : (
|
|
39
|
+
<PasswordRegistrationForm
|
|
40
|
+
error={registration.error?.message}
|
|
41
|
+
footer={
|
|
42
|
+
<>
|
|
43
|
+
{t('auth.existingAccount')}{' '}
|
|
44
|
+
<Link to='/login'>{t('auth.signIn')}</Link>
|
|
45
|
+
</>
|
|
46
|
+
}
|
|
47
|
+
labels={{
|
|
48
|
+
confirmPassword: t('auth.confirmPassword'),
|
|
49
|
+
email: t('auth.email'),
|
|
50
|
+
name: t('auth.name'),
|
|
51
|
+
password: t('auth.password'),
|
|
52
|
+
passwordMismatch: t('auth.passwordMismatch'),
|
|
53
|
+
submit: t('auth.createAccount'),
|
|
54
|
+
submitting: t('auth.creatingAccount'),
|
|
55
|
+
username: t('auth.username'),
|
|
56
|
+
}}
|
|
57
|
+
onSubmit={registration.submit}
|
|
58
|
+
submitting={registration.isPending}
|
|
59
|
+
/>
|
|
60
|
+
)}
|
|
44
61
|
</AuthPage>
|
|
45
62
|
);
|
|
46
63
|
}
|
|
@@ -78,12 +78,10 @@ function BrandPanel(): ReactElement {
|
|
|
78
78
|
<p className='text-xs font-semibold tracking-[0.16em] text-(--auth-brand-highlight) uppercase'>
|
|
79
79
|
{t('auth.platform')}
|
|
80
80
|
</p>
|
|
81
|
-
<h2 className='mt-6 text-
|
|
81
|
+
<h2 className='mt-6 text-3xl leading-tight font-semibold tracking-tight'>
|
|
82
82
|
{t('auth.marketingTitleFirst')}
|
|
83
83
|
<br />
|
|
84
84
|
{t('auth.marketingTitleSecond')}
|
|
85
|
-
<br />
|
|
86
|
-
{t('auth.marketingTitleThird')}
|
|
87
85
|
</h2>
|
|
88
86
|
<p className='mt-6 text-sm leading-7 text-(--auth-brand-muted)'>
|
|
89
87
|
{t('auth.marketingDescription')}
|
|
@@ -399,7 +399,7 @@ export default function DefaultAccessPage(): ReactElement {
|
|
|
399
399
|
getRowId={(rule) => rule.key}
|
|
400
400
|
showSelectedCount={false}
|
|
401
401
|
pageSize={20}
|
|
402
|
-
loading={rules.loading || catalog.loading}
|
|
402
|
+
loading={active && (rules.loading || catalog.loading)}
|
|
403
403
|
listState={listState}
|
|
404
404
|
onRetry={() => {
|
|
405
405
|
rules.reload();
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import { useNavigationGuard } from '@nocobase/app-client';
|
|
2
1
|
import { useCan } from '@nocobase/app-plugin-authorization/client';
|
|
3
|
-
import { useTranslation } from '@nocobase/i18n/client';
|
|
4
|
-
import { useEffect, useRef, useState } from 'react';
|
|
5
|
-
import { useLocation } from 'react-router';
|
|
6
|
-
import { ConfirmAction } from './shared.js';
|
|
7
2
|
|
|
8
3
|
/** Compatibility entry for prepared settings pages and application-owned consumers. */
|
|
9
4
|
export { useListData as useSettingsData } from '#hooks/use-list-data';
|
|
@@ -17,66 +12,6 @@ export function useRolePermission(action: string) {
|
|
|
17
12
|
action,
|
|
18
13
|
}).can;
|
|
19
14
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
busy: boolean,
|
|
24
|
-
onDiscard?: () => void,
|
|
25
|
-
) {
|
|
26
|
-
const { t } = useTranslation();
|
|
27
|
-
const location = useLocation();
|
|
28
|
-
const allowedRef = useRef(false);
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
allowedRef.current = false;
|
|
31
|
-
}, [location.key]);
|
|
32
|
-
const pendingRef = useRef<
|
|
33
|
-
{ promise: Promise<boolean>; resolve: (value: boolean) => void } | undefined
|
|
34
|
-
>(undefined);
|
|
35
|
-
const [open, setOpen] = useState(false);
|
|
36
|
-
const guard = () => {
|
|
37
|
-
if (allowedRef.current) return true;
|
|
38
|
-
if (busy) return false;
|
|
39
|
-
if (!dirty) return true;
|
|
40
|
-
if (pendingRef.current) return pendingRef.current.promise;
|
|
41
|
-
let resolve!: (value: boolean) => void;
|
|
42
|
-
const promise = new Promise<boolean>((done) => {
|
|
43
|
-
resolve = done;
|
|
44
|
-
});
|
|
45
|
-
pendingRef.current = { promise, resolve };
|
|
46
|
-
setOpen(true);
|
|
47
|
-
return promise;
|
|
48
|
-
};
|
|
49
|
-
useNavigationGuard(guard);
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
if (!dirty) return;
|
|
52
|
-
const handler = (event: BeforeUnloadEvent) => {
|
|
53
|
-
event.preventDefault();
|
|
54
|
-
event.returnValue = '';
|
|
55
|
-
};
|
|
56
|
-
window.addEventListener('beforeunload', handler);
|
|
57
|
-
return () => window.removeEventListener('beforeunload', handler);
|
|
58
|
-
}, [dirty]);
|
|
59
|
-
const decide = (value: boolean) => {
|
|
60
|
-
if (value) onDiscard?.();
|
|
61
|
-
allowedRef.current = value;
|
|
62
|
-
pendingRef.current?.resolve(value);
|
|
63
|
-
pendingRef.current = undefined;
|
|
64
|
-
setOpen(false);
|
|
65
|
-
};
|
|
66
|
-
return {
|
|
67
|
-
guard,
|
|
68
|
-
allow: () => {
|
|
69
|
-
allowedRef.current = true;
|
|
70
|
-
},
|
|
71
|
-
dialog: (
|
|
72
|
-
<ConfirmAction
|
|
73
|
-
open={open}
|
|
74
|
-
title={t('settings.discardTitle')}
|
|
75
|
-
description={t('settings.discardHint')}
|
|
76
|
-
busy={false}
|
|
77
|
-
onCancel={() => decide(false)}
|
|
78
|
-
onConfirm={() => decide(true)}
|
|
79
|
-
/>
|
|
80
|
-
),
|
|
81
|
-
};
|
|
82
|
-
}
|
|
15
|
+
|
|
16
|
+
/** Leave protection is owned by the shell while page Effects are suspended. */
|
|
17
|
+
export { usePageLeave as useSettingsLeave } from '../../routing/route-leave-boundary.js';
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Bell, FileText, Settings } from 'lucide-react';
|
|
2
|
+
import {
|
|
3
|
+
defineAppRoutes,
|
|
4
|
+
type AppClientRouteContribution,
|
|
5
|
+
type AppClientRouteDefinition,
|
|
6
|
+
} from '@nocobase/app-client/plugins';
|
|
7
|
+
|
|
8
|
+
/** Enable once notification business scenarios are used. Reuse this module for logs and other real views. */
|
|
9
|
+
export const notificationTemplateRoute = {
|
|
10
|
+
name: 'notificationTemplates',
|
|
11
|
+
path: '/settings/notifications/templates',
|
|
12
|
+
auth: 'required' as const,
|
|
13
|
+
authz: {
|
|
14
|
+
resource: { type: 'page', id: 'notification.templates' },
|
|
15
|
+
action: 'access',
|
|
16
|
+
},
|
|
17
|
+
navigation: { title: 'settings.notificationTemplates', icon: FileText },
|
|
18
|
+
breadcrumb: { title: 'settings.notificationTemplates' },
|
|
19
|
+
componentLoader: () => import('./notification-templates.js'),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Add the template view once to the existing module, preserving logs, paths and grants. */
|
|
23
|
+
export function includeNotificationTemplateRoutes(
|
|
24
|
+
contributions: readonly AppClientRouteContribution[],
|
|
25
|
+
invitationConfigured: boolean,
|
|
26
|
+
): readonly AppClientRouteContribution[] {
|
|
27
|
+
const roots = contributions.flatMap((contribution) => contribution.routes);
|
|
28
|
+
const nodes = new Map<
|
|
29
|
+
string,
|
|
30
|
+
{ route: AppClientRouteDefinition; parent?: string }
|
|
31
|
+
>();
|
|
32
|
+
const collect = (
|
|
33
|
+
items: readonly AppClientRouteDefinition[],
|
|
34
|
+
parent?: string,
|
|
35
|
+
): void => {
|
|
36
|
+
for (const route of items) {
|
|
37
|
+
nodes.set(route.name, { route, parent: route.parent ?? parent });
|
|
38
|
+
collect(route.children ?? [], route.name);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
collect(roots);
|
|
42
|
+
const pathOf = (name: string): string => {
|
|
43
|
+
const node = nodes.get(name);
|
|
44
|
+
if (!node) return '';
|
|
45
|
+
const base = node.parent ? pathOf(node.parent) : '';
|
|
46
|
+
return `${base}/${node.route.path ?? ''}`
|
|
47
|
+
.replace(/\/{2,}/g, '/')
|
|
48
|
+
.replace(/\/$/, '');
|
|
49
|
+
};
|
|
50
|
+
const templateUnder = (base: string) => ({
|
|
51
|
+
...notificationTemplateRoute,
|
|
52
|
+
path:
|
|
53
|
+
base && notificationTemplateRoute.path.startsWith(`${base}/`)
|
|
54
|
+
? notificationTemplateRoute.path.slice(base.length)
|
|
55
|
+
: notificationTemplateRoute.path,
|
|
56
|
+
});
|
|
57
|
+
if (nodes.has('notificationTemplates')) return contributions;
|
|
58
|
+
if (nodes.has('notificationManagement')) {
|
|
59
|
+
const add = (
|
|
60
|
+
items: readonly AppClientRouteDefinition[],
|
|
61
|
+
): readonly AppClientRouteDefinition[] =>
|
|
62
|
+
items.map((route) => ({
|
|
63
|
+
...route,
|
|
64
|
+
...(route.name === 'notificationManagement'
|
|
65
|
+
? {
|
|
66
|
+
children: [
|
|
67
|
+
...(route.children ?? []),
|
|
68
|
+
templateUnder(pathOf('notificationManagement')),
|
|
69
|
+
],
|
|
70
|
+
}
|
|
71
|
+
: route.children
|
|
72
|
+
? { children: add(route.children) }
|
|
73
|
+
: {}),
|
|
74
|
+
}));
|
|
75
|
+
return contributions.map((contribution) =>
|
|
76
|
+
defineAppRoutes(add(contribution.routes)),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (!invitationConfigured) return contributions;
|
|
80
|
+
const parent = nodes.has('settingsSystem') ? 'settingsSystem' : 'settings';
|
|
81
|
+
const module = {
|
|
82
|
+
name: 'notificationManagement',
|
|
83
|
+
navigation: {
|
|
84
|
+
title: 'settings.notificationManagement',
|
|
85
|
+
description: 'settings.notificationManagementDescription',
|
|
86
|
+
icon: Bell,
|
|
87
|
+
},
|
|
88
|
+
breadcrumb: { title: 'settings.notificationManagement' },
|
|
89
|
+
children: [templateUnder(pathOf(parent))],
|
|
90
|
+
};
|
|
91
|
+
if (nodes.has(parent))
|
|
92
|
+
return [
|
|
93
|
+
...contributions.slice(0, 1),
|
|
94
|
+
defineAppRoutes([{ ...module, parent }]),
|
|
95
|
+
...contributions.slice(1),
|
|
96
|
+
];
|
|
97
|
+
// A configured built-in invitation is business use even before other settings pages are enabled.
|
|
98
|
+
return [
|
|
99
|
+
...contributions,
|
|
100
|
+
defineAppRoutes([
|
|
101
|
+
{
|
|
102
|
+
name: 'settings',
|
|
103
|
+
navigation: { title: 'settings.title', icon: Settings, order: 100 },
|
|
104
|
+
breadcrumb: { title: 'settings.title' },
|
|
105
|
+
children: [module],
|
|
106
|
+
},
|
|
107
|
+
]),
|
|
108
|
+
];
|
|
109
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import { useSearchParams } from 'react-router';
|
|
3
|
+
import { useTranslation } from '@nocobase/i18n/client';
|
|
4
|
+
import { NotificationTemplates } from '@nocobase/app-plugin-notification/client/templates';
|
|
5
|
+
import { SettingsModulePage } from './settings-module-page.js';
|
|
6
|
+
|
|
7
|
+
export default function NotificationTemplatesPage(): ReactElement {
|
|
8
|
+
const { t } = useTranslation('@nocobase/app-plugin-notification');
|
|
9
|
+
const [query, setQuery] = useSearchParams();
|
|
10
|
+
return (
|
|
11
|
+
// A view of the Notifications settings module: it gains tabs once the module has another view, such as logs.
|
|
12
|
+
<SettingsModulePage title={t('templates.title')}>
|
|
13
|
+
<NotificationTemplates
|
|
14
|
+
selectedId={query.get('template')}
|
|
15
|
+
onSelect={(id) =>
|
|
16
|
+
setQuery((previous) => {
|
|
17
|
+
const next = new URLSearchParams(previous);
|
|
18
|
+
if (id) next.set('template', id);
|
|
19
|
+
else next.delete('template');
|
|
20
|
+
return next;
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
/>
|
|
24
|
+
</SettingsModulePage>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useNavigate, useResolvedPath, useLocation } from 'react-router';
|
|
3
|
-
import { useCan } from '@nocobase/app-plugin-authorization/client';
|
|
1
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
4
2
|
import { useTranslation } from '@nocobase/i18n/client';
|
|
5
|
-
import { PageContainer } from '#components/page-container';
|
|
6
|
-
import { PageHeader } from '#components/page-header';
|
|
7
|
-
import { Tabs, TabsList, TabsTrigger, TabsContent } from '#components/ui/tabs';
|
|
8
3
|
|
|
4
|
+
import { SettingsModulePage } from './settings-module-page.js';
|
|
5
|
+
|
|
6
|
+
/** Permission management: Roles and permissions and Default data scopes as tabs of one settings module. */
|
|
9
7
|
export function PermissionManagementPage({
|
|
10
8
|
tab,
|
|
11
9
|
actions,
|
|
@@ -16,90 +14,17 @@ export function PermissionManagementPage({
|
|
|
16
14
|
children: ReactNode;
|
|
17
15
|
}): ReactElement {
|
|
18
16
|
const { t } = useTranslation();
|
|
19
|
-
const navigate = useNavigate();
|
|
20
|
-
const location = useLocation();
|
|
21
|
-
const activeRef = useRef<HTMLButtonElement>(null);
|
|
22
|
-
const focusedRef = useRef(false);
|
|
23
|
-
const current = useResolvedPath('.').pathname;
|
|
24
|
-
const prefix = current.slice(0, current.lastIndexOf('/'));
|
|
25
|
-
const roles = useCan({
|
|
26
|
-
resource: { type: 'settings', id: 'authorization.permission-sets' },
|
|
27
|
-
action: 'read',
|
|
28
|
-
}).can;
|
|
29
|
-
const defaults = useCan({
|
|
30
|
-
resource: { type: 'settings', id: 'authorization.default-access' },
|
|
31
|
-
action: 'read',
|
|
32
|
-
}).can;
|
|
33
|
-
// Route changes remount each existing page; keep keyboard focus on the selected tab.
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
if (
|
|
36
|
-
!focusedRef.current &&
|
|
37
|
-
location.state &&
|
|
38
|
-
typeof location.state === 'object' &&
|
|
39
|
-
Reflect.get(location.state, 'permissionTabNavigation') === true &&
|
|
40
|
-
activeRef.current
|
|
41
|
-
) {
|
|
42
|
-
const frame = requestAnimationFrame(() => {
|
|
43
|
-
if (activeRef.current) {
|
|
44
|
-
const selected = activeRef.current;
|
|
45
|
-
if (
|
|
46
|
-
!selected
|
|
47
|
-
.closest('[role="tablist"]')
|
|
48
|
-
?.contains(selected.ownerDocument.activeElement)
|
|
49
|
-
)
|
|
50
|
-
selected.focus();
|
|
51
|
-
focusedRef.current = true;
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
return () => cancelAnimationFrame(frame);
|
|
55
|
-
}
|
|
56
|
-
}, [location.state, roles, defaults]);
|
|
57
17
|
return (
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
className='gap-6'
|
|
70
|
-
value={tab}
|
|
71
|
-
onValueChange={(value) => {
|
|
72
|
-
if (value === 'roles' || value === 'default-access')
|
|
73
|
-
void navigate(`${prefix}/${value}`, {
|
|
74
|
-
state: { permissionTabNavigation: true },
|
|
75
|
-
});
|
|
76
|
-
}}
|
|
77
|
-
>
|
|
78
|
-
<TabsList
|
|
79
|
-
variant='line'
|
|
80
|
-
aria-label={t('settings.permissionManagement')}
|
|
81
|
-
>
|
|
82
|
-
{roles && (
|
|
83
|
-
<TabsTrigger
|
|
84
|
-
ref={tab === 'roles' ? activeRef : undefined}
|
|
85
|
-
value='roles'
|
|
86
|
-
>
|
|
87
|
-
{t('settings.roles')}
|
|
88
|
-
</TabsTrigger>
|
|
89
|
-
)}
|
|
90
|
-
{defaults && (
|
|
91
|
-
<TabsTrigger
|
|
92
|
-
ref={tab === 'default-access' ? activeRef : undefined}
|
|
93
|
-
value='default-access'
|
|
94
|
-
>
|
|
95
|
-
{t('settings.defaultAccess.title')}
|
|
96
|
-
</TabsTrigger>
|
|
97
|
-
)}
|
|
98
|
-
</TabsList>
|
|
99
|
-
<TabsContent value={tab} className='space-y-6'>
|
|
100
|
-
{children}
|
|
101
|
-
</TabsContent>
|
|
102
|
-
</Tabs>
|
|
103
|
-
</PageContainer>
|
|
18
|
+
<SettingsModulePage
|
|
19
|
+
title={t('settings.permissionManagement')}
|
|
20
|
+
description={t(
|
|
21
|
+
tab === 'roles'
|
|
22
|
+
? 'settings.rolesDescription'
|
|
23
|
+
: 'settings.defaultAccess.description',
|
|
24
|
+
)}
|
|
25
|
+
actions={actions}
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</SettingsModulePage>
|
|
104
29
|
);
|
|
105
30
|
}
|
|
@@ -12,7 +12,6 @@ import { PlusIcon } from 'lucide-react';
|
|
|
12
12
|
import { PermissionManagementPage } from './permission-management-page.js';
|
|
13
13
|
import { Input } from '#components/ui/input';
|
|
14
14
|
import { Button } from '#components/ui/button';
|
|
15
|
-
import { Spinner } from '#components/ui/spinner';
|
|
16
15
|
import { useTableUrlState } from '#hooks/use-table-url-state';
|
|
17
16
|
import { RoleList } from '#components/role-list';
|
|
18
17
|
import { ListFeedback, ListFeedbackActions } from '#components/list-feedback';
|
|
@@ -123,34 +122,35 @@ export default function RolesPage(): ReactElement {
|
|
|
123
122
|
state={listState}
|
|
124
123
|
onClear={clearSearch}
|
|
125
124
|
onRetry={request.reload}
|
|
126
|
-
busy={request.loading}
|
|
125
|
+
busy={active && request.loading}
|
|
127
126
|
/>
|
|
128
|
-
{active && request.loading && request.data ? (
|
|
129
|
-
<Spinner className='text-muted-foreground' />
|
|
130
|
-
) : null}
|
|
131
127
|
</div>
|
|
132
|
-
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
128
|
+
{active || listState !== 'loading' ? (
|
|
129
|
+
<ListFeedback state={listState}>
|
|
130
|
+
<RoleList
|
|
131
|
+
tableState={tableState}
|
|
132
|
+
// After a refresh failure the toolbar's retry button owns the wait.
|
|
133
|
+
loading={active && request.loading && listState === 'ready'}
|
|
134
|
+
ariaLabel={t('settings.roles')}
|
|
135
|
+
roles={rows.map((role) => ({
|
|
136
|
+
key: role.key,
|
|
137
|
+
title: text(role.title ?? role.key),
|
|
138
|
+
to: {
|
|
139
|
+
pathname: `${encodeURIComponent(role.key)}/edit`,
|
|
140
|
+
search: location.search,
|
|
141
|
+
},
|
|
142
|
+
protected: Boolean(role.protection),
|
|
143
|
+
}))}
|
|
144
|
+
labels={{
|
|
145
|
+
name: t('settings.roleName'),
|
|
146
|
+
key: t('settings.key'),
|
|
147
|
+
type: t('settings.roleType'),
|
|
148
|
+
protected: t('settings.protected'),
|
|
149
|
+
custom: t('settings.custom'),
|
|
150
|
+
}}
|
|
151
|
+
/>
|
|
152
|
+
</ListFeedback>
|
|
153
|
+
) : null}
|
|
154
154
|
</PermissionManagementPage>
|
|
155
155
|
<Outlet context={{ reload: request.reload }} />
|
|
156
156
|
</>
|