@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
|
@@ -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';
|
|
@@ -89,15 +90,15 @@ describe('application shell', () => {
|
|
|
89
90
|
expect(
|
|
90
91
|
screen.queryByRole('link', { name: 'Settings' }),
|
|
91
92
|
).not.toBeInTheDocument();
|
|
92
|
-
expect(screen.getByText('
|
|
93
|
+
expect(screen.getByText('Build reliable business systems')).toBeVisible();
|
|
93
94
|
expect(screen.getByRole('link', { name: 'NocoBase' })).toHaveAttribute(
|
|
94
95
|
'href',
|
|
95
96
|
'https://www.nocobase.com',
|
|
96
97
|
);
|
|
97
98
|
expect(
|
|
98
99
|
screen.getByText('NocoBase', { selector: 'a' }).parentElement,
|
|
99
|
-
).toHaveTextContent('
|
|
100
|
-
expect(screen.
|
|
100
|
+
).toHaveTextContent('AI + NocoBase');
|
|
101
|
+
expect(screen.queryByText('Default Template')).not.toBeInTheDocument();
|
|
101
102
|
expect(screen.getByText('v0.0.0')).toHaveClass('truncate');
|
|
102
103
|
// shadcn's edge rail, with a translated label in place of its built-in one.
|
|
103
104
|
expect(
|
|
@@ -201,13 +202,25 @@ describe('application shell', () => {
|
|
|
201
202
|
expect(
|
|
202
203
|
await screen.findByRole('button', { name: 'Expand navigation' }),
|
|
203
204
|
).toHaveAttribute('aria-pressed', 'true');
|
|
204
|
-
// 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.
|
|
205
206
|
const footerIcon = screen.getByRole('img', {
|
|
206
|
-
name: 'AI
|
|
207
|
+
name: 'AI + NocoBase Build reliable business systems v0.0.0',
|
|
207
208
|
});
|
|
208
209
|
act(() => footerIcon.focus());
|
|
209
210
|
expect(
|
|
210
|
-
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', {
|
|
211
224
|
selector: '[data-slot="tooltip-content"] span',
|
|
212
225
|
}),
|
|
213
226
|
).toBeVisible();
|
|
@@ -223,7 +236,7 @@ describe('application shell', () => {
|
|
|
223
236
|
),
|
|
224
237
|
);
|
|
225
238
|
expect(
|
|
226
|
-
screen.queryByRole('img', { name: /
|
|
239
|
+
screen.queryByRole('img', { name: /AI \+ NocoBase/ }),
|
|
227
240
|
).not.toBeInTheDocument();
|
|
228
241
|
});
|
|
229
242
|
|
|
@@ -316,7 +329,9 @@ function renderApplication(
|
|
|
316
329
|
<SidebarPreferenceProvider
|
|
317
330
|
value={createAppSidebarPreferenceStore('/main')}
|
|
318
331
|
>
|
|
319
|
-
<
|
|
332
|
+
<NavigationGuardProvider>
|
|
333
|
+
<AppRouter clientRoutes={clientRoutes} />
|
|
334
|
+
</NavigationGuardProvider>
|
|
320
335
|
</SidebarPreferenceProvider>
|
|
321
336
|
</AppThemeProvider>
|
|
322
337
|
</MemoryRouter>
|
|
@@ -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
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ClientApplicationContext,
|
|
3
|
+
NavigationGuardProvider,
|
|
3
4
|
type ClientApplication,
|
|
4
5
|
createAppClientConfig,
|
|
5
6
|
} from '@nocobase/app-client';
|
|
@@ -70,7 +71,9 @@ async function setup(children: ReactNode) {
|
|
|
70
71
|
<I18nProvider runtime={runtime}>
|
|
71
72
|
<ClientApplicationContext.Provider value={app}>
|
|
72
73
|
<SidebarPreferenceProvider value={sidebarPreference}>
|
|
73
|
-
<MemoryRouter initialEntries={['/']}>
|
|
74
|
+
<MemoryRouter initialEntries={['/']}>
|
|
75
|
+
<NavigationGuardProvider>{children}</NavigationGuardProvider>
|
|
76
|
+
</MemoryRouter>
|
|
74
77
|
</SidebarPreferenceProvider>
|
|
75
78
|
</ClientApplicationContext.Provider>
|
|
76
79
|
</I18nProvider>,
|
|
@@ -85,13 +88,15 @@ describe('shell translations', () => {
|
|
|
85
88
|
expect(
|
|
86
89
|
screen.getByRole('navigation', { name: 'Breadcrumb' }),
|
|
87
90
|
).toHaveTextContent('Overview');
|
|
88
|
-
expect(screen.getByText('
|
|
91
|
+
expect(screen.getByText('Build reliable business systems')).toBeVisible();
|
|
92
|
+
expect(screen.queryByText('Default Template')).not.toBeInTheDocument();
|
|
89
93
|
await act(() => runtime.changeLanguage('zh-CN'));
|
|
90
94
|
expect(screen.getByRole('navigation', { name: '面包屑' })).toBeVisible();
|
|
91
|
-
expect(screen.getByText('
|
|
95
|
+
expect(screen.getByText('Build reliable business systems')).toBeVisible();
|
|
96
|
+
expect(screen.queryByText('Default Template')).not.toBeInTheDocument();
|
|
92
97
|
expect(
|
|
93
98
|
screen.getByRole('link', { name: 'NocoBase' }).parentElement,
|
|
94
|
-
).toHaveTextContent('NocoBase
|
|
99
|
+
).toHaveTextContent('AI + NocoBase');
|
|
95
100
|
expect(
|
|
96
101
|
screen.getByRole('button', { name: '展开或收起导航' }),
|
|
97
102
|
).toHaveAttribute('title', '展开或收起导航');
|