@nocobase/app-template-default 1.0.0-beta.58 → 1.0.0-beta.59
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.md +26 -23
- package/README.MD +7 -57
- package/client/agent-annotations-host.ts +8 -2
- package/client/app.ts +0 -2
- package/client/components/brand-loading-mark.tsx +46 -0
- package/client/components/brand-mark-geometry.ts +7 -0
- package/client/components/brand-spinner.tsx +22 -0
- package/client/components/brand-watermark.tsx +32 -0
- package/client/components/loading.tsx +4 -3
- package/client/components/route-overlay.tsx +15 -4
- package/client/components/status-badge.tsx +29 -0
- package/client/components/ui/alert-dialog.tsx +186 -0
- package/client/components/ui/card.tsx +102 -0
- package/client/components/ui/dialog.tsx +2 -2
- package/client/components/ui/radio-group.tsx +35 -0
- package/client/components/ui/spinner.tsx +2 -20
- package/client/components/ui/toast.tsx +3 -2
- package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +3 -3
- package/client/extensions/nocobase-inbox/inbox-item.tsx +4 -3
- package/client/layouts/app-layout.tsx +8 -28
- package/client/layouts/components/app-sidebar.tsx +4 -4
- package/client/layouts/components/header-actions.tsx +3 -36
- package/client/layouts/index.ts +0 -1
- package/client/lib/toaster.ts +1 -1
- package/client/locales/en-US.ts +31 -17
- package/client/locales/zh-CN.ts +24 -14
- package/client/pages/auth/login.tsx +1 -0
- package/client/pages/auth/shared.tsx +9 -9
- package/client/pages/home.tsx +80 -7
- package/client/plugins.ts +2 -2
- package/client/routes.ts +4 -10
- package/client/routing/app-router.tsx +5 -55
- package/client/routing/route-navigation.ts +1 -1
- package/client/styles.css +244 -0
- package/client/theme/theme-settings.tsx +22 -20
- package/client/theme/themes/compact.css +34 -16
- package/client/theme/themes/default.css +34 -16
- package/eslint.config.js +0 -13
- package/index.html +48 -16
- package/package.json +22 -26
- package/tests/components/app-layout-routes.test.tsx +1 -1
- package/tests/components/inbox.test.tsx +0 -19
- package/tests/components/layout-i18n.test.tsx +5 -92
- package/tests/components/primitive-labels.test.tsx +61 -1
- package/tests/fixtures/theme-tokens.html +1 -1
- package/tests/fixtures/theme-tokens.tsx +7 -0
- package/tests/logic/app-server.test.ts +0 -15
- package/tests/logic/client-plugin-registry.test.ts +0 -21
- package/tests/logic/client-routes.test.ts +2 -6
- package/tests/logic/client-shell.test.tsx +11 -107
- package/tests/logic/client-theme.test.tsx +17 -1
- package/tests/logic/config.test.ts +0 -1
- package/tests/logic/plugin-commands.test.ts +1 -1
- package/tsconfig.json +1 -4
- package/tsconfig.server.json +1 -6
- package/vitest.config.ts +1 -8
- package/client/components/inbox-badge.ts +0 -49
- package/client/components/inbox-button.tsx +0 -132
- package/client/components/inbox-header-button.tsx +0 -25
- package/client/layouts/components/surface-empty.tsx +0 -35
- package/client/layouts/settings-layout.tsx +0 -172
- package/server/config/workflow.ts +0 -24
- package/tests/logic/client-settings.test.tsx +0 -841
- package/tests/logic/dev-routes-production-build.test.ts +0 -168
- package/tests/logic/plugin-page-authz.test.ts +0 -53
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/app-template-default",
|
|
3
3
|
"displayName": "Default Template",
|
|
4
|
-
"version": "1.0.0-beta.
|
|
4
|
+
"version": "1.0.0-beta.59",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"prettier": "@nocobase/dev-config/prettier",
|
|
7
7
|
"engines": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"nocobase": {
|
|
11
11
|
"templateKind": "app",
|
|
12
|
-
"defaultTemplateVersion": "1.0.0-beta.
|
|
12
|
+
"defaultTemplateVersion": "1.0.0-beta.59"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@ai-sdk/react": "2.0.0",
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
"@dicebear/core": "^9.4.3",
|
|
18
18
|
"@gchust/agent-annotations": "0.1.9",
|
|
19
19
|
"@hookform/resolvers": "^5.0.1",
|
|
20
|
-
"@nocobase/ai-employee-avatars": "^1.0.2",
|
|
21
20
|
"@playwright/test": "^1.62.1",
|
|
22
21
|
"@refinedev/cli": "^2.16.48",
|
|
23
22
|
"@refinedev/core": "^5.0.8",
|
|
@@ -76,14 +75,13 @@
|
|
|
76
75
|
"vite": "^6.3.5",
|
|
77
76
|
"vitest": "^4.1.10",
|
|
78
77
|
"tar": "^7.5.22",
|
|
79
|
-
"@nocobase/app-client": "^
|
|
78
|
+
"@nocobase/app-client": "^3.0.0-beta.0",
|
|
80
79
|
"@nocobase/app-plugin-template-print": "0.0.2-beta.1",
|
|
81
|
-
"@nocobase/app-skills": "0.1.0-beta.
|
|
82
|
-
"@nocobase/app-testing": "0.1.0-beta.
|
|
80
|
+
"@nocobase/app-skills": "0.1.0-beta.27",
|
|
81
|
+
"@nocobase/app-testing": "0.1.0-beta.2",
|
|
83
82
|
"@nocobase/app-websocket": "^0.0.1",
|
|
84
|
-
"@nocobase/dev-config": "0.1.0-beta.
|
|
83
|
+
"@nocobase/dev-config": "0.1.0-beta.21",
|
|
85
84
|
"@nocobase/drive": "^0.1.0-beta.4",
|
|
86
|
-
"@nocobase/hub-cli": "^1.0.0-beta.2",
|
|
87
85
|
"@nocobase/logging": "^0.1.0-beta.6",
|
|
88
86
|
"@nocobase/session": "^0.1.0-beta.2"
|
|
89
87
|
},
|
|
@@ -95,7 +93,6 @@
|
|
|
95
93
|
"cli",
|
|
96
94
|
"client",
|
|
97
95
|
"server",
|
|
98
|
-
"workflows",
|
|
99
96
|
"database/tsconfig.json",
|
|
100
97
|
"database/*/migrations/**",
|
|
101
98
|
"database/*/seeds/**",
|
|
@@ -144,34 +141,33 @@
|
|
|
144
141
|
"@oclif/core": "^4.14.0",
|
|
145
142
|
"better-auth": "1.7.5",
|
|
146
143
|
"hono": "^4.13.2",
|
|
147
|
-
"@nocobase/app-plugin-api-keys": "^1.0.0-beta.
|
|
148
|
-
"@nocobase/app-plugin-authentication": "^2.0.0-beta.
|
|
149
|
-
"@nocobase/app-plugin-authorization": "^1.0.0-beta.
|
|
150
|
-
"@nocobase/app-plugin-database-explorer": "^1.0.0-beta.
|
|
151
|
-
"@nocobase/app-plugin-file": "^1.0.0-beta.
|
|
152
|
-
"@nocobase/app-plugin-i18n": "^1.0.0-beta.
|
|
153
|
-
"@nocobase/app-plugin-notification": "^1.0.0-beta.
|
|
154
|
-
"@nocobase/app-plugin-notification-in-app": "^1.0.0-beta.
|
|
144
|
+
"@nocobase/app-plugin-api-keys": "^1.0.0-beta.14",
|
|
145
|
+
"@nocobase/app-plugin-authentication": "^2.0.0-beta.3",
|
|
146
|
+
"@nocobase/app-plugin-authorization": "^1.0.0-beta.26",
|
|
147
|
+
"@nocobase/app-plugin-database-explorer": "^1.0.0-beta.12",
|
|
148
|
+
"@nocobase/app-plugin-file": "^1.0.0-beta.22",
|
|
149
|
+
"@nocobase/app-plugin-i18n": "^1.0.0-beta.14",
|
|
150
|
+
"@nocobase/app-plugin-notification": "^1.0.0-beta.25",
|
|
151
|
+
"@nocobase/app-plugin-notification-in-app": "^1.0.0-beta.23",
|
|
155
152
|
"@nocobase/app-plugin-notification-providers": "^0.2.0-beta.10",
|
|
156
|
-
"@nocobase/app-plugin-scheduler": "^1.0.0-beta.
|
|
157
|
-
"@nocobase/app-plugin-users": "^2.0.0-beta.
|
|
158
|
-
"@nocobase/app-
|
|
159
|
-
"@nocobase/app-server": "^2.0.0-beta.2",
|
|
153
|
+
"@nocobase/app-plugin-scheduler": "^1.0.0-beta.16",
|
|
154
|
+
"@nocobase/app-plugin-users": "^2.0.0-beta.2",
|
|
155
|
+
"@nocobase/app-server": "^2.0.0-beta.3",
|
|
160
156
|
"@nocobase/config": "^0.1.0-beta.2",
|
|
161
157
|
"@nocobase/db": "^1.0.0-beta.18",
|
|
162
158
|
"@nocobase/db-sqlite": "^0.1.0-beta.4",
|
|
163
159
|
"@nocobase/service-provider": "^0.0.2-beta.1",
|
|
164
160
|
"@nocobase/snowflake": "^1.0.0-beta.3",
|
|
165
|
-
"@nocobase/app-plugin-authz-default-access": "^1.0.0-beta.
|
|
166
|
-
"@nocobase/app-plugin-authz-sharing-rules": "^1.0.0-beta.
|
|
167
|
-
"@nocobase/app-plugin-authz-restriction-rules": "^1.0.0-beta.
|
|
161
|
+
"@nocobase/app-plugin-authz-default-access": "^1.0.0-beta.11",
|
|
162
|
+
"@nocobase/app-plugin-authz-sharing-rules": "^1.0.0-beta.11",
|
|
163
|
+
"@nocobase/app-plugin-authz-restriction-rules": "^1.0.0-beta.10",
|
|
168
164
|
"@nocobase/authorization": "^1.0.0-beta.12",
|
|
169
165
|
"@nocobase/queue": "^0.1.0-beta.8",
|
|
170
166
|
"@nocobase/jobs": "^0.1.0-beta.2",
|
|
171
|
-
"@nocobase/i18n": "^1.0.0-beta.
|
|
167
|
+
"@nocobase/i18n": "^1.0.0-beta.6",
|
|
172
168
|
"@nocobase/caching": "^0.1.0-beta.2",
|
|
173
169
|
"@nocobase/repository-input": "^0.1.0-beta.2",
|
|
174
|
-
"@nocobase/app-cli": "^1.0.0-beta.
|
|
170
|
+
"@nocobase/app-cli": "^1.0.0-beta.17"
|
|
175
171
|
},
|
|
176
172
|
"description": "NocoBase 3 application template with the standard product capabilities.",
|
|
177
173
|
"imports": {
|
|
@@ -31,7 +31,7 @@ vi.mock('@nocobase/app-plugin-i18n/client', async (importOriginal) => ({
|
|
|
31
31
|
}));
|
|
32
32
|
vi.mock('@nocobase/app-client', async (original) => ({
|
|
33
33
|
...(await original<typeof import('@nocobase/app-client')>()),
|
|
34
|
-
useClientApplication: () => ({ runtime: {
|
|
34
|
+
useClientApplication: () => ({ runtime: {} }),
|
|
35
35
|
}));
|
|
36
36
|
vi.mock('../../client/routing/route-navigation.js', async (original) => ({
|
|
37
37
|
...(await original<
|
|
@@ -29,7 +29,6 @@ vi.mock('@nocobase/app-client', async (importOriginal) => {
|
|
|
29
29
|
};
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
import { InboxHeaderButton } from '../../client/components/inbox-header-button.tsx';
|
|
33
32
|
import InboxPage from '../../client/pages/inbox.tsx';
|
|
34
33
|
|
|
35
34
|
const messages = [
|
|
@@ -97,24 +96,6 @@ function Frame({
|
|
|
97
96
|
);
|
|
98
97
|
}
|
|
99
98
|
|
|
100
|
-
it('links the header to the inbox with the unread count, for a signed-in person only', async () => {
|
|
101
|
-
const view = render(
|
|
102
|
-
<Frame path='/'>
|
|
103
|
-
<InboxHeaderButton />
|
|
104
|
-
</Frame>,
|
|
105
|
-
);
|
|
106
|
-
const link = await screen.findByRole('link', { name: 'Inbox, 2 unread' });
|
|
107
|
-
expect(link).toHaveAttribute('href', '/inbox');
|
|
108
|
-
expect(screen.getByText('2')).toBeInTheDocument();
|
|
109
|
-
mocks.session = null;
|
|
110
|
-
view.rerender(
|
|
111
|
-
<Frame path='/'>
|
|
112
|
-
<InboxHeaderButton />
|
|
113
|
-
</Frame>,
|
|
114
|
-
);
|
|
115
|
-
expect(screen.queryByRole('link')).toBeNull();
|
|
116
|
-
});
|
|
117
|
-
|
|
118
99
|
it("lists the plugin's messages as notifications and marks one read when it is opened", async () => {
|
|
119
100
|
render(
|
|
120
101
|
<Frame path='/inbox'>
|
|
@@ -6,10 +6,9 @@ import {
|
|
|
6
6
|
import type { AppClientRegisteredRoute } from '@nocobase/app-client/plugins';
|
|
7
7
|
import { I18nRuntime } from '@nocobase/i18n';
|
|
8
8
|
import { I18nProvider } from '@nocobase/i18n/client';
|
|
9
|
-
import userEvent from '@testing-library/user-event';
|
|
10
9
|
import { act, render, screen } from '@testing-library/react';
|
|
11
10
|
import type { ReactNode } from 'react';
|
|
12
|
-
import { MemoryRouter
|
|
11
|
+
import { MemoryRouter } from 'react-router';
|
|
13
12
|
import { describe, expect, it, vi } from 'vitest';
|
|
14
13
|
|
|
15
14
|
import locales from '../../client/locales/index.js';
|
|
@@ -17,17 +16,7 @@ import { AppLayout } from '../../client/layouts/app-layout.js';
|
|
|
17
16
|
vi.mock('@nocobase/app-plugin-i18n/client', () => ({
|
|
18
17
|
useSyncServerLocale: () => {},
|
|
19
18
|
}));
|
|
20
|
-
import { SettingsLayout } from '../../client/layouts/settings-layout.js';
|
|
21
19
|
|
|
22
|
-
// The header's inbox button reads the in-app notification plugin's API, which these tests do not serve; it is
|
|
23
|
-
// covered by tests/components/inbox.test.tsx.
|
|
24
|
-
vi.mock('../../client/components/inbox-header-button', () => ({
|
|
25
|
-
InboxHeaderButton: () => null,
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
vi.mock('../../client/routing/client-route.js', () => ({
|
|
29
|
-
ClientRoute: () => <p>Preferences content</p>,
|
|
30
|
-
}));
|
|
31
20
|
vi.mock('../../client/theme/index.js', () => ({ ThemeSettings: () => null }));
|
|
32
21
|
vi.mock('../../client/layouts/components/user-menu.js', () => ({
|
|
33
22
|
UserMenu: () => null,
|
|
@@ -43,17 +32,6 @@ vi.mock('../../client/routing/route-navigation.js', async (importOriginal) => ({
|
|
|
43
32
|
}),
|
|
44
33
|
}));
|
|
45
34
|
|
|
46
|
-
const route: AppClientRegisteredRoute = {
|
|
47
|
-
id: 'preferences',
|
|
48
|
-
name: 'preferences',
|
|
49
|
-
path: '/settings/preferences',
|
|
50
|
-
packageName: 'test',
|
|
51
|
-
source: 'application',
|
|
52
|
-
auth: 'optional',
|
|
53
|
-
navigation: { title: 'Preferences' },
|
|
54
|
-
componentLoader: async () => ({ default: () => <p>Preferences content</p> }),
|
|
55
|
-
};
|
|
56
|
-
|
|
57
35
|
/** The page the shell opens on: the header names it. */
|
|
58
36
|
const overview: AppClientRegisteredRoute = {
|
|
59
37
|
id: 'overview',
|
|
@@ -66,7 +44,7 @@ const overview: AppClientRegisteredRoute = {
|
|
|
66
44
|
componentLoader: async () => ({ default: () => null }),
|
|
67
45
|
};
|
|
68
46
|
|
|
69
|
-
async function setup(children: ReactNode
|
|
47
|
+
async function setup(children: ReactNode) {
|
|
70
48
|
vi.stubGlobal('matchMedia', () => ({
|
|
71
49
|
matches: true,
|
|
72
50
|
addEventListener: vi.fn(),
|
|
@@ -81,12 +59,12 @@ async function setup(children: ReactNode, path = '/') {
|
|
|
81
59
|
await runtime.init('en-US');
|
|
82
60
|
const app = {
|
|
83
61
|
config: createAppClientConfig({ rawConfig: {} }),
|
|
84
|
-
runtime: {
|
|
62
|
+
runtime: {},
|
|
85
63
|
} as unknown as ClientApplication;
|
|
86
64
|
render(
|
|
87
65
|
<I18nProvider runtime={runtime}>
|
|
88
66
|
<ClientApplicationContext.Provider value={app}>
|
|
89
|
-
<MemoryRouter initialEntries={[
|
|
67
|
+
<MemoryRouter initialEntries={['/']}>{children}</MemoryRouter>
|
|
90
68
|
</ClientApplicationContext.Provider>
|
|
91
69
|
</I18nProvider>,
|
|
92
70
|
);
|
|
@@ -94,7 +72,7 @@ async function setup(children: ReactNode, path = '/') {
|
|
|
94
72
|
}
|
|
95
73
|
|
|
96
74
|
describe('shell translations', () => {
|
|
97
|
-
it('updates header, footer
|
|
75
|
+
it('updates header, footer and accessible labels without remounting', async () => {
|
|
98
76
|
const runtime = await setup(<AppLayout routes={[overview]} />);
|
|
99
77
|
// The header's trail, where a tagline used to be.
|
|
100
78
|
expect(
|
|
@@ -114,74 +92,9 @@ describe('shell translations', () => {
|
|
|
114
92
|
'href',
|
|
115
93
|
'https://www.nocobase.com',
|
|
116
94
|
);
|
|
117
|
-
const user = userEvent.setup();
|
|
118
|
-
const settings = screen.getByRole('link', { name: '设置' });
|
|
119
|
-
expect(settings).not.toHaveAttribute('title');
|
|
120
|
-
await user.hover(settings);
|
|
121
|
-
expect(
|
|
122
|
-
await screen.findByText('设置', {
|
|
123
|
-
selector: '[data-slot=tooltip-content]',
|
|
124
|
-
}),
|
|
125
|
-
).toBeVisible();
|
|
126
|
-
await user.unhover(settings);
|
|
127
95
|
await act(() => runtime.changeLanguage('en-US'));
|
|
128
96
|
expect(
|
|
129
97
|
screen.getByRole('navigation', { name: 'Breadcrumb' }),
|
|
130
98
|
).toBeVisible();
|
|
131
|
-
act(() => settings.focus());
|
|
132
|
-
expect(
|
|
133
|
-
await screen.findByText('Settings', {
|
|
134
|
-
selector: '[data-slot=tooltip-content]',
|
|
135
|
-
}),
|
|
136
|
-
).toBeVisible();
|
|
137
99
|
});
|
|
138
|
-
|
|
139
|
-
it.each([
|
|
140
|
-
[
|
|
141
|
-
'settings',
|
|
142
|
-
SettingsLayout,
|
|
143
|
-
'暂无可用设置',
|
|
144
|
-
'没有已启用的插件提供你有权访问的设置页面。',
|
|
145
|
-
],
|
|
146
|
-
] as const)(
|
|
147
|
-
'translates the %s empty state',
|
|
148
|
-
async (surface, Layout, title, description) => {
|
|
149
|
-
const runtime = await setup(<Layout routeTree={[]} />, `/${surface}`);
|
|
150
|
-
await act(() => runtime.changeLanguage('zh-CN'));
|
|
151
|
-
expect(screen.getByRole('heading', { name: title })).toBeVisible();
|
|
152
|
-
expect(screen.getByText(description)).toBeVisible();
|
|
153
|
-
expect(screen.getByRole('link', { name: '返回应用' })).toHaveAttribute(
|
|
154
|
-
'href',
|
|
155
|
-
'/',
|
|
156
|
-
);
|
|
157
|
-
},
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
it.each(['settings'] as const)(
|
|
161
|
-
'translates the %s page header',
|
|
162
|
-
async (surface) => {
|
|
163
|
-
const Layout = SettingsLayout;
|
|
164
|
-
const page = { ...route, path: `/${surface}/preferences` };
|
|
165
|
-
const runtime = await setup(
|
|
166
|
-
<Routes>
|
|
167
|
-
<Route
|
|
168
|
-
path={`/${surface}/*`}
|
|
169
|
-
element={<Layout routeTree={[page]} />}
|
|
170
|
-
/>
|
|
171
|
-
</Routes>,
|
|
172
|
-
page.path,
|
|
173
|
-
);
|
|
174
|
-
await screen.findByText('Preferences content');
|
|
175
|
-
await act(() => runtime.changeLanguage('zh-CN'));
|
|
176
|
-
expect(screen.getByRole('link', { name: '返回应用' })).toHaveAttribute(
|
|
177
|
-
'href',
|
|
178
|
-
'/',
|
|
179
|
-
);
|
|
180
|
-
expect(
|
|
181
|
-
screen.getByRole('navigation', {
|
|
182
|
-
name: '设置',
|
|
183
|
-
}),
|
|
184
|
-
).toBeVisible();
|
|
185
|
-
},
|
|
186
|
-
);
|
|
187
100
|
});
|
|
@@ -6,9 +6,21 @@ import { act, render, screen } from '@testing-library/react';
|
|
|
6
6
|
import type { ReactElement, ReactNode } from 'react';
|
|
7
7
|
import { describe, expect, it } from 'vitest';
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
Dialog,
|
|
11
|
+
DialogContent,
|
|
12
|
+
DialogFooter,
|
|
13
|
+
DialogTitle,
|
|
14
|
+
} from '#components/ui/dialog';
|
|
15
|
+
import {
|
|
16
|
+
AlertDialog,
|
|
17
|
+
AlertDialogContent,
|
|
18
|
+
AlertDialogTitle,
|
|
19
|
+
} from '#components/ui/alert-dialog';
|
|
10
20
|
import { Sheet, SheetContent, SheetTitle } from '#components/ui/sheet';
|
|
11
21
|
import { Spinner } from '#components/ui/spinner';
|
|
22
|
+
import { BrandSpinner } from '#components/brand-spinner';
|
|
23
|
+
import { Loading } from '#components/loading';
|
|
12
24
|
import { createToastManager, Toaster } from '#components/ui/toast';
|
|
13
25
|
|
|
14
26
|
import zhCN from '../../client/locales/zh-CN.js';
|
|
@@ -70,6 +82,19 @@ describe('labels built into the shipped primitives', () => {
|
|
|
70
82
|
expect(close).toHaveAttribute('data-slot', 'toast-close');
|
|
71
83
|
});
|
|
72
84
|
|
|
85
|
+
it('labels brand loading and allows decorative inline indicators', () => {
|
|
86
|
+
const { rerender } = render(<BrandSpinner />, { wrapper: I18n });
|
|
87
|
+
expect(
|
|
88
|
+
screen.getByRole('status', { name: zhCN['status.loading'] }),
|
|
89
|
+
).toBeVisible();
|
|
90
|
+
rerender(<BrandSpinner aria-hidden='true' />);
|
|
91
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
|
92
|
+
rerender(<Loading label='正在加载客户' />);
|
|
93
|
+
expect(
|
|
94
|
+
screen.getByRole('status', { name: '正在加载客户' }),
|
|
95
|
+
).toHaveTextContent('正在加载客户');
|
|
96
|
+
});
|
|
97
|
+
|
|
73
98
|
it('labels the Spinner through status.loading', () => {
|
|
74
99
|
render(<Spinner />, { wrapper: I18n });
|
|
75
100
|
|
|
@@ -78,3 +103,38 @@ describe('labels built into the shipped primitives', () => {
|
|
|
78
103
|
).toBeInTheDocument();
|
|
79
104
|
});
|
|
80
105
|
});
|
|
106
|
+
|
|
107
|
+
describe('application-owned dialog layout overrides', () => {
|
|
108
|
+
it('allows a form dialog to replace default width, padding and spacing', () => {
|
|
109
|
+
render(
|
|
110
|
+
<Dialog open>
|
|
111
|
+
<DialogContent className='sm:max-w-3xl p-8 gap-2'>
|
|
112
|
+
<DialogTitle>Custom form</DialogTitle>
|
|
113
|
+
<DialogFooter className='mx-0 mb-0 px-8 py-2 gap-1'>
|
|
114
|
+
Actions
|
|
115
|
+
</DialogFooter>
|
|
116
|
+
</DialogContent>
|
|
117
|
+
</Dialog>,
|
|
118
|
+
{ wrapper: I18n },
|
|
119
|
+
);
|
|
120
|
+
const content = screen.getByRole('dialog');
|
|
121
|
+
expect(content).toHaveClass('sm:max-w-3xl', 'p-8', 'gap-2');
|
|
122
|
+
expect(content).not.toHaveClass('sm:max-w-lg', 'p-6', 'gap-5');
|
|
123
|
+
const footer = screen.getByText('Actions');
|
|
124
|
+
expect(footer).toHaveClass('mx-0', 'mb-0', 'px-8', 'py-2', 'gap-1');
|
|
125
|
+
expect(footer).not.toHaveClass('-mx-6', '-mb-6', 'px-6', 'py-4', 'gap-3');
|
|
126
|
+
});
|
|
127
|
+
it('allows a compact confirmation to replace its size default', () => {
|
|
128
|
+
render(
|
|
129
|
+
<AlertDialog open>
|
|
130
|
+
<AlertDialogContent size='sm' className='sm:max-w-xl p-8 gap-2'>
|
|
131
|
+
<AlertDialogTitle>Custom confirmation</AlertDialogTitle>
|
|
132
|
+
</AlertDialogContent>
|
|
133
|
+
</AlertDialog>,
|
|
134
|
+
{ wrapper: I18n },
|
|
135
|
+
);
|
|
136
|
+
const content = screen.getByRole('alertdialog');
|
|
137
|
+
expect(content).toHaveClass('sm:max-w-xl', 'p-8', 'gap-2');
|
|
138
|
+
expect(content).not.toHaveClass('sm:max-w-sm', 'p-6', 'gap-5');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Refine } from '@refinedev/core';
|
|
2
2
|
import { createRoot } from 'react-dom/client';
|
|
3
3
|
import { MemoryRouter } from 'react-router';
|
|
4
|
+
import { Loading } from '../../client/components/loading';
|
|
4
5
|
import { Button } from '../../client/components/ui/button';
|
|
5
6
|
import { Input } from '../../client/components/ui/input';
|
|
6
7
|
import {
|
|
@@ -36,6 +37,12 @@ export default function Fixture() {
|
|
|
36
37
|
<AppSidebarToggle />
|
|
37
38
|
</div>
|
|
38
39
|
<h1 className='text-3xl'>Theme tokens · 主题样式</h1>
|
|
40
|
+
<section
|
|
41
|
+
className='flex h-32 items-center justify-center rounded-lg border bg-background'
|
|
42
|
+
aria-label='Brand loading preview'
|
|
43
|
+
>
|
|
44
|
+
<Loading label='Loading preview' />
|
|
45
|
+
</section>
|
|
39
46
|
<p data-testid='body'>Body text · 中文内容与 English text</p>
|
|
40
47
|
<p className='font-serif'>Serif text</p>
|
|
41
48
|
<code>const theme = 'compact';</code>
|
|
@@ -276,14 +276,6 @@ describe('app server', () => {
|
|
|
276
276
|
}
|
|
277
277
|
});
|
|
278
278
|
|
|
279
|
-
it('starts without optional plugins or workflow routes', async () => {
|
|
280
|
-
const app = createTestApp();
|
|
281
|
-
|
|
282
|
-
expect(
|
|
283
|
-
app.router.routes.some((route) => route.path.includes('/workflows')),
|
|
284
|
-
).toBe(false);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
279
|
it('creates embedded apps from a scope', async () => {
|
|
288
280
|
const app = await createEmbeddedServer(
|
|
289
281
|
await createEmbeddedTestScope({
|
|
@@ -1230,12 +1222,6 @@ function createTestApp(options: CreateTestAppOptions = {}): TestApp {
|
|
|
1230
1222
|
logging: createSilentLoggingConfig(),
|
|
1231
1223
|
queue: options.queue ?? createTestQueueConfig(),
|
|
1232
1224
|
session: createNullSessionConfig(),
|
|
1233
|
-
workflow: {
|
|
1234
|
-
sourceRoot: path.resolve(process.cwd(), 'workflows'),
|
|
1235
|
-
distRoot: path.resolve(process.cwd(), 'dist/workflows'),
|
|
1236
|
-
artifactDisk: 'local',
|
|
1237
|
-
production: false,
|
|
1238
|
-
},
|
|
1239
1225
|
snowflake: {
|
|
1240
1226
|
workerId: 0,
|
|
1241
1227
|
},
|
|
@@ -1566,7 +1552,6 @@ async function writeRuntimeTestConfig(
|
|
|
1566
1552
|
main: { seeds: { autoRun: env.DB_SEEDS_AUTO_RUN === 'true' } },
|
|
1567
1553
|
},
|
|
1568
1554
|
},
|
|
1569
|
-
hub: { host: { enabled: false } },
|
|
1570
1555
|
},
|
|
1571
1556
|
});
|
|
1572
1557
|
testConfigs.push(config);
|
|
@@ -34,27 +34,6 @@ describe('client plugin registry consistency', () => {
|
|
|
34
34
|
expect(undeclared).toEqual([]);
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
it('mounts the standard user and API key pages under Settings', () => {
|
|
38
|
-
for (const [packageName, routePath] of [
|
|
39
|
-
['@nocobase/app-plugin-users', '/users'],
|
|
40
|
-
['@nocobase/app-plugin-api-keys', '/api-keys'],
|
|
41
|
-
]) {
|
|
42
|
-
const plugin = clientPlugins.plugins.find(
|
|
43
|
-
(entry) => entry.packageName === packageName,
|
|
44
|
-
);
|
|
45
|
-
expect(plugin?.routes).toEqual(
|
|
46
|
-
expect.arrayContaining([
|
|
47
|
-
expect.objectContaining({
|
|
48
|
-
parent: 'settings',
|
|
49
|
-
routes: expect.arrayContaining([
|
|
50
|
-
expect.objectContaining({ path: routePath }),
|
|
51
|
-
]),
|
|
52
|
-
}),
|
|
53
|
-
]),
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
|
|
58
37
|
it('registers no package twice', () => {
|
|
59
38
|
expect(new Set(registeredClientPackages).size).toBe(
|
|
60
39
|
registeredClientPackages.length,
|
|
@@ -29,12 +29,8 @@ describe('app client routes', () => {
|
|
|
29
29
|
|
|
30
30
|
it('loads every page component', async () => {
|
|
31
31
|
const resolved = resolveRoutes();
|
|
32
|
-
const loaders =
|
|
33
|
-
|
|
34
|
-
...componentLoadersIn(resolved.settingsRouteTree),
|
|
35
|
-
...componentLoadersIn(resolved.devRouteTree),
|
|
36
|
-
];
|
|
37
|
-
// The trees above are filtered by loader, so an empty list would make the loop below pass without loading
|
|
32
|
+
const loaders = componentLoadersIn(resolved.routes);
|
|
33
|
+
// The tree above is filtered by loader, so an empty list would make the loop below pass without loading
|
|
38
34
|
// anything at all.
|
|
39
35
|
expect(loaders).not.toHaveLength(0);
|
|
40
36
|
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
} from '@nocobase/app-plugin-authentication/client';
|
|
12
12
|
import {
|
|
13
13
|
AuthorizationClient,
|
|
14
|
-
type AuthorizationCheck,
|
|
15
14
|
authorizationClientToken,
|
|
16
15
|
} from '@nocobase/app-plugin-authorization/client';
|
|
17
16
|
import {
|
|
@@ -29,12 +28,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
29
28
|
import { AppRouter } from '../../client/routing/app-router.tsx';
|
|
30
29
|
import { AppThemeProvider } from '../../client/theme/index.ts';
|
|
31
30
|
|
|
32
|
-
// The header's inbox button reads the in-app notification plugin's API, which these tests do not serve; it is
|
|
33
|
-
// covered by tests/components/inbox.test.tsx.
|
|
34
|
-
vi.mock('../../client/components/inbox-header-button', () => ({
|
|
35
|
-
InboxHeaderButton: () => null,
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
31
|
afterEach(() => vi.unstubAllGlobals());
|
|
39
32
|
|
|
40
33
|
/** The sidebar's entries; the header's breadcrumb names the current page as well. */
|
|
@@ -109,47 +102,16 @@ describe('application shell', () => {
|
|
|
109
102
|
).toBeVisible();
|
|
110
103
|
});
|
|
111
104
|
|
|
112
|
-
it
|
|
113
|
-
'
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
createRoute(
|
|
123
|
-
'preferences',
|
|
124
|
-
'/settings/preferences',
|
|
125
|
-
'required',
|
|
126
|
-
() => <h2>Preferences</h2>,
|
|
127
|
-
'plugin',
|
|
128
|
-
'Preferences',
|
|
129
|
-
true,
|
|
130
|
-
),
|
|
131
|
-
],
|
|
132
|
-
});
|
|
133
|
-
await screen.findByRole('heading', { name: 'App client is ready' });
|
|
134
|
-
await waitFor(() =>
|
|
135
|
-
expect(can).toHaveBeenCalledWith(
|
|
136
|
-
expect.objectContaining({
|
|
137
|
-
resource: { type: 'page', id: 'preferences' },
|
|
138
|
-
action: 'access',
|
|
139
|
-
}),
|
|
140
|
-
),
|
|
141
|
-
);
|
|
142
|
-
if (allowed) {
|
|
143
|
-
expect(
|
|
144
|
-
await screen.findByRole('link', { name: 'Settings' }),
|
|
145
|
-
).toBeVisible();
|
|
146
|
-
} else {
|
|
147
|
-
expect(
|
|
148
|
-
screen.queryByRole('link', { name: 'Settings' }),
|
|
149
|
-
).not.toBeInTheDocument();
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
);
|
|
105
|
+
it('renders no Settings or Inbox entry in the header', async () => {
|
|
106
|
+
renderApplication('/', true);
|
|
107
|
+
await screen.findByRole('heading', { name: 'App client is ready' });
|
|
108
|
+
expect(
|
|
109
|
+
screen.queryByRole('link', { name: 'Settings' }),
|
|
110
|
+
).not.toBeInTheDocument();
|
|
111
|
+
expect(
|
|
112
|
+
screen.queryByRole('link', { name: /inbox/i }),
|
|
113
|
+
).not.toBeInTheDocument();
|
|
114
|
+
});
|
|
153
115
|
|
|
154
116
|
it('renders nested pages through manual outlets and selects the nearest menu ancestor', async () => {
|
|
155
117
|
const child = createRoute('detail', '/orders/42', 'required', () => (
|
|
@@ -286,57 +248,6 @@ describe('application shell', () => {
|
|
|
286
248
|
);
|
|
287
249
|
});
|
|
288
250
|
|
|
289
|
-
it('renders dev pages inside the application shell, outside its navigation', async () => {
|
|
290
|
-
const playground = createRoute(
|
|
291
|
-
'playground',
|
|
292
|
-
'/dev/playground',
|
|
293
|
-
'required',
|
|
294
|
-
() => <h2>Playground page</h2>,
|
|
295
|
-
'plugin',
|
|
296
|
-
'Playground',
|
|
297
|
-
);
|
|
298
|
-
const demos: AppClientRegisteredRoute = {
|
|
299
|
-
...createRoute('demos', '/dev/demos', 'required', () => null),
|
|
300
|
-
componentLoader: undefined,
|
|
301
|
-
navigation: { title: 'Demos' },
|
|
302
|
-
children: [
|
|
303
|
-
createRoute('chat', '/dev/demos/chat', 'required', () => (
|
|
304
|
-
<h2>Chat demo page</h2>
|
|
305
|
-
)),
|
|
306
|
-
],
|
|
307
|
-
};
|
|
308
|
-
renderApplication('/dev/playground', true, [], {
|
|
309
|
-
devRouteTree: [playground, demos],
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
expect(await screen.findByText('Playground page')).toBeVisible();
|
|
313
|
-
expect(
|
|
314
|
-
screen.getByRole('navigation', { name: 'Application navigation' }),
|
|
315
|
-
).toBeVisible();
|
|
316
|
-
expect(
|
|
317
|
-
within(sidebar()).queryByRole('link', { name: 'Playground' }),
|
|
318
|
-
).not.toBeInTheDocument();
|
|
319
|
-
expect(screen.queryByText('Demos')).not.toBeInTheDocument();
|
|
320
|
-
expect(
|
|
321
|
-
screen.queryByRole('link', { name: 'Settings' }),
|
|
322
|
-
).not.toBeInTheDocument();
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
it('renders a dev page nested under a dev group', async () => {
|
|
326
|
-
const demos: AppClientRegisteredRoute = {
|
|
327
|
-
...createRoute('demos', '/dev/demos', 'required', () => null),
|
|
328
|
-
componentLoader: undefined,
|
|
329
|
-
children: [
|
|
330
|
-
createRoute('chat', '/dev/demos/chat', 'required', () => (
|
|
331
|
-
<h2>Chat demo page</h2>
|
|
332
|
-
)),
|
|
333
|
-
],
|
|
334
|
-
};
|
|
335
|
-
renderApplication('/dev/demos/chat', true, [], { devRouteTree: [demos] });
|
|
336
|
-
|
|
337
|
-
expect(await screen.findByText('Chat demo page')).toBeVisible();
|
|
338
|
-
});
|
|
339
|
-
|
|
340
251
|
it('keeps guest pages outside the application shell', async () => {
|
|
341
252
|
renderApplication('/login', false, [
|
|
342
253
|
createRoute('login', '/login', 'guest', GuestPage),
|
|
@@ -356,8 +267,6 @@ function renderApplication(
|
|
|
356
267
|
routes: readonly AppClientRegisteredRoute[] = [],
|
|
357
268
|
options: {
|
|
358
269
|
readonly authorization?: Pick<AuthorizationClient, 'can'>;
|
|
359
|
-
readonly settingsRouteTree?: readonly AppClientRegisteredRoute[];
|
|
360
|
-
readonly devRouteTree?: readonly AppClientRegisteredRoute[];
|
|
361
270
|
} = {},
|
|
362
271
|
): void {
|
|
363
272
|
const clientRoutes = [
|
|
@@ -383,7 +292,6 @@ function renderApplication(
|
|
|
383
292
|
]);
|
|
384
293
|
const app = {
|
|
385
294
|
config: createAppClientConfig({ rawConfig: {} }),
|
|
386
|
-
runtime: { settingsRouteTree: options.settingsRouteTree ?? [] },
|
|
387
295
|
services: {
|
|
388
296
|
has: (token: unknown) => registered.has(token),
|
|
389
297
|
resolve: (token: unknown) => {
|
|
@@ -397,11 +305,7 @@ function renderApplication(
|
|
|
397
305
|
<AuthenticationProvider>
|
|
398
306
|
<MemoryRouter initialEntries={[initialEntry]}>
|
|
399
307
|
<AppThemeProvider>
|
|
400
|
-
<AppRouter
|
|
401
|
-
devRouteTree={options.devRouteTree ?? []}
|
|
402
|
-
clientRoutes={clientRoutes}
|
|
403
|
-
settingsRouteTree={options.settingsRouteTree ?? []}
|
|
404
|
-
/>
|
|
308
|
+
<AppRouter clientRoutes={clientRoutes} />
|
|
405
309
|
</AppThemeProvider>
|
|
406
310
|
</MemoryRouter>
|
|
407
311
|
</AuthenticationProvider>
|