@nocobase/app-template-default 1.0.0-beta.87 → 1.0.0-beta.88
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/notification-templates.md +5 -0
- package/.agents/rules/shell.md +2 -2
- package/AGENTS.md +3 -1
- package/README.MD +11 -5
- package/client/components/body-portal.tsx +25 -0
- package/client/components/breadcrumbs.tsx +13 -0
- package/client/components/flow-diagram.tsx +2 -2
- package/client/layouts/app-layout.tsx +8 -4
- package/client/layouts/components/sidebar-footer.tsx +18 -25
- package/client/lib/navigation-transform.ts +156 -22
- package/client/locales/system/en-US.ts +7 -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 +7 -5
- package/client/pages/auth/register.tsx +39 -22
- package/client/pages/auth/shared.tsx +1 -3
- 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/routes.ts +96 -98
- 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/routes.ts +17 -1
- package/client/routing/app-router.tsx +1 -14
- package/client/routing/client-route.tsx +16 -9
- package/client/routing/route-context.tsx +30 -3
- package/client/routing/route-leave-boundary.tsx +249 -0
- package/client/routing/route-navigation.ts +72 -8
- package/client/routing/route-tree.tsx +7 -12
- package/client/runtime.ts +2 -2
- package/config.example.yml +1 -1
- package/package.json +12 -12
- 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/client-auth.test.tsx +10 -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/permission-management-tabs.test.tsx +421 -16
- package/tests/client/registration-feedback.test.tsx +143 -0
- package/tests/client/settings-home.test.tsx +110 -0
- package/tests/client/settings-navigation.test.ts +230 -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')}
|
|
@@ -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
|
}
|
|
@@ -1,134 +1,132 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Settings,
|
|
3
|
-
Users,
|
|
4
|
-
ShieldCheck,
|
|
5
|
-
KeyRound,
|
|
6
|
-
ListFilter,
|
|
7
|
-
} from 'lucide-react';
|
|
1
|
+
import { Users, ShieldCheck, KeyRound, ListFilter } from 'lucide-react';
|
|
8
2
|
import {
|
|
9
3
|
defineAppRoutes,
|
|
10
4
|
type AppClientRouteContribution,
|
|
11
5
|
} from '@nocobase/app-client/plugins';
|
|
12
6
|
|
|
13
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Users and Permission management, in the Accounts and permissions category of the System settings shell
|
|
9
|
+
* (`shell-routes.ts`). Register this contribution as business capabilities are developed; a bare application stays
|
|
10
|
+
* without them.
|
|
11
|
+
*/
|
|
14
12
|
const systemSettingsRoutes: AppClientRouteContribution = defineAppRoutes([
|
|
15
13
|
{
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
parent: 'settingsAccount',
|
|
15
|
+
name: 'settingsUsers',
|
|
16
|
+
path: '/users',
|
|
17
|
+
auth: 'required',
|
|
18
|
+
authz: { resource: { type: 'user', id: '*' }, action: 'read' },
|
|
19
|
+
navigation: {
|
|
20
|
+
title: 'settings.users',
|
|
21
|
+
description: 'settings.usersDescription',
|
|
22
|
+
icon: Users,
|
|
23
|
+
},
|
|
24
|
+
breadcrumb: { title: 'settings.users' },
|
|
25
|
+
componentLoader: () => import('./users.js'),
|
|
19
26
|
children: [
|
|
20
27
|
{
|
|
21
|
-
name: '
|
|
22
|
-
path: '
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
name: 'settingsUserCreate',
|
|
29
|
+
path: 'create',
|
|
30
|
+
authz: { resource: { type: 'user', id: '*' }, action: 'create' },
|
|
31
|
+
componentLoader: () => import('./user-form.js'),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'settingsUser',
|
|
35
|
+
path: ':userId',
|
|
36
|
+
componentLoader: () => import('./user-detail.js'),
|
|
28
37
|
children: [
|
|
29
38
|
{
|
|
30
|
-
name: '
|
|
31
|
-
path: '
|
|
32
|
-
authz: { resource: { type: 'user', id: '*' }, action: 'create' },
|
|
39
|
+
name: 'settingsUserEdit',
|
|
40
|
+
path: 'edit',
|
|
33
41
|
componentLoader: () => import('./user-form.js'),
|
|
34
42
|
},
|
|
35
|
-
{
|
|
36
|
-
name: 'settingsUser',
|
|
37
|
-
path: ':userId',
|
|
38
|
-
componentLoader: () => import('./user-detail.js'),
|
|
39
|
-
children: [
|
|
40
|
-
{
|
|
41
|
-
name: 'settingsUserEdit',
|
|
42
|
-
path: 'edit',
|
|
43
|
-
componentLoader: () => import('./user-form.js'),
|
|
44
|
-
},
|
|
45
|
-
],
|
|
46
|
-
},
|
|
47
43
|
],
|
|
48
44
|
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
parent: 'settingsAccount',
|
|
49
|
+
name: 'permissionManagement',
|
|
50
|
+
navigation: {
|
|
51
|
+
title: 'settings.permissionManagement',
|
|
52
|
+
description: 'settings.permissionManagementDescription',
|
|
53
|
+
icon: ShieldCheck,
|
|
54
|
+
},
|
|
55
|
+
breadcrumb: { title: 'settings.permissionManagement' },
|
|
56
|
+
children: [
|
|
49
57
|
{
|
|
50
|
-
name: '
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
name: 'settingsRoles',
|
|
59
|
+
path: '/roles',
|
|
60
|
+
auth: 'required',
|
|
61
|
+
authz: {
|
|
62
|
+
resource: {
|
|
63
|
+
type: 'settings',
|
|
64
|
+
id: 'authorization.permission-sets',
|
|
65
|
+
},
|
|
66
|
+
action: 'read',
|
|
54
67
|
},
|
|
55
|
-
|
|
68
|
+
navigation: { title: 'settings.roles', icon: KeyRound },
|
|
69
|
+
breadcrumb: { title: 'settings.roles' },
|
|
70
|
+
componentLoader: () => import('./roles.js'),
|
|
56
71
|
children: [
|
|
57
72
|
{
|
|
58
|
-
name: '
|
|
59
|
-
path: '
|
|
60
|
-
auth: 'required',
|
|
73
|
+
name: 'settingsRoleCreate',
|
|
74
|
+
path: 'create',
|
|
61
75
|
authz: {
|
|
62
76
|
resource: {
|
|
63
77
|
type: 'settings',
|
|
64
78
|
id: 'authorization.permission-sets',
|
|
65
79
|
},
|
|
66
|
-
action: '
|
|
80
|
+
action: 'create',
|
|
67
81
|
},
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
componentLoader: () => import('./roles.js'),
|
|
71
|
-
children: [
|
|
72
|
-
{
|
|
73
|
-
name: 'settingsRoleCreate',
|
|
74
|
-
path: 'create',
|
|
75
|
-
authz: {
|
|
76
|
-
resource: {
|
|
77
|
-
type: 'settings',
|
|
78
|
-
id: 'authorization.permission-sets',
|
|
79
|
-
},
|
|
80
|
-
action: 'create',
|
|
81
|
-
},
|
|
82
|
-
breadcrumb: { title: 'settings.newRole' },
|
|
83
|
-
componentLoader: () => import('./role-form.js'),
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
name: 'settingsRoleEdit',
|
|
87
|
-
path: ':key/edit',
|
|
88
|
-
breadcrumb: { title: 'settings.editRole' },
|
|
89
|
-
componentLoader: () => import('./role-form.js'),
|
|
90
|
-
},
|
|
91
|
-
],
|
|
82
|
+
breadcrumb: { title: 'settings.newRole' },
|
|
83
|
+
componentLoader: () => import('./role-form.js'),
|
|
92
84
|
},
|
|
93
85
|
{
|
|
94
|
-
name: '
|
|
95
|
-
path: '/
|
|
96
|
-
|
|
86
|
+
name: 'settingsRoleEdit',
|
|
87
|
+
path: ':key/edit',
|
|
88
|
+
breadcrumb: { title: 'settings.editRole' },
|
|
89
|
+
componentLoader: () => import('./role-form.js'),
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'settingsDefaultAccess',
|
|
95
|
+
path: '/default-access',
|
|
96
|
+
auth: 'required',
|
|
97
|
+
authz: {
|
|
98
|
+
resource: {
|
|
99
|
+
type: 'settings',
|
|
100
|
+
id: 'authorization.default-access',
|
|
101
|
+
},
|
|
102
|
+
action: 'read',
|
|
103
|
+
},
|
|
104
|
+
navigation: {
|
|
105
|
+
title: 'settings.defaultAccess.title',
|
|
106
|
+
icon: ListFilter,
|
|
107
|
+
},
|
|
108
|
+
breadcrumb: { title: 'settings.defaultAccess.title' },
|
|
109
|
+
componentLoader: () => import('./default-access.js'),
|
|
110
|
+
children: [
|
|
111
|
+
{
|
|
112
|
+
name: 'settingsDefaultAccessCreate',
|
|
113
|
+
path: 'create',
|
|
97
114
|
authz: {
|
|
98
115
|
resource: {
|
|
99
116
|
type: 'settings',
|
|
100
117
|
id: 'authorization.default-access',
|
|
101
118
|
},
|
|
102
|
-
action: '
|
|
103
|
-
},
|
|
104
|
-
navigation: {
|
|
105
|
-
title: 'settings.defaultAccess.title',
|
|
106
|
-
icon: ListFilter,
|
|
119
|
+
action: 'create',
|
|
107
120
|
},
|
|
108
|
-
breadcrumb: { title: 'settings.defaultAccess.
|
|
109
|
-
componentLoader: () => import('./default-access.js'),
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
id: 'authorization.default-access',
|
|
118
|
-
},
|
|
119
|
-
action: 'create',
|
|
120
|
-
},
|
|
121
|
-
breadcrumb: { title: 'settings.defaultAccess.new' },
|
|
122
|
-
componentLoader: () => import('./default-access-form.js'),
|
|
123
|
-
},
|
|
124
|
-
// Read permission opens the full rule even when the viewer may not update it.
|
|
125
|
-
{
|
|
126
|
-
name: 'settingsDefaultAccessEdit',
|
|
127
|
-
path: ':key/edit',
|
|
128
|
-
breadcrumb: { title: 'settings.defaultAccess.edit' },
|
|
129
|
-
componentLoader: () => import('./default-access-form.js'),
|
|
130
|
-
},
|
|
131
|
-
],
|
|
121
|
+
breadcrumb: { title: 'settings.defaultAccess.new' },
|
|
122
|
+
componentLoader: () => import('./default-access-form.js'),
|
|
123
|
+
},
|
|
124
|
+
// Read permission opens the full rule even when the viewer may not update it.
|
|
125
|
+
{
|
|
126
|
+
name: 'settingsDefaultAccessEdit',
|
|
127
|
+
path: ':key/edit',
|
|
128
|
+
breadcrumb: { title: 'settings.defaultAccess.edit' },
|
|
129
|
+
componentLoader: () => import('./default-access-form.js'),
|
|
132
130
|
},
|
|
133
131
|
],
|
|
134
132
|
},
|