@nocobase/app-template-default 1.0.0-beta.88 → 1.0.0-beta.90

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 (103) hide show
  1. package/.agents/rules/components.md +3 -1
  2. package/.agents/rules/shell.md +12 -2
  3. package/AGENTS.md +1 -1
  4. package/LICENSE.txt +129 -0
  5. package/client/components/back-button.tsx +37 -12
  6. package/client/components/brand-loading-mark.tsx +5 -2
  7. package/client/components/brand-mark-geometry.ts +3 -3
  8. package/client/components/breadcrumbs.tsx +47 -3
  9. package/client/components/data-table/data-table.tsx +90 -19
  10. package/client/components/data-table/pagination.tsx +17 -2
  11. package/client/components/list-feedback.tsx +31 -22
  12. package/client/components/loading.tsx +127 -16
  13. package/client/components/pending-button.tsx +111 -0
  14. package/client/components/role-list.tsx +7 -0
  15. package/client/components/route-child-page.tsx +72 -1
  16. package/client/components/route-overlay.tsx +149 -15
  17. package/client/components/task-progress.tsx +155 -0
  18. package/client/components/use-route-overlay.ts +8 -11
  19. package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +62 -32
  20. package/client/extensions/nocobase-file-component-ui/locales/de-DE.ts +2 -0
  21. package/client/extensions/nocobase-file-component-ui/locales/en-US.ts +1 -0
  22. package/client/extensions/nocobase-file-component-ui/locales/es-ES.ts +1 -0
  23. package/client/extensions/nocobase-file-component-ui/locales/fr-FR.ts +1 -0
  24. package/client/extensions/nocobase-file-component-ui/locales/id-ID.ts +1 -0
  25. package/client/extensions/nocobase-file-component-ui/locales/ja-JP.ts +2 -0
  26. package/client/extensions/nocobase-file-component-ui/locales/pt-BR.ts +1 -0
  27. package/client/extensions/nocobase-file-component-ui/locales/vi-VN.ts +1 -0
  28. package/client/extensions/nocobase-file-component-ui/locales/zh-CN.ts +1 -0
  29. package/client/extensions/nocobase-file-component-ui/locales/zh-TW.ts +1 -0
  30. package/client/extensions/nocobase-workflow-management/README.md +5 -5
  31. package/client/extensions/nocobase-workflow-management/definition-list.tsx +2 -9
  32. package/client/extensions/nocobase-workflow-management/execution-list.tsx +2 -4
  33. package/client/layouts/app-layout.tsx +0 -4
  34. package/client/lib/navigation-transform.ts +29 -1
  35. package/client/locales/system/en-US.ts +23 -0
  36. package/client/locales/system/zh-CN.ts +23 -0
  37. package/client/pages/home/index.tsx +66 -51
  38. package/client/pages/settings/database-explorer/en-US.ts +33 -0
  39. package/client/pages/settings/database-explorer/index.tsx +358 -0
  40. package/client/pages/settings/database-explorer/routes.ts +14 -0
  41. package/client/pages/settings/database-explorer/zh-CN.ts +30 -0
  42. package/client/pages/settings/default-access.tsx +1 -1
  43. package/client/pages/settings/roles.tsx +27 -27
  44. package/client/pages/settings/routes.ts +15 -7
  45. package/client/pages/settings/user-detail.tsx +3 -17
  46. package/client/pages/settings/user-form.tsx +9 -15
  47. package/client/pages/settings/users.tsx +11 -27
  48. package/client/plugins.ts +8 -1
  49. package/client/routes.ts +2 -1
  50. package/client/routing/client-route.tsx +199 -37
  51. package/client/routing/navigation-destination.ts +42 -0
  52. package/client/routing/route-leave-boundary.tsx +13 -1
  53. package/client/styles.css +18 -1
  54. package/config.example.yml +17 -11
  55. package/index.html +8 -11
  56. package/package.json +27 -27
  57. package/server/config/auth.ts +1 -1
  58. package/server/config/index.ts +3 -0
  59. package/server/config/jobs.ts +16 -8
  60. package/server/config/queue.ts +14 -13
  61. package/server/config/realtime.ts +4 -0
  62. package/tests/client/auth-i18n.test.tsx +1 -1
  63. package/tests/client/authenticated-client-fixture.test.tsx +37 -0
  64. package/tests/client/back-button.test.tsx +34 -0
  65. package/tests/client/client-route-recovery.test.tsx +93 -1
  66. package/tests/client/client-routes.test.ts +5 -3
  67. package/tests/client/database-explorer-integration.test.tsx +30 -0
  68. package/tests/client/database-explorer.test.tsx +152 -0
  69. package/tests/client/foundation-integration.test.ts +42 -29
  70. package/tests/client/home.test.tsx +26 -85
  71. package/tests/client/layout-i18n.test.tsx +83 -2
  72. package/tests/client/list-feedback.test.tsx +5 -5
  73. package/tests/client/list-loading.test.tsx +397 -0
  74. package/tests/client/loading.test.tsx +20 -11
  75. package/tests/client/manual-list-retry.test.tsx +2 -2
  76. package/tests/client/navigation-parent.test.tsx +172 -0
  77. package/tests/client/pending-button.test.tsx +106 -0
  78. package/tests/client/permission-management-tabs.test.tsx +48 -12
  79. package/tests/client/primitive-labels.test.tsx +2 -2
  80. package/tests/client/role-return.test.tsx +21 -3
  81. package/tests/client/roles.test.tsx +84 -3
  82. package/tests/client/route-leave-recheck.test.tsx +74 -0
  83. package/tests/client/route-presentation.test.tsx +557 -0
  84. package/tests/client/session-preparation.test.tsx +20 -17
  85. package/tests/client/settings-continuity.test.tsx +35 -11
  86. package/tests/client/settings-home.test.tsx +4 -2
  87. package/tests/client/settings-idle-list.test.tsx +124 -0
  88. package/tests/client/settings-navigation.test.ts +20 -0
  89. package/tests/client/user-drawer.test.tsx +3 -4
  90. package/tests/fixtures/dialog-footer.html +12 -0
  91. package/tests/fixtures/dialog-footer.tsx +215 -0
  92. package/tests/helpers/authenticated-client.tsx +77 -22
  93. package/tests/playwright/dialog-footer.test.ts +227 -0
  94. package/tests/project/bootstrap-loading.test.ts +60 -0
  95. package/tests/server/app-server.test.ts +18 -12
  96. package/tests/server/auth-email-config.test.ts +21 -0
  97. package/tests/server/backends-config.test.ts +118 -0
  98. package/tests/server/concurrency.test.ts +1 -1
  99. package/tests/server/config.test.ts +22 -14
  100. package/tests/server/lifecycle.test.ts +2 -2
  101. package/tests/server/realtime-config.test.ts +114 -0
  102. package/client/pages/home/capabilities.ts +0 -62
  103. package/client/pages/home/prompt-dialog.tsx +0 -122
@@ -1,12 +1,17 @@
1
1
  import { describe, expect, it } from 'vitest';
2
+ import {
3
+ createCapabilityCatalog,
4
+ type DependencyManifest,
5
+ } from '@nocobase/app-plugin-foundation/capability-guide';
6
+ import packageMetadata from '../../package.json' with { type: 'json' };
2
7
  import { resolveAppRuntime } from '@nocobase/app-client/runtime';
3
8
  import { defineAppRuntime } from '@nocobase/app-client/runtime';
4
9
  import appRuntime from '../../client/runtime.js';
5
10
  import { createApplicationRoutes } from '../../client/routes.js';
6
11
  import cliPlugins from '../../cli/plugins.js';
7
- import serverPlugins from '../../server/plugins.js';
8
12
  import settingsRoutes from '../../client/pages/settings/routes.js';
9
13
  import {
14
+ routeKey,
10
15
  buildRouteNavigation,
11
16
  buildSettingsCatalog,
12
17
  } from '../../client/routing/route-navigation.js';
@@ -19,38 +24,21 @@ describe('default Foundation integration', () => {
19
24
  ),
20
25
  ).toHaveLength(1);
21
26
  });
22
- it('exposes Foundation in System settings for an unrestricted administrator and removes the entry when denied', async () => {
27
+ it('provides Features and extensions in System settings with its existing route', async () => {
23
28
  const runtime = await resolveAppRuntime(appRuntime);
24
- const menu = buildRouteNavigation(runtime.routes, new Set());
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
29
  const category = buildSettingsCatalog(runtime.routes, new Set()).find(
31
30
  ({ route }) => route.name === 'settingsSystem',
32
31
  );
33
- const page = category?.modules.find(
34
- ({ route }) => route.name === 'foundation',
35
- )?.route;
36
- expect(page).toMatchObject({
37
- path: '/foundation',
38
- auth: 'required',
39
- authz: 'unrestricted',
40
- });
41
- if (!page) throw new Error('Foundation route missing');
42
- const denied = new Set([`${page.packageName}:${page.path}:${page.id}`]);
43
- expect(
44
- buildRouteNavigation(runtime.routes, denied).map(
45
- ({ route }) => route.name,
46
- ),
47
- ).toEqual(['home']);
48
- expect(buildSettingsCatalog(runtime.routes, denied)).toEqual([]);
32
+ const guide = category?.modules.find(
33
+ ({ route }) => route.name === 'capabilityGuide',
34
+ );
35
+ expect(guide?.target.path).toBe('/capabilities');
36
+ expect(guide?.route.auth).toBe('required');
49
37
  expect(
50
- serverPlugins.plugins.filter(
51
- (plugin) => plugin.packageName === '@nocobase/app-plugin-foundation',
52
- ),
53
- ).toHaveLength(1);
38
+ buildSettingsCatalog(runtime.routes, new Set([routeKey(guide!.route)]))
39
+ .flatMap((item) => item.modules)
40
+ .some((item) => item.route.name === 'capabilityGuide'),
41
+ ).toBe(false);
54
42
  });
55
43
 
56
44
  it('keeps Foundation beside business settings without changing its protected URL', async () => {
@@ -77,7 +65,32 @@ describe('default Foundation integration', () => {
77
65
  ['permissionManagement', '/settings/roles'],
78
66
  ],
79
67
  ],
80
- ['settingsSystem', [['foundation', '/foundation']]],
68
+ ['settingsSystem', [['capabilityGuide', '/capabilities']]],
81
69
  ]);
82
70
  });
71
+
72
+ it('lists every plugin this application declares in the capability guide, with the six core modules introduced on their own', () => {
73
+ const manifest = packageMetadata as DependencyManifest;
74
+ const { entries } = createCapabilityCatalog(manifest);
75
+ const declared = Object.keys({
76
+ ...manifest.dependencies,
77
+ ...manifest.devDependencies,
78
+ })
79
+ .filter((name) => name.includes('app-plugin-'))
80
+ .sort();
81
+ expect(
82
+ entries
83
+ .filter((entry) => entry.source === 'plugin')
84
+ .map((entry) => entry.packageName)
85
+ .sort(),
86
+ ).toEqual(declared);
87
+ expect(
88
+ entries
89
+ .filter((entry) => entry.source === 'kernel')
90
+ .map((entry) => entry.name),
91
+ ).toEqual(['db', 'jobs', 'lifecycle', 'queue', 'realtime']);
92
+ // Every entry has its own wording; none falls back to the generic text.
93
+ expect(entries.filter((entry) => !entry.known)).toEqual([]);
94
+ expect(entries.filter((entry) => entry.role === 'example').length).toBe(0);
95
+ });
83
96
  });
@@ -1,13 +1,23 @@
1
1
  import { renderWithApp } from '@nocobase/app-testing/client';
2
- import { screen, waitFor, within } from '@testing-library/react';
2
+ import { screen } from '@testing-library/react';
3
3
  import userEvent from '@testing-library/user-event';
4
4
  import { beforeEach, describe, expect, it, vi } from 'vitest';
5
5
 
6
6
  import packageMetadata from '../../package.json' with { type: 'json' };
7
7
  import enUS from '../../client/locales/en-US.js';
8
- import zhCN from '../../client/locales/zh-CN.js';
9
8
  import HomePage from '../../client/pages/home/index.js';
10
- import { CAPABILITIES } from '../../client/pages/home/capabilities.js';
9
+ import {
10
+ CAPABILITY_GUIDE_PATH,
11
+ capabilityPath,
12
+ createCapabilityCatalog,
13
+ entryContent,
14
+ format,
15
+ guideContent,
16
+ } from '@nocobase/app-plugin-foundation/capability-guide';
17
+
18
+ const catalog = createCapabilityCatalog(packageMetadata);
19
+ const escape = (text: string): string =>
20
+ text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
11
21
 
12
22
  function renderHome() {
13
23
  return renderWithApp(<HomePage />, {
@@ -48,79 +58,28 @@ describe('home page', () => {
48
58
  expect(screen.getByText(enUS.home.steps.describe.title)).toBeVisible();
49
59
  });
50
60
 
51
- it('shows one card per built-in capability', async () => {
61
+ it('features capabilities from the shared guide catalog, each linking to its entry', async () => {
52
62
  await renderHome();
63
+ const content = guideContent('en-US');
53
64
 
54
- for (const { id } of CAPABILITIES) {
55
- const title =
56
- enUS.home.capabilities[id as keyof typeof enUS.home.capabilities].title;
65
+ expect(catalog.featured.length).toBeGreaterThan(0);
66
+ for (const entry of catalog.featured) {
67
+ const title = entryContent(content, entry).title;
57
68
  expect(
58
- screen.getByRole('button', { name: new RegExp(title) }),
59
- ).toBeInTheDocument();
69
+ screen.getByRole('link', { name: new RegExp(escape(title)) }),
70
+ ).toHaveAttribute('href', capabilityPath(entry.id));
60
71
  }
61
72
  });
62
73
 
63
- it('opens a capability, switches prompts, and copies the edited text', async () => {
64
- const user = setup();
65
- const view = await renderHome();
66
- const scheduler = enUS.home.capabilities.scheduler;
67
-
68
- await user.click(
69
- screen.getByRole('button', { name: new RegExp(scheduler.title) }),
70
- );
71
- const dialog = await screen.findByRole('dialog');
72
- const prompt = within(dialog).getByRole('textbox', {
73
- name: enUS.home.prompt.label,
74
- });
75
- expect(prompt).toHaveValue(scheduler.prompts.reminder.text);
76
-
77
- await user.click(
78
- within(dialog).getByRole('tab', {
79
- name: scheduler.prompts.weeklyReport.label,
80
- }),
81
- );
82
- expect(prompt).toHaveValue(scheduler.prompts.weeklyReport.text);
83
-
84
- await user.clear(prompt);
85
- await user.type(prompt, 'Every Friday');
86
- await user.click(
87
- within(dialog).getByRole('button', { name: enUS.home.prompt.copy }),
88
- );
89
-
90
- expect(writeText).toHaveBeenCalledWith('Every Friday');
91
- expect(view.toasts()).toEqual([
92
- expect.objectContaining({
93
- type: 'success',
94
- title: enUS.home.prompt.copied,
95
- }),
96
- ]);
97
-
98
- await user.click(
99
- within(dialog).getByRole('button', { name: enUS.home.prompt.reset }),
100
- );
101
- expect(prompt).toHaveValue(scheduler.prompts.weeklyReport.text);
102
- });
103
-
104
- it('starts from the default prompt each time the dialog opens', async () => {
105
- const user = setup();
74
+ it('leads to the full catalog, counted from the dependencies this application declares', async () => {
106
75
  await renderHome();
107
- const theme = enUS.home.capabilities.theme;
108
- const card = screen.getByRole('button', { name: new RegExp(theme.title) });
76
+ const content = guideContent('en-US');
109
77
 
110
- await user.click(card);
111
- const prompt = within(await screen.findByRole('dialog')).getByRole(
112
- 'textbox',
113
- );
114
- await user.type(prompt, ' Edited.');
115
- await user.keyboard('{Escape}');
116
- await waitFor(() =>
117
- expect(screen.queryByRole('dialog')).not.toBeInTheDocument(),
118
- );
119
-
120
- await user.click(card);
121
78
  expect(
122
- within(await screen.findByRole('dialog')).getByRole('textbox'),
123
- ).toHaveValue(theme.prompts.changeTheme.text);
79
+ screen.getByRole('link', {
80
+ name: format(content.home.viewAll, { count: catalog.entries.length }),
81
+ }),
82
+ ).toHaveAttribute('href', CAPABILITY_GUIDE_PATH);
124
83
  });
125
84
 
126
85
  it('reports a copy the browser refuses', async () => {
@@ -140,21 +99,3 @@ describe('home page', () => {
140
99
  ]);
141
100
  });
142
101
  });
143
-
144
- describe('home page copy', () => {
145
- it('translates every capability prompt into Chinese', () => {
146
- for (const { id, prompts } of CAPABILITIES) {
147
- const key = id as keyof typeof zhCN.home.capabilities;
148
- for (const prompt of prompts) {
149
- const zh = (
150
- zhCN.home.capabilities[key].prompts as Record<
151
- string,
152
- { label: string; text: string }
153
- >
154
- )[prompt];
155
- expect(zh?.text, `${id}.${prompt}`).toBeTruthy();
156
- expect(zh?.label, `${id}.${prompt}`).toBeTruthy();
157
- }
158
- }
159
- });
160
- });
@@ -1,3 +1,4 @@
1
+ import foundation from '@nocobase/app-plugin-foundation/client/plugin';
1
2
  import {
2
3
  ClientApplicationContext,
3
4
  NavigationGuardProvider,
@@ -7,10 +8,17 @@ import {
7
8
  import type { AppClientRegisteredRoute } from '@nocobase/app-client/plugins';
8
9
  import { I18nRuntime } from '@nocobase/i18n';
9
10
  import { I18nProvider } from '@nocobase/i18n/client';
10
- import { act, render, screen } from '@testing-library/react';
11
+ import {
12
+ act,
13
+ fireEvent,
14
+ render,
15
+ screen,
16
+ waitFor,
17
+ within,
18
+ } from '@testing-library/react';
11
19
  import type { ReactNode } from 'react';
12
20
  import { MemoryRouter } from 'react-router';
13
- import { describe, expect, it, vi } from 'vitest';
21
+ import { afterEach, describe, expect, it, vi } from 'vitest';
14
22
 
15
23
  import locales from '../../client/locales/index.js';
16
24
  import { AppLayout } from '../../client/layouts/app-layout.js';
@@ -37,6 +45,8 @@ vi.mock('../../client/routing/route-navigation.js', async (importOriginal) => ({
37
45
  }),
38
46
  }));
39
47
 
48
+ afterEach(() => vi.unstubAllGlobals());
49
+
40
50
  /** The page the shell opens on: the header names it. */
41
51
  const overview: AppClientRegisteredRoute = {
42
52
  id: 'overview',
@@ -62,6 +72,10 @@ async function setup(children: ReactNode) {
62
72
  applicationNamespace: 'test-app',
63
73
  });
64
74
  runtime.registerApplicationNamespace('test-app', locales);
75
+ runtime.registerNamespace(
76
+ '@nocobase/app-plugin-foundation',
77
+ foundation().locales!,
78
+ );
65
79
  await runtime.init('en-US');
66
80
  const app = {
67
81
  config: createAppClientConfig({ rawConfig: {} }),
@@ -110,3 +124,70 @@ describe('shell translations', () => {
110
124
  ).toBeVisible();
111
125
  });
112
126
  });
127
+
128
+ it('translates the settings navigation without a separate resources footer', async () => {
129
+ const settings: AppClientRegisteredRoute = {
130
+ ...overview,
131
+ id: 'settings',
132
+ name: 'settings',
133
+ path: '/settings',
134
+ packageName: 'test',
135
+ source: 'application',
136
+ auth: 'required',
137
+ authz: 'skip',
138
+ navigation: { title: 'settings.title' },
139
+ };
140
+ const runtime = await setup(<AppLayout routes={[overview, settings]} />);
141
+ const navigation = screen.getByRole('navigation', {
142
+ name: 'Application navigation',
143
+ });
144
+ const link = within(navigation).getByRole('link', {
145
+ name: 'System settings',
146
+ });
147
+ expect(link).toHaveAttribute('href', '/settings');
148
+ expect(link.closest('[data-slot="sidebar-content"]')).not.toBeNull();
149
+ expect(
150
+ screen.queryByRole('navigation', { name: 'Resources' }),
151
+ ).not.toBeInTheDocument();
152
+ fireEvent.click(link);
153
+ await waitFor(() =>
154
+ expect(
155
+ within(navigation).getByRole('link', { name: 'System settings' }),
156
+ ).toHaveAttribute('aria-current', 'page'),
157
+ );
158
+ await act(() => runtime.changeLanguage('zh-CN'));
159
+ expect(
160
+ within(screen.getByRole('navigation', { name: '应用导航' })).getByRole(
161
+ 'link',
162
+ { name: '系统设置' },
163
+ ),
164
+ ).toHaveAttribute('href', '/settings');
165
+ });
166
+
167
+ it('closes the mobile sidebar after opening System settings', async () => {
168
+ vi.stubGlobal('innerWidth', 375);
169
+ const settings: AppClientRegisteredRoute = {
170
+ ...overview,
171
+ id: 'settings',
172
+ name: 'settings',
173
+ path: '/settings',
174
+ packageName: 'test',
175
+ source: 'application',
176
+ auth: 'required',
177
+ authz: 'skip',
178
+ navigation: { title: 'settings.title' },
179
+ };
180
+ await setup(<AppLayout routes={[overview, settings]} />);
181
+ fireEvent.click(screen.getByRole('button', { name: 'Open navigation' }));
182
+ const navigation = await screen.findByRole('navigation', {
183
+ name: 'Application navigation',
184
+ });
185
+ fireEvent.click(
186
+ within(navigation).getByRole('link', { name: 'System settings' }),
187
+ );
188
+ await waitFor(() =>
189
+ expect(
190
+ screen.queryByRole('navigation', { name: 'Application navigation' }),
191
+ ).not.toBeInTheDocument(),
192
+ );
193
+ });
@@ -97,7 +97,7 @@ describe('shared table and card feedback', () => {
97
97
  />,
98
98
  options,
99
99
  );
100
- expect(screen.getByRole('status').textContent).toBe(
100
+ expect((await screen.findByRole('status')).textContent).toBe(
101
101
  enUS.listFeedback.loading,
102
102
  );
103
103
  expect(screen.getByRole('columnheader', { name: 'ID' })).toBeVisible();
@@ -159,7 +159,7 @@ describe('shared table and card feedback', () => {
159
159
  expect(
160
160
  screen.getByRole('button', { name: 'Search tools' }),
161
161
  ).toBeVisible();
162
- expect(screen.getByRole(role).closest('td')).not.toBeNull();
162
+ expect((await screen.findByRole(role)).closest('td')).not.toBeNull();
163
163
  }
164
164
 
165
165
  view.rerender(
@@ -178,9 +178,8 @@ describe('shared table and card feedback', () => {
178
178
  });
179
179
 
180
180
  it('renders each built-in state as one icon and one short line without inline actions', async () => {
181
- const view = await renderWithApp(<ListFeedback state='loading' />, options);
181
+ const view = await renderWithApp(<ListFeedback state='empty' />, options);
182
182
  const states = [
183
- ['loading', 'status'],
184
183
  ['empty', 'status'],
185
184
  ['no-results', 'status'],
186
185
  ['forbidden', 'alert'],
@@ -273,8 +272,9 @@ describe('shared table and card feedback', () => {
273
272
  expect(screen.getByRole('alert').textContent).toBe(
274
273
  enUS.listFeedback.failed,
275
274
  );
275
+ expect(screen.getByRole('button')).toBeDisabled();
276
276
  expect(
277
- screen.getByRole('button', { name: enUS.listFeedback.retrying }),
277
+ await screen.findByRole('button', { name: enUS.listFeedback.retrying }),
278
278
  ).toBeDisabled();
279
279
  });
280
280