@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.
Files changed (250) hide show
  1. package/.cursor/mcp.json +8 -0
  2. package/.mcp.json +8 -0
  3. package/.prettierignore +20 -0
  4. package/.vscode/mcp.json +8 -0
  5. package/AGENTS.md +432 -0
  6. package/CLAUDE.md +9 -0
  7. package/Dockerfile +132 -0
  8. package/Dockerfile.dockerignore +45 -0
  9. package/README.MD +481 -0
  10. package/cli/plugins.ts +14 -0
  11. package/client/agent-annotations-host.ts +19 -0
  12. package/client/app.ts +38 -0
  13. package/client/components/back-button.tsx +49 -0
  14. package/client/components/breadcrumbs.tsx +167 -0
  15. package/client/components/inbox-badge.ts +49 -0
  16. package/client/components/inbox-button.tsx +132 -0
  17. package/client/components/inbox-header-button.tsx +25 -0
  18. package/client/components/loading.tsx +35 -0
  19. package/client/components/page-container.tsx +24 -0
  20. package/client/components/page-header.tsx +33 -0
  21. package/client/components/route-child-page.tsx +121 -0
  22. package/client/components/route-dialog.tsx +10 -0
  23. package/client/components/route-drawer.tsx +10 -0
  24. package/client/components/route-overlay.tsx +190 -0
  25. package/client/components/ui/alert.tsx +75 -0
  26. package/client/components/ui/badge.tsx +51 -0
  27. package/client/components/ui/breadcrumb.tsx +121 -0
  28. package/client/components/ui/button.tsx +58 -0
  29. package/client/components/ui/collapsible.tsx +19 -0
  30. package/client/components/ui/context-menu.tsx +271 -0
  31. package/client/components/ui/dialog.tsx +160 -0
  32. package/client/components/ui/dropdown-menu.tsx +270 -0
  33. package/client/components/ui/empty.tsx +103 -0
  34. package/client/components/ui/field.tsx +236 -0
  35. package/client/components/ui/input-group.tsx +158 -0
  36. package/client/components/ui/input.tsx +20 -0
  37. package/client/components/ui/kbd.tsx +26 -0
  38. package/client/components/ui/label.tsx +18 -0
  39. package/client/components/ui/popover.tsx +89 -0
  40. package/client/components/ui/select.tsx +200 -0
  41. package/client/components/ui/separator.tsx +22 -0
  42. package/client/components/ui/sheet.tsx +137 -0
  43. package/client/components/ui/sidebar.tsx +723 -0
  44. package/client/components/ui/skeleton.tsx +13 -0
  45. package/client/components/ui/spinner.tsx +20 -0
  46. package/client/components/ui/tabs.tsx +79 -0
  47. package/client/components/ui/textarea.tsx +17 -0
  48. package/client/components/ui/toast.tsx +231 -0
  49. package/client/components/ui/tooltip.tsx +63 -0
  50. package/client/components/use-route-overlay.ts +26 -0
  51. package/client/config/api.ts +13 -0
  52. package/client/config/app.ts +8 -0
  53. package/client/config/auth.ts +18 -0
  54. package/client/config/index.ts +17 -0
  55. package/client/extensions/nocobase-auth-forms/field-props.ts +13 -0
  56. package/client/extensions/nocobase-auth-forms/form-parts.tsx +101 -0
  57. package/client/extensions/nocobase-auth-forms/password-input.tsx +41 -0
  58. package/client/extensions/nocobase-auth-forms/password-login-form.tsx +123 -0
  59. package/client/extensions/nocobase-auth-forms/password-registration-form.tsx +193 -0
  60. package/client/extensions/nocobase-auth-forms/password-reset-form.tsx +132 -0
  61. package/client/extensions/nocobase-auth-forms/password-reset-request-form.tsx +99 -0
  62. package/client/extensions/nocobase-auth-methods/auth-methods.tsx +170 -0
  63. package/client/extensions/nocobase-auth-split-layout/auth-split-layout.tsx +126 -0
  64. package/client/extensions/nocobase-device-approval/device-approval.tsx +277 -0
  65. package/client/extensions/nocobase-device-approval/locales/en-US.ts +55 -0
  66. package/client/extensions/nocobase-device-approval/locales/zh-CN.ts +27 -0
  67. package/client/extensions/nocobase-device-approval/use-device-approval.ts +176 -0
  68. package/client/extensions/nocobase-file-component-ui/README.md +13 -0
  69. package/client/extensions/nocobase-file-component-ui/components/file-list.tsx +147 -0
  70. package/client/extensions/nocobase-file-component-ui/components/file-preview-dialog.tsx +263 -0
  71. package/client/extensions/nocobase-file-component-ui/components/file-preview-field.tsx +68 -0
  72. package/client/extensions/nocobase-file-component-ui/components/file-thumbnail.tsx +73 -0
  73. package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +386 -0
  74. package/client/extensions/nocobase-file-component-ui/components/previewers/file-preview-content.tsx +255 -0
  75. package/client/extensions/nocobase-file-component-ui/components/previewers/office-open-xml-preview.tsx +226 -0
  76. package/client/extensions/nocobase-file-component-ui/index.ts +8 -0
  77. package/client/extensions/nocobase-file-component-ui/lib/file-preview.ts +194 -0
  78. package/client/extensions/nocobase-file-component-ui/lib/file-url.ts +22 -0
  79. package/client/extensions/nocobase-file-component-ui/lib/message-key.ts +18 -0
  80. package/client/extensions/nocobase-file-component-ui/types.ts +69 -0
  81. package/client/extensions/nocobase-inbox/decision-actions-bar.tsx +140 -0
  82. package/client/extensions/nocobase-inbox/frame.ts +31 -0
  83. package/client/extensions/nocobase-inbox/inbox-actions.ts +21 -0
  84. package/client/extensions/nocobase-inbox/inbox-detail.tsx +386 -0
  85. package/client/extensions/nocobase-inbox/inbox-item.tsx +274 -0
  86. package/client/extensions/nocobase-inbox/inbox-list.tsx +433 -0
  87. package/client/extensions/nocobase-inbox/inbox-page.tsx +545 -0
  88. package/client/extensions/nocobase-inbox/locales/en-US.ts +111 -0
  89. package/client/extensions/nocobase-inbox/locales/zh-CN.ts +57 -0
  90. package/client/extensions/nocobase-inbox/model.ts +428 -0
  91. package/client/extensions/nocobase-inbox/registry-scope.tsx +49 -0
  92. package/client/extensions/nocobase-inbox/registry.ts +303 -0
  93. package/client/extensions/nocobase-inbox/source.ts +117 -0
  94. package/client/hooks/use-mobile.ts +21 -0
  95. package/client/index.tsx +60 -0
  96. package/client/layouts/app-layout.tsx +105 -0
  97. package/client/layouts/components/app-brand.tsx +49 -0
  98. package/client/layouts/components/app-sidebar.tsx +196 -0
  99. package/client/layouts/components/header-actions.tsx +50 -0
  100. package/client/layouts/components/language-switcher.tsx +95 -0
  101. package/client/layouts/components/layout-header.tsx +17 -0
  102. package/client/layouts/components/navigation-menu.tsx +411 -0
  103. package/client/layouts/components/sidebar-footer.tsx +107 -0
  104. package/client/layouts/components/surface-empty.tsx +35 -0
  105. package/client/layouts/components/user-menu.tsx +127 -0
  106. package/client/layouts/index.ts +2 -0
  107. package/client/layouts/settings-layout.tsx +172 -0
  108. package/client/layouts/use-sidebar-preference.ts +57 -0
  109. package/client/lib/constants.ts +5 -0
  110. package/client/lib/toaster.ts +56 -0
  111. package/client/lib/utils.ts +1 -0
  112. package/client/locales/en-US.ts +178 -0
  113. package/client/locales/index.ts +9 -0
  114. package/client/locales/zh-CN.ts +161 -0
  115. package/client/pages/auth/device.tsx +41 -0
  116. package/client/pages/auth/forgot-password.tsx +39 -0
  117. package/client/pages/auth/login.tsx +66 -0
  118. package/client/pages/auth/register.tsx +46 -0
  119. package/client/pages/auth/reset-password.tsx +38 -0
  120. package/client/pages/auth/shared.tsx +111 -0
  121. package/client/pages/home.tsx +16 -0
  122. package/client/pages/inbox.tsx +12 -0
  123. package/client/plugins.ts +41 -0
  124. package/client/react-providers.ts +27 -0
  125. package/client/route-overrides.ts +9 -0
  126. package/client/routes.ts +75 -0
  127. package/client/routing/app-router.tsx +127 -0
  128. package/client/routing/client-route.tsx +129 -0
  129. package/client/routing/route-context.tsx +85 -0
  130. package/client/routing/route-navigation.ts +153 -0
  131. package/client/routing/route-tree.tsx +42 -0
  132. package/client/routing/standalone-page-layout.tsx +15 -0
  133. package/client/runtime.ts +26 -0
  134. package/client/service-provider.ts +40 -0
  135. package/client/source-extensions.ts +19 -0
  136. package/client/startup.tsx +22 -0
  137. package/client/styles.css +122 -0
  138. package/client/theme/index.ts +4 -0
  139. package/client/theme/theme-context.ts +12 -0
  140. package/client/theme/theme-preferences.ts +84 -0
  141. package/client/theme/theme-presets.ts +7 -0
  142. package/client/theme/theme-provider.tsx +106 -0
  143. package/client/theme/theme-settings.tsx +176 -0
  144. package/client/theme/themes/compact.css +140 -0
  145. package/client/theme/themes/default.css +117 -0
  146. package/client/vite-env.d.ts +5 -0
  147. package/components.json +27 -0
  148. package/config.example.yml +172 -0
  149. package/database/main/migrations/.gitkeep +0 -0
  150. package/database/main/migrations/202610060001_create_device_code.ts +37 -0
  151. package/database/main/seeds/.gitkeep +0 -0
  152. package/database/tsconfig.json +4 -0
  153. package/ecosystem.config.js +35 -0
  154. package/eslint.config.js +24 -0
  155. package/index.html +100 -0
  156. package/package.json +200 -3
  157. package/playwright.config.ts +10 -0
  158. package/public/assets/favicon.ico +0 -0
  159. package/public/assets/logo-dark.png +0 -0
  160. package/public/assets/logo-mark-dark.png +0 -0
  161. package/public/assets/logo-mark.png +0 -0
  162. package/public/assets/logo.png +0 -0
  163. package/server/app.ts +50 -0
  164. package/server/config/ai.ts +27 -0
  165. package/server/config/api.ts +8 -0
  166. package/server/config/app.ts +40 -0
  167. package/server/config/auth.ts +36 -0
  168. package/server/config/authorization.ts +18 -0
  169. package/server/config/caching.ts +21 -0
  170. package/server/config/database.ts +22 -0
  171. package/server/config/drive.ts +30 -0
  172. package/server/config/i18n.ts +17 -0
  173. package/server/config/index.ts +73 -0
  174. package/server/config/jobs.ts +27 -0
  175. package/server/config/logging.ts +23 -0
  176. package/server/config/notification.ts +11 -0
  177. package/server/config/queue.ts +28 -0
  178. package/server/config/scheduler.ts +23 -0
  179. package/server/config/secrets.ts +10 -0
  180. package/server/config/server.ts +26 -0
  181. package/server/config/session.ts +60 -0
  182. package/server/config/snowflake.ts +21 -0
  183. package/server/config/spa.ts +24 -0
  184. package/server/config/users.ts +11 -0
  185. package/server/config/workflow.ts +24 -0
  186. package/server/config.ts +24 -0
  187. package/server/embedded.ts +23 -0
  188. package/server/locales/en-US.ts +10 -0
  189. package/server/locales/index.ts +10 -0
  190. package/server/locales/zh-CN.ts +5 -0
  191. package/server/plugins.ts +41 -0
  192. package/server/providers/index.ts +5 -0
  193. package/server/routes/index.ts +6 -0
  194. package/server/runtime.ts +24 -0
  195. package/server/standalone.ts +29 -0
  196. package/server/tsconfig.json +4 -0
  197. package/tailwind.config.mjs +59 -0
  198. package/tests/components/app-layout-routes.test.tsx +132 -0
  199. package/tests/components/app-title.test.tsx +57 -0
  200. package/tests/components/auth-i18n.test.tsx +87 -0
  201. package/tests/components/back-button.test.tsx +80 -0
  202. package/tests/components/file-preview.test.tsx +101 -0
  203. package/tests/components/header-hover.test.tsx +87 -0
  204. package/tests/components/inbox.test.tsx +139 -0
  205. package/tests/components/language-switcher.test.tsx +298 -0
  206. package/tests/components/layout-containers.test.tsx +15 -0
  207. package/tests/components/layout-i18n.test.tsx +187 -0
  208. package/tests/components/loading.test.tsx +25 -0
  209. package/tests/components/navigation-menu.test.tsx +355 -0
  210. package/tests/components/page-harness.test.tsx +318 -0
  211. package/tests/components/primitive-labels.test.tsx +80 -0
  212. package/tests/components/sidebar-permissions.test.tsx +110 -0
  213. package/tests/components/sidebar-preference.test.tsx +42 -0
  214. package/tests/fixtures/theme-tokens.html +12 -0
  215. package/tests/fixtures/theme-tokens.tsx +111 -0
  216. package/tests/logic/account-permissions.test.tsx +204 -0
  217. package/tests/logic/app-locale-coverage.test.ts +136 -0
  218. package/tests/logic/app-server.test.ts +1589 -0
  219. package/tests/logic/asset-url-build.test.ts +106 -0
  220. package/tests/logic/client-auth.test.tsx +122 -0
  221. package/tests/logic/client-plugin-registry.test.ts +63 -0
  222. package/tests/logic/client-route-recovery.test.tsx +80 -0
  223. package/tests/logic/client-routes.test.ts +126 -0
  224. package/tests/logic/client-runtime.test.ts +112 -0
  225. package/tests/logic/client-settings.test.tsx +841 -0
  226. package/tests/logic/client-shell.test.tsx +466 -0
  227. package/tests/logic/client-theme.test.tsx +441 -0
  228. package/tests/logic/config.test.ts +197 -0
  229. package/tests/logic/dev-client-watch.test.ts +108 -0
  230. package/tests/logic/dev-routes-production-build.test.ts +168 -0
  231. package/tests/logic/device-code-migration.test.ts +70 -0
  232. package/tests/logic/lifecycle.test.ts +221 -0
  233. package/tests/logic/plugin-commands.test.ts +69 -0
  234. package/tests/logic/plugin-page-authz.test.ts +76 -0
  235. package/tests/logic/route-navigation.test.ts +66 -0
  236. package/tests/logic/route-overlay.test.tsx +353 -0
  237. package/tests/logic/server-imports.test.ts +88 -0
  238. package/tests/logic/tailwind-sources.test.ts +151 -0
  239. package/tests/logic/template-package-name.test.ts +88 -0
  240. package/tests/logic/theme-preferences.test.ts +86 -0
  241. package/tests/logic/theme-tokens.test.ts +248 -0
  242. package/tests/logic/toaster.test.tsx +108 -0
  243. package/tests/logic/user-menu-sign-out.test.tsx +98 -0
  244. package/tests/setup/client-config.ts +27 -0
  245. package/tsconfig.json +11 -0
  246. package/tsconfig.node.json +12 -0
  247. package/tsconfig.server.json +19 -0
  248. package/vite.config.ts +92 -0
  249. package/vitest.config.ts +37 -0
  250. 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;
@@ -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
+ }