@nocobase/app-template-default 1.0.0-beta.86 → 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 +4 -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/extensions/nocobase-file-component-ui/components/previewers/file-preview-content.tsx +1 -1
- package/client/layouts/app-layout.tsx +8 -4
- package/client/layouts/components/header-actions.tsx +0 -2
- package/client/layouts/components/sidebar-footer.tsx +18 -25
- package/client/layouts/components/user-menu.tsx +6 -13
- package/client/lib/navigation-transform.ts +156 -22
- package/client/locales/system/en-US.ts +8 -6
- 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 +8 -6
- 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/routing/standalone-page-layout.tsx +0 -5
- package/client/runtime.ts +2 -2
- package/client/theme/theme-settings.tsx +60 -162
- package/config.example.yml +51 -32
- package/package.json +28 -28
- package/server/config/auth.ts +2 -2
- package/server/config/index.ts +3 -0
- package/server/config/mailers.ts +11 -0
- 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 +29 -10
- package/tests/client/client-theme.test.tsx +118 -39
- package/tests/client/foundation-integration.test.ts +38 -17
- package/tests/client/header-hover.test.tsx +33 -5
- 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/client/standalone-page-layout.test.tsx +45 -0
- package/tests/client/user-menu-sign-out.test.tsx +38 -14
- package/tests/client/user-menu.test.tsx +65 -0
- package/tests/fixtures/theme-tokens.tsx +13 -1
- package/tests/server/auth-email-config.test.ts +51 -17
- package/tests/server/notification-templates.test.ts +144 -0
- package/tests/client/permission-management-routes.test.ts +0 -64
|
@@ -6,9 +6,139 @@ import {
|
|
|
6
6
|
import { describe, expect, it } from 'vitest';
|
|
7
7
|
|
|
8
8
|
import systemSettingsRoutes from '../../client/pages/settings/routes.js';
|
|
9
|
-
import applicationRoutes
|
|
9
|
+
import applicationRoutes, {
|
|
10
|
+
createApplicationRoutes,
|
|
11
|
+
} from '../../client/routes.ts';
|
|
12
|
+
import { includeNotificationTemplateRoutes } from '../../client/pages/settings/notification-routes.js';
|
|
13
|
+
import { defineAppRoutes } from '@nocobase/app-client/plugins';
|
|
10
14
|
|
|
11
15
|
describe('app client routes', () => {
|
|
16
|
+
it('joins the settings system category across contributions without moving the log URL', () => {
|
|
17
|
+
const shell = defineAppRoutes([
|
|
18
|
+
{
|
|
19
|
+
name: 'settings',
|
|
20
|
+
path: '/settings',
|
|
21
|
+
navigation: { title: 'Settings' },
|
|
22
|
+
children: [
|
|
23
|
+
{
|
|
24
|
+
name: 'settingsSystem',
|
|
25
|
+
navigation: { title: 'System' },
|
|
26
|
+
children: [],
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
]);
|
|
31
|
+
const logs = defineAppRoutes([
|
|
32
|
+
{
|
|
33
|
+
name: 'notificationManagement',
|
|
34
|
+
parent: 'settingsSystem',
|
|
35
|
+
navigation: { title: 'Notifications' },
|
|
36
|
+
children: [
|
|
37
|
+
{
|
|
38
|
+
name: 'notificationLogs',
|
|
39
|
+
path: '/notifications/logs',
|
|
40
|
+
auth: 'required',
|
|
41
|
+
authz: {
|
|
42
|
+
resource: { type: 'page', id: 'notification.logs' },
|
|
43
|
+
action: 'access',
|
|
44
|
+
},
|
|
45
|
+
componentLoader: () =>
|
|
46
|
+
import('../../client/pages/settings/notification-templates.js'),
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
]);
|
|
51
|
+
const routes = includeNotificationTemplateRoutes([shell, logs], true);
|
|
52
|
+
const built = resolveAppClientContributions([
|
|
53
|
+
{
|
|
54
|
+
packageName: '@nocobase/app-template-default',
|
|
55
|
+
source: 'application',
|
|
56
|
+
routes,
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
expect(pagePaths(built.routes)).toEqual([
|
|
60
|
+
'/settings/notifications/logs',
|
|
61
|
+
'/settings/notifications/templates',
|
|
62
|
+
]);
|
|
63
|
+
expect(built.routes[0].children[0].children).toHaveLength(1);
|
|
64
|
+
const created = includeNotificationTemplateRoutes([shell], true);
|
|
65
|
+
expect(
|
|
66
|
+
created
|
|
67
|
+
.flatMap((part) => part.routes)
|
|
68
|
+
.find((route) => route.name === 'notificationManagement')?.parent,
|
|
69
|
+
).toBe('settingsSystem');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('exposes configured invitation templates through the real application route factory', async () => {
|
|
73
|
+
const built = resolveAppClientContributions([
|
|
74
|
+
{
|
|
75
|
+
packageName: '@nocobase/app-template-default',
|
|
76
|
+
source: 'application',
|
|
77
|
+
routes: createApplicationRoutes({
|
|
78
|
+
notificationTemplates: { invitationConfigured: true },
|
|
79
|
+
}),
|
|
80
|
+
},
|
|
81
|
+
]);
|
|
82
|
+
expect(pagePaths(built.routes)).toContain(
|
|
83
|
+
'/settings/notifications/templates',
|
|
84
|
+
);
|
|
85
|
+
expect(pageAuthorizations(built.routes)).toContainEqual({
|
|
86
|
+
name: 'notificationTemplates',
|
|
87
|
+
authorizedAs: 'notification.templates',
|
|
88
|
+
});
|
|
89
|
+
for (const loader of componentLoadersIn(built.routes))
|
|
90
|
+
await expect(loader()).resolves.toMatchObject({
|
|
91
|
+
default: expect.any(Function),
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('reuses the notification module and preserves its logs and existing grants', () => {
|
|
96
|
+
const existing = defineAppRoutes([
|
|
97
|
+
{
|
|
98
|
+
name: 'settings',
|
|
99
|
+
path: '/settings',
|
|
100
|
+
navigation: { title: 'Settings' },
|
|
101
|
+
children: [
|
|
102
|
+
{
|
|
103
|
+
name: 'notificationManagement',
|
|
104
|
+
navigation: { title: 'Notifications' },
|
|
105
|
+
children: [
|
|
106
|
+
{
|
|
107
|
+
name: 'notificationLogs',
|
|
108
|
+
path: '/notification-logs',
|
|
109
|
+
auth: 'required',
|
|
110
|
+
authz: {
|
|
111
|
+
resource: { type: 'page', id: 'existing.notification.logs' },
|
|
112
|
+
action: 'access',
|
|
113
|
+
},
|
|
114
|
+
componentLoader: () =>
|
|
115
|
+
import('../../client/pages/settings/notification-templates.js'),
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
]);
|
|
122
|
+
const once = includeNotificationTemplateRoutes([existing], false);
|
|
123
|
+
expect(includeNotificationTemplateRoutes(once, true)).toBe(once);
|
|
124
|
+
const built = resolveAppClientContributions([
|
|
125
|
+
{
|
|
126
|
+
packageName: '@nocobase/app-template-default',
|
|
127
|
+
source: 'application',
|
|
128
|
+
routes: once,
|
|
129
|
+
},
|
|
130
|
+
]);
|
|
131
|
+
expect(pagePaths(built.routes)).toEqual([
|
|
132
|
+
'/settings/notification-logs',
|
|
133
|
+
'/settings/notifications/templates',
|
|
134
|
+
]);
|
|
135
|
+
expect(pageAuthorizations(built.routes)).toEqual([
|
|
136
|
+
{ name: 'notificationLogs', authorizedAs: 'existing.notification.logs' },
|
|
137
|
+
{ name: 'notificationTemplates', authorizedAs: 'notification.templates' },
|
|
138
|
+
]);
|
|
139
|
+
expect(built.routes[0].children).toHaveLength(1);
|
|
140
|
+
});
|
|
141
|
+
|
|
12
142
|
it('keeps the landing page and the authentication pages', () => {
|
|
13
143
|
// The authentication plugin and this application have to agree on these paths: it sends an unknown visitor to
|
|
14
144
|
// /login, sends a signed-in user who opens a guest page back to /, and mails a reset link to /reset-password,
|
|
@@ -55,15 +185,17 @@ describe('app client routes', () => {
|
|
|
55
185
|
expect(pageAuthorizations(resolved.routes)).toEqual([
|
|
56
186
|
{ name: 'home', authorizedAs: null },
|
|
57
187
|
{ name: 'inbox', authorizedAs: null },
|
|
188
|
+
{ name: 'settingsHome', authorizedAs: null },
|
|
58
189
|
]);
|
|
59
190
|
});
|
|
60
191
|
|
|
61
|
-
it('keeps a bare application
|
|
192
|
+
it('keeps a bare application free of settings modules and supports settings registration as business capabilities are developed', async () => {
|
|
193
|
+
// Only the settings home, which lists what the viewer may open and grants nothing itself.
|
|
62
194
|
expect(
|
|
63
|
-
pagePaths(resolveRoutes().routes).
|
|
195
|
+
pagePaths(resolveRoutes().routes).filter((path) =>
|
|
64
196
|
path.startsWith('/settings'),
|
|
65
197
|
),
|
|
66
|
-
).
|
|
198
|
+
).toEqual(['/settings']);
|
|
67
199
|
const built = resolveAppClientContributions([
|
|
68
200
|
{
|
|
69
201
|
packageName: '@nocobase/app-template-default',
|
|
@@ -74,6 +206,7 @@ describe('app client routes', () => {
|
|
|
74
206
|
expect(pageAuthorizations(built.routes)).toEqual([
|
|
75
207
|
{ name: 'home', authorizedAs: null },
|
|
76
208
|
{ name: 'inbox', authorizedAs: null },
|
|
209
|
+
{ name: 'settingsHome', authorizedAs: null },
|
|
77
210
|
{ name: 'settingsUsers', authorizedAs: 'user:*' },
|
|
78
211
|
{ name: 'settingsUserCreate', authorizedAs: 'user:*' },
|
|
79
212
|
{ name: 'settingsUser', authorizedAs: 'user:*' },
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
apiClientToken,
|
|
7
7
|
ClientApplicationContext,
|
|
8
|
+
NavigationGuardProvider,
|
|
8
9
|
type ClientApplication,
|
|
9
10
|
createAppClientConfig,
|
|
10
11
|
} from '@nocobase/app-client';
|
|
@@ -83,19 +84,21 @@ describe('application shell', () => {
|
|
|
83
84
|
expect(
|
|
84
85
|
await screen.findByRole('button', { name: 'Open account menu' }),
|
|
85
86
|
).not.toHaveAttribute('title');
|
|
86
|
-
expect(
|
|
87
|
+
expect(
|
|
88
|
+
screen.queryByRole('button', { name: 'Theme' }),
|
|
89
|
+
).not.toBeInTheDocument();
|
|
87
90
|
expect(
|
|
88
91
|
screen.queryByRole('link', { name: 'Settings' }),
|
|
89
92
|
).not.toBeInTheDocument();
|
|
90
|
-
expect(screen.getByText('
|
|
93
|
+
expect(screen.getByText('Build reliable business systems')).toBeVisible();
|
|
91
94
|
expect(screen.getByRole('link', { name: 'NocoBase' })).toHaveAttribute(
|
|
92
95
|
'href',
|
|
93
96
|
'https://www.nocobase.com',
|
|
94
97
|
);
|
|
95
98
|
expect(
|
|
96
99
|
screen.getByText('NocoBase', { selector: 'a' }).parentElement,
|
|
97
|
-
).toHaveTextContent('
|
|
98
|
-
expect(screen.
|
|
100
|
+
).toHaveTextContent('AI + NocoBase');
|
|
101
|
+
expect(screen.queryByText('Default Template')).not.toBeInTheDocument();
|
|
99
102
|
expect(screen.getByText('v0.0.0')).toHaveClass('truncate');
|
|
100
103
|
// shadcn's edge rail, with a translated label in place of its built-in one.
|
|
101
104
|
expect(
|
|
@@ -199,13 +202,25 @@ describe('application shell', () => {
|
|
|
199
202
|
expect(
|
|
200
203
|
await screen.findByRole('button', { name: 'Expand navigation' }),
|
|
201
204
|
).toHaveAttribute('aria-pressed', 'true');
|
|
202
|
-
// The footer keeps only its shield; focusing it shows the slogan
|
|
205
|
+
// The footer keeps only its shield; focusing it shows the slogan and version.
|
|
203
206
|
const footerIcon = screen.getByRole('img', {
|
|
204
|
-
name: 'AI
|
|
207
|
+
name: 'AI + NocoBase Build reliable business systems v0.0.0',
|
|
205
208
|
});
|
|
206
209
|
act(() => footerIcon.focus());
|
|
207
210
|
expect(
|
|
208
|
-
await screen.findByText('
|
|
211
|
+
await screen.findByText('v0.0.0', {
|
|
212
|
+
selector: '[data-slot="tooltip-content"] span',
|
|
213
|
+
}),
|
|
214
|
+
).toBeVisible();
|
|
215
|
+
|
|
216
|
+
expect(screen.queryByText(/Default Template/)).not.toBeInTheDocument();
|
|
217
|
+
expect(
|
|
218
|
+
screen.getByText('AI + NocoBase', {
|
|
219
|
+
selector: '[data-slot="tooltip-content"] span',
|
|
220
|
+
}),
|
|
221
|
+
).toBeVisible();
|
|
222
|
+
expect(
|
|
223
|
+
screen.getByText('Build reliable business systems', {
|
|
209
224
|
selector: '[data-slot="tooltip-content"] span',
|
|
210
225
|
}),
|
|
211
226
|
).toBeVisible();
|
|
@@ -221,7 +236,7 @@ describe('application shell', () => {
|
|
|
221
236
|
),
|
|
222
237
|
);
|
|
223
238
|
expect(
|
|
224
|
-
screen.queryByRole('img', { name: /
|
|
239
|
+
screen.queryByRole('img', { name: /AI \+ NocoBase/ }),
|
|
225
240
|
).not.toBeInTheDocument();
|
|
226
241
|
});
|
|
227
242
|
|
|
@@ -261,7 +276,9 @@ describe('application shell', () => {
|
|
|
261
276
|
expect(
|
|
262
277
|
screen.queryByRole('navigation', { name: 'Application navigation' }),
|
|
263
278
|
).not.toBeInTheDocument();
|
|
264
|
-
expect(
|
|
279
|
+
expect(
|
|
280
|
+
screen.queryByRole('button', { name: 'Theme' }),
|
|
281
|
+
).not.toBeInTheDocument();
|
|
265
282
|
});
|
|
266
283
|
});
|
|
267
284
|
|
|
@@ -312,7 +329,9 @@ function renderApplication(
|
|
|
312
329
|
<SidebarPreferenceProvider
|
|
313
330
|
value={createAppSidebarPreferenceStore('/main')}
|
|
314
331
|
>
|
|
315
|
-
<
|
|
332
|
+
<NavigationGuardProvider>
|
|
333
|
+
<AppRouter clientRoutes={clientRoutes} />
|
|
334
|
+
</NavigationGuardProvider>
|
|
316
335
|
</SidebarPreferenceProvider>
|
|
317
336
|
</AppThemeProvider>
|
|
318
337
|
</MemoryRouter>
|
|
@@ -16,10 +16,27 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
16
16
|
|
|
17
17
|
import {
|
|
18
18
|
AppThemeProvider,
|
|
19
|
-
ThemeSettings,
|
|
19
|
+
ThemeSettings as ThemeMenu,
|
|
20
20
|
useTheme,
|
|
21
21
|
} from '../../client/theme/index.ts';
|
|
22
22
|
|
|
23
|
+
import {
|
|
24
|
+
DropdownMenu,
|
|
25
|
+
DropdownMenuContent,
|
|
26
|
+
DropdownMenuTrigger,
|
|
27
|
+
} from '../../client/components/ui/dropdown-menu';
|
|
28
|
+
|
|
29
|
+
function ThemeSettings(): ReactElement {
|
|
30
|
+
return (
|
|
31
|
+
<DropdownMenu>
|
|
32
|
+
<DropdownMenuTrigger>Account</DropdownMenuTrigger>
|
|
33
|
+
<DropdownMenuContent>
|
|
34
|
+
<ThemeMenu />
|
|
35
|
+
</DropdownMenuContent>
|
|
36
|
+
</DropdownMenu>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
23
40
|
/**
|
|
24
41
|
* Renders the client configuration the server would, mounted at `/crm` so the theme's storage keys are
|
|
25
42
|
* `nocobase:crm:*`. It replaces any block already on the page, since the client reads the first one it finds.
|
|
@@ -38,19 +55,65 @@ function renderClientConfig(
|
|
|
38
55
|
document.head.append(element);
|
|
39
56
|
}
|
|
40
57
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*/
|
|
45
|
-
async function openAppearance(name = 'Appearance'): Promise<void> {
|
|
46
|
-
act(() => screen.getByRole('button', { name }).focus());
|
|
58
|
+
/** Opens the account menu and its Theme submenu using the keyboard. */
|
|
59
|
+
async function openTheme(name = 'Theme'): Promise<void> {
|
|
60
|
+
act(() => screen.getByRole('button', { name: 'Account' }).focus());
|
|
47
61
|
await userEvent.keyboard('{Enter}');
|
|
62
|
+
act(() => screen.getByRole('menuitem', { name }).focus());
|
|
63
|
+
await userEvent.keyboard('{ArrowRight}');
|
|
48
64
|
}
|
|
49
65
|
|
|
50
66
|
describe('app client theme', () => {
|
|
51
67
|
it.each([
|
|
52
|
-
['
|
|
53
|
-
['
|
|
68
|
+
['Light', 'light', 'Compact', 'compact'],
|
|
69
|
+
['Dark', 'dark', 'Compact', 'compact'],
|
|
70
|
+
['System', 'system', 'Compact', 'compact'],
|
|
71
|
+
['Light', 'light', 'Default', 'default'],
|
|
72
|
+
['Dark', 'dark', 'Default', 'default'],
|
|
73
|
+
['System', 'system', 'Default', 'default'],
|
|
74
|
+
])(
|
|
75
|
+
'saves and restores %s mode with %s / %s density',
|
|
76
|
+
async (modeLabel, mode, presetLabel, preset) => {
|
|
77
|
+
const settings = (
|
|
78
|
+
<AppThemeProvider>
|
|
79
|
+
<ThemeSettings />
|
|
80
|
+
</AppThemeProvider>
|
|
81
|
+
);
|
|
82
|
+
const view = render(settings);
|
|
83
|
+
await openTheme();
|
|
84
|
+
await userEvent.click(
|
|
85
|
+
screen.getByRole('menuitemradio', { name: modeLabel }),
|
|
86
|
+
);
|
|
87
|
+
await userEvent.click(
|
|
88
|
+
screen.getByRole('menuitemradio', { name: presetLabel }),
|
|
89
|
+
);
|
|
90
|
+
expect(localStorage.getItem('nocobase:crm:theme:color-scheme')).toBe(
|
|
91
|
+
mode,
|
|
92
|
+
);
|
|
93
|
+
expect(localStorage.getItem('nocobase:crm:theme:preset')).toBe(preset);
|
|
94
|
+
view.unmount();
|
|
95
|
+
initializeTheme(
|
|
96
|
+
'/crm/',
|
|
97
|
+
themePresets.map(({ id }) => id),
|
|
98
|
+
);
|
|
99
|
+
render(settings);
|
|
100
|
+
await openTheme();
|
|
101
|
+
expect(document.documentElement).toHaveClass(
|
|
102
|
+
mode === 'light' ? 'light' : 'dark',
|
|
103
|
+
);
|
|
104
|
+
expect(document.documentElement).toHaveAttribute('data-theme', preset);
|
|
105
|
+
expect(
|
|
106
|
+
screen.getByRole('menuitemradio', { name: modeLabel }),
|
|
107
|
+
).toHaveAttribute('aria-checked', 'true');
|
|
108
|
+
expect(
|
|
109
|
+
screen.getByRole('menuitemradio', { name: presetLabel }),
|
|
110
|
+
).toHaveAttribute('aria-checked', 'true');
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
it.each([
|
|
115
|
+
['en-US', 'Theme', 'Spacious'],
|
|
116
|
+
['zh-CN', '主题', '宽松'],
|
|
54
117
|
])(
|
|
55
118
|
'labels the default preset without implying default selection (%s)',
|
|
56
119
|
async (locale, appearance, label) => {
|
|
@@ -68,28 +131,32 @@ describe('app client theme', () => {
|
|
|
68
131
|
</AppThemeProvider>
|
|
69
132
|
</I18nProvider>,
|
|
70
133
|
);
|
|
71
|
-
await
|
|
72
|
-
const option = await screen.findByRole('
|
|
134
|
+
await openTheme(appearance);
|
|
135
|
+
const option = await screen.findByRole('menuitemradio', { name: label });
|
|
73
136
|
await userEvent.click(option);
|
|
74
|
-
expect(option).
|
|
137
|
+
expect(option).toHaveAttribute('aria-checked', 'true');
|
|
75
138
|
expect(localStorage.getItem('nocobase:crm:theme:preset')).toBe('default');
|
|
76
139
|
},
|
|
77
140
|
);
|
|
78
141
|
|
|
79
|
-
it('
|
|
142
|
+
it('selects a preset by keyboard and returns focus on Escape', async () => {
|
|
80
143
|
render(
|
|
81
144
|
<AppThemeProvider>
|
|
82
145
|
<ThemeSettings />
|
|
83
146
|
</AppThemeProvider>,
|
|
84
147
|
);
|
|
85
|
-
await
|
|
86
|
-
const compact = await screen.findByRole('
|
|
148
|
+
await openTheme();
|
|
149
|
+
const compact = await screen.findByRole('menuitemradio', {
|
|
150
|
+
name: 'Compact',
|
|
151
|
+
});
|
|
87
152
|
act(() => compact.focus());
|
|
88
|
-
await userEvent.keyboard('{
|
|
89
|
-
expect(
|
|
153
|
+
await userEvent.keyboard('{ArrowDown}{Enter}');
|
|
154
|
+
expect(
|
|
155
|
+
screen.getByRole('menuitemradio', { name: 'Default' }),
|
|
156
|
+
).toHaveAttribute('aria-checked', 'true');
|
|
90
157
|
expect(localStorage.getItem('nocobase:crm:theme:preset')).toBe('default');
|
|
91
158
|
await userEvent.keyboard('{Escape}');
|
|
92
|
-
expect(screen.getByRole('
|
|
159
|
+
expect(screen.getByRole('menuitem', { name: 'Theme' })).toHaveFocus();
|
|
93
160
|
});
|
|
94
161
|
|
|
95
162
|
it('falls back from the removed Ant Design preset without changing mode', async () => {
|
|
@@ -100,11 +167,13 @@ describe('app client theme', () => {
|
|
|
100
167
|
<ThemeSettings />
|
|
101
168
|
</AppThemeProvider>,
|
|
102
169
|
);
|
|
103
|
-
await
|
|
170
|
+
await openTheme();
|
|
104
171
|
expect(
|
|
105
|
-
screen.queryByRole('
|
|
172
|
+
screen.queryByRole('menuitemradio', { name: 'Ant-design' }),
|
|
106
173
|
).not.toBeInTheDocument();
|
|
107
|
-
expect(
|
|
174
|
+
expect(
|
|
175
|
+
await screen.findByRole('menuitemradio', { name: 'Compact' }),
|
|
176
|
+
).toHaveAttribute('aria-checked', 'true');
|
|
108
177
|
expect(document.documentElement).toHaveAttribute('data-theme', 'compact');
|
|
109
178
|
expect(document.documentElement).toHaveClass('light');
|
|
110
179
|
});
|
|
@@ -115,13 +184,15 @@ describe('app client theme', () => {
|
|
|
115
184
|
<ThemeSettings />
|
|
116
185
|
</AppThemeProvider>,
|
|
117
186
|
);
|
|
118
|
-
await
|
|
187
|
+
await openTheme();
|
|
119
188
|
expect(
|
|
120
|
-
screen.queryByRole('
|
|
189
|
+
screen.queryByRole('menuitemradio', { name: 'Ocean' }),
|
|
121
190
|
).not.toBeInTheDocument();
|
|
122
|
-
expect(await screen.findByRole('radio', { name: 'Compact' })).toBeChecked();
|
|
123
191
|
expect(
|
|
124
|
-
await screen.findByRole('
|
|
192
|
+
await screen.findByRole('menuitemradio', { name: 'Compact' }),
|
|
193
|
+
).toHaveAttribute('aria-checked', 'true');
|
|
194
|
+
expect(
|
|
195
|
+
await screen.findByRole('menuitemradio', { name: 'Default' }),
|
|
125
196
|
).toBeInTheDocument();
|
|
126
197
|
expect(document.documentElement).toHaveAttribute('data-theme', 'compact');
|
|
127
198
|
});
|
|
@@ -218,12 +289,12 @@ describe('app client theme', () => {
|
|
|
218
289
|
<ThemeSettings />
|
|
219
290
|
</AppThemeProvider>,
|
|
220
291
|
);
|
|
221
|
-
await
|
|
292
|
+
await openTheme();
|
|
222
293
|
expect(
|
|
223
|
-
await screen.findByRole('
|
|
294
|
+
await screen.findByRole('menuitemradio', {
|
|
224
295
|
name: expected === 'compact' ? 'Compact' : 'Default',
|
|
225
296
|
}),
|
|
226
|
-
).
|
|
297
|
+
).toHaveAttribute('aria-checked', 'true');
|
|
227
298
|
expect(document.documentElement).toHaveAttribute('data-theme', expected);
|
|
228
299
|
expect(localStorage.getItem(key)).toBe(saved ?? null);
|
|
229
300
|
},
|
|
@@ -334,14 +405,16 @@ describe('app client theme', () => {
|
|
|
334
405
|
</AppThemeProvider>,
|
|
335
406
|
);
|
|
336
407
|
|
|
337
|
-
await
|
|
408
|
+
await openTheme();
|
|
338
409
|
await userEvent.click(
|
|
339
|
-
await screen.findByRole('
|
|
410
|
+
await screen.findByRole('menuitemradio', { name: 'Compact' }),
|
|
340
411
|
);
|
|
341
412
|
expect(document.documentElement).toHaveAttribute('data-theme', 'compact');
|
|
342
413
|
expect(document.documentElement).toHaveClass('dark');
|
|
343
414
|
expect(localStorage.getItem('nocobase:crm:theme:preset')).toBe('compact');
|
|
344
|
-
await userEvent.click(
|
|
415
|
+
await userEvent.click(
|
|
416
|
+
await screen.findByRole('menuitemradio', { name: 'Light' }),
|
|
417
|
+
);
|
|
345
418
|
expect(document.documentElement).toHaveClass('light');
|
|
346
419
|
expect(document.documentElement).toHaveAttribute('data-theme', 'compact');
|
|
347
420
|
fireEvent(
|
|
@@ -359,10 +432,12 @@ describe('app client theme', () => {
|
|
|
359
432
|
newValue: 'default',
|
|
360
433
|
}),
|
|
361
434
|
);
|
|
362
|
-
expect(
|
|
435
|
+
expect(
|
|
436
|
+
await screen.findByRole('menuitemradio', { name: 'Default' }),
|
|
437
|
+
).toHaveAttribute('aria-checked', 'true');
|
|
363
438
|
await userEvent.keyboard('{Escape}');
|
|
364
439
|
await waitFor(() =>
|
|
365
|
-
expect(screen.getByRole('
|
|
440
|
+
expect(screen.getByRole('menuitem', { name: 'Theme' })).toHaveFocus(),
|
|
366
441
|
);
|
|
367
442
|
});
|
|
368
443
|
|
|
@@ -395,11 +470,13 @@ describe('app client theme', () => {
|
|
|
395
470
|
<ThemeSettings />
|
|
396
471
|
</AppThemeProvider>,
|
|
397
472
|
);
|
|
398
|
-
await
|
|
473
|
+
await openTheme();
|
|
474
|
+
await userEvent.click(
|
|
475
|
+
await screen.findByRole('menuitemradio', { name: 'Default' }),
|
|
476
|
+
);
|
|
399
477
|
await userEvent.click(
|
|
400
|
-
await screen.findByRole('
|
|
478
|
+
await screen.findByRole('menuitemradio', { name: 'Light' }),
|
|
401
479
|
);
|
|
402
|
-
await userEvent.click(await screen.findByRole('radio', { name: 'Light' }));
|
|
403
480
|
expect(document.documentElement).toHaveAttribute('data-theme', 'default');
|
|
404
481
|
expect(document.documentElement).toHaveClass('light');
|
|
405
482
|
});
|
|
@@ -410,9 +487,9 @@ describe('app client theme', () => {
|
|
|
410
487
|
<ThemeSettings />
|
|
411
488
|
</AppThemeProvider>,
|
|
412
489
|
);
|
|
413
|
-
await
|
|
490
|
+
await openTheme();
|
|
414
491
|
await userEvent.click(
|
|
415
|
-
await screen.findByRole('
|
|
492
|
+
await screen.findByRole('menuitemradio', { name: 'Default' }),
|
|
416
493
|
);
|
|
417
494
|
fireEvent(
|
|
418
495
|
window,
|
|
@@ -438,7 +515,9 @@ describe('app client theme', () => {
|
|
|
438
515
|
}),
|
|
439
516
|
);
|
|
440
517
|
await waitFor(() => expect(document.documentElement).toHaveClass('dark'));
|
|
441
|
-
expect(
|
|
518
|
+
expect(
|
|
519
|
+
await screen.findByRole('menuitemradio', { name: 'System' }),
|
|
520
|
+
).toHaveAttribute('aria-checked', 'true');
|
|
442
521
|
});
|
|
443
522
|
});
|
|
444
523
|
|
|
@@ -2,10 +2,14 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import { resolveAppRuntime } from '@nocobase/app-client/runtime';
|
|
3
3
|
import { defineAppRuntime } from '@nocobase/app-client/runtime';
|
|
4
4
|
import appRuntime from '../../client/runtime.js';
|
|
5
|
+
import { createApplicationRoutes } from '../../client/routes.js';
|
|
5
6
|
import cliPlugins from '../../cli/plugins.js';
|
|
6
7
|
import serverPlugins from '../../server/plugins.js';
|
|
7
8
|
import settingsRoutes from '../../client/pages/settings/routes.js';
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
buildRouteNavigation,
|
|
11
|
+
buildSettingsCatalog,
|
|
12
|
+
} from '../../client/routing/route-navigation.js';
|
|
9
13
|
|
|
10
14
|
describe('default Foundation integration', () => {
|
|
11
15
|
it('registers the Foundation deployment evidence build hook', () => {
|
|
@@ -15,11 +19,18 @@ describe('default Foundation integration', () => {
|
|
|
15
19
|
),
|
|
16
20
|
).toHaveLength(1);
|
|
17
21
|
});
|
|
18
|
-
it('exposes Foundation in settings for an unrestricted administrator and removes the
|
|
22
|
+
it('exposes Foundation in System settings for an unrestricted administrator and removes the entry when denied', async () => {
|
|
19
23
|
const runtime = await resolveAppRuntime(appRuntime);
|
|
20
24
|
const menu = buildRouteNavigation(runtime.routes, new Set());
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
// One System settings entry; Foundation is a card on its home, not a sidebar entry of its own.
|
|
26
|
+
expect(menu.map(({ route }) => route.name)).toEqual([
|
|
27
|
+
'home',
|
|
28
|
+
'settingsHome',
|
|
29
|
+
]);
|
|
30
|
+
const category = buildSettingsCatalog(runtime.routes, new Set()).find(
|
|
31
|
+
({ route }) => route.name === 'settingsSystem',
|
|
32
|
+
);
|
|
33
|
+
const page = category?.modules.find(
|
|
23
34
|
({ route }) => route.name === 'foundation',
|
|
24
35
|
)?.route;
|
|
25
36
|
expect(page).toMatchObject({
|
|
@@ -28,12 +39,13 @@ describe('default Foundation integration', () => {
|
|
|
28
39
|
authz: 'unrestricted',
|
|
29
40
|
});
|
|
30
41
|
if (!page) throw new Error('Foundation route missing');
|
|
42
|
+
const denied = new Set([`${page.packageName}:${page.path}:${page.id}`]);
|
|
31
43
|
expect(
|
|
32
|
-
buildRouteNavigation(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
).
|
|
44
|
+
buildRouteNavigation(runtime.routes, denied).map(
|
|
45
|
+
({ route }) => route.name,
|
|
46
|
+
),
|
|
47
|
+
).toEqual(['home']);
|
|
48
|
+
expect(buildSettingsCatalog(runtime.routes, denied)).toEqual([]);
|
|
37
49
|
expect(
|
|
38
50
|
serverPlugins.plugins.filter(
|
|
39
51
|
(plugin) => plugin.packageName === '@nocobase/app-plugin-foundation',
|
|
@@ -41,22 +53,31 @@ describe('default Foundation integration', () => {
|
|
|
41
53
|
).toHaveLength(1);
|
|
42
54
|
});
|
|
43
55
|
|
|
44
|
-
it('
|
|
56
|
+
it('keeps Foundation beside business settings without changing its protected URL', async () => {
|
|
45
57
|
const runtime = await resolveAppRuntime(
|
|
46
58
|
defineAppRuntime({
|
|
47
59
|
...appRuntime,
|
|
48
|
-
routes: [...
|
|
60
|
+
routes: () => [...createApplicationRoutes(), settingsRoutes],
|
|
49
61
|
}),
|
|
50
62
|
);
|
|
51
63
|
const menu = buildRouteNavigation(runtime.routes, new Set());
|
|
52
64
|
expect(menu.some(({ route }) => route.name === 'foundationSettings')).toBe(
|
|
53
65
|
false,
|
|
54
66
|
);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
expect(
|
|
68
|
+
buildSettingsCatalog(runtime.routes, new Set()).map((category) => [
|
|
69
|
+
category.route.name,
|
|
70
|
+
category.modules.map(({ route, target }) => [route.name, target.path]),
|
|
71
|
+
]),
|
|
72
|
+
).toEqual([
|
|
73
|
+
[
|
|
74
|
+
'settingsAccount',
|
|
75
|
+
[
|
|
76
|
+
['settingsUsers', '/settings/users'],
|
|
77
|
+
['permissionManagement', '/settings/roles'],
|
|
78
|
+
],
|
|
79
|
+
],
|
|
80
|
+
['settingsSystem', [['foundation', '/foundation']]],
|
|
81
|
+
]);
|
|
61
82
|
});
|
|
62
83
|
});
|