@nocobase/app-template-default 1.0.0-beta.87 → 1.0.0-beta.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/rules/notification-templates.md +5 -0
- package/.agents/rules/shell.md +2 -2
- package/AGENTS.md +3 -1
- package/README.MD +11 -5
- package/client/components/body-portal.tsx +25 -0
- package/client/components/breadcrumbs.tsx +13 -0
- package/client/components/flow-diagram.tsx +2 -2
- package/client/layouts/app-layout.tsx +8 -4
- package/client/layouts/components/sidebar-footer.tsx +18 -25
- package/client/lib/navigation-transform.ts +156 -22
- package/client/locales/system/en-US.ts +7 -5
- package/client/locales/system/settings-en-US.ts +17 -0
- package/client/locales/system/settings-zh-CN.ts +15 -0
- package/client/locales/system/zh-CN.ts +7 -5
- package/client/pages/auth/register.tsx +39 -22
- package/client/pages/auth/shared.tsx +1 -3
- package/client/pages/settings/hooks.tsx +3 -68
- package/client/pages/settings/notification-routes.ts +109 -0
- package/client/pages/settings/notification-templates.tsx +26 -0
- package/client/pages/settings/permission-management-page.tsx +15 -90
- package/client/pages/settings/routes.ts +96 -98
- package/client/pages/settings/settings-home.tsx +109 -0
- package/client/pages/settings/settings-module-page.tsx +158 -0
- package/client/pages/settings/shell-routes.ts +53 -0
- package/client/routes.ts +17 -1
- package/client/routing/app-router.tsx +1 -14
- package/client/routing/client-route.tsx +16 -9
- package/client/routing/route-context.tsx +30 -3
- package/client/routing/route-leave-boundary.tsx +249 -0
- package/client/routing/route-navigation.ts +72 -8
- package/client/routing/route-tree.tsx +7 -12
- package/client/runtime.ts +2 -2
- package/config.example.yml +1 -1
- package/package.json +12 -12
- package/server/config/auth.ts +1 -1
- package/server/providers/index.ts +2 -0
- package/server/providers/notification-management.ts +27 -0
- package/tests/client/account-permissions.test.tsx +1 -1
- package/tests/client/client-auth.test.tsx +10 -1
- package/tests/client/client-routes.test.ts +137 -4
- package/tests/client/client-shell.test.tsx +23 -8
- package/tests/client/foundation-integration.test.ts +38 -17
- package/tests/client/layout-i18n.test.tsx +9 -4
- package/tests/client/permission-management-tabs.test.tsx +421 -16
- package/tests/client/registration-feedback.test.tsx +143 -0
- package/tests/client/settings-home.test.tsx +110 -0
- package/tests/client/settings-navigation.test.ts +230 -0
- package/tests/server/notification-templates.test.ts +144 -0
- package/tests/client/permission-management-routes.test.ts +0 -64
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Notification template administration
|
|
2
|
+
|
|
3
|
+
When notification business is used, register the real scenarios alongside their shared sending renderers and deliver the read-only template directory with synthetic preview. Read the installed Notification Skill’s `references/template-directory.md` for registration, safety and coverage requirements. The app-owned `NotificationManagementProvider` projects only `client.notificationTemplates.invitationConfigured` from the same channel selector used by Users sending and the directory. Default's runtime route factory calls `includeNotificationTemplateRoutes` with that bootstrap flag; a configured invitation mailer (including a configured but disabled legacy channel) is real business use. No transport configuration enters the browser. Restart after changing server mailer configuration. The helper reuses `notificationManagement` and preserves existing log children, paths and grants; when integrating a richer settings module container, insert `notificationTemplateRoute` into its template view once. Preserve the host's settings navigation container. The template page permission and API permission are both `page:notification.templates/access`; grant it only to intended administrators. Default does not register unused business settings. Examples registers task assignment/update and Users invitation scenarios.
|
|
4
|
+
|
|
5
|
+
For an existing application, follow the installed upgrade Skill’s version-selected workflow and `references/notification-template-upgrade.md`. Reconcile source, dependency ranges and Skills; retain custom templates, workflows, grants and data. Updating packages alone does not insert or update application pages.
|
package/.agents/rules/shell.md
CHANGED
|
@@ -12,7 +12,7 @@ The route-tree provider remembers each visited ancestor's query while its descen
|
|
|
12
12
|
|
|
13
13
|
`client/layouts/components/layout-header.tsx` is a presentation container accepting children. The sidebar is shadcn's Sidebar: `components/app-sidebar.tsx` wraps its provider, desktop icon mode and phone sheet, and `components/navigation-menu.tsx` draws the permission-filtered route tree with `SidebarMenu*`: a top-level group with no page of its own is a section, its entries always shown under a `SidebarGroupLabel` heading, while clickable parents and nested groups are controlled `Collapsible` sub-menus, and an item `client/lib/navigation-transform.ts` marks `section` deeper in the tree is a heading over its direct links inside that menu; a count beside an entry, such as unread items, is passed by the App layout in `NavigationMenu`'s `badges`, keyed by route name. Keep `client/components/ui/sidebar.tsx` as the shadcn CLI writes it except for removing its unused origin-wide `sidebar_state` cookie write; persistence belongs to the application-scoped preference. Customize from outside: widths go through the provider's `style` in spacing units, the phone sheet is rendered in `app-sidebar.tsx` with a translated title, and the provider's Ctrl/Cmd+B shortcut is stopped at `document`. The App layout owns menus, branding, permissions and redirects. Desktop icon mode hides top-level section headings and keeps their entries' icons, shows leaf labels as zero-delay tooltips and opens groups in hover and keyboard-focus popovers, preserving filtered navigation, parent-page links, nested groups and the selected group's highlight. Keep this behavior aligned across layouts. Desktop collapse state is shared through `useSidebarPreference` at `nocobase:sidebar:collapsed:app:<basename>` across layouts and tabs of the same application. `createApp(runtime)` provides a stable store using `runtime.basename`, with only empty/root and boundary slashes normalized; paths and case remain intact. Missing new keys default to expanded without migrating the old shared preference; whether the phone sheet is open stays local to each layout.
|
|
14
14
|
|
|
15
|
-
The header carries no Settings or Inbox entry
|
|
15
|
+
The header carries no Settings or Inbox entry, and there is no separate settings layout. A page that configures something is an ordinary route in `client/routes.ts` registered into System settings: the `settings` group (`client/pages/settings/shell-routes.ts`) with its `settingsHome` page and category groups. `client/lib/navigation-transform.ts` folds that group into one sidebar entry opening the home, which stays selected on every page below it; the home (`client/pages/settings/settings-home.tsx`) shows the modules a viewer may open as cards by category, and `SettingsModulePage` turns the pages of a module group into URL-backed tabs. Both read the shell's single page permission check through `RouteTreeProvider`'s `access`. See [System settings navigation](../skills/nocobase-app-development/references/settings-navigation.md). The `/inbox` page declares no navigation; give it an entry where the application wants one.
|
|
16
16
|
|
|
17
17
|
`client/lib/navigation-transform.ts` is the application-owned hook for customizing the permission-filtered navigation tree. The shared sidebar supports static section labels with direct links as well as collapsible groups.
|
|
18
18
|
|
|
@@ -24,7 +24,7 @@ Theme lives in the account menu as a submenu beside Language, rather than a sepa
|
|
|
24
24
|
|
|
25
25
|
The account menu checks Better Auth sign-out results before refreshing the session and shows a localized error toast for API or network failures. Preserve this behavior when upgrading the shell; navigation alone does not revoke a session.
|
|
26
26
|
|
|
27
|
-
The authorization provider clears the permission snapshot before rendering a new session. Route navigation and page guards subscribe to the authorization revision; preserve these checks when customizing the shell so account changes and permission updates take effect without a reload. Pending checks hide protected content
|
|
27
|
+
The authorization provider clears the permission snapshot before rendering a new session. Route navigation and page guards subscribe to the authorization revision; preserve these checks when customizing the shell so account changes and permission updates take effect without a reload. Keep `RouteLeaveBoundary` in `AppLayout`, outside all page Activities including nested routes, so `useSettingsLeave` navigation guards, confirmation UI and `beforeunload` survive permission checks. Clear retained guards on denial and session changes. Restored Activity Effects re-run existing data requests. Pending checks hide protected content with React Activity while retaining drafts; failed checks unmount it and deny access. Body portals must detach in an Effect cleanup so they also disappear while the page is hidden; use `BodyPortal` from `#components/body-portal` for application-owned content mounted on `document.body`.
|
|
28
28
|
|
|
29
29
|
The account menu language control in `client/layouts/components/language-switcher.tsx` uses a shadcn submenu with radio items. Render it inside `DropdownMenuContent` to preserve menu keyboard navigation and selection semantics.
|
|
30
30
|
|
package/AGENTS.md
CHANGED
|
@@ -57,7 +57,7 @@ A feature with a page and an API touches five places: a migration for the table,
|
|
|
57
57
|
|
|
58
58
|
## System settings are part of feature delivery
|
|
59
59
|
|
|
60
|
-
A bare create-app shows Home to signed-in users and System settings
|
|
60
|
+
A bare create-app shows Home to signed-in users and, only to unrestricted administrators, one System settings entry whose home page holds the App foundation card. Every administration surface joins that structure — one sidebar entry, a home of cards by category, tabs inside a module — as [System settings navigation](.agents/skills/nocobase-app-development/references/settings-navigation.md) describes; never add another sidebar entry for administration. As the application develops business capabilities, inspect its current state on every iteration and automatically register the prepared Users and Permission management contribution from `client/pages/settings/routes.ts` once in `client/routes.ts`; preserve it and reconcile missing management surfaces on every iteration, without a first-build flag or one-time setup stage. Whenever an application adds lifecycles, workflows, approvals, notifications, files or other configurable capabilities, automatically include their real administration surfaces without requiring a second user request. Follow [system settings delivery](.agents/skills/nocobase-app-development/references/system-settings.md); in particular, every used process includes a definitions directory, standard diagram, supported parameters and authorized execution history. Do not add demo plugins or empty settings for unused capabilities. The template preinstalls `#components/lifecycle-diagram` (React Flow + ELK) and `client/extensions/nocobase-workflow-management/`: use them by default, preserve their visuals unless the user explicitly requests another style, and connect authorized parameter bindings through `LifecycleDiagram.configuration` to the real editor. These are UI Registry sources; an npm package lookup is not a component availability check.
|
|
61
61
|
|
|
62
62
|
## Rules that apply everywhere
|
|
63
63
|
|
|
@@ -138,3 +138,5 @@ Standard Repository writes require an operation key through the registered submi
|
|
|
138
138
|
Assign application business roles to explicitly identified users through the prepared User management role-assignment UI. Never seed a business permission set onto `authenticated:*`, broaden the inherited platform `member` role, or write startup reconciliation that grants access to everyone merely to make a feature usable. Unchecked actions in one role do not revoke grants inherited from another. If the user explicitly requests a global default role, first deliver a permission-protected administration control that shows its recipients and allows changing or removing the assignment; show inherited roles and their source in user access details. Without that UI, use explicit user assignments and explain the limitation. Keep page access independent from business operations.
|
|
139
139
|
|
|
140
140
|
Run `pnpm exec vitest run tests/server/authorization-assignment-safety.test.ts` after permission initialization changes and before delivery. It boots the real application on an isolated database and inspects initialized wildcard assignments and the inherited member role. Keep this test in generated applications. An intentional global default requires explicit user authorization, a working administration surface and corresponding acceptance tests before deliberately updating the expected baseline; do not delete, skip or loosen the guard to make tests pass. Test an ordinary user with only the configured role: inspect all effective grant sources and verify both hidden buttons and rejected direct API calls.
|
|
141
|
+
|
|
142
|
+
For notification business, follow [notification template delivery](.agents/rules/notification-templates.md), including source-owned scenario registration and synthetic-only preview.
|
package/README.MD
CHANGED
|
@@ -139,11 +139,11 @@ The three files named `runtime.ts`, `plugins.ts`, and `routes.ts` are compositio
|
|
|
139
139
|
|
|
140
140
|
### Everyday development
|
|
141
141
|
|
|
142
|
-
| Script | What it does
|
|
143
|
-
| ------------------ |
|
|
144
|
-
| `pnpm dev` | Start the API server and client with hot reload
|
|
145
|
-
| `pnpm build` | Build the client and server into `dist/`
|
|
146
|
-
| `pnpm start` | Run the built server; requires `pnpm build` first
|
|
142
|
+
| Script | What it does |
|
|
143
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
144
|
+
| `pnpm dev` | Start the API server and client with hot reload |
|
|
145
|
+
| `pnpm build` | Build the client and server into `dist/` |
|
|
146
|
+
| `pnpm start` | Run the built server; requires `pnpm build` first |
|
|
147
147
|
| `pnpm build --tar` | Build, then pack `dist/`, `config.example.yml` and the `Dockerfile` with its `.dockerignore` into `storage/exports/dist.tar.gz` |
|
|
148
148
|
|
|
149
149
|
### Database
|
|
@@ -453,3 +453,9 @@ Set `NOCOBASE_STRICT_STARTUP=true` when running `pnpm dev` or `pnpm start` in au
|
|
|
453
453
|
## Internal workspace packages
|
|
454
454
|
|
|
455
455
|
The application can also be the root of a pnpm workspace, with internal packages under `packages/*` and the existing `client/`, `server/` and `database/` directories unchanged. New projects receive this workspace pattern automatically. Internal packages use `workspace:*` dependencies and may remain private. Run installation, development and deployment builds from the application root; `pnpm check` includes the internal packages' own checks. See [internal packages](.agents/rules/workspace.md) for configuration and development conventions.
|
|
456
|
+
|
|
457
|
+
### Registration feedback
|
|
458
|
+
|
|
459
|
+
Password registration signs in new accounts by default (`auth.emailAndPassword.autoSignIn: true` in `server/config/auth.ts` and `config.example.yml`). An existing email returns an explicit error, exposing whether that email is registered. For enumeration protection, explicitly set `autoSignIn: false` and configure email verification with a sender or `onExistingUserSignUp` to notify the email owner. Existing explicit overrides remain effective; no configuration migration is performed. The authentication plugin warns at startup about registration with auto sign-in disabled and neither verification nor an existing-user notification.
|
|
460
|
+
|
|
461
|
+
The registration page treats `usePasswordRegistration().isSuccess` as request acceptance only. A refreshed session navigates signed-in users into the application. Visitors who remain signed out see neutral sign-in and recovery guidance, without an account-creation or email-delivery promise. Applications customizing this page must keep that distinction.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useLayoutEffect,
|
|
3
|
+
useState,
|
|
4
|
+
type ReactElement,
|
|
5
|
+
type ReactNode,
|
|
6
|
+
} from 'react';
|
|
7
|
+
import { createPortal } from 'react-dom';
|
|
8
|
+
|
|
9
|
+
export interface BodyPortalProps {
|
|
10
|
+
readonly children: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Detach body content when its page is hidden by Activity, retaining its React state for the next permission result. */
|
|
14
|
+
export function BodyPortal({ children }: BodyPortalProps): ReactElement {
|
|
15
|
+
const [container] = useState(() => {
|
|
16
|
+
const element = document.createElement('div');
|
|
17
|
+
element.style.display = 'contents';
|
|
18
|
+
return element;
|
|
19
|
+
});
|
|
20
|
+
useLayoutEffect(() => {
|
|
21
|
+
document.body.append(container);
|
|
22
|
+
return () => container.remove();
|
|
23
|
+
}, [container]);
|
|
24
|
+
return createPortal(children, container);
|
|
25
|
+
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
DropdownMenuItem,
|
|
20
20
|
DropdownMenuTrigger,
|
|
21
21
|
} from '#components/ui/dropdown-menu';
|
|
22
|
+
import { settingsHomeRoute } from '#lib/navigation-transform';
|
|
22
23
|
import { cn } from 'cn';
|
|
23
24
|
|
|
24
25
|
import {
|
|
@@ -60,6 +61,18 @@ export function Breadcrumbs({
|
|
|
60
61
|
trail.flatMap((entry): PageBreadcrumbLevel[] => {
|
|
61
62
|
const title = routeLevelTitle(entry);
|
|
62
63
|
if (title === undefined || denied?.has(routeKey(entry.route))) return [];
|
|
64
|
+
// The settings group links to its home page, the way back from any settings module.
|
|
65
|
+
const home = settingsHomeRoute(entry.route);
|
|
66
|
+
if (home)
|
|
67
|
+
return [
|
|
68
|
+
{
|
|
69
|
+
label: t(title, {
|
|
70
|
+
ns: entry.route.packageName,
|
|
71
|
+
defaultValue: title,
|
|
72
|
+
}),
|
|
73
|
+
to: { pathname: home.path, search: searches.get(home.path) ?? '' },
|
|
74
|
+
},
|
|
75
|
+
];
|
|
63
76
|
return [
|
|
64
77
|
{
|
|
65
78
|
label: t(title, {
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type ReactElement,
|
|
9
9
|
type ReactNode,
|
|
10
10
|
} from 'react';
|
|
11
|
-
import {
|
|
11
|
+
import { BodyPortal } from './body-portal.js';
|
|
12
12
|
import {
|
|
13
13
|
BaseEdge,
|
|
14
14
|
Controls,
|
|
@@ -934,7 +934,7 @@ export function FlowDiagram({
|
|
|
934
934
|
)}
|
|
935
935
|
</div>
|
|
936
936
|
);
|
|
937
|
-
return expanded ?
|
|
937
|
+
return expanded ? <BodyPortal>{canvas}</BodyPortal> : canvas;
|
|
938
938
|
}
|
|
939
939
|
|
|
940
940
|
/** View controls belong inside React Flow so they always use this canvas's viewport. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useSyncServerLocale } from '@nocobase/app-plugin-i18n/client';
|
|
2
|
-
import type
|
|
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';
|
|
@@ -34,7 +35,8 @@ export function AppLayout({ routes }: AppLayoutProps): ReactElement {
|
|
|
34
35
|
useSyncServerLocale();
|
|
35
36
|
|
|
36
37
|
const { t } = useTranslation();
|
|
37
|
-
const { items: menuItems, denied } = useRouteNavigation(routes);
|
|
38
|
+
const { items: menuItems, denied, loading } = useRouteNavigation(routes);
|
|
39
|
+
const access = useMemo(() => ({ denied, loading }), [denied, loading]);
|
|
38
40
|
const selectedKey = selectedNavigationId(
|
|
39
41
|
routes,
|
|
40
42
|
useLocation().pathname,
|
|
@@ -46,7 +48,7 @@ export function AppLayout({ routes }: AppLayoutProps): ReactElement {
|
|
|
46
48
|
|
|
47
49
|
return (
|
|
48
50
|
// The shell owns the business route tree used by its pages and navigation.
|
|
49
|
-
<RouteTreeProvider routes={routes}>
|
|
51
|
+
<RouteTreeProvider routes={routes} access={access}>
|
|
50
52
|
<PageBreadcrumbProvider>
|
|
51
53
|
<AppSidebarProvider>
|
|
52
54
|
<AppSidebar label={navigationLabel} footer={<AppSidebarFooter />}>
|
|
@@ -74,7 +76,9 @@ export function AppLayout({ routes }: AppLayoutProps): ReactElement {
|
|
|
74
76
|
{/* main only positions; the page scrolls in here, so a child page layer laid over main is neither
|
|
75
77
|
moved by the page's scrolling nor stretched by its height. */}
|
|
76
78
|
<div className='h-full overflow-y-auto'>
|
|
77
|
-
<
|
|
79
|
+
<RouteLeaveBoundary hidden={loading}>
|
|
80
|
+
<Outlet />
|
|
81
|
+
</RouteLeaveBoundary>
|
|
78
82
|
</div>
|
|
79
83
|
</main>
|
|
80
84
|
</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
|
|
16
|
-
*
|
|
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
|
|
32
|
-
defaultValue: '<brand>NocoBase</brand>
|
|
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
|
-
? `${
|
|
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'>{
|
|
73
|
-
<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.
|
|
87
|
-
defaults='<brand>NocoBase</brand>
|
|
80
|
+
i18nKey='shell.brandPartnership'
|
|
81
|
+
defaults='AI + <brand>NocoBase</brand>'
|
|
88
82
|
components={{ brand: brandLink }}
|
|
89
83
|
>
|
|
90
|
-
{brandLink}
|
|
84
|
+
AI + {brandLink}
|
|
91
85
|
</Trans>
|
|
92
86
|
</p>
|
|
93
|
-
|
|
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={
|
|
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
|
|
34
|
+
return collapseSettingsNavigation(organizeNavigation(items, routes));
|
|
9
35
|
}
|
|
10
36
|
|
|
11
|
-
/**
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -88,6 +88,9 @@ const systemEnUS = {
|
|
|
88
88
|
'auth.loginDescription': 'Sign in with your username or email and password.',
|
|
89
89
|
'auth.registerTitle': 'Create an account',
|
|
90
90
|
'auth.registerDescription': 'Create an account to get started.',
|
|
91
|
+
'auth.registrationAccepted': 'Registration request submitted',
|
|
92
|
+
'auth.registrationGuidance':
|
|
93
|
+
'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
94
|
'auth.forgotTitle': 'Forgot password',
|
|
92
95
|
'auth.forgotDescription':
|
|
93
96
|
'Enter your email and we will send a reset link if the account exists.',
|
|
@@ -130,9 +133,8 @@ const systemEnUS = {
|
|
|
130
133
|
'auth.continueWith': 'Continue with {provider}',
|
|
131
134
|
'auth.about': 'About this application',
|
|
132
135
|
'auth.platform': 'AI-native application platform',
|
|
133
|
-
'auth.marketingTitleFirst': '
|
|
134
|
-
'auth.marketingTitleSecond': '
|
|
135
|
-
'auth.marketingTitleThird': 'reliable.',
|
|
136
|
+
'auth.marketingTitleFirst': 'AI + NocoBase',
|
|
137
|
+
'auth.marketingTitleSecond': 'Build reliable business systems',
|
|
136
138
|
'auth.marketingDescription':
|
|
137
139
|
'Give AI a flexible frontend framework to shape each experience, while NocoBase secures the data, permissions and governance underneath.',
|
|
138
140
|
'auth.frontend': 'AI-native frontend',
|
|
@@ -153,8 +155,8 @@ const systemEnUS = {
|
|
|
153
155
|
'status.routeFailedDescription':
|
|
154
156
|
'Route {{label}} from {{packageName}} could not be loaded.',
|
|
155
157
|
shell: {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
+
brandPartnership: 'AI + <brand>NocoBase</brand>',
|
|
159
|
+
buildReliableSystems: 'Build reliable business systems',
|
|
158
160
|
},
|
|
159
161
|
home: {
|
|
160
162
|
platform: 'NocoBase · AI-native application platform',
|
|
@@ -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: '管理用户账户、角色及登录会话。',
|
|
@@ -81,6 +81,9 @@ const systemZhCN: SystemResource = {
|
|
|
81
81
|
'auth.loginDescription': '使用用户名或邮箱和密码登录',
|
|
82
82
|
'auth.registerTitle': '创建账户',
|
|
83
83
|
'auth.registerDescription': '创建账户以开始使用',
|
|
84
|
+
'auth.registrationAccepted': '注册请求已提交',
|
|
85
|
+
'auth.registrationGuidance':
|
|
86
|
+
'请尝试使用刚填写的用户名和密码登录。如果此邮箱已有账户,新填写的信息不会生效,请使用原账户登录或找回密码。如应用提供了验证指引,请先完成验证。',
|
|
84
87
|
'auth.forgotTitle': '忘记密码',
|
|
85
88
|
'auth.forgotDescription': '输入邮箱,如果账户存在,我们将发送重置链接',
|
|
86
89
|
'auth.passwordResetUnavailable': '未开启自助找回,请联系管理员重置密码。',
|
|
@@ -120,9 +123,8 @@ const systemZhCN: SystemResource = {
|
|
|
120
123
|
'auth.continueWith': '使用 {provider} 继续',
|
|
121
124
|
'auth.about': '关于此应用',
|
|
122
125
|
'auth.platform': 'AI 原生应用平台',
|
|
123
|
-
'auth.marketingTitleFirst': '
|
|
124
|
-
'auth.marketingTitleSecond': '
|
|
125
|
-
'auth.marketingTitleThird': '可靠运行',
|
|
126
|
+
'auth.marketingTitleFirst': 'AI + NocoBase',
|
|
127
|
+
'auth.marketingTitleSecond': 'Build reliable business systems',
|
|
126
128
|
'auth.marketingDescription':
|
|
127
129
|
'让 AI 在灵活的前端框架上构建体验,由 NocoBase 保障底层数据、权限与治理',
|
|
128
130
|
'auth.frontend': 'AI 原生前端',
|
|
@@ -142,8 +144,8 @@ const systemZhCN: SystemResource = {
|
|
|
142
144
|
'status.routeFailedDescription':
|
|
143
145
|
'无法加载 {{packageName}} 的路由 {{label}}。',
|
|
144
146
|
shell: {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
+
brandPartnership: 'AI + <brand>NocoBase</brand>',
|
|
148
|
+
buildReliableSystems: 'Build reliable business systems',
|
|
147
149
|
},
|
|
148
150
|
home: {
|
|
149
151
|
platform: 'NocoBase · AI 原生应用平台',
|