@nocobase/app-template-default 1.0.0-beta.87 → 1.0.0-beta.89

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 (107) hide show
  1. package/.agents/rules/components.md +3 -1
  2. package/.agents/rules/notification-templates.md +5 -0
  3. package/.agents/rules/shell.md +9 -3
  4. package/AGENTS.md +4 -2
  5. package/README.MD +11 -5
  6. package/client/components/back-button.tsx +37 -12
  7. package/client/components/body-portal.tsx +25 -0
  8. package/client/components/brand-loading-mark.tsx +5 -2
  9. package/client/components/brand-mark-geometry.ts +3 -3
  10. package/client/components/breadcrumbs.tsx +60 -3
  11. package/client/components/data-table/data-table.tsx +90 -19
  12. package/client/components/data-table/pagination.tsx +17 -2
  13. package/client/components/flow-diagram.tsx +2 -2
  14. package/client/components/list-feedback.tsx +31 -22
  15. package/client/components/loading.tsx +127 -16
  16. package/client/components/pending-button.tsx +111 -0
  17. package/client/components/role-list.tsx +7 -0
  18. package/client/components/route-child-page.tsx +72 -1
  19. package/client/components/route-overlay.tsx +149 -15
  20. package/client/components/task-progress.tsx +155 -0
  21. package/client/components/use-route-overlay.ts +8 -11
  22. package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +62 -32
  23. package/client/extensions/nocobase-file-component-ui/locales/de-DE.ts +2 -0
  24. package/client/extensions/nocobase-file-component-ui/locales/en-US.ts +1 -0
  25. package/client/extensions/nocobase-file-component-ui/locales/es-ES.ts +1 -0
  26. package/client/extensions/nocobase-file-component-ui/locales/fr-FR.ts +1 -0
  27. package/client/extensions/nocobase-file-component-ui/locales/id-ID.ts +1 -0
  28. package/client/extensions/nocobase-file-component-ui/locales/ja-JP.ts +2 -0
  29. package/client/extensions/nocobase-file-component-ui/locales/pt-BR.ts +1 -0
  30. package/client/extensions/nocobase-file-component-ui/locales/vi-VN.ts +1 -0
  31. package/client/extensions/nocobase-file-component-ui/locales/zh-CN.ts +1 -0
  32. package/client/extensions/nocobase-file-component-ui/locales/zh-TW.ts +1 -0
  33. package/client/extensions/nocobase-workflow-management/README.md +5 -5
  34. package/client/extensions/nocobase-workflow-management/definition-list.tsx +2 -9
  35. package/client/extensions/nocobase-workflow-management/execution-list.tsx +2 -4
  36. package/client/layouts/app-layout.tsx +8 -8
  37. package/client/layouts/components/sidebar-footer.tsx +18 -25
  38. package/client/lib/navigation-transform.ts +156 -22
  39. package/client/locales/system/en-US.ts +26 -5
  40. package/client/locales/system/settings-en-US.ts +17 -0
  41. package/client/locales/system/settings-zh-CN.ts +15 -0
  42. package/client/locales/system/zh-CN.ts +26 -5
  43. package/client/pages/auth/register.tsx +39 -22
  44. package/client/pages/auth/shared.tsx +1 -3
  45. package/client/pages/settings/default-access.tsx +1 -1
  46. package/client/pages/settings/hooks.tsx +3 -68
  47. package/client/pages/settings/notification-routes.ts +109 -0
  48. package/client/pages/settings/notification-templates.tsx +26 -0
  49. package/client/pages/settings/permission-management-page.tsx +15 -90
  50. package/client/pages/settings/roles.tsx +27 -27
  51. package/client/pages/settings/routes.ts +107 -101
  52. package/client/pages/settings/settings-home.tsx +109 -0
  53. package/client/pages/settings/settings-module-page.tsx +158 -0
  54. package/client/pages/settings/shell-routes.ts +53 -0
  55. package/client/pages/settings/user-detail.tsx +3 -17
  56. package/client/pages/settings/user-form.tsx +9 -15
  57. package/client/pages/settings/users.tsx +11 -27
  58. package/client/routes.ts +17 -1
  59. package/client/routing/app-router.tsx +1 -14
  60. package/client/routing/client-route.tsx +199 -30
  61. package/client/routing/navigation-destination.ts +42 -0
  62. package/client/routing/route-context.tsx +30 -3
  63. package/client/routing/route-leave-boundary.tsx +261 -0
  64. package/client/routing/route-navigation.ts +72 -8
  65. package/client/routing/route-tree.tsx +7 -12
  66. package/client/runtime.ts +2 -2
  67. package/client/styles.css +18 -1
  68. package/config.example.yml +1 -1
  69. package/package.json +13 -13
  70. package/server/config/auth.ts +1 -1
  71. package/server/providers/index.ts +2 -0
  72. package/server/providers/notification-management.ts +27 -0
  73. package/tests/client/account-permissions.test.tsx +1 -1
  74. package/tests/client/auth-i18n.test.tsx +1 -1
  75. package/tests/client/authenticated-client-fixture.test.tsx +37 -0
  76. package/tests/client/back-button.test.tsx +34 -0
  77. package/tests/client/client-auth.test.tsx +10 -1
  78. package/tests/client/client-route-recovery.test.tsx +93 -1
  79. package/tests/client/client-routes.test.ts +137 -4
  80. package/tests/client/client-shell.test.tsx +23 -8
  81. package/tests/client/foundation-integration.test.ts +38 -17
  82. package/tests/client/layout-i18n.test.tsx +9 -4
  83. package/tests/client/list-feedback.test.tsx +5 -5
  84. package/tests/client/list-loading.test.tsx +397 -0
  85. package/tests/client/loading.test.tsx +20 -11
  86. package/tests/client/manual-list-retry.test.tsx +2 -2
  87. package/tests/client/navigation-parent.test.tsx +172 -0
  88. package/tests/client/pending-button.test.tsx +106 -0
  89. package/tests/client/permission-management-tabs.test.tsx +458 -17
  90. package/tests/client/primitive-labels.test.tsx +2 -2
  91. package/tests/client/registration-feedback.test.tsx +143 -0
  92. package/tests/client/role-return.test.tsx +21 -3
  93. package/tests/client/roles.test.tsx +84 -3
  94. package/tests/client/route-leave-recheck.test.tsx +74 -0
  95. package/tests/client/route-presentation.test.tsx +557 -0
  96. package/tests/client/session-preparation.test.tsx +20 -17
  97. package/tests/client/settings-continuity.test.tsx +35 -11
  98. package/tests/client/settings-home.test.tsx +112 -0
  99. package/tests/client/settings-idle-list.test.tsx +124 -0
  100. package/tests/client/settings-navigation.test.ts +230 -0
  101. package/tests/client/user-drawer.test.tsx +3 -4
  102. package/tests/fixtures/dialog-footer.html +12 -0
  103. package/tests/fixtures/dialog-footer.tsx +215 -0
  104. package/tests/helpers/authenticated-client.tsx +70 -22
  105. package/tests/playwright/dialog-footer.test.ts +227 -0
  106. package/tests/server/notification-templates.test.ts +144 -0
  107. package/tests/client/permission-management-routes.test.ts +0 -64
@@ -5,8 +5,8 @@ Install with `yes n | pnpm exec shadcn add @nocobase/workflow-management`. This
5
5
  | File | Exports and responsibility |
6
6
  | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7
7
  | `layout.tsx` | `WorkflowManagementLayout`: page container, shared header and line-style tabs, hidden in detail mode. `WorkflowDefinitionDetail`: diagram and parameter form cards in two columns. |
8
- | `definition-list.tsx` | `WorkflowDefinitionList`: optional category rail, search slot, refresh and execution-activity sort controls, adapted definition rows and empty/status content. |
9
- | `execution-list.tsx` | `WorkflowExecutionList`: filters slot, refresh, controlled ordering, adapted execution rows, empty/status content and controlled pagination. |
8
+ | `definition-list.tsx` | `WorkflowDefinitionList`: optional category rail, search slot and execution-activity sort controls, adapted definition rows and empty/status content. |
9
+ | `execution-list.tsx` | `WorkflowExecutionList`: filters slot, controlled ordering, adapted execution rows, empty/status content and controlled pagination. |
10
10
  | `execution-detail.tsx` | `WorkflowExecutionDetail`: ordered history, step selection, diagram rendering callback, effects and optional recorded-parameter content. |
11
11
 
12
12
  ## Routes and ownership
@@ -53,11 +53,11 @@ const routes = defineAppRoutes([
53
53
 
54
54
  Add definition and execution detail routes beneath the corresponding tab route, also without `navigation`. The parent page maps the URL to `tab`, navigates in `onTabChange`, and supplies its `Outlet` as children. Reuse an existing system group instead of registering another one.
55
55
 
56
- Keep requests, query parameters, IME-aware search, filtering, pagination, category CRUD and permissions in application adapters. Map rows into display values and router links before passing them to the lists. `loaded` distinguishes an empty response from data that has not arrived; `loading` disables refresh, and the caller disables pagination while pending. Supply error/loading/retry content through `status`, and separate empty-catalog and no-results wording through `empty`. The search slot is constrained beside Refresh on wider screens; the category rail stacks above the list on small screens. Omit `categories` when classification is unnecessary. The installable demo provides in-memory category filtering, named color selection and add/edit/delete dialogs as an application adapter; deleting a category preserves its workflows and clears the selected category filter. Its definition header also demonstrates a colored category dropdown with an Uncategorized option; assignment uses the same category state as list filtering. The production adapter keeps the assignment API and read/update permission checks in the application.
56
+ Keep requests, query parameters, IME-aware search, filtering, pagination, category CRUD and permissions in application adapters. Map rows into display values and router links before passing them to the lists. `loaded` distinguishes an empty response from data that has not arrived; `loading` supplies pending feedback, and the caller disables pagination while pending. Supply error/loading/retry content through `status`, and separate empty-catalog and no-results wording through `empty`. The search slot stays compact on wider screens; the category rail stacks above the list on small screens. Omit `categories` when classification is unnecessary. The installable demo provides in-memory category filtering, named color selection and add/edit/delete dialogs as an application adapter; deleting a category preserves its workflows and clears the selected category filter. Its definition header also demonstrates a colored category dropdown with an Uncategorized option; assignment uses the same category state as list filtering. The production adapter keeps the assignment API and read/update permission checks in the application.
57
57
 
58
58
  `WorkflowDefinitionDetail` accepts a diagram slot and a parameter form slot. Render the installed `@nocobase/lifecycle-diagram` or an existing compatible diagram in a definite-height or content-sized canvas. Keep schema validation, effective timing, complete-group saves, CAS conflicts and read-only permissions in the application's parameter editor. Show a translated empty state when a definition declares no adjustable parameters.
59
59
 
60
- `WorkflowExecutionDetail` sorts transitions by version, timestamp and id without mutating the input. Selecting a step passes only that history prefix and its destination state to `renderDiagram`; no transition or server action occurs. Pass the runtime's transition records directly, retaining extra fields through its generic history type. `effects` displays persisted statuses, attempts and failures; it does not retry them. Pass recorded parameter content through `parameters` and label historical facts separately from current configuration. Remount the detail on record changes or an explicit refresh to reset historical selection, as the example application does.
60
+ `WorkflowExecutionDetail` sorts transitions by version, timestamp and id without mutating the input. Selecting a step passes only that history prefix and its destination state to `renderDiagram`; no transition or server action occurs. Pass the runtime's transition records directly, retaining extra fields through its generic history type. `effects` displays persisted statuses, attempts and failures; it does not retry them. Pass recorded parameter content through `parameters` and label historical facts separately from current configuration. Remount the detail on record identity changes; preserve a selected historical step during automatic updates.
61
61
 
62
62
  The application owns authentication, initial administrator grants, navigation and direct-page guards, server read/update authorization and any recovery actions. Configuration permission does not grant business approval permission. Installing this block creates source files, not routes, permissions, runtime services or database collections.
63
63
 
@@ -71,4 +71,4 @@ Omit `WorkflowDefinitionDetail.parameters` when the editor opens contextually so
71
71
 
72
72
  Ordering is optional: omit `onSort` when activity ordering is not supported so the header stays plain text. Omit a row's optional `name` when its linked `title` already supplies the complete display name. Hosts own filtering, pagination and their URL state; keep that state when opening and returning from a definition or execution.
73
73
 
74
- List hosts pass `listState={resolveListState(request, rows.length, hasEffectiveFilters)}` and `onClearFilters`, using the current request error and data. The DataTable dependency supplies shared loading, denial, session and failure copy; merge the `listFeedback` English/Chinese keys from the components README into application translations. `onRefresh` is reused for retry and must preserve the current query. Keep `loaded`, `loading`, custom `empty` content and existing permission checks when reconciling installed copies. A successful empty filter is `no-results`; an empty unfiltered read is `empty`. Hosts that retain the legacy status slot without a listState still own their error rendering.
74
+ List hosts pass `listState={resolveListState(request, rows.length, hasEffectiveFilters)}` and `onClearFilters`, using the current request error and data. The DataTable dependency supplies shared loading, denial, session and failure copy; merge the `listFeedback` English/Chinese keys from the components README into application translations. `onRefresh` handles contextual failed-read retry and must preserve the current query; ready lists render no Refresh action. The host owns subscription/snapshot updates for open progress views. Keep `loaded`, `loading`, custom `empty` content and existing permission checks when reconciling installed copies. A successful empty filter is `no-results`; an empty unfiltered read is `empty`. Hosts that retain the legacy status slot without a listState still own their error rendering.
@@ -27,7 +27,8 @@ export interface WorkflowDefinitionListProps {
27
27
  loading: boolean;
28
28
  empty?: ReactNode;
29
29
  labels: {
30
- refresh: string;
30
+ /** Legacy adapter label; ready lists no longer render a Refresh control. */
31
+ refresh?: string;
31
32
  workflow: string;
32
33
  executionCount: string;
33
34
  executionActivity: string;
@@ -66,14 +67,6 @@ export function WorkflowDefinitionList({
66
67
  <div className='flex w-full min-w-0 flex-1 flex-col gap-4'>
67
68
  <div className='flex flex-col items-stretch justify-between gap-2 sm:flex-row sm:items-center'>
68
69
  <div className='min-w-0 flex-1 sm:max-w-xs'>{search}</div>
69
- <Button
70
- className='shrink-0 self-start sm:self-auto'
71
- variant='outline'
72
- disabled={loading}
73
- onClick={onRefresh}
74
- >
75
- {labels.refresh}
76
- </Button>
77
70
  </div>
78
71
  {status}
79
72
  {(listState !== undefined || loaded || loading) && (
@@ -27,7 +27,8 @@ export interface WorkflowExecutionListProps {
27
27
  status?: ReactNode;
28
28
  empty?: ReactNode;
29
29
  labels: {
30
- refresh: string;
30
+ /** Legacy adapter label; ready lists no longer render a Refresh control. */
31
+ refresh?: string;
31
32
  record: string;
32
33
  workflow: string;
33
34
  state: string;
@@ -72,9 +73,6 @@ export function WorkflowExecutionList({
72
73
  <section className='flex flex-col gap-4'>
73
74
  <div className='flex flex-wrap items-center justify-between gap-3'>
74
75
  <div className='flex flex-wrap items-center gap-2'>{filters}</div>
75
- <Button variant='outline' disabled={loading} onClick={onRefresh}>
76
- {labels.refresh}
77
- </Button>
78
76
  </div>
79
77
  {status}
80
78
  {(listState !== undefined || loaded || loading) && (
@@ -1,5 +1,5 @@
1
1
  import { useSyncServerLocale } from '@nocobase/app-plugin-i18n/client';
2
- import type { ReactElement } from 'react';
2
+ import { useMemo, type ReactElement } from 'react';
3
3
  import type { AppClientRegisteredRoute } from '@nocobase/app-client/plugins';
4
4
  import { Outlet, useLocation } from 'react-router';
5
5
 
@@ -7,6 +7,7 @@ import { BrandWatermark } from '#components/brand-watermark';
7
7
  import { Breadcrumbs } from '#components/breadcrumbs';
8
8
 
9
9
  import { RouteTreeProvider } from '../routing/route-context.js';
10
+ import { RouteLeaveBoundary } from '../routing/route-leave-boundary.js';
10
11
 
11
12
  import { PageBreadcrumbProvider } from '@nocobase/app-client';
12
13
  import { useTranslation } from '@nocobase/i18n/client';
@@ -17,7 +18,6 @@ import {
17
18
  AppSidebarToggle,
18
19
  } from './components/app-sidebar.js';
19
20
  import { NavigationMenu } from './components/navigation-menu.js';
20
- import { AppBrand } from './components/app-brand.js';
21
21
  import { HeaderActions } from './components/header-actions.js';
22
22
  import { AppSidebarFooter } from './components/sidebar-footer.js';
23
23
  import {
@@ -34,7 +34,8 @@ export function AppLayout({ routes }: AppLayoutProps): ReactElement {
34
34
  useSyncServerLocale();
35
35
 
36
36
  const { t } = useTranslation();
37
- const { items: menuItems, denied } = useRouteNavigation(routes);
37
+ const { items: menuItems, denied, loading } = useRouteNavigation(routes);
38
+ const access = useMemo(() => ({ denied, loading }), [denied, loading]);
38
39
  const selectedKey = selectedNavigationId(
39
40
  routes,
40
41
  useLocation().pathname,
@@ -46,7 +47,7 @@ export function AppLayout({ routes }: AppLayoutProps): ReactElement {
46
47
 
47
48
  return (
48
49
  // The shell owns the business route tree used by its pages and navigation.
49
- <RouteTreeProvider routes={routes}>
50
+ <RouteTreeProvider routes={routes} access={access}>
50
51
  <PageBreadcrumbProvider>
51
52
  <AppSidebarProvider>
52
53
  <AppSidebar label={navigationLabel} footer={<AppSidebarFooter />}>
@@ -60,9 +61,6 @@ export function AppLayout({ routes }: AppLayoutProps): ReactElement {
60
61
  <LayoutHeader className='sticky top-0 z-40 justify-between'>
61
62
  <div className='flex min-w-0 items-center gap-3'>
62
63
  <AppSidebarToggle />
63
- <div className='md:hidden'>
64
- <AppBrand />
65
- </div>
66
64
  <div className='h-5 w-px shrink-0 bg-border' />
67
65
  {/* The current page's trail: the route tree's, or the one the page declares (`usePageBreadcrumb`). */}
68
66
  <Breadcrumbs denied={denied} />
@@ -74,7 +72,9 @@ export function AppLayout({ routes }: AppLayoutProps): ReactElement {
74
72
  {/* main only positions; the page scrolls in here, so a child page layer laid over main is neither
75
73
  moved by the page's scrolling nor stretched by its height. */}
76
74
  <div className='h-full overflow-y-auto'>
77
- <Outlet />
75
+ <RouteLeaveBoundary hidden={loading}>
76
+ <Outlet />
77
+ </RouteLeaveBoundary>
78
78
  </div>
79
79
  </main>
80
80
  </div>
@@ -12,8 +12,8 @@ import {
12
12
  } from '#components/ui/tooltip';
13
13
 
14
14
  /**
15
- * The application's sidebar footer: the NocoBase slogan's two lines, then the template's name and its version, each
16
- * on its own line. In icon mode only the shield remains, and a tooltip carries the same text.
15
+ * The application's sidebar footer: the NocoBase slogan's two lines, then the application's version.
16
+ * In icon mode only the shield remains, and a tooltip carries the same text.
17
17
  */
18
18
  export function AppSidebarFooter(): ReactElement {
19
19
  const { t } = useTranslation();
@@ -21,16 +21,15 @@ export function AppSidebarFooter(): ReactElement {
21
21
  const iconOnly = state === 'collapsed' && !isMobile;
22
22
  // Published by the server from the application's package.json.
23
23
  const publicConfig = useClientApplication().config.public;
24
- const templateName = publicConfig.get('app.displayName', 'Default Template');
25
24
  const templateVersion = publicConfig.get('app.version', '0.0.0');
26
25
  const versionLabel = `v${templateVersion}`;
27
- const buildFreely = t('shell.buildFreely', {
28
- defaultValue: 'AI builds freely.',
29
- });
30
26
  // The tooltip and the icon's accessible name have no link, so the brand's markup is dropped there.
31
- const reliability = t('shell.reliability', {
32
- defaultValue: '<brand>NocoBase</brand> keeps it reliable.',
27
+ const brandPartnership = t('shell.brandPartnership', {
28
+ defaultValue: 'AI + <brand>NocoBase</brand>',
33
29
  }).replace(/<\/?brand>/g, '');
30
+ const buildReliableSystems = t('shell.buildReliableSystems', {
31
+ defaultValue: 'Build reliable business systems',
32
+ });
34
33
 
35
34
  const brandLink = (
36
35
  <a
@@ -53,7 +52,7 @@ export function AppSidebarFooter(): ReactElement {
53
52
  <span
54
53
  aria-label={
55
54
  iconOnly
56
- ? `${buildFreely} ${reliability} ${templateName} ${versionLabel}`
55
+ ? `${brandPartnership} ${buildReliableSystems} ${versionLabel}`
57
56
  : undefined
58
57
  }
59
58
  className='flex h-5 shrink-0 items-center rounded-sm text-sidebar-foreground/80 outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring'
@@ -69,35 +68,29 @@ export function AppSidebarFooter(): ReactElement {
69
68
  hidden={!iconOnly}
70
69
  side='right'
71
70
  >
72
- <span className='font-medium'>{buildFreely}</span>
73
- <span>{reliability}</span>
74
- <span className='mt-1 opacity-70'>
75
- {templateName} {versionLabel}
76
- </span>
71
+ <span className='font-medium'>{brandPartnership}</span>
72
+ <span>{buildReliableSystems}</span>
73
+ <span className='mt-1 opacity-70'>{versionLabel}</span>
77
74
  </TooltipContent>
78
75
  </Tooltip>
79
76
  <div className='min-w-0 flex-1 group-data-[collapsible=icon]:hidden'>
80
77
  <p className='font-medium text-balance text-sidebar-foreground'>
81
- {buildFreely}
82
- </p>
83
- <p className='text-balance text-muted-foreground'>
84
78
  <Trans
85
79
  t={t}
86
- i18nKey='shell.reliability'
87
- defaults='<brand>NocoBase</brand> keeps it reliable.'
80
+ i18nKey='shell.brandPartnership'
81
+ defaults='AI + <brand>NocoBase</brand>'
88
82
  components={{ brand: brandLink }}
89
83
  >
90
- {brandLink} keeps it reliable.
84
+ AI + {brandLink}
91
85
  </Trans>
92
86
  </p>
93
- {/* Name and version stay on one line each; a long name is cut off, with the whole name as its title. */}
87
+ <p className='text-balance text-muted-foreground'>
88
+ {buildReliableSystems}
89
+ </p>
94
90
  <p
95
91
  className='mt-1 truncate text-muted-foreground'
96
- title={templateName}
92
+ title={versionLabel}
97
93
  >
98
- {templateName}
99
- </p>
100
- <p className='truncate text-muted-foreground' title={versionLabel}>
101
94
  {versionLabel}
102
95
  </p>
103
96
  </div>
@@ -1,36 +1,170 @@
1
1
  import { groupFoundationNavigation } from '@nocobase/app-plugin-foundation/client';
2
+ import type { AppClientRegisteredRoute } from '@nocobase/app-client/plugins';
2
3
  import type { RouteNavigationItem } from '../routing/route-navigation.js';
3
4
 
5
+ /**
6
+ * The settings route group: one sidebar entry opening its home page. Its direct child groups are categories on that
7
+ * page, the entries inside a category are modules (one card each), and the pages of a module group are its tabs.
8
+ */
9
+ export const SETTINGS_GROUP = 'settings';
10
+ /** The settings home page, a navigation-less child of the settings group. */
11
+ export const SETTINGS_HOME = 'settingsHome';
12
+ /** The category a module registered directly under the settings group is shown in. */
13
+ export const SETTINGS_FALLBACK_CATEGORY = 'settingsSystem';
14
+
15
+ /**
16
+ * Application-owned navigation structure, before the sidebar folds the settings group into one entry. `routes` is the
17
+ * registered tree the permission-filtered `items` came from.
18
+ */
19
+ export function organizeNavigation(
20
+ items: RouteNavigationItem[],
21
+ routes: readonly AppClientRegisteredRoute[],
22
+ ): RouteNavigationItem[] {
23
+ return groupFoundationNavigation(
24
+ withSettingsGroup(items, routes),
25
+ SETTINGS_GROUP,
26
+ );
27
+ }
28
+
4
29
  /** Application-owned navigation customization, applied after permission filtering. */
5
30
  export function transformNavigation(
6
31
  items: RouteNavigationItem[],
32
+ routes: readonly AppClientRegisteredRoute[],
7
33
  ): RouteNavigationItem[] {
8
- return groupFoundationNavigation(collapsePermissionNavigation(items));
34
+ return collapseSettingsNavigation(organizeNavigation(items, routes));
9
35
  }
10
36
 
11
- /** One sidebar entry opens the first readable tab; original routes and guards remain unchanged. */
12
- export function collapsePermissionNavigation(
37
+ /**
38
+ * Permission filtering drops a group with nothing visible in it, which the settings group is until a module joins it.
39
+ * Keep it in place so a plugin's settings entry, such as App Foundation, still has a group to join.
40
+ */
41
+ function withSettingsGroup(
13
42
  items: RouteNavigationItem[],
43
+ routes: readonly AppClientRegisteredRoute[],
14
44
  ): RouteNavigationItem[] {
15
- return items.map((item) => {
16
- if (item.route.name !== 'permissionManagement')
17
- return {
18
- ...item,
19
- children: collapsePermissionNavigation([...item.children]),
20
- };
21
- const first = item.children[0];
22
- if (!first) return item;
23
- return {
24
- route: {
25
- ...first.route,
26
- name: item.route.name,
27
- navigation: item.route.navigation,
45
+ const index = routes.findIndex((route) => settingsHomeRoute(route));
46
+ if (index < 0 || items.some((item) => item.route === routes[index]))
47
+ return items;
48
+ const position = items.filter(
49
+ (item) => routes.indexOf(item.route) < index,
50
+ ).length;
51
+ return [
52
+ ...items.slice(0, position),
53
+ { route: routes[index], children: [] },
54
+ ...items.slice(position),
55
+ ];
56
+ }
57
+
58
+ /** The settings home page of a settings group route, when the application registers one. */
59
+ export function settingsHomeRoute(
60
+ route: AppClientRegisteredRoute,
61
+ ): AppClientRegisteredRoute | undefined {
62
+ if (route.name !== SETTINGS_GROUP || route.componentLoader) return undefined;
63
+ return route.children?.find(
64
+ (child) => child.name === SETTINGS_HOME && child.componentLoader,
65
+ );
66
+ }
67
+
68
+ /**
69
+ * The sidebar shows the settings group as one entry opening its home page; it stays selected on every page below the
70
+ * group. Without a module the viewer may open, the entry disappears. An application without a home page keeps the
71
+ * expanded group.
72
+ */
73
+ export function collapseSettingsNavigation(
74
+ items: RouteNavigationItem[],
75
+ ): RouteNavigationItem[] {
76
+ return items.flatMap((item) => {
77
+ const home = settingsHomeRoute(item.route);
78
+ if (!home) return [item];
79
+ if (item.children.length === 0) return [];
80
+ return [
81
+ {
82
+ route: { ...home, navigation: item.route.navigation },
83
+ children: [],
84
+ activeRouteKeys: [home, ...descendantPages(item.route)].map(
85
+ (route) => `${route.packageName}:${route.path}:${route.id}`,
86
+ ),
28
87
  },
29
- children: [],
30
- activeRouteKeys: item.children.map(
31
- (child) =>
32
- `${child.route.packageName}:${child.route.path}:${child.route.id}`,
33
- ),
34
- };
88
+ ];
35
89
  });
36
90
  }
91
+
92
+ function descendantPages(
93
+ route: AppClientRegisteredRoute,
94
+ ): AppClientRegisteredRoute[] {
95
+ return (route.children ?? []).flatMap((child) => [
96
+ ...(child.componentLoader ? [child] : []),
97
+ ...descendantPages(child),
98
+ ]);
99
+ }
100
+
101
+ export interface SettingsModule {
102
+ /** The module's own route: a page, or the group whose pages are its tabs. */
103
+ readonly route: AppClientRegisteredRoute;
104
+ /** The page a card opens: the module page, or the first tab the viewer may open. */
105
+ readonly target: AppClientRegisteredRoute;
106
+ }
107
+
108
+ export interface SettingsCategory {
109
+ readonly route: AppClientRegisteredRoute;
110
+ readonly modules: readonly SettingsModule[];
111
+ }
112
+
113
+ /**
114
+ * The settings home's cards, from the permission-filtered navigation (`organizeNavigation` output): one category per
115
+ * child group of the settings group, in route order, and the modules the viewer may open in each. A module registered
116
+ * directly under the settings group joins the fallback category. Empty categories are left out.
117
+ */
118
+ export function settingsCatalog(
119
+ items: readonly RouteNavigationItem[],
120
+ ): SettingsCategory[] {
121
+ const settings = items.find((item) => settingsHomeRoute(item.route));
122
+ if (!settings) return [];
123
+ const categories = settings.children.filter(
124
+ (child) => !child.route.componentLoader,
125
+ );
126
+ const loose = settings.children
127
+ .filter((child) => child.route.componentLoader)
128
+ .flatMap(toModule);
129
+ const result = categories.map((category) => ({
130
+ route: category.route,
131
+ modules: category.children.flatMap(toModule),
132
+ }));
133
+ if (loose.length) {
134
+ const fallback = result.find(
135
+ (category) => category.route.name === SETTINGS_FALLBACK_CATEGORY,
136
+ );
137
+ const declared = settings.route.children?.find(
138
+ (child) => child.name === SETTINGS_FALLBACK_CATEGORY,
139
+ );
140
+ if (fallback)
141
+ result[result.indexOf(fallback)] = {
142
+ ...fallback,
143
+ modules: [...fallback.modules, ...loose],
144
+ };
145
+ else
146
+ result.push({
147
+ route: declared ?? settings.route,
148
+ modules: loose,
149
+ });
150
+ }
151
+ return result.filter((category) => category.modules.length > 0);
152
+ }
153
+
154
+ function toModule(item: RouteNavigationItem): SettingsModule[] {
155
+ if (item.route.componentLoader)
156
+ return [{ route: item.route, target: item.route }];
157
+ const target = firstPage(item);
158
+ return target ? [{ route: item.route, target }] : [];
159
+ }
160
+
161
+ function firstPage(
162
+ item: RouteNavigationItem,
163
+ ): AppClientRegisteredRoute | undefined {
164
+ for (const child of item.children) {
165
+ if (child.route.componentLoader) return child.route;
166
+ const nested = firstPage(child);
167
+ if (nested) return nested;
168
+ }
169
+ return undefined;
170
+ }
@@ -8,6 +8,20 @@ import inboxEnUS from '#extensions/nocobase-inbox/locales/en-US';
8
8
  * upgrade replaces this file, so put the application's own copy in `../en-US.ts` rather than here.
9
9
  */
10
10
  const systemEnUS = {
11
+ pendingFeedback: {
12
+ working: 'Working…',
13
+ longWait: 'This is taking longer than usual. You can keep waiting.',
14
+ },
15
+ taskProgress: {
16
+ queued: 'Waiting to start',
17
+ running: 'In progress',
18
+ completed: 'Completed',
19
+ failed: 'Failed',
20
+ canceled: 'Canceled',
21
+ count: '{{processed}} of {{total}}',
22
+ processed: '{{processed}} processed',
23
+ background: 'It keeps running if you leave this page.',
24
+ },
11
25
  listFeedback: {
12
26
  loading: 'Loading',
13
27
  empty: 'No data yet',
@@ -88,6 +102,9 @@ const systemEnUS = {
88
102
  'auth.loginDescription': 'Sign in with your username or email and password.',
89
103
  'auth.registerTitle': 'Create an account',
90
104
  'auth.registerDescription': 'Create an account to get started.',
105
+ 'auth.registrationAccepted': 'Registration request submitted',
106
+ 'auth.registrationGuidance':
107
+ 'Try signing in with the username and password you submitted. If this email already has an account, the new details will not take effect. Sign in with your original account or recover your password. Follow any verification instructions provided by the application before signing in.',
91
108
  'auth.forgotTitle': 'Forgot password',
92
109
  'auth.forgotDescription':
93
110
  'Enter your email and we will send a reset link if the account exists.',
@@ -130,9 +147,8 @@ const systemEnUS = {
130
147
  'auth.continueWith': 'Continue with {provider}',
131
148
  'auth.about': 'About this application',
132
149
  'auth.platform': 'AI-native application platform',
133
- 'auth.marketingTitleFirst': 'Let AI build freely.',
134
- 'auth.marketingTitleSecond': 'NocoBase keeps it',
135
- 'auth.marketingTitleThird': 'reliable.',
150
+ 'auth.marketingTitleFirst': 'AI + NocoBase',
151
+ 'auth.marketingTitleSecond': 'Build reliable business systems',
136
152
  'auth.marketingDescription':
137
153
  'Give AI a flexible frontend framework to shape each experience, while NocoBase secures the data, permissions and governance underneath.',
138
154
  'auth.frontend': 'AI-native frontend',
@@ -142,10 +158,14 @@ const systemEnUS = {
142
158
  'auth.foundationDescription': 'Reliable data, access control and governance.',
143
159
  'auth.marketingFooter': 'Freedom above. Confidence below.',
144
160
  'status.loading': 'Loading',
161
+ 'status.refreshing': 'Refreshing',
145
162
  'status.loadingPage': 'Loading page',
146
163
  'status.denied': 'Access denied',
147
164
  'status.pageFailed': 'Unable to load page',
148
165
  'status.retry': 'Retry',
166
+ 'status.reloadPage': 'Reload page',
167
+ 'status.retryFailedDescription':
168
+ 'It still could not be loaded. Reloading the whole page may help; unsaved changes on this page may be lost.',
149
169
  'navigation.brandHome': 'NocoBase home',
150
170
  'navigation.brandApps': 'NocoBase applications',
151
171
  'routeOverlay.close': 'Close',
@@ -153,8 +173,8 @@ const systemEnUS = {
153
173
  'status.routeFailedDescription':
154
174
  'Route {{label}} from {{packageName}} could not be loaded.',
155
175
  shell: {
156
- buildFreely: 'AI builds freely.',
157
- reliability: '<brand>NocoBase</brand> keeps it reliable.',
176
+ brandPartnership: 'AI + <brand>NocoBase</brand>',
177
+ buildReliableSystems: 'Build reliable business systems',
158
178
  },
159
179
  home: {
160
180
  platform: 'NocoBase · AI-native application platform',
@@ -410,6 +430,7 @@ const systemEnUS = {
410
430
  breadcrumb: 'Breadcrumb',
411
431
  breadcrumbMore: 'Show the levels in between',
412
432
  back: 'Back',
433
+ backTo: 'Back to {{label}}',
413
434
  },
414
435
  dataTable: {
415
436
  noResults: 'No results',
@@ -1,4 +1,8 @@
1
1
  const settings = {
2
+ notificationManagementDescription:
3
+ 'View registered notification templates and synthetic content previews.',
4
+ notificationTemplates: 'Notification templates',
5
+ notificationManagement: 'Notification management',
2
6
  listChanged: 'The list has changed. Please retry.',
3
7
  setup: {
4
8
  title: 'Feature prerequisites',
@@ -111,6 +115,19 @@ const settings = {
111
115
 
112
116
  reload: 'Reload role',
113
117
  title: 'System settings',
118
+ home: {
119
+ description:
120
+ 'Accounts, permissions, business configuration and system capabilities are all here.',
121
+ empty: 'No settings are available to you yet.',
122
+ },
123
+ categories: {
124
+ account: 'Accounts and permissions',
125
+ business: 'Business configuration',
126
+ ai: 'AI',
127
+ system: 'System',
128
+ },
129
+ permissionManagementDescription:
130
+ 'Set what each role can open and do, and the data it can see.',
114
131
  users: 'Users',
115
132
  roles: 'Roles and permissions',
116
133
  usersDescription: 'Manage accounts, access and active sessions.',
@@ -1,4 +1,7 @@
1
1
  const settings = {
2
+ notificationManagementDescription: '查看已登记的通知模板和合成内容预览。',
3
+ notificationTemplates: '通知模板',
4
+ notificationManagement: '通知管理',
2
5
  listChanged: '列表已变化,请重试。',
3
6
  setup: {
4
7
  title: '功能前置配置',
@@ -101,6 +104,18 @@ const settings = {
101
104
 
102
105
  reload: '重新加载角色',
103
106
  title: '系统设置',
107
+ home: {
108
+ description: '账号、权限、业务配置和系统能力都在这里。',
109
+ empty: '暂时没有你可以管理的设置。',
110
+ },
111
+ categories: {
112
+ account: '账号与权限',
113
+ business: '业务配置',
114
+ ai: 'AI',
115
+ system: '系统',
116
+ },
117
+ permissionManagementDescription:
118
+ '设置每个角色能打开的页面、能做的操作和能看到的数据。',
104
119
  users: '用户管理',
105
120
  roles: '角色与权限',
106
121
  usersDescription: '管理用户账户、角色及登录会话。',
@@ -5,6 +5,20 @@ import inboxZhCN from '#extensions/nocobase-inbox/locales/zh-CN';
5
5
 
6
6
  /** The Chinese copy of what the template ships; see `./en-US.ts`. */
7
7
  const systemZhCN: SystemResource = {
8
+ pendingFeedback: {
9
+ working: '正在处理…',
10
+ longWait: '耗时比平常更长,您可以继续等待。',
11
+ },
12
+ taskProgress: {
13
+ queued: '等待开始',
14
+ running: '进行中',
15
+ completed: '已完成',
16
+ failed: '失败',
17
+ canceled: '已取消',
18
+ count: '{{processed}} / {{total}}',
19
+ processed: '已处理 {{processed}}',
20
+ background: '离开此页面后任务会继续运行。',
21
+ },
8
22
  listFeedback: {
9
23
  loading: '加载中',
10
24
  empty: '暂无数据',
@@ -81,6 +95,9 @@ const systemZhCN: SystemResource = {
81
95
  'auth.loginDescription': '使用用户名或邮箱和密码登录',
82
96
  'auth.registerTitle': '创建账户',
83
97
  'auth.registerDescription': '创建账户以开始使用',
98
+ 'auth.registrationAccepted': '注册请求已提交',
99
+ 'auth.registrationGuidance':
100
+ '请尝试使用刚填写的用户名和密码登录。如果此邮箱已有账户,新填写的信息不会生效,请使用原账户登录或找回密码。如应用提供了验证指引,请先完成验证。',
84
101
  'auth.forgotTitle': '忘记密码',
85
102
  'auth.forgotDescription': '输入邮箱,如果账户存在,我们将发送重置链接',
86
103
  'auth.passwordResetUnavailable': '未开启自助找回,请联系管理员重置密码。',
@@ -120,9 +137,8 @@ const systemZhCN: SystemResource = {
120
137
  'auth.continueWith': '使用 {provider} 继续',
121
138
  'auth.about': '关于此应用',
122
139
  'auth.platform': 'AI 原生应用平台',
123
- 'auth.marketingTitleFirst': '让 AI 自由构建',
124
- 'auth.marketingTitleSecond': 'NocoBase 保障',
125
- 'auth.marketingTitleThird': '可靠运行',
140
+ 'auth.marketingTitleFirst': 'AI + NocoBase',
141
+ 'auth.marketingTitleSecond': 'Build reliable business systems',
126
142
  'auth.marketingDescription':
127
143
  '让 AI 在灵活的前端框架上构建体验,由 NocoBase 保障底层数据、权限与治理',
128
144
  'auth.frontend': 'AI 原生前端',
@@ -131,10 +147,14 @@ const systemZhCN: SystemResource = {
131
147
  'auth.foundationDescription': '可靠的数据、访问控制与治理',
132
148
  'auth.marketingFooter': '自由构建,可靠支撑',
133
149
  'status.loading': '加载中',
150
+ 'status.refreshing': '正在刷新',
134
151
  'status.loadingPage': '正在加载页面',
135
152
  'status.denied': '无权访问',
136
153
  'status.pageFailed': '无法加载页面',
137
154
  'status.retry': '重试',
155
+ 'status.reloadPage': '重新加载页面',
156
+ 'status.retryFailedDescription':
157
+ '仍然无法加载。可以重新加载整个页面,但本页未保存的修改可能丢失。',
138
158
  'navigation.brandHome': 'NocoBase 首页',
139
159
  'navigation.brandApps': 'NocoBase 应用',
140
160
  'routeOverlay.close': '关闭',
@@ -142,8 +162,8 @@ const systemZhCN: SystemResource = {
142
162
  'status.routeFailedDescription':
143
163
  '无法加载 {{packageName}} 的路由 {{label}}。',
144
164
  shell: {
145
- buildFreely: 'AI 自由构建',
146
- reliability: '<brand>NocoBase</brand> 保障可靠',
165
+ brandPartnership: 'AI + <brand>NocoBase</brand>',
166
+ buildReliableSystems: 'Build reliable business systems',
147
167
  },
148
168
  home: {
149
169
  platform: 'NocoBase · AI 原生应用平台',
@@ -385,6 +405,7 @@ const systemZhCN: SystemResource = {
385
405
  breadcrumb: '面包屑',
386
406
  breadcrumbMore: '显示中间层级',
387
407
  back: '返回',
408
+ backTo: '返回{{label}}',
388
409
  },
389
410
  dataTable: {
390
411
  noResults: '无结果',