@nocobase/app-template-default 0.0.0-stage → 1.0.0-beta.57
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/.cursor/mcp.json +8 -0
- package/.mcp.json +8 -0
- package/.prettierignore +20 -0
- package/.vscode/mcp.json +8 -0
- package/AGENTS.md +432 -0
- package/CLAUDE.md +9 -0
- package/Dockerfile +132 -0
- package/Dockerfile.dockerignore +45 -0
- package/README.MD +481 -0
- package/cli/plugins.ts +14 -0
- package/client/agent-annotations-host.ts +19 -0
- package/client/app.ts +38 -0
- package/client/components/back-button.tsx +49 -0
- package/client/components/breadcrumbs.tsx +167 -0
- package/client/components/inbox-badge.ts +49 -0
- package/client/components/inbox-button.tsx +132 -0
- package/client/components/inbox-header-button.tsx +25 -0
- package/client/components/loading.tsx +35 -0
- package/client/components/page-container.tsx +24 -0
- package/client/components/page-header.tsx +33 -0
- package/client/components/route-child-page.tsx +121 -0
- package/client/components/route-dialog.tsx +10 -0
- package/client/components/route-drawer.tsx +10 -0
- package/client/components/route-overlay.tsx +190 -0
- package/client/components/ui/alert.tsx +75 -0
- package/client/components/ui/badge.tsx +51 -0
- package/client/components/ui/breadcrumb.tsx +121 -0
- package/client/components/ui/button.tsx +58 -0
- package/client/components/ui/collapsible.tsx +19 -0
- package/client/components/ui/context-menu.tsx +271 -0
- package/client/components/ui/dialog.tsx +160 -0
- package/client/components/ui/dropdown-menu.tsx +270 -0
- package/client/components/ui/empty.tsx +103 -0
- package/client/components/ui/field.tsx +236 -0
- package/client/components/ui/input-group.tsx +158 -0
- package/client/components/ui/input.tsx +20 -0
- package/client/components/ui/kbd.tsx +26 -0
- package/client/components/ui/label.tsx +18 -0
- package/client/components/ui/popover.tsx +89 -0
- package/client/components/ui/select.tsx +200 -0
- package/client/components/ui/separator.tsx +22 -0
- package/client/components/ui/sheet.tsx +137 -0
- package/client/components/ui/sidebar.tsx +723 -0
- package/client/components/ui/skeleton.tsx +13 -0
- package/client/components/ui/spinner.tsx +20 -0
- package/client/components/ui/tabs.tsx +79 -0
- package/client/components/ui/textarea.tsx +17 -0
- package/client/components/ui/toast.tsx +231 -0
- package/client/components/ui/tooltip.tsx +63 -0
- package/client/components/use-route-overlay.ts +26 -0
- package/client/config/api.ts +13 -0
- package/client/config/app.ts +8 -0
- package/client/config/auth.ts +18 -0
- package/client/config/index.ts +17 -0
- package/client/extensions/nocobase-auth-forms/field-props.ts +13 -0
- package/client/extensions/nocobase-auth-forms/form-parts.tsx +101 -0
- package/client/extensions/nocobase-auth-forms/password-input.tsx +41 -0
- package/client/extensions/nocobase-auth-forms/password-login-form.tsx +123 -0
- package/client/extensions/nocobase-auth-forms/password-registration-form.tsx +193 -0
- package/client/extensions/nocobase-auth-forms/password-reset-form.tsx +132 -0
- package/client/extensions/nocobase-auth-forms/password-reset-request-form.tsx +99 -0
- package/client/extensions/nocobase-auth-methods/auth-methods.tsx +170 -0
- package/client/extensions/nocobase-auth-split-layout/auth-split-layout.tsx +126 -0
- package/client/extensions/nocobase-device-approval/device-approval.tsx +277 -0
- package/client/extensions/nocobase-device-approval/locales/en-US.ts +55 -0
- package/client/extensions/nocobase-device-approval/locales/zh-CN.ts +27 -0
- package/client/extensions/nocobase-device-approval/use-device-approval.ts +176 -0
- package/client/extensions/nocobase-file-component-ui/README.md +13 -0
- package/client/extensions/nocobase-file-component-ui/components/file-list.tsx +147 -0
- package/client/extensions/nocobase-file-component-ui/components/file-preview-dialog.tsx +263 -0
- package/client/extensions/nocobase-file-component-ui/components/file-preview-field.tsx +68 -0
- package/client/extensions/nocobase-file-component-ui/components/file-thumbnail.tsx +73 -0
- package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +386 -0
- package/client/extensions/nocobase-file-component-ui/components/previewers/file-preview-content.tsx +255 -0
- package/client/extensions/nocobase-file-component-ui/components/previewers/office-open-xml-preview.tsx +226 -0
- package/client/extensions/nocobase-file-component-ui/index.ts +8 -0
- package/client/extensions/nocobase-file-component-ui/lib/file-preview.ts +194 -0
- package/client/extensions/nocobase-file-component-ui/lib/file-url.ts +22 -0
- package/client/extensions/nocobase-file-component-ui/lib/message-key.ts +18 -0
- package/client/extensions/nocobase-file-component-ui/types.ts +69 -0
- package/client/extensions/nocobase-inbox/decision-actions-bar.tsx +140 -0
- package/client/extensions/nocobase-inbox/frame.ts +31 -0
- package/client/extensions/nocobase-inbox/inbox-actions.ts +21 -0
- package/client/extensions/nocobase-inbox/inbox-detail.tsx +386 -0
- package/client/extensions/nocobase-inbox/inbox-item.tsx +274 -0
- package/client/extensions/nocobase-inbox/inbox-list.tsx +433 -0
- package/client/extensions/nocobase-inbox/inbox-page.tsx +545 -0
- package/client/extensions/nocobase-inbox/locales/en-US.ts +111 -0
- package/client/extensions/nocobase-inbox/locales/zh-CN.ts +57 -0
- package/client/extensions/nocobase-inbox/model.ts +428 -0
- package/client/extensions/nocobase-inbox/registry-scope.tsx +49 -0
- package/client/extensions/nocobase-inbox/registry.ts +303 -0
- package/client/extensions/nocobase-inbox/source.ts +117 -0
- package/client/hooks/use-mobile.ts +21 -0
- package/client/index.tsx +60 -0
- package/client/layouts/app-layout.tsx +105 -0
- package/client/layouts/components/app-brand.tsx +49 -0
- package/client/layouts/components/app-sidebar.tsx +196 -0
- package/client/layouts/components/header-actions.tsx +50 -0
- package/client/layouts/components/language-switcher.tsx +95 -0
- package/client/layouts/components/layout-header.tsx +17 -0
- package/client/layouts/components/navigation-menu.tsx +411 -0
- package/client/layouts/components/sidebar-footer.tsx +107 -0
- package/client/layouts/components/surface-empty.tsx +35 -0
- package/client/layouts/components/user-menu.tsx +127 -0
- package/client/layouts/index.ts +2 -0
- package/client/layouts/settings-layout.tsx +172 -0
- package/client/layouts/use-sidebar-preference.ts +57 -0
- package/client/lib/constants.ts +5 -0
- package/client/lib/toaster.ts +56 -0
- package/client/lib/utils.ts +1 -0
- package/client/locales/en-US.ts +178 -0
- package/client/locales/index.ts +9 -0
- package/client/locales/zh-CN.ts +161 -0
- package/client/pages/auth/device.tsx +41 -0
- package/client/pages/auth/forgot-password.tsx +39 -0
- package/client/pages/auth/login.tsx +66 -0
- package/client/pages/auth/register.tsx +46 -0
- package/client/pages/auth/reset-password.tsx +38 -0
- package/client/pages/auth/shared.tsx +111 -0
- package/client/pages/home.tsx +16 -0
- package/client/pages/inbox.tsx +12 -0
- package/client/plugins.ts +41 -0
- package/client/react-providers.ts +27 -0
- package/client/route-overrides.ts +9 -0
- package/client/routes.ts +75 -0
- package/client/routing/app-router.tsx +127 -0
- package/client/routing/client-route.tsx +129 -0
- package/client/routing/route-context.tsx +85 -0
- package/client/routing/route-navigation.ts +153 -0
- package/client/routing/route-tree.tsx +42 -0
- package/client/routing/standalone-page-layout.tsx +15 -0
- package/client/runtime.ts +26 -0
- package/client/service-provider.ts +40 -0
- package/client/source-extensions.ts +19 -0
- package/client/startup.tsx +22 -0
- package/client/styles.css +122 -0
- package/client/theme/index.ts +4 -0
- package/client/theme/theme-context.ts +12 -0
- package/client/theme/theme-preferences.ts +84 -0
- package/client/theme/theme-presets.ts +7 -0
- package/client/theme/theme-provider.tsx +106 -0
- package/client/theme/theme-settings.tsx +176 -0
- package/client/theme/themes/compact.css +140 -0
- package/client/theme/themes/default.css +117 -0
- package/client/vite-env.d.ts +5 -0
- package/components.json +27 -0
- package/config.example.yml +172 -0
- package/database/main/migrations/.gitkeep +0 -0
- package/database/main/migrations/202610060001_create_device_code.ts +37 -0
- package/database/main/seeds/.gitkeep +0 -0
- package/database/tsconfig.json +4 -0
- package/ecosystem.config.js +35 -0
- package/eslint.config.js +24 -0
- package/index.html +100 -0
- package/package.json +200 -3
- package/playwright.config.ts +10 -0
- package/public/assets/favicon.ico +0 -0
- package/public/assets/logo-dark.png +0 -0
- package/public/assets/logo-mark-dark.png +0 -0
- package/public/assets/logo-mark.png +0 -0
- package/public/assets/logo.png +0 -0
- package/server/app.ts +50 -0
- package/server/config/ai.ts +27 -0
- package/server/config/api.ts +8 -0
- package/server/config/app.ts +40 -0
- package/server/config/auth.ts +36 -0
- package/server/config/authorization.ts +18 -0
- package/server/config/caching.ts +21 -0
- package/server/config/database.ts +22 -0
- package/server/config/drive.ts +30 -0
- package/server/config/i18n.ts +17 -0
- package/server/config/index.ts +73 -0
- package/server/config/jobs.ts +27 -0
- package/server/config/logging.ts +23 -0
- package/server/config/notification.ts +11 -0
- package/server/config/queue.ts +28 -0
- package/server/config/scheduler.ts +23 -0
- package/server/config/secrets.ts +10 -0
- package/server/config/server.ts +26 -0
- package/server/config/session.ts +60 -0
- package/server/config/snowflake.ts +21 -0
- package/server/config/spa.ts +24 -0
- package/server/config/users.ts +11 -0
- package/server/config/workflow.ts +24 -0
- package/server/config.ts +24 -0
- package/server/embedded.ts +23 -0
- package/server/locales/en-US.ts +10 -0
- package/server/locales/index.ts +10 -0
- package/server/locales/zh-CN.ts +5 -0
- package/server/plugins.ts +41 -0
- package/server/providers/index.ts +5 -0
- package/server/routes/index.ts +6 -0
- package/server/runtime.ts +24 -0
- package/server/standalone.ts +29 -0
- package/server/tsconfig.json +4 -0
- package/tailwind.config.mjs +59 -0
- package/tests/components/app-layout-routes.test.tsx +132 -0
- package/tests/components/app-title.test.tsx +57 -0
- package/tests/components/auth-i18n.test.tsx +87 -0
- package/tests/components/back-button.test.tsx +80 -0
- package/tests/components/file-preview.test.tsx +101 -0
- package/tests/components/header-hover.test.tsx +87 -0
- package/tests/components/inbox.test.tsx +139 -0
- package/tests/components/language-switcher.test.tsx +298 -0
- package/tests/components/layout-containers.test.tsx +15 -0
- package/tests/components/layout-i18n.test.tsx +187 -0
- package/tests/components/loading.test.tsx +25 -0
- package/tests/components/navigation-menu.test.tsx +355 -0
- package/tests/components/page-harness.test.tsx +318 -0
- package/tests/components/primitive-labels.test.tsx +80 -0
- package/tests/components/sidebar-permissions.test.tsx +110 -0
- package/tests/components/sidebar-preference.test.tsx +42 -0
- package/tests/fixtures/theme-tokens.html +12 -0
- package/tests/fixtures/theme-tokens.tsx +111 -0
- package/tests/logic/account-permissions.test.tsx +204 -0
- package/tests/logic/app-locale-coverage.test.ts +136 -0
- package/tests/logic/app-server.test.ts +1589 -0
- package/tests/logic/asset-url-build.test.ts +106 -0
- package/tests/logic/client-auth.test.tsx +122 -0
- package/tests/logic/client-plugin-registry.test.ts +63 -0
- package/tests/logic/client-route-recovery.test.tsx +80 -0
- package/tests/logic/client-routes.test.ts +126 -0
- package/tests/logic/client-runtime.test.ts +112 -0
- package/tests/logic/client-settings.test.tsx +841 -0
- package/tests/logic/client-shell.test.tsx +466 -0
- package/tests/logic/client-theme.test.tsx +441 -0
- package/tests/logic/config.test.ts +197 -0
- package/tests/logic/dev-client-watch.test.ts +108 -0
- package/tests/logic/dev-routes-production-build.test.ts +168 -0
- package/tests/logic/device-code-migration.test.ts +70 -0
- package/tests/logic/lifecycle.test.ts +221 -0
- package/tests/logic/plugin-commands.test.ts +69 -0
- package/tests/logic/plugin-page-authz.test.ts +76 -0
- package/tests/logic/route-navigation.test.ts +66 -0
- package/tests/logic/route-overlay.test.tsx +353 -0
- package/tests/logic/server-imports.test.ts +88 -0
- package/tests/logic/tailwind-sources.test.ts +151 -0
- package/tests/logic/template-package-name.test.ts +88 -0
- package/tests/logic/theme-preferences.test.ts +86 -0
- package/tests/logic/theme-tokens.test.ts +248 -0
- package/tests/logic/toaster.test.tsx +108 -0
- package/tests/logic/user-menu-sign-out.test.tsx +98 -0
- package/tests/setup/client-config.ts +27 -0
- package/tsconfig.json +11 -0
- package/tsconfig.node.json +12 -0
- package/tsconfig.server.json +19 -0
- package/vite.config.ts +92 -0
- package/vitest.config.ts +37 -0
- package/README.md +0 -3
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useAuthentication } from '@nocobase/app-plugin-authentication/client';
|
|
2
|
+
import { useTranslation } from '@nocobase/i18n/client';
|
|
3
|
+
import type { ReactElement } from 'react';
|
|
4
|
+
import { Navigate, useLocation, useSearchParams } from 'react-router';
|
|
5
|
+
|
|
6
|
+
import { DeviceApproval } from '#extensions/nocobase-device-approval/device-approval';
|
|
7
|
+
|
|
8
|
+
import { AuthPage } from './shared.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `/device?user_code=…`: where a command line's sign-in (Better Auth's device authorization, `deviceAuthorization()` in
|
|
12
|
+
* `server/config/auth.ts`) sends the person to approve it. The route is `auth: 'optional'`, so it renders outside the
|
|
13
|
+
* application's shell, like the sign-in pages; a person who is not signed in goes to the sign-in page, which brings
|
|
14
|
+
* them back here with the code.
|
|
15
|
+
*/
|
|
16
|
+
export default function DevicePage(): ReactElement | null {
|
|
17
|
+
const { t } = useTranslation();
|
|
18
|
+
const { isPending, session } = useAuthentication();
|
|
19
|
+
const { pathname, search } = useLocation();
|
|
20
|
+
const [params, setParams] = useSearchParams();
|
|
21
|
+
|
|
22
|
+
if (isPending) return null;
|
|
23
|
+
if (!session)
|
|
24
|
+
return (
|
|
25
|
+
<Navigate
|
|
26
|
+
replace
|
|
27
|
+
to={`/login?redirect=${encodeURIComponent(`${pathname}${search}`)}`}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
return (
|
|
31
|
+
<AuthPage
|
|
32
|
+
description={t('deviceApproval.description')}
|
|
33
|
+
title={t('deviceApproval.title')}
|
|
34
|
+
>
|
|
35
|
+
<DeviceApproval
|
|
36
|
+
onUserCodeChange={(code) => setParams(code ? { user_code: code } : {})}
|
|
37
|
+
userCode={params.get('user_code')}
|
|
38
|
+
/>
|
|
39
|
+
</AuthPage>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { usePasswordResetRequest } from '@nocobase/app-plugin-authentication/client/actions';
|
|
2
|
+
import { useTranslation } from '@nocobase/i18n/client';
|
|
3
|
+
import type { ReactElement } from 'react';
|
|
4
|
+
import { Link } from 'react-router';
|
|
5
|
+
|
|
6
|
+
import { PasswordResetRequestForm } from '#extensions/nocobase-auth-forms/password-reset-request-form';
|
|
7
|
+
|
|
8
|
+
import { AuthPage } from './shared.js';
|
|
9
|
+
|
|
10
|
+
export default function ForgotPasswordPage(): ReactElement {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
const request = usePasswordResetRequest();
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<AuthPage
|
|
16
|
+
description={t('auth.forgotDescription')}
|
|
17
|
+
title={t('auth.forgotTitle')}
|
|
18
|
+
>
|
|
19
|
+
<PasswordResetRequestForm
|
|
20
|
+
error={request.error?.message}
|
|
21
|
+
footer={
|
|
22
|
+
<>
|
|
23
|
+
{t('auth.rememberPassword')}{' '}
|
|
24
|
+
<Link to='/login'>{t('auth.signIn')}</Link>
|
|
25
|
+
</>
|
|
26
|
+
}
|
|
27
|
+
labels={{
|
|
28
|
+
email: t('auth.email'),
|
|
29
|
+
submit: t('auth.sendResetLink'),
|
|
30
|
+
submitting: t('auth.sending'),
|
|
31
|
+
success: t('auth.resetSent'),
|
|
32
|
+
}}
|
|
33
|
+
onSubmit={request.submit}
|
|
34
|
+
submitting={request.isPending}
|
|
35
|
+
success={request.isSuccess}
|
|
36
|
+
/>
|
|
37
|
+
</AuthPage>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useSignUpAvailable } from '@nocobase/app-plugin-authentication/client';
|
|
2
|
+
import { usePasswordLogin } from '@nocobase/app-plugin-authentication/client/actions';
|
|
3
|
+
import { useTranslation } from '@nocobase/i18n/client';
|
|
4
|
+
import type { ReactElement } from 'react';
|
|
5
|
+
import { Link } from 'react-router';
|
|
6
|
+
|
|
7
|
+
import { PasswordLoginForm } from '#extensions/nocobase-auth-forms/password-login-form';
|
|
8
|
+
import { AuthMethods } from '#extensions/nocobase-auth-methods/auth-methods';
|
|
9
|
+
|
|
10
|
+
import { AuthPage } from './shared.js';
|
|
11
|
+
|
|
12
|
+
export default function LoginPage(): ReactElement {
|
|
13
|
+
const { t } = useTranslation();
|
|
14
|
+
const login = usePasswordLogin();
|
|
15
|
+
// The sign-up link follows the server: it is hidden while `auth.emailAndPassword` turns sign-up off.
|
|
16
|
+
const signUpAvailable = useSignUpAvailable();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<AuthPage
|
|
20
|
+
description={t('auth.loginDescription')}
|
|
21
|
+
title={t('auth.welcome')}
|
|
22
|
+
>
|
|
23
|
+
{/* Add another sign-in method to `methods`, or SSO buttons as `providers`; the guest route redirects once
|
|
24
|
+
the session exists. */}
|
|
25
|
+
<AuthMethods
|
|
26
|
+
labels={{
|
|
27
|
+
continueWith: t('auth.continueWith'),
|
|
28
|
+
methods: t('auth.methods'),
|
|
29
|
+
separator: t('auth.or'),
|
|
30
|
+
}}
|
|
31
|
+
methods={[
|
|
32
|
+
{
|
|
33
|
+
content: (
|
|
34
|
+
<PasswordLoginForm
|
|
35
|
+
error={login.error?.message}
|
|
36
|
+
footer={
|
|
37
|
+
signUpAvailable ? (
|
|
38
|
+
<>
|
|
39
|
+
{t('auth.noAccount')}{' '}
|
|
40
|
+
<Link to='/register'>{t('auth.signUp')}</Link>
|
|
41
|
+
</>
|
|
42
|
+
) : undefined
|
|
43
|
+
}
|
|
44
|
+
forgotPasswordLink={
|
|
45
|
+
<Link to='/forgot-password'>{t('auth.forgotLink')}</Link>
|
|
46
|
+
}
|
|
47
|
+
labels={{
|
|
48
|
+
hidePassword: t('auth.hidePassword'),
|
|
49
|
+
identifier: t('auth.identifier'),
|
|
50
|
+
password: t('auth.password'),
|
|
51
|
+
showPassword: t('auth.showPassword'),
|
|
52
|
+
submit: t('auth.signIn'),
|
|
53
|
+
submitting: t('auth.signingIn'),
|
|
54
|
+
}}
|
|
55
|
+
onSubmit={login.submit}
|
|
56
|
+
submitting={login.isPending}
|
|
57
|
+
/>
|
|
58
|
+
),
|
|
59
|
+
id: 'password',
|
|
60
|
+
label: t('auth.password'),
|
|
61
|
+
},
|
|
62
|
+
]}
|
|
63
|
+
/>
|
|
64
|
+
</AuthPage>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useSignUpAvailable } from '@nocobase/app-plugin-authentication/client';
|
|
2
|
+
import { usePasswordRegistration } from '@nocobase/app-plugin-authentication/client/actions';
|
|
3
|
+
import { useTranslation } from '@nocobase/i18n/client';
|
|
4
|
+
import type { ReactElement } from 'react';
|
|
5
|
+
import { Link, Navigate } from 'react-router';
|
|
6
|
+
|
|
7
|
+
import { PasswordRegistrationForm } from '#extensions/nocobase-auth-forms/password-registration-form';
|
|
8
|
+
|
|
9
|
+
import { AuthPage } from './shared.js';
|
|
10
|
+
|
|
11
|
+
export default function RegisterPage(): ReactElement {
|
|
12
|
+
const { t } = useTranslation();
|
|
13
|
+
const registration = usePasswordRegistration();
|
|
14
|
+
const signUpAvailable = useSignUpAvailable();
|
|
15
|
+
// The page stays so registration can be turned back on; while the server refuses sign-up it only redirects.
|
|
16
|
+
if (!signUpAvailable) return <Navigate replace to='/login' />;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<AuthPage
|
|
20
|
+
description={t('auth.registerDescription')}
|
|
21
|
+
title={t('auth.registerTitle')}
|
|
22
|
+
>
|
|
23
|
+
<PasswordRegistrationForm
|
|
24
|
+
error={registration.error?.message}
|
|
25
|
+
footer={
|
|
26
|
+
<>
|
|
27
|
+
{t('auth.existingAccount')}{' '}
|
|
28
|
+
<Link to='/login'>{t('auth.signIn')}</Link>
|
|
29
|
+
</>
|
|
30
|
+
}
|
|
31
|
+
labels={{
|
|
32
|
+
confirmPassword: t('auth.confirmPassword'),
|
|
33
|
+
email: t('auth.email'),
|
|
34
|
+
name: t('auth.name'),
|
|
35
|
+
password: t('auth.password'),
|
|
36
|
+
passwordMismatch: t('auth.passwordMismatch'),
|
|
37
|
+
submit: t('auth.createAccount'),
|
|
38
|
+
submitting: t('auth.creatingAccount'),
|
|
39
|
+
username: t('auth.username'),
|
|
40
|
+
}}
|
|
41
|
+
onSubmit={registration.submit}
|
|
42
|
+
submitting={registration.isPending}
|
|
43
|
+
/>
|
|
44
|
+
</AuthPage>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { usePasswordReset } from '@nocobase/app-plugin-authentication/client/actions';
|
|
2
|
+
import { useTranslation } from '@nocobase/i18n/client';
|
|
3
|
+
import type { ReactElement } from 'react';
|
|
4
|
+
import { Link, useSearchParams } from 'react-router';
|
|
5
|
+
|
|
6
|
+
import { PasswordResetForm } from '#extensions/nocobase-auth-forms/password-reset-form';
|
|
7
|
+
|
|
8
|
+
import { AuthPage } from './shared.js';
|
|
9
|
+
|
|
10
|
+
export default function ResetPasswordPage(): ReactElement {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
const reset = usePasswordReset();
|
|
13
|
+
const [searchParams] = useSearchParams();
|
|
14
|
+
// The link in the reset email carries the token; without one the form cannot do anything.
|
|
15
|
+
const token = searchParams.get('token') ?? '';
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<AuthPage
|
|
19
|
+
description={t('auth.resetDescription')}
|
|
20
|
+
title={t('auth.resetTitle')}
|
|
21
|
+
>
|
|
22
|
+
<PasswordResetForm
|
|
23
|
+
disabled={!token}
|
|
24
|
+
error={token ? reset.error?.message : t('auth.invalidResetLink')}
|
|
25
|
+
footer={<Link to='/login'>{t('auth.backToSignIn')}</Link>}
|
|
26
|
+
labels={{
|
|
27
|
+
confirmPassword: t('auth.confirmNewPassword'),
|
|
28
|
+
password: t('auth.newPassword'),
|
|
29
|
+
passwordMismatch: t('auth.passwordMismatch'),
|
|
30
|
+
submit: t('auth.resetTitle'),
|
|
31
|
+
submitting: t('auth.resetting'),
|
|
32
|
+
}}
|
|
33
|
+
onSubmit={({ password }) => reset.submit({ password, token })}
|
|
34
|
+
submitting={reset.isPending}
|
|
35
|
+
/>
|
|
36
|
+
</AuthPage>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { resolveAssetUrl } from '@nocobase/app-client';
|
|
2
|
+
import { useTranslation } from '@nocobase/i18n/client';
|
|
3
|
+
import { Blocks, ShieldCheck, Sparkles } from 'lucide-react';
|
|
4
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
import { AuthSplitLayout } from '#extensions/nocobase-auth-split-layout/auth-split-layout';
|
|
7
|
+
|
|
8
|
+
export interface AuthPageProps {
|
|
9
|
+
readonly title: ReactNode;
|
|
10
|
+
readonly description: ReactNode;
|
|
11
|
+
readonly children: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The frame every sign-in, sign-up and password page shares: the form on the left, and on wide screens the NocoBase
|
|
16
|
+
* brand panel on the right. Replace `BrandPanel` with the application's own content, or install
|
|
17
|
+
* `auth-centered-layout` from the UI Library to drop the panel.
|
|
18
|
+
*/
|
|
19
|
+
export function AuthPage({
|
|
20
|
+
children,
|
|
21
|
+
description,
|
|
22
|
+
title,
|
|
23
|
+
}: AuthPageProps): ReactElement {
|
|
24
|
+
const { t } = useTranslation();
|
|
25
|
+
return (
|
|
26
|
+
<AuthSplitLayout
|
|
27
|
+
aside={<BrandPanel />}
|
|
28
|
+
asideLabel={t('auth.about')}
|
|
29
|
+
description={description}
|
|
30
|
+
logo={
|
|
31
|
+
<>
|
|
32
|
+
<img
|
|
33
|
+
alt=''
|
|
34
|
+
className='dark:hidden'
|
|
35
|
+
src={resolveAssetUrl('/assets/logo-mark.png')}
|
|
36
|
+
/>
|
|
37
|
+
<img
|
|
38
|
+
alt=''
|
|
39
|
+
className='hidden dark:block'
|
|
40
|
+
src={resolveAssetUrl('/assets/logo-mark-dark.png')}
|
|
41
|
+
/>
|
|
42
|
+
</>
|
|
43
|
+
}
|
|
44
|
+
name={t('app.title')}
|
|
45
|
+
title={title}
|
|
46
|
+
>
|
|
47
|
+
{children}
|
|
48
|
+
</AuthSplitLayout>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The brand panel is deliberately dark in both color modes, so it uses fixed neutrals rather than theme tokens.
|
|
53
|
+
function BrandPanel(): ReactElement {
|
|
54
|
+
const { t } = useTranslation();
|
|
55
|
+
const features = [
|
|
56
|
+
{
|
|
57
|
+
description: t('auth.frontendDescription'),
|
|
58
|
+
icon: Sparkles,
|
|
59
|
+
id: 'frontend',
|
|
60
|
+
title: t('auth.frontend'),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
description: t('auth.foundationDescription'),
|
|
64
|
+
icon: ShieldCheck,
|
|
65
|
+
id: 'foundation',
|
|
66
|
+
title: t('auth.foundation'),
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className='relative grid h-full place-items-center overflow-hidden bg-neutral-950 p-12 text-white'>
|
|
72
|
+
<div className='pointer-events-none absolute inset-0 [background-image:linear-gradient(currentColor_1px,transparent_1px),linear-gradient(90deg,currentColor_1px,transparent_1px)] [background-size:48px_48px] opacity-[0.08]' />
|
|
73
|
+
<div className='relative w-full max-w-xl'>
|
|
74
|
+
<p className='text-xs font-semibold tracking-[0.16em] text-white/55 uppercase'>
|
|
75
|
+
{t('auth.platform')}
|
|
76
|
+
</p>
|
|
77
|
+
<h2 className='mt-4 text-5xl leading-[1.05] font-semibold tracking-[-0.045em]'>
|
|
78
|
+
{t('auth.marketingTitleFirst')}
|
|
79
|
+
<br />
|
|
80
|
+
{t('auth.marketingTitleSecond')}
|
|
81
|
+
<br />
|
|
82
|
+
{t('auth.marketingTitleThird')}
|
|
83
|
+
</h2>
|
|
84
|
+
<p className='mt-5 text-sm leading-6 text-white/60'>
|
|
85
|
+
{t('auth.marketingDescription')}
|
|
86
|
+
</p>
|
|
87
|
+
<div className='mt-8 overflow-hidden rounded-2xl bg-white text-neutral-950 shadow-2xl'>
|
|
88
|
+
<div className='space-y-5 p-6'>
|
|
89
|
+
{features.map(({ description, icon: Icon, id, title }) => (
|
|
90
|
+
<div className='flex gap-4' key={id}>
|
|
91
|
+
<span className='grid size-11 shrink-0 place-items-center rounded-xl bg-neutral-100 text-neutral-700'>
|
|
92
|
+
<Icon aria-hidden='true' className='size-5' />
|
|
93
|
+
</span>
|
|
94
|
+
<div>
|
|
95
|
+
<p className='font-semibold'>{title}</p>
|
|
96
|
+
<p className='mt-1 text-sm leading-6 text-neutral-500'>
|
|
97
|
+
{description}
|
|
98
|
+
</p>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
))}
|
|
102
|
+
</div>
|
|
103
|
+
<div className='flex items-center gap-3 bg-neutral-100 px-6 py-4 text-sm font-medium text-neutral-600'>
|
|
104
|
+
<Blocks aria-hidden='true' className='size-4' />
|
|
105
|
+
<span>{t('auth.marketingFooter')}</span>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useTranslation } from '@nocobase/i18n/client';
|
|
2
|
+
import type { ReactElement } from 'react';
|
|
3
|
+
|
|
4
|
+
export default function HomePage(): ReactElement {
|
|
5
|
+
const { t } = useTranslation();
|
|
6
|
+
return (
|
|
7
|
+
<section className='mx-auto grid min-h-[calc(100svh-4rem)] w-full max-w-5xl place-items-center px-6 py-10'>
|
|
8
|
+
<div className='max-w-xl space-y-6 text-center'>
|
|
9
|
+
<h1 className='font-heading text-3xl font-semibold tracking-tight'>
|
|
10
|
+
{t('home.title')}
|
|
11
|
+
</h1>
|
|
12
|
+
<p className='text-muted-foreground'>{t('home.description')}</p>
|
|
13
|
+
</div>
|
|
14
|
+
</section>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
|
|
3
|
+
import { InboxPage as InboxBlock } from '#extensions/nocobase-inbox/inbox-page';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Route `/inbox`: the UI Library's inbox block over the in-app notification plugin, reached from the header's inbox
|
|
7
|
+
* button. Every message reads as a notification; to add decisions that wait on the viewer, pass a `source` and a
|
|
8
|
+
* `registry`, as `client/extensions/nocobase-inbox/source.ts` describes.
|
|
9
|
+
*/
|
|
10
|
+
export default function InboxPage(): ReactElement {
|
|
11
|
+
return <InboxBlock />;
|
|
12
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import defaultAccess from '@nocobase/app-plugin-authz-default-access/client';
|
|
2
|
+
import sharingRules from '@nocobase/app-plugin-authz-sharing-rules/client';
|
|
3
|
+
import restrictionRules from '@nocobase/app-plugin-authz-restriction-rules/client';
|
|
4
|
+
import {
|
|
5
|
+
defineClientPlugins,
|
|
6
|
+
type AppClientPlugins,
|
|
7
|
+
} from '@nocobase/app-client/plugins';
|
|
8
|
+
import aiEmployee from '@nocobase/app-plugin-ai-employee/client';
|
|
9
|
+
import apiKeys from '@nocobase/app-plugin-api-keys/client';
|
|
10
|
+
import authentication from '@nocobase/app-plugin-authentication/client';
|
|
11
|
+
import authorization from '@nocobase/app-plugin-authorization/client';
|
|
12
|
+
import databaseExplorer from '@nocobase/app-plugin-database-explorer/client';
|
|
13
|
+
import users from '@nocobase/app-plugin-users/client';
|
|
14
|
+
import notificationInApp from '@nocobase/app-plugin-notification-in-app/client';
|
|
15
|
+
import i18n from '@nocobase/app-plugin-i18n/client';
|
|
16
|
+
import workflow from '@nocobase/app-plugin-workflow/client';
|
|
17
|
+
import notification from '@nocobase/app-plugin-notification/client';
|
|
18
|
+
import scheduler from '@nocobase/app-plugin-scheduler/client';
|
|
19
|
+
import file from '@nocobase/app-plugin-file/client';
|
|
20
|
+
|
|
21
|
+
// Array order is contribution order. A plugin is enabled by appearing in this
|
|
22
|
+
// list; removing its entry and its import disables it.
|
|
23
|
+
const clientPlugins: AppClientPlugins = defineClientPlugins([
|
|
24
|
+
authentication(),
|
|
25
|
+
aiEmployee(),
|
|
26
|
+
authorization(),
|
|
27
|
+
defaultAccess(),
|
|
28
|
+
sharingRules(),
|
|
29
|
+
restrictionRules(),
|
|
30
|
+
databaseExplorer(),
|
|
31
|
+
users({ mount: 'settings', path: '/users' }),
|
|
32
|
+
apiKeys({ path: '/api-keys' }),
|
|
33
|
+
i18n(),
|
|
34
|
+
notificationInApp(),
|
|
35
|
+
workflow(),
|
|
36
|
+
notification(),
|
|
37
|
+
file(),
|
|
38
|
+
scheduler(),
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
export default clientPlugins;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineClientReactProviders,
|
|
3
|
+
type AppClientReactProviderDefinition,
|
|
4
|
+
} from '@nocobase/app-client/plugins';
|
|
5
|
+
|
|
6
|
+
import { Toaster } from '#components/ui/toast';
|
|
7
|
+
|
|
8
|
+
import { AppThemeProvider } from './theme/theme-provider.js';
|
|
9
|
+
|
|
10
|
+
export const reactProviders: readonly AppClientReactProviderDefinition[] =
|
|
11
|
+
defineClientReactProviders([
|
|
12
|
+
{
|
|
13
|
+
component: AppThemeProvider,
|
|
14
|
+
layer: 'root',
|
|
15
|
+
name: 'theme',
|
|
16
|
+
},
|
|
17
|
+
// The one toast host. It renders what plugins and pages report through
|
|
18
|
+
// `useToaster()`, which `client/service-provider.ts` connects to it.
|
|
19
|
+
// Pages must not mount another.
|
|
20
|
+
{
|
|
21
|
+
component: Toaster,
|
|
22
|
+
layer: 'application',
|
|
23
|
+
name: 'toaster',
|
|
24
|
+
},
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
export default reactProviders;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineClientRouteComponentOverrides,
|
|
3
|
+
type AppClientRouteComponentOverrideDefinition,
|
|
4
|
+
} from '@nocobase/app-client/plugins';
|
|
5
|
+
|
|
6
|
+
export const routeComponentOverrides: readonly AppClientRouteComponentOverrideDefinition[] =
|
|
7
|
+
defineClientRouteComponentOverrides([]);
|
|
8
|
+
|
|
9
|
+
export default routeComponentOverrides;
|
package/client/routes.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Home } from 'lucide-react';
|
|
2
|
+
import {
|
|
3
|
+
defineAppRoutes,
|
|
4
|
+
defineSettingsRoutes,
|
|
5
|
+
type AppClientRouteContribution,
|
|
6
|
+
} from '@nocobase/app-client/plugins';
|
|
7
|
+
|
|
8
|
+
const appRoutes: AppClientRouteContribution = defineAppRoutes([
|
|
9
|
+
{
|
|
10
|
+
// Every signed-in user reaches the landing page. `authz: 'skip'` takes it out of page authorization entirely, so
|
|
11
|
+
// no permission change can leave a user signed in with nowhere to land.
|
|
12
|
+
authz: 'skip',
|
|
13
|
+
auth: 'required',
|
|
14
|
+
componentLoader: () => import('./pages/home.js'),
|
|
15
|
+
name: 'home',
|
|
16
|
+
navigation: { title: 'navigation.home', icon: Home },
|
|
17
|
+
path: '/',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
// The header's inbox button is where a user looks for messages, so this page is reached from there. Declaring no
|
|
21
|
+
// navigation keeps a second menu entry from pointing at the one destination the button already owns. The plugin's
|
|
22
|
+
// API answers each person's own messages only, so the page needs no authorization of its own.
|
|
23
|
+
authz: 'skip',
|
|
24
|
+
auth: 'required',
|
|
25
|
+
componentLoader: () => import('./pages/inbox.js'),
|
|
26
|
+
name: 'inbox',
|
|
27
|
+
path: '/inbox',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
auth: 'guest',
|
|
31
|
+
authz: 'skip',
|
|
32
|
+
componentLoader: () => import('./pages/auth/login.js'),
|
|
33
|
+
name: 'login',
|
|
34
|
+
path: '/login',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
auth: 'guest',
|
|
38
|
+
authz: 'skip',
|
|
39
|
+
componentLoader: () => import('./pages/auth/register.js'),
|
|
40
|
+
name: 'register',
|
|
41
|
+
path: '/register',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
auth: 'guest',
|
|
45
|
+
authz: 'skip',
|
|
46
|
+
componentLoader: () => import('./pages/auth/forgot-password.js'),
|
|
47
|
+
name: 'forgot-password',
|
|
48
|
+
path: '/forgot-password',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
auth: 'guest',
|
|
52
|
+
authz: 'skip',
|
|
53
|
+
componentLoader: () => import('./pages/auth/reset-password.js'),
|
|
54
|
+
name: 'reset-password',
|
|
55
|
+
path: '/reset-password',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
// Where a command line's sign-in sends the person to approve it (`pages/auth/device.tsx`). Optional rather than
|
|
59
|
+
// required so it renders outside the shell, like the sign-in pages; the page sends a guest to sign in and back.
|
|
60
|
+
auth: 'optional',
|
|
61
|
+
authz: 'skip',
|
|
62
|
+
componentLoader: () => import('./pages/auth/device.js'),
|
|
63
|
+
name: 'device',
|
|
64
|
+
path: '/device',
|
|
65
|
+
},
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
const settingsRoutes: AppClientRouteContribution = defineSettingsRoutes([]);
|
|
69
|
+
|
|
70
|
+
const routes: readonly AppClientRouteContribution[] = [
|
|
71
|
+
appRoutes,
|
|
72
|
+
settingsRoutes,
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
export default routes;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { useTranslation } from '@nocobase/i18n/client';
|
|
2
|
+
import {
|
|
3
|
+
GuestAuthentication,
|
|
4
|
+
RequiredAuthentication,
|
|
5
|
+
} from '@nocobase/app-plugin-authentication/client';
|
|
6
|
+
import type { AppClientRegisteredRoute } from '@nocobase/app-client/plugins';
|
|
7
|
+
import { lazy, Suspense, useMemo, type ReactElement } from 'react';
|
|
8
|
+
import { Navigate, Outlet, Route, Routes } from 'react-router';
|
|
9
|
+
|
|
10
|
+
import { Loading } from '#components/loading';
|
|
11
|
+
import { EMPTY_ARRAY } from '#lib/constants';
|
|
12
|
+
|
|
13
|
+
import { AppLayout } from '../layouts/app-layout.js';
|
|
14
|
+
import { renderRouteTree } from './route-tree.js';
|
|
15
|
+
import { StandalonePageLayout } from './standalone-page-layout.js';
|
|
16
|
+
|
|
17
|
+
// The settings centre brings its own chrome and navigation, none of which the application needs until someone opens
|
|
18
|
+
// it. Loading it lazily keeps it out of the entry chunk, the same way every page it hosts stays out.
|
|
19
|
+
const SettingsLayout = lazy(async () => ({
|
|
20
|
+
default: (await import('../layouts/settings-layout.js')).SettingsLayout,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
export interface AppRouterProps {
|
|
24
|
+
readonly settingsRouteTree: readonly AppClientRegisteredRoute[];
|
|
25
|
+
readonly devRouteTree: readonly AppClientRegisteredRoute[];
|
|
26
|
+
readonly clientRoutes: readonly AppClientRegisteredRoute[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function AppRouter(inputProps: AppRouterProps): ReactElement {
|
|
30
|
+
const { t } = useTranslation();
|
|
31
|
+
const { settingsRouteTree, devRouteTree, clientRoutes } = inputProps;
|
|
32
|
+
|
|
33
|
+
const settingsRoutes = useMemo(
|
|
34
|
+
() =>
|
|
35
|
+
filterRouteTree(
|
|
36
|
+
clientRoutes,
|
|
37
|
+
(route) =>
|
|
38
|
+
route.auth === 'required' && route.path.startsWith('/settings/'),
|
|
39
|
+
),
|
|
40
|
+
[clientRoutes],
|
|
41
|
+
);
|
|
42
|
+
const routeGroups = useMemo(
|
|
43
|
+
() => ({
|
|
44
|
+
guest: clientRoutes.filter((route) => route.auth === 'guest'),
|
|
45
|
+
optional: clientRoutes.filter((route) => route.auth === 'optional'),
|
|
46
|
+
required: filterRouteTree(
|
|
47
|
+
clientRoutes,
|
|
48
|
+
(route) =>
|
|
49
|
+
route.auth === 'required' && !route.path.startsWith('/settings/'),
|
|
50
|
+
),
|
|
51
|
+
}),
|
|
52
|
+
[clientRoutes],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Routes>
|
|
57
|
+
<Route
|
|
58
|
+
element={
|
|
59
|
+
<RequiredAuthentication>
|
|
60
|
+
<Outlet />
|
|
61
|
+
</RequiredAuthentication>
|
|
62
|
+
}
|
|
63
|
+
>
|
|
64
|
+
<Route
|
|
65
|
+
element={
|
|
66
|
+
<AppLayout routes={routeGroups.required} devRoutes={devRouteTree} />
|
|
67
|
+
}
|
|
68
|
+
>
|
|
69
|
+
{renderRouteTree(routeGroups.required)}
|
|
70
|
+
{/* Pages plugins declare with `defineDevRoutes()` keep their `/dev/...` paths inside the application shell.
|
|
71
|
+
A production build resolves no dev routes, so nothing is rendered here. */}
|
|
72
|
+
{renderRouteTree(devRouteTree)}
|
|
73
|
+
</Route>
|
|
74
|
+
<Route
|
|
75
|
+
path='/settings/*'
|
|
76
|
+
element={
|
|
77
|
+
<Suspense
|
|
78
|
+
fallback={
|
|
79
|
+
<Loading
|
|
80
|
+
className='min-h-svh'
|
|
81
|
+
label={t('status.loadingSettings', {
|
|
82
|
+
defaultValue: 'Loading settings',
|
|
83
|
+
})}
|
|
84
|
+
/>
|
|
85
|
+
}
|
|
86
|
+
>
|
|
87
|
+
<SettingsLayout
|
|
88
|
+
routeTree={settingsRouteTree}
|
|
89
|
+
routes={settingsRoutes}
|
|
90
|
+
/>
|
|
91
|
+
</Suspense>
|
|
92
|
+
}
|
|
93
|
+
/>
|
|
94
|
+
</Route>
|
|
95
|
+
|
|
96
|
+
<Route
|
|
97
|
+
element={
|
|
98
|
+
<GuestAuthentication>
|
|
99
|
+
<Outlet />
|
|
100
|
+
</GuestAuthentication>
|
|
101
|
+
}
|
|
102
|
+
>
|
|
103
|
+
<Route element={<StandalonePageLayout />}>
|
|
104
|
+
{renderRouteTree(routeGroups.guest)}
|
|
105
|
+
</Route>
|
|
106
|
+
</Route>
|
|
107
|
+
|
|
108
|
+
<Route element={<StandalonePageLayout />}>
|
|
109
|
+
{renderRouteTree(routeGroups.optional)}
|
|
110
|
+
</Route>
|
|
111
|
+
|
|
112
|
+
<Route path='*' element={<Navigate to='/' replace />} />
|
|
113
|
+
</Routes>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Pure groups can span surfaces; a page and its descendants always share their shell. */
|
|
118
|
+
function filterRouteTree(
|
|
119
|
+
routes: readonly AppClientRegisteredRoute[],
|
|
120
|
+
predicate: (route: AppClientRegisteredRoute) => boolean,
|
|
121
|
+
): AppClientRegisteredRoute[] {
|
|
122
|
+
return routes.flatMap((route) => {
|
|
123
|
+
if (route.componentLoader) return predicate(route) ? [route] : [];
|
|
124
|
+
const children = filterRouteTree(route.children ?? EMPTY_ARRAY, predicate);
|
|
125
|
+
return children.length ? [{ ...route, children }] : [];
|
|
126
|
+
});
|
|
127
|
+
}
|