@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.
Files changed (49) hide show
  1. package/.agents/rules/notification-templates.md +5 -0
  2. package/.agents/rules/shell.md +2 -2
  3. package/AGENTS.md +3 -1
  4. package/README.MD +11 -5
  5. package/client/components/body-portal.tsx +25 -0
  6. package/client/components/breadcrumbs.tsx +13 -0
  7. package/client/components/flow-diagram.tsx +2 -2
  8. package/client/layouts/app-layout.tsx +8 -4
  9. package/client/layouts/components/sidebar-footer.tsx +18 -25
  10. package/client/lib/navigation-transform.ts +156 -22
  11. package/client/locales/system/en-US.ts +7 -5
  12. package/client/locales/system/settings-en-US.ts +17 -0
  13. package/client/locales/system/settings-zh-CN.ts +15 -0
  14. package/client/locales/system/zh-CN.ts +7 -5
  15. package/client/pages/auth/register.tsx +39 -22
  16. package/client/pages/auth/shared.tsx +1 -3
  17. package/client/pages/settings/hooks.tsx +3 -68
  18. package/client/pages/settings/notification-routes.ts +109 -0
  19. package/client/pages/settings/notification-templates.tsx +26 -0
  20. package/client/pages/settings/permission-management-page.tsx +15 -90
  21. package/client/pages/settings/routes.ts +96 -98
  22. package/client/pages/settings/settings-home.tsx +109 -0
  23. package/client/pages/settings/settings-module-page.tsx +158 -0
  24. package/client/pages/settings/shell-routes.ts +53 -0
  25. package/client/routes.ts +17 -1
  26. package/client/routing/app-router.tsx +1 -14
  27. package/client/routing/client-route.tsx +16 -9
  28. package/client/routing/route-context.tsx +30 -3
  29. package/client/routing/route-leave-boundary.tsx +249 -0
  30. package/client/routing/route-navigation.ts +72 -8
  31. package/client/routing/route-tree.tsx +7 -12
  32. package/client/runtime.ts +2 -2
  33. package/config.example.yml +1 -1
  34. package/package.json +12 -12
  35. package/server/config/auth.ts +1 -1
  36. package/server/providers/index.ts +2 -0
  37. package/server/providers/notification-management.ts +27 -0
  38. package/tests/client/account-permissions.test.tsx +1 -1
  39. package/tests/client/client-auth.test.tsx +10 -1
  40. package/tests/client/client-routes.test.ts +137 -4
  41. package/tests/client/client-shell.test.tsx +23 -8
  42. package/tests/client/foundation-integration.test.ts +38 -17
  43. package/tests/client/layout-i18n.test.tsx +9 -4
  44. package/tests/client/permission-management-tabs.test.tsx +421 -16
  45. package/tests/client/registration-feedback.test.tsx +143 -0
  46. package/tests/client/settings-home.test.tsx +110 -0
  47. package/tests/client/settings-navigation.test.ts +230 -0
  48. package/tests/server/notification-templates.test.ts +144 -0
  49. 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('auth.registerTitle')}
21
+ description={registration.isSuccess ? '' : t('auth.registerDescription')}
22
+ title={t(
23
+ registration.isSuccess
24
+ ? 'auth.registrationAccepted'
25
+ : 'auth.registerTitle',
26
+ )}
22
27
  >
23
- <PasswordRegistrationForm
24
- error={registration.error?.message}
25
- footer={
26
- <>
27
- {t('auth.existingAccount')}{' '}
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
- labels={{
32
- confirmPassword: t('auth.confirmPassword'),
33
- email: t('auth.email'),
34
- name: t('auth.name'),
35
- password: t('auth.password'),
36
- passwordMismatch: t('auth.passwordMismatch'),
37
- submit: t('auth.createAccount'),
38
- submitting: t('auth.creatingAccount'),
39
- username: t('auth.username'),
40
- }}
41
- onSubmit={registration.submit}
42
- submitting={registration.isPending}
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-4xl leading-tight font-semibold tracking-tight'>
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
- /** The same decision covers overlay close, sidebar links, browser history and refresh. */
21
- export function useSettingsLeave(
22
- dirty: boolean,
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 { useEffect, useRef, type ReactElement, type ReactNode } from 'react';
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
- <PageContainer>
59
- <PageHeader
60
- title={t('settings.permissionManagement')}
61
- description={t(
62
- tab === 'roles'
63
- ? 'settings.rolesDescription'
64
- : 'settings.defaultAccess.description',
65
- )}
66
- actions={actions}
67
- />
68
- <Tabs
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
- /** Register this contribution as business capabilities are developed; keep a bare Home-only application free of settings. */
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
- name: 'settings',
17
- path: '/settings',
18
- navigation: { title: 'settings.title', icon: Settings, order: 100 },
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: 'settingsUsers',
22
- path: '/users',
23
- auth: 'required',
24
- authz: { resource: { type: 'user', id: '*' }, action: 'read' },
25
- navigation: { title: 'settings.users', icon: Users },
26
- breadcrumb: { title: 'settings.users' },
27
- componentLoader: () => import('./users.js'),
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: 'settingsUserCreate',
31
- path: 'create',
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: 'permissionManagement',
51
- navigation: {
52
- title: 'settings.permissionManagement',
53
- icon: ShieldCheck,
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
- breadcrumb: { title: 'settings.permissionManagement' },
68
+ navigation: { title: 'settings.roles', icon: KeyRound },
69
+ breadcrumb: { title: 'settings.roles' },
70
+ componentLoader: () => import('./roles.js'),
56
71
  children: [
57
72
  {
58
- name: 'settingsRoles',
59
- path: '/roles',
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: 'read',
80
+ action: 'create',
67
81
  },
68
- navigation: { title: 'settings.roles', icon: KeyRound },
69
- breadcrumb: { title: 'settings.roles' },
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: 'settingsDefaultAccess',
95
- path: '/default-access',
96
- auth: 'required',
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: 'read',
103
- },
104
- navigation: {
105
- title: 'settings.defaultAccess.title',
106
- icon: ListFilter,
119
+ action: 'create',
107
120
  },
108
- breadcrumb: { title: 'settings.defaultAccess.title' },
109
- componentLoader: () => import('./default-access.js'),
110
- children: [
111
- {
112
- name: 'settingsDefaultAccessCreate',
113
- path: 'create',
114
- authz: {
115
- resource: {
116
- type: 'settings',
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
  },