@allbluecn/web-app 0.10.3 → 0.11.1

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 (246) hide show
  1. package/package.json +24 -11
  2. package/public/favicon-dark.svg +1 -0
  3. package/public/favicon.ico +0 -0
  4. package/public/favicon.svg +1 -0
  5. package/public/logo/allblue-logo-app-icon.svg +7 -0
  6. package/public/logo/allblue-logo-dark-square.svg +4 -0
  7. package/public/logo/allblue-logo-dark.png +0 -0
  8. package/public/logo/allblue-logo-dark.svg +4 -0
  9. package/public/logo/allblue-logo-light-square.svg +4 -0
  10. package/public/logo/allblue-logo-light.png +0 -0
  11. package/public/logo/allblue-logo-light.svg +4 -0
  12. package/src/__tests__/bench/baselines/format-timestamp.json +44 -0
  13. package/src/__tests__/bench/core-hot-path.bench.ts +71 -0
  14. package/src/__tests__/integration/db-crud.integration.test.ts +79 -0
  15. package/src/__tests__/integration/login-email-acceptance.test.ts +202 -0
  16. package/src/__tests__/integration/login-email-p2-acceptance.test.ts +142 -0
  17. package/src/__tests__/rejects-tothrow-regression.test.ts +31 -0
  18. package/src/__tests__/security/ai/prompt-injection.test.ts +105 -0
  19. package/src/__tests__/security/ssrf-url-whitelist.test.ts +54 -0
  20. package/src/__tests__/security/xss-sanitize.test.ts +99 -0
  21. package/src/__tests__/setup.ts +9 -0
  22. package/src/__tests__/visual/__screenshots__/theme-surface.vrt.test.tsx/theme-surface-dark-baseline-1-chromium-darwin.png +0 -0
  23. package/src/__tests__/visual/__screenshots__/theme-surface.vrt.test.tsx/theme-surface-light-baseline-1-chromium-darwin.png +0 -0
  24. package/src/__tests__/visual/__screenshots__/user-avatar.vrt.test.tsx/user-avatar-initial-fallback-baseline-1-chromium-darwin.png +0 -0
  25. package/src/__tests__/visual/theme-surface.vrt.test.tsx +54 -0
  26. package/src/__tests__/visual/user-avatar.vrt.test.tsx +32 -0
  27. package/src/components/auth/icon-input.tsx +9 -1
  28. package/src/components/auth/password-input.tsx +9 -1
  29. package/src/components/changelog/changelog-page.tsx +44 -1
  30. package/src/components/error/global-error-boundary.tsx +2 -2
  31. package/src/components/forms/app-field.tsx +3 -1
  32. package/src/components/knowledge/general-groups.ts +15 -0
  33. package/src/components/knowledge/kb-filter-popover.tsx +143 -0
  34. package/src/components/knowledge/knowledge-toolbar.tsx +17 -21
  35. package/src/components/knowledge-public/legal-compliance-editor.tsx +4 -2
  36. package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +9 -6
  37. package/src/components/layout/sidebar-03/app-sidebar.tsx +4 -15
  38. package/src/components/layout/sidebar-03/nav-knowledge.tsx +19 -17
  39. package/src/components/layout/sidebar-03/nav-workspace.tsx +20 -22
  40. package/src/components/notifications/settings-notifications.tsx +41 -1
  41. package/src/components/settings/personal-team/account/settings-profile.tsx +68 -1
  42. package/src/components/shared-kit/avatars/notion-avatar.tsx +2 -1
  43. package/src/components/shared-kit/icons/__tests__/lucide-curated-icons.test.ts +2 -2
  44. package/src/components/shared-kit/icons/__tests__/lucide-icon-picker.test.tsx +3 -2
  45. package/src/components/shared-kit/icons/lucide-icon-picker/curated-icons.ts +5 -10
  46. package/src/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name.tsx +3 -3
  47. package/src/components/shared-kit/theme/theme-surface.tsx +52 -0
  48. package/src/components/story/relations/general-groups.ts +1 -15
  49. package/src/components/story/relations/knowledge-select-dialog.tsx +13 -87
  50. package/src/components/story/story-list-view.tsx +7 -1
  51. package/src/components/tiptap/node/table-node/ui/table-move-row-column-button/use-table-move-row-column.ts +0 -2
  52. package/src/components/workspace/__tests__/activity/workspace-activity-card.test.tsx +36 -0
  53. package/src/components/workspace/__tests__/contribution/workspace-contribution-card.test.tsx +17 -0
  54. package/src/components/workspace/__tests__/dialogs/create-workspace-dialog.test.tsx +63 -0
  55. package/src/components/workspace/__tests__/dialogs/delete-workspace-options.test.tsx +89 -0
  56. package/src/components/workspace/__tests__/dialogs/edit-workspace-dialog.test.tsx +68 -0
  57. package/src/components/workspace/__tests__/header/workspace-header.test.tsx +40 -0
  58. package/src/components/workspace/__tests__/kb/workspace-kb-card.test.tsx +154 -0
  59. package/src/components/workspace/__tests__/members/workspace-member-item.test.tsx +24 -0
  60. package/src/components/workspace/__tests__/members/workspace-members-manager.test.tsx +51 -0
  61. package/src/components/workspace/__tests__/projects/workspace-project-list.test.tsx +46 -0
  62. package/src/components/workspace/{create-workspace-dialog.tsx → dialogs/create-workspace-dialog.tsx} +1 -1
  63. package/src/components/workspace/{edit-workspace-dialog.tsx → dialogs/edit-workspace-dialog.tsx} +1 -1
  64. package/src/components/workspace/kb/workspace-kb-card.tsx +594 -0
  65. package/src/components/workspace/kb/workspace-kb-manage-dialog.tsx +174 -0
  66. package/src/components/workspace/kb/workspace-quick-doc-dialog.tsx +431 -0
  67. package/src/components/workspace/{workspace-members-manager.tsx → members/workspace-members-manager.tsx} +78 -40
  68. package/src/lib/__tests__/edition-desktop.test.ts +23 -0
  69. package/src/lib/__tests__/msw-sample.test.ts +68 -0
  70. package/src/lib/__tests__/pgvector.test.ts +2 -2
  71. package/src/lib/app-version.ts +1 -1
  72. package/src/lib/auth/auth.config.ts +123 -99
  73. package/src/lib/changelog/sdk-versions.ts +21 -21
  74. package/src/lib/collections/knowledge/knowledge-bases-collection.ts +3 -1
  75. package/src/lib/collections/reference/workspaces-collection.ts +3 -1
  76. package/src/lib/edition-desktop.ts +22 -0
  77. package/src/lib/fetch-json.ts +23 -0
  78. package/src/{components/workspace/workspace-kb-card.tsx → lib/i18n/resolver.ts} +6 -12
  79. package/src/lib/sanitize.ts +14 -1
  80. package/src/lib/session.ts +10 -2
  81. package/src/lib/vector-store/__tests__/factory.test.ts +1 -1
  82. package/src/lib/vector-store/__tests__/sqlite-vec-store.test.ts +2 -2
  83. package/src/plugins/core/tags/__tests__/tags.test.ts +10 -10
  84. package/src/routeTree.gen.ts +114 -0
  85. package/src/routes/__root.tsx +20 -3
  86. package/src/routes/__tests__/-auth-schemas.test.ts +62 -0
  87. package/src/routes/__tests__/-edition-routing.test.tsx +18 -5
  88. package/src/routes/__tests__/route-tree-structure.test.ts +66 -0
  89. package/src/routes/_login/changelog/confirm/route.lazy.tsx +72 -0
  90. package/src/routes/_login/changelog/confirm/route.tsx +28 -0
  91. package/src/routes/_login/email-unsubscribe/route.lazy.tsx +76 -0
  92. package/src/routes/_login/email-unsubscribe/route.tsx +28 -0
  93. package/src/routes/_login/forgot-password/route.lazy.tsx +2 -1
  94. package/src/routes/_login/login/route.lazy.tsx +4 -2
  95. package/src/routes/_login/register/route.lazy.tsx +28 -17
  96. package/src/routes/_login/register/route.tsx +8 -1
  97. package/src/routes/_login/reset-password/route.lazy.tsx +4 -2
  98. package/src/routes/_login/schemas/-auth.ts +30 -7
  99. package/src/routes/_login/verify-email/route.lazy.tsx +82 -0
  100. package/src/routes/_login/verify-email/route.tsx +28 -0
  101. package/src/routes/_nav/knowledge/compliance/index.lazy.tsx +5 -2
  102. package/src/routes/_nav/knowledge/index.lazy.tsx +1 -1
  103. package/src/routes/_nav/knowledge/laws/index.lazy.tsx +5 -2
  104. package/src/routes/_nav/route.tsx +26 -0
  105. package/src/routes/_nav/workspaces/$workspaceId/route.lazy.tsx +13 -11
  106. package/src/routes/_nav/workspaces/$workspaceId/route.tsx +4 -2
  107. package/src/routes/_nav/workspaces/$workspaceId/settings/route.lazy.tsx +1 -1
  108. package/src/routes/_nav/workspaces/index.lazy.tsx +3 -3
  109. package/src/routes/_public/index/route.tsx +2 -1
  110. package/src/routes/api/auth/desktop-callback.ts +2 -2
  111. package/src/routes/api/billing-webhook.ts +4 -0
  112. package/src/routes/api/cron/changelog-release.ts +100 -0
  113. package/src/routes/api/cron/subscription-reminders.ts +110 -0
  114. package/src/routes/api.auth.$.ts +3 -5
  115. package/src/server/__tests__/agent-crud.test.ts +4 -4
  116. package/src/server/__tests__/auth-registration.test.ts +60 -0
  117. package/src/server/__tests__/chat-conversations.test.ts +1 -3
  118. package/src/server/__tests__/fetch-guard.test.ts +99 -0
  119. package/src/server/__tests__/permissions.test.ts +1 -3
  120. package/src/server/__tests__/resources.team-seats.test.ts +2 -0
  121. package/src/server/__tests__/skill-crud.test.ts +3 -3
  122. package/src/server/ai-gateway/__tests__/ai-output-guard.test.ts +95 -0
  123. package/src/server/ai-gateway/__tests__/kb-context-guard.test.ts +136 -0
  124. package/src/server/ai-gateway/__tests__/knowledge-search-fence.test.ts +240 -0
  125. package/src/server/ai-gateway/__tests__/llm-fence.test.ts +125 -0
  126. package/src/server/ai-gateway/__tests__/run-store.test.ts +52 -0
  127. package/src/server/ai-gateway/ai-output-guard.ts +77 -0
  128. package/src/server/ai-gateway/index.ts +5 -0
  129. package/src/server/ai-gateway/kb-context-guard.ts +46 -0
  130. package/src/server/ai-gateway/llm-fence.ts +82 -0
  131. package/src/server/ai-gateway/persistence/run-store.ts +19 -0
  132. package/src/server/ai-gateway/story-prompt.ts +4 -2
  133. package/src/server/ai-gateway/tools/definitions.ts +12 -6
  134. package/src/server/ai-gateway/tools/server.ts +97 -54
  135. package/src/server/auth-registration.ts +40 -0
  136. package/src/server/billing-email.ts +39 -0
  137. package/src/server/builtin-project.ts +21 -0
  138. package/src/server/email/__tests__/deliverability.test.ts +68 -0
  139. package/src/server/email/__tests__/index.test.ts +32 -0
  140. package/src/server/email/__tests__/outbox.test.tsx +129 -0
  141. package/src/server/email/__tests__/product-templates.test.tsx +106 -0
  142. package/src/server/email/__tests__/registry.test.tsx +50 -0
  143. package/src/server/email/deliverability.ts +57 -0
  144. package/src/server/email/drivers/console.ts +11 -0
  145. package/src/server/email/drivers/resend.ts +31 -0
  146. package/src/server/email/drivers/smtp.ts +27 -0
  147. package/src/server/email/index.ts +56 -0
  148. package/src/server/email/outbox.ts +141 -0
  149. package/src/server/email/preferences.ts +20 -0
  150. package/src/server/email/registry.ts +38 -0
  151. package/src/server/email/sender.ts +36 -13
  152. package/src/server/email/templates/_base/button.tsx +12 -0
  153. package/src/server/email/templates/_base/email-layout.tsx +117 -0
  154. package/src/server/email/templates/_base/theme-fonts.tsx +43 -0
  155. package/src/server/email/templates/_base/theme.ts +22 -0
  156. package/src/server/email/templates/auth/reset-password.tsx +58 -0
  157. package/src/server/email/templates/auth/verify-email.tsx +59 -0
  158. package/src/server/email/templates/auth/welcome.tsx +49 -0
  159. package/src/server/email/templates/billing/payment-receipt.tsx +51 -0
  160. package/src/server/email/templates/billing/subscription-canceled.tsx +51 -0
  161. package/src/server/email/templates/billing/subscription-ending.tsx +51 -0
  162. package/src/server/email/templates/notification/mirror.tsx +50 -0
  163. package/src/server/email/templates/product/changelog-confirm.tsx +53 -0
  164. package/src/server/email/templates/product/changelog-release.tsx +65 -0
  165. package/src/server/email/templates/team/apply-notify.tsx +53 -0
  166. package/src/server/email/templates/team/approved.tsx +53 -0
  167. package/src/server/email/templates/team/invite.tsx +57 -0
  168. package/src/server/fetch-guard.ts +170 -0
  169. package/src/server/middlewares/security-headers.ts +122 -0
  170. package/src/server/notifications/__tests__/email-mirror.test.ts +193 -0
  171. package/src/server/notifications/__tests__/verify-reminder.test.ts +94 -0
  172. package/src/server/notifications/notification-service.ts +71 -1
  173. package/src/server/notifications/verify-reminder.ts +55 -0
  174. package/src/server/oauth-config.ts +59 -0
  175. package/src/server/serverFns/__tests__/auth-server-schemas.test.ts +168 -0
  176. package/src/server/serverFns/__tests__/billing.test.ts +263 -0
  177. package/src/server/serverFns/__tests__/email-verification.test.ts +149 -0
  178. package/src/server/serverFns/__tests__/link-preview.test.ts +239 -0
  179. package/src/server/serverFns/__tests__/project-kb.test.ts +3 -3
  180. package/src/server/serverFns/__tests__/project-team-assign.test.ts +1 -1
  181. package/src/server/serverFns/__tests__/story-comments.test.ts +265 -0
  182. package/src/server/serverFns/__tests__/story-crud-archive-detail.test.ts +266 -0
  183. package/src/server/serverFns/__tests__/story-crud-labels-users.test.ts +227 -0
  184. package/src/server/serverFns/__tests__/story-crud.test.ts +526 -0
  185. package/src/server/serverFns/__tests__/story-links.test.ts +262 -0
  186. package/src/server/serverFns/__tests__/story-list-query.test.ts +259 -0
  187. package/src/server/serverFns/__tests__/story-reactions.test.ts +230 -0
  188. package/src/server/serverFns/__tests__/story-tree.test.ts +317 -0
  189. package/src/server/serverFns/__tests__/story-types.test.ts +294 -0
  190. package/src/server/serverFns/__tests__/team-accept.test.ts +6 -6
  191. package/src/server/serverFns/__tests__/team-invitation.test.ts +5 -5
  192. package/src/server/serverFns/__tests__/team-join-request.test.ts +8 -8
  193. package/src/server/serverFns/__tests__/update-seats.test.ts +4 -4
  194. package/src/server/serverFns/__tests__/workspace-kb-idor.test.ts +160 -0
  195. package/src/server/serverFns/__tests__/workspace-kb-utils.test.ts +54 -0
  196. package/src/server/serverFns/__tests__/workspace-kb.test.ts +348 -0
  197. package/src/server/serverFns/__tests__/workspace.test.ts +28 -8
  198. package/src/server/serverFns/auth/email-verification.ts +132 -0
  199. package/src/server/serverFns/auth/forgot-password.ts +22 -2
  200. package/src/server/serverFns/auth/providers.ts +4 -3
  201. package/src/server/serverFns/auth/register.ts +63 -19
  202. package/src/server/serverFns/auth/registration-mode.ts +23 -0
  203. package/src/server/serverFns/auth/reset-password.ts +1 -1
  204. package/src/server/serverFns/auth/{invitation.ts → verify-reminder.ts} +9 -26
  205. package/src/server/serverFns/changelog/__tests__/subscribe.test.ts +119 -0
  206. package/src/server/serverFns/changelog/subscribe.ts +89 -0
  207. package/src/server/serverFns/email-unsubscribe.ts +66 -0
  208. package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +3 -5
  209. package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +1 -1
  210. package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +2 -2
  211. package/src/server/serverFns/kb-doc-search.ts +111 -0
  212. package/src/server/serverFns/knowledge-public/compliance/create.ts +2 -1
  213. package/src/server/serverFns/knowledge-public/legal/create.ts +2 -1
  214. package/src/server/serverFns/notifications.ts +10 -0
  215. package/src/server/serverFns/story/__tests__/bulk.test.ts +1 -1
  216. package/src/server/serverFns/story/__tests__/label-suggest-fn.test.ts +2 -6
  217. package/src/server/serverFns/story/__tests__/story-assoc.test.ts +2 -4
  218. package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +2 -2
  219. package/src/server/serverFns/story/__tests__/story-groups.test.ts +2 -2
  220. package/src/server/serverFns/story/kb-general-consts.ts +39 -0
  221. package/src/server/serverFns/story/kb-general.ts +30 -17
  222. package/src/server/serverFns/story/story-assoc.ts +13 -103
  223. package/src/server/serverFns/story/story-crud.ts +2 -5
  224. package/src/server/serverFns/story/story-groups.ts +4 -13
  225. package/src/server/serverFns/team.ts +50 -12
  226. package/src/server/serverFns/workspace-kb-utils.ts +138 -0
  227. package/src/server/serverFns/workspace-kb.ts +403 -0
  228. package/src/server/serverFns/workspace.ts +4 -1
  229. package/src/shared/ai-cli/__tests__/adapter.test.ts +67 -0
  230. package/src/shared/ai-cli/__tests__/cli-detector.test.ts +99 -0
  231. package/src/shared/ai-cli/adapter.ts +2 -2
  232. package/src/shared/ai-cli/cli-detector.ts +18 -9
  233. package/src/start.ts +2 -1
  234. package/src/styles/globals.css +114 -0
  235. package/src/types/jest-dom-vitest.d.ts +8 -7
  236. package/vite.shared.ts +105 -4
  237. package/public/file.svg +0 -1
  238. package/public/globe.svg +0 -1
  239. package/public/window.svg +0 -1
  240. package/src/lib/__tests__/cli-detector.test.ts +0 -80
  241. /package/src/components/workspace/{workspace-activity-card.tsx → activity/workspace-activity-card.tsx} +0 -0
  242. /package/src/components/workspace/{workspace-contribution-card.tsx → contribution/workspace-contribution-card.tsx} +0 -0
  243. /package/src/components/workspace/{delete-workspace-options.tsx → dialogs/delete-workspace-options.tsx} +0 -0
  244. /package/src/components/workspace/{workspace-header.tsx → header/workspace-header.tsx} +0 -0
  245. /package/src/components/workspace/{workspace-member-item.tsx → members/workspace-member-item.tsx} +0 -0
  246. /package/src/components/workspace/{workspace-project-list.tsx → projects/workspace-project-list.tsx} +0 -0
@@ -0,0 +1,72 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { createLazyFileRoute, Link } from '@tanstack/react-router'
19
+ import { RainbowButton } from '@/components/magicui/rainbow-button'
20
+ import { ArrowRight } from 'lucide-react'
21
+ import { AuthLayout, AuthLayoutLeft, AuthLayoutRight } from '@/components/auth/auth-layout'
22
+ import { AuthHeader } from '@/components/auth/auth-header'
23
+ import { AuthBrandPanel } from '@/components/auth/auth-brand-panel'
24
+ import { m } from '@/paraglide/messages.js'
25
+
26
+ export const Route = createLazyFileRoute('/_login/changelog/confirm')({
27
+ component: ChangelogConfirmPage,
28
+ })
29
+
30
+ function ChangelogConfirmPage() {
31
+ const { ok } = Route.useRouteContext()
32
+
33
+ return (
34
+ <AuthLayout>
35
+ <AuthLayoutLeft>
36
+ {ok ? (
37
+ <>
38
+ <AuthHeader
39
+ title={m.common_changelog_confirm_success()}
40
+ subtitle={m.common_changelog_confirm_successDesc()}
41
+ />
42
+ <RainbowButton asChild className="w-full">
43
+ <Link to="/changelog">
44
+ {m.common_changelog_confirm_view()}
45
+ <ArrowRight className="ml-2 h-4 w-4" />
46
+ </Link>
47
+ </RainbowButton>
48
+ </>
49
+ ) : (
50
+ <>
51
+ <AuthHeader
52
+ title={m.common_changelog_confirm_invalid()}
53
+ subtitle={m.common_changelog_confirm_invalidDesc()}
54
+ />
55
+ <RainbowButton asChild className="w-full">
56
+ <Link to="/changelog">
57
+ {m.common_changelog_confirm_resubscribe()}
58
+ <ArrowRight className="ml-2 h-4 w-4" />
59
+ </Link>
60
+ </RainbowButton>
61
+ </>
62
+ )}
63
+ </AuthLayoutLeft>
64
+ <AuthLayoutRight>
65
+ <AuthBrandPanel
66
+ title="AllBlue"
67
+ subtitle={ok ? m.common_changelog_confirm_success() : m.common_changelog_confirm_invalid()}
68
+ />
69
+ </AuthLayoutRight>
70
+ </AuthLayout>
71
+ )
72
+ }
@@ -0,0 +1,28 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { createFileRoute } from '@tanstack/react-router'
19
+ import { confirmChangelogSubscriptionFn } from '@/server/serverFns/changelog/subscribe'
20
+
21
+ export const Route = createFileRoute('/_login/changelog/confirm')({
22
+ validateSearch: (s: { token?: string }) => ({ token: s.token ?? '' }),
23
+ beforeLoad: async ({ search }) => {
24
+ if (!search.token) return { ok: false }
25
+ const r = await confirmChangelogSubscriptionFn({ data: { token: search.token } })
26
+ return { ok: r.ok }
27
+ },
28
+ })
@@ -0,0 +1,76 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { createLazyFileRoute, Link } from '@tanstack/react-router'
19
+ import { RainbowButton } from '@/components/magicui/rainbow-button'
20
+ import { ArrowRight, Settings } from 'lucide-react'
21
+ import { AuthLayout, AuthLayoutLeft, AuthLayoutRight } from '@/components/auth/auth-layout'
22
+ import { AuthHeader } from '@/components/auth/auth-header'
23
+ import { AuthBrandPanel } from '@/components/auth/auth-brand-panel'
24
+ import { m } from '@/paraglide/messages.js'
25
+
26
+ export const Route = createLazyFileRoute('/_login/email-unsubscribe')({
27
+ component: EmailUnsubscribePage,
28
+ })
29
+
30
+ function EmailUnsubscribePage() {
31
+ const { ok } = Route.useRouteContext()
32
+
33
+ return (
34
+ <AuthLayout>
35
+ <AuthLayoutLeft>
36
+ {ok ? (
37
+ <>
38
+ <AuthHeader
39
+ title={m.common_email_unsubscribe_success()}
40
+ subtitle={m.common_email_unsubscribe_successDesc()}
41
+ />
42
+ <RainbowButton asChild className="w-full">
43
+ <Link to="/settings/notifications">
44
+ <Settings className="mr-2 h-4 w-4" />
45
+ {m.common_email_unsubscribe_manage()}
46
+ <ArrowRight className="ml-2 h-4 w-4" />
47
+ </Link>
48
+ </RainbowButton>
49
+ <div className="mt-4 text-center text-sm text-muted-foreground">
50
+ {m.common_email_unsubscribe_transactionalNote()}
51
+ </div>
52
+ </>
53
+ ) : (
54
+ <>
55
+ <AuthHeader
56
+ title={m.common_email_unsubscribe_invalid()}
57
+ subtitle={m.common_email_unsubscribe_invalidDesc()}
58
+ />
59
+ <RainbowButton asChild className="w-full">
60
+ <Link to="/login">
61
+ {m.common_email_unsubscribe_login()}
62
+ <ArrowRight className="ml-2 h-4 w-4" />
63
+ </Link>
64
+ </RainbowButton>
65
+ </>
66
+ )}
67
+ </AuthLayoutLeft>
68
+ <AuthLayoutRight>
69
+ <AuthBrandPanel
70
+ title="AllBlue"
71
+ subtitle={ok ? m.common_email_unsubscribe_success() : m.common_email_unsubscribe_invalid()}
72
+ />
73
+ </AuthLayoutRight>
74
+ </AuthLayout>
75
+ )
76
+ }
@@ -0,0 +1,28 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { createFileRoute } from '@tanstack/react-router'
19
+ import { unsubscribeByEmailTokenFn } from '@/server/serverFns/email-unsubscribe'
20
+
21
+ export const Route = createFileRoute('/_login/email-unsubscribe')({
22
+ validateSearch: (s: { token?: string }) => ({ token: s.token ?? '' }),
23
+ beforeLoad: async ({ search }) => {
24
+ if (!search.token) return { ok: false }
25
+ const r = await unsubscribeByEmailTokenFn({ data: { token: search.token } })
26
+ return { ok: r.ok }
27
+ },
28
+ })
@@ -62,7 +62,7 @@ function ForgotPasswordPage() {
62
62
  onSubmit={(e) => { e.preventDefault(); e.stopPropagation(); form.handleSubmit() }}
63
63
  className="space-y-6"
64
64
  >
65
- <AppField name="email" form={form}>
65
+ <AppField name="email" form={form} renderError={false}>
66
66
  {(field) => (
67
67
  <IconInput
68
68
  icon={Mail}
@@ -71,6 +71,7 @@ function ForgotPasswordPage() {
71
71
  value={field.value}
72
72
  onBlur={field.handleBlur}
73
73
  onChange={(e) => field.handleChange(e.target.value)}
74
+ error={field.meta.isTouched && field.errors.length > 0 ? (field.errors.map((e) => e.message).filter(Boolean)[0] as string | undefined) : undefined}
74
75
  />
75
76
  )}
76
77
  </AppField>
@@ -166,7 +166,7 @@ function LoginPage() {
166
166
  onSubmit={(e) => { e.preventDefault(); e.stopPropagation(); form.handleSubmit() }}
167
167
  className="space-y-6"
168
168
  >
169
- <AppField name="email" form={form}>
169
+ <AppField name="email" form={form} renderError={false}>
170
170
  {(field) => (
171
171
  <IconInput
172
172
  icon={Mail}
@@ -175,6 +175,7 @@ function LoginPage() {
175
175
  value={field.value}
176
176
  onBlur={field.handleBlur}
177
177
  onChange={(e) => field.handleChange(e.target.value)}
178
+ error={field.meta.isTouched && field.errors.length > 0 ? (field.errors.map((e) => e.message).filter(Boolean)[0] as string | undefined) : undefined}
178
179
  />
179
180
  )}
180
181
  </AppField>
@@ -184,7 +185,7 @@ function LoginPage() {
184
185
  <SsoSection email={email} />
185
186
  )}
186
187
  />
187
- <AppField name="password" form={form}>
188
+ <AppField name="password" form={form} renderError={false}>
188
189
  {(field) => (
189
190
  <PasswordInput
190
191
  label={m.auth_login_passwordLabel()}
@@ -192,6 +193,7 @@ function LoginPage() {
192
193
  value={field.value}
193
194
  onBlur={field.handleBlur}
194
195
  onChange={(e) => field.handleChange(e.target.value)}
196
+ error={field.meta.isTouched && field.errors.length > 0 ? (field.errors.map((e) => e.message).filter(Boolean)[0] as string | undefined) : undefined}
195
197
  />
196
198
  )}
197
199
  </AppField>
@@ -39,7 +39,7 @@ export const Route = createLazyFileRoute('/_login/register')({
39
39
 
40
40
  function RegisterPage() {
41
41
  const router = useRouter()
42
- const { googleEnabled } = Route.useLoaderData()
42
+ const { googleEnabled, registrationMode } = Route.useLoaderData()
43
43
  const { invite, email: emailFromUrl } = Route.useSearch()
44
44
  const registerFeatures = [
45
45
  { icon: Users, label: m.auth_features_userResearch(), desc: m.auth_features_userResearchDesc() },
@@ -58,6 +58,10 @@ function RegisterPage() {
58
58
  },
59
59
  validators: [{ run: registerSchema as any, triggers: ['change'] }],
60
60
  onSubmit: async ({ value }) => {
61
+ if (registrationMode === 'invite' && !value.invitationCode?.trim()) {
62
+ toast.error(m.common_form_invitationCodeRequired())
63
+ return
64
+ }
61
65
  try {
62
66
  await registerFn({ data: value })
63
67
  toast.success(m.auth_register_success())
@@ -86,7 +90,7 @@ function RegisterPage() {
86
90
  onSubmit={(e) => { e.preventDefault(); e.stopPropagation(); form.handleSubmit() }}
87
91
  className="space-y-6"
88
92
  >
89
- <AppField name="username" form={form}>
93
+ <AppField name="username" form={form} renderError={false}>
90
94
  {(field) => (
91
95
  <IconInput
92
96
  icon={User}
@@ -95,10 +99,11 @@ function RegisterPage() {
95
99
  value={field.value}
96
100
  onBlur={field.handleBlur}
97
101
  onChange={(e) => field.handleChange(e.target.value)}
102
+ error={field.meta.isTouched && field.errors.length > 0 ? (field.errors.map((e) => e.message).filter(Boolean)[0] as string | undefined) : undefined}
98
103
  />
99
104
  )}
100
105
  </AppField>
101
- <AppField name="email" form={form}>
106
+ <AppField name="email" form={form} renderError={false}>
102
107
  {(field) => (
103
108
  <IconInput
104
109
  icon={Mail}
@@ -107,11 +112,12 @@ function RegisterPage() {
107
112
  value={field.value}
108
113
  onBlur={field.handleBlur}
109
114
  onChange={(e) => field.handleChange(e.target.value)}
115
+ error={field.meta.isTouched && field.errors.length > 0 ? (field.errors.map((e) => e.message).filter(Boolean)[0] as string | undefined) : undefined}
110
116
  />
111
117
  )}
112
118
  </AppField>
113
119
  <div className="grid grid-cols-2 gap-4">
114
- <AppField name="password" form={form}>
120
+ <AppField name="password" form={form} renderError={false}>
115
121
  {(field) => (
116
122
  <PasswordInput
117
123
  label={m.auth_register_passwordLabel()}
@@ -119,10 +125,11 @@ function RegisterPage() {
119
125
  value={field.value}
120
126
  onBlur={field.handleBlur}
121
127
  onChange={(e) => field.handleChange(e.target.value)}
128
+ error={field.meta.isTouched && field.errors.length > 0 ? (field.errors.map((e) => e.message).filter(Boolean)[0] as string | undefined) : undefined}
122
129
  />
123
130
  )}
124
131
  </AppField>
125
- <AppField name="confirmPassword" form={form}>
132
+ <AppField name="confirmPassword" form={form} renderError={false}>
126
133
  {(field) => (
127
134
  <PasswordInput
128
135
  label={m.auth_register_confirmPasswordLabel()}
@@ -130,22 +137,26 @@ function RegisterPage() {
130
137
  value={field.value}
131
138
  onBlur={field.handleBlur}
132
139
  onChange={(e) => field.handleChange(e.target.value)}
140
+ error={field.meta.isTouched && field.errors.length > 0 ? (field.errors.map((e) => e.message).filter(Boolean)[0] as string | undefined) : undefined}
133
141
  />
134
142
  )}
135
143
  </AppField>
136
144
  </div>
137
- <AppField name="invitationCode" form={form}>
138
- {(field) => (
139
- <IconInput
140
- icon={QrCode}
141
- label={m.auth_register_invitationCodeLabel()}
142
- placeholder={m.auth_register_invitationCodePlaceholder()}
143
- value={field.value}
144
- onBlur={field.handleBlur}
145
- onChange={(e) => field.handleChange(e.target.value)}
146
- />
147
- )}
148
- </AppField>
145
+ {registrationMode === 'invite' && (
146
+ <AppField name="invitationCode" form={form} renderError={false}>
147
+ {(field) => (
148
+ <IconInput
149
+ icon={QrCode}
150
+ label={m.auth_register_invitationCodeLabel()}
151
+ placeholder={m.auth_register_invitationCodePlaceholder()}
152
+ value={field.value}
153
+ onBlur={field.handleBlur}
154
+ onChange={(e) => field.handleChange(e.target.value)}
155
+ error={field.meta.isTouched && field.errors.length > 0 ? (field.errors.map((e) => e.message).filter(Boolean)[0] as string | undefined) : undefined}
156
+ />
157
+ )}
158
+ </AppField>
159
+ )}
149
160
  <form.Subscribe
150
161
  selector={(state) => state.isSubmitting}
151
162
  children={(isSubmitting) => (
@@ -18,11 +18,18 @@
18
18
  import { createFileRoute } from '@tanstack/react-router'
19
19
  import { z } from 'zod'
20
20
  import { getAuthProviders } from '@/server/serverFns/auth/providers'
21
+ import { getRegistrationModeFn } from '@/server/serverFns/auth/registration-mode'
21
22
 
22
23
  export const Route = createFileRoute('/_login/register')({
23
24
  validateSearch: z.object({
24
25
  invite: z.string().optional(),
25
26
  email: z.string().optional(),
26
27
  }),
27
- loader: () => getAuthProviders(),
28
+ loader: async () => {
29
+ const [providers, { mode: registrationMode }] = await Promise.all([
30
+ getAuthProviders(),
31
+ getRegistrationModeFn(),
32
+ ])
33
+ return { ...providers, registrationMode }
34
+ },
28
35
  })
@@ -66,7 +66,7 @@ function ResetPasswordPage() {
66
66
  className="space-y-6"
67
67
  >
68
68
  <div className="grid grid-cols-2 gap-4">
69
- <AppField name="password" form={form}>
69
+ <AppField name="password" form={form} renderError={false}>
70
70
  {(field) => (
71
71
  <PasswordInput
72
72
  label={m.auth_resetPassword_newPasswordLabel()}
@@ -74,10 +74,11 @@ function ResetPasswordPage() {
74
74
  value={field.value}
75
75
  onBlur={field.handleBlur}
76
76
  onChange={(e) => field.handleChange(e.target.value)}
77
+ error={field.meta.isTouched && field.errors.length > 0 ? (field.errors.map((e) => e.message).filter(Boolean)[0] as string | undefined) : undefined}
77
78
  />
78
79
  )}
79
80
  </AppField>
80
- <AppField name="confirmPassword" form={form}>
81
+ <AppField name="confirmPassword" form={form} renderError={false}>
81
82
  {(field) => (
82
83
  <PasswordInput
83
84
  label={m.auth_resetPassword_confirmPasswordLabel()}
@@ -85,6 +86,7 @@ function ResetPasswordPage() {
85
86
  value={field.value}
86
87
  onBlur={field.handleBlur}
87
88
  onChange={(e) => field.handleChange(e.target.value)}
89
+ error={field.meta.isTouched && field.errors.length > 0 ? (field.errors.map((e) => e.message).filter(Boolean)[0] as string | undefined) : undefined}
88
90
  />
89
91
  )}
90
92
  </AppField>
@@ -18,29 +18,52 @@
18
18
  import { z } from "zod"
19
19
  import { m } from '@/paraglide/messages.js'
20
20
 
21
+ const passwordRule = z
22
+ .string()
23
+ .min(8, m.common_form_passwordMin8())
24
+ .max(32, m.common_form_passwordMax32())
25
+ .regex(/[a-zA-Z]/, m.common_form_passwordNeedLetter())
26
+ .regex(/[0-9]/, m.common_form_passwordNeedDigit())
27
+
28
+ const usernameRule = z
29
+ .string()
30
+ .regex(/^[a-zA-Z0-9_-]{3,20}$/, m.common_form_usernameCharset())
31
+
32
+ const invitationCodeRule = z
33
+ .string()
34
+ .refine((v) => v === "" || /^[A-Za-z0-9]{3,32}$/.test(v), {
35
+ message: m.common_form_invitationCodeFormat(),
36
+ })
37
+ .default("")
38
+
39
+ const emailRule = z
40
+ .string()
41
+ .transform((v) => v.toLowerCase().trim())
42
+ .pipe(z.string().email(m.common_form_invalidEmail()))
43
+
21
44
  export const loginSchema = z.object({
22
- email: z.string().email(m.common_form_invalidEmail()),
45
+ email: emailRule,
23
46
  password: z.string().min(1, m.common_form_passwordNotEmpty()),
24
47
  remember: z.boolean(),
25
48
  })
26
49
 
27
50
  export const registerSchema = z.object({
28
- username: z.string().min(3, m.common_form_usernameMin3()).max(20),
29
- email: z.string().email(m.common_form_invalidEmail()),
30
- password: z.string().min(8, m.common_form_passwordMin8()),
51
+ username: usernameRule,
52
+ email: emailRule,
53
+ password: passwordRule,
31
54
  confirmPassword: z.string(),
32
- invitationCode: z.string().min(1, m.common_form_invitationCodeRequired()),
55
+ invitationCode: invitationCodeRule,
33
56
  }).refine((data) => data.password === data.confirmPassword, {
34
57
  message: m.common_form_passwordMismatch(),
35
58
  path: ["confirmPassword"],
36
59
  })
37
60
 
38
61
  export const forgotPasswordSchema = z.object({
39
- email: z.string().email(m.common_form_invalidEmail()),
62
+ email: emailRule,
40
63
  })
41
64
 
42
65
  export const resetPasswordSchema = z.object({
43
- password: z.string().min(8, m.common_form_passwordMin8()),
66
+ password: passwordRule,
44
67
  confirmPassword: z.string(),
45
68
  }).refine((d) => d.password === d.confirmPassword, {
46
69
  message: m.common_form_passwordMismatch(),
@@ -0,0 +1,82 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { createLazyFileRoute, Link } from '@tanstack/react-router'
19
+ import { toast } from '@/components/ui/sonner'
20
+ import { RainbowButton } from '@/components/magicui/rainbow-button'
21
+ import { ArrowRight } from 'lucide-react'
22
+ import { AuthLayout, AuthLayoutLeft, AuthLayoutRight } from '@/components/auth/auth-layout'
23
+ import { AuthHeader } from '@/components/auth/auth-header'
24
+ import { AuthBrandPanel } from '@/components/auth/auth-brand-panel'
25
+ import { resendVerificationEmailFn } from '@/server/serverFns/auth/email-verification'
26
+ import { m } from '@/paraglide/messages.js'
27
+
28
+ export const Route = createLazyFileRoute('/_login/verify-email')({
29
+ component: VerifyEmailPage,
30
+ })
31
+
32
+ function VerifyEmailPage() {
33
+ const { verified } = Route.useRouteContext()
34
+
35
+ const handleResend = async () => {
36
+ try {
37
+ const r = await resendVerificationEmailFn({ data: {} })
38
+ toast.success(
39
+ r.alreadyVerified ? m.auth_verify_alreadyVerified() : m.auth_verify_resendSuccess(),
40
+ )
41
+ } catch (err) {
42
+ toast.error(err instanceof Error ? err.message : String(err))
43
+ }
44
+ }
45
+
46
+ return (
47
+ <AuthLayout>
48
+ <AuthLayoutLeft>
49
+ {verified ? (
50
+ <>
51
+ <AuthHeader title={m.auth_verify_success()} subtitle={m.auth_verify_successDesc()} />
52
+ <RainbowButton asChild className="w-full">
53
+ <Link to="/dashboard">
54
+ {m.auth_verify_goDashboard()}
55
+ <ArrowRight className="ml-2 h-4 w-4" />
56
+ </Link>
57
+ </RainbowButton>
58
+ </>
59
+ ) : (
60
+ <>
61
+ <AuthHeader title={m.auth_verify_invalid()} subtitle={m.auth_verify_invalidDesc()} />
62
+ <RainbowButton className="w-full" onClick={handleResend}>
63
+ {m.auth_verify_resend()}
64
+ </RainbowButton>
65
+ <div className="mt-4 flex items-center justify-center gap-2 text-sm text-muted-foreground">
66
+ <span>{m.auth_verify_notLogged()}</span>
67
+ <Link to="/login" className="text-primary hover:underline">
68
+ {m.auth_login_submit()}
69
+ </Link>
70
+ </div>
71
+ </>
72
+ )}
73
+ </AuthLayoutLeft>
74
+ <AuthLayoutRight>
75
+ <AuthBrandPanel
76
+ title="AllBlue"
77
+ subtitle={verified ? m.auth_verify_success() : m.auth_verify_invalid()}
78
+ />
79
+ </AuthLayoutRight>
80
+ </AuthLayout>
81
+ )
82
+ }
@@ -0,0 +1,28 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { createFileRoute } from '@tanstack/react-router'
19
+ import { confirmEmailVerificationFn } from '@/server/serverFns/auth/email-verification'
20
+
21
+ export const Route = createFileRoute('/_login/verify-email')({
22
+ validateSearch: (s: { token?: string }) => ({ token: s.token ?? '' }),
23
+ beforeLoad: async ({ search }) => {
24
+ if (!search.token) return { verified: false }
25
+ const r = await confirmEmailVerificationFn({ data: { token: search.token } })
26
+ return { verified: r.ok }
27
+ },
28
+ })
@@ -18,7 +18,7 @@
18
18
  import { cn } from "@/lib/utils"
19
19
  import { createLazyFileRoute, useNavigate } from '@tanstack/react-router'
20
20
  import { useState } from 'react'
21
- import { useQuery, keepPreviousData } from '@tanstack/react-query'
21
+ import { useQuery, keepPreviousData, useQueryClient } from '@tanstack/react-query'
22
22
  import { listComplianceFn } from '@/server/serverFns/knowledge-public/compliance/list'
23
23
  import { createComplianceFn } from '@/server/serverFns/knowledge-public/compliance/create'
24
24
  import { getEntityTagsFn } from '@/server/serverFns/knowledge-public/entity-tags'
@@ -50,6 +50,7 @@ export const Route = createLazyFileRoute('/_nav/knowledge/compliance/')({ compon
50
50
 
51
51
  function CompliancePage() {
52
52
  const navigate = useNavigate()
53
+ const qc = useQueryClient()
53
54
  const industryOptions = buildIndustryOptions()
54
55
  const industryGroups = [
55
56
  { key: 'industry', label: m.knowledge_compliance_index_industry(), options: industryOptions },
@@ -94,7 +95,9 @@ function CompliancePage() {
94
95
  search={search}
95
96
  onSearchChange={setSearch}
96
97
  onCreate={async () => {
97
- const r = await createComplianceFn({ data: { title: m.knowledge_compliance_index_untitled(), industry: '', tags: [] } })
98
+ const r = await createComplianceFn({ data: { title: '新合规', industry: '', tags: [] } })
99
+ qc.invalidateQueries({ queryKey: ['compliance'] })
100
+ await qc.refetchQueries({ queryKey: ['compliance'], type: 'all' })
98
101
  navigate({ to: '/knowledge/compliance/edit/$id', params: { id: r.id } })
99
102
  }}
100
103
  createLabel={m.knowledge_compliance_index_add()}
@@ -146,7 +146,7 @@ function KnowledgeListPage() {
146
146
  <Card key={kb.id} className={`group relative p-5 transition-colors hover:bg-accent/40 border rounded-lg ${isLaw ? 'overflow-hidden' : ''}`}>
147
147
  <Link
148
148
  to={kb.link}
149
- className="absolute inset-0"
149
+ className="absolute inset-0 z-10"
150
150
  aria-label={name}
151
151
  />
152
152
  {isLaw ? (
@@ -18,7 +18,7 @@
18
18
  import { cn } from "@/lib/utils"
19
19
  import { createLazyFileRoute, useNavigate } from '@tanstack/react-router'
20
20
  import { useState } from 'react'
21
- import { useQuery, keepPreviousData } from '@tanstack/react-query'
21
+ import { useQuery, keepPreviousData, useQueryClient } from '@tanstack/react-query'
22
22
  import { listLegalFn } from '@/server/serverFns/knowledge-public/legal/list'
23
23
  import { createLegalFn } from '@/server/serverFns/knowledge-public/legal/create'
24
24
  import { getEntityTagsFn } from '@/server/serverFns/knowledge-public/entity-tags'
@@ -44,6 +44,7 @@ export const Route = createLazyFileRoute('/_nav/knowledge/laws/')({ component: L
44
44
 
45
45
  function LawPage() {
46
46
  const navigate = useNavigate()
47
+ const qc = useQueryClient()
47
48
  const jurisdictionOptions = buildJurisdictionOptions()
48
49
  const jurisdictionGroups = [
49
50
  { key: 'jurisdiction', label: m.knowledge_laws_index_jurisdiction(), options: jurisdictionOptions },
@@ -79,7 +80,9 @@ function LawPage() {
79
80
  search={search}
80
81
  onSearchChange={setSearch}
81
82
  onCreate={async () => {
82
- const r = await createLegalFn({ data: { title: m.knowledge_laws_index_untitled(), jurisdiction: '', category: '', tags: [] } })
83
+ const r = await createLegalFn({ data: { title: '新法规', jurisdiction: '', category: '', tags: [] } })
84
+ qc.invalidateQueries({ queryKey: ['legal'] })
85
+ await qc.refetchQueries({ queryKey: ['legal'], type: 'all' })
83
86
  navigate({ to: '/knowledge/laws/edit/$id', params: { id: r.id } })
84
87
  }}
85
88
  createLabel={m.knowledge_laws_index_add()}