@allbluecn/web-app 0.22.4 → 0.23.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 (500) hide show
  1. package/package.json +4 -14
  2. package/src/__tests__/integration/login-email-acceptance.test.ts +16 -0
  3. package/src/__tests__/story-view.test.ts +33 -1
  4. package/src/components/agents/__tests__/agent-bindings-tab.test.tsx +221 -0
  5. package/src/components/agents/__tests__/agents-page.test.tsx +284 -0
  6. package/src/components/agents/agent-bindings-tab.tsx +59 -45
  7. package/src/components/agents/agent-form-dialog.tsx +3 -3
  8. package/src/components/agents/agent-selector.tsx +2 -3
  9. package/src/components/agents/agents-page.tsx +66 -31
  10. package/src/components/agents/slash-skill-menu.tsx +2 -3
  11. package/src/components/auth/__tests__/session-guard.test.tsx +74 -0
  12. package/src/components/auth/session-guard.tsx +8 -0
  13. package/src/components/billing/login-prompt-dialog.tsx +1 -1
  14. package/src/components/billing/subscription-provider.tsx +2 -16
  15. package/src/components/bug/__tests__/peek-modes.test.tsx +8 -0
  16. package/src/components/bug/__tests__/subgroup-header.test.tsx +202 -40
  17. package/src/components/bug/peek-panel.tsx +5 -3
  18. package/src/components/bug/views/bug-list/group.tsx +12 -2
  19. package/src/components/bug/views/bug-list/index.tsx +94 -67
  20. package/src/components/command/command-palette.tsx +10 -9
  21. package/src/components/design/canvas/document/frame-custom-presets.ts +9 -1
  22. package/src/components/design/canvas/panels/toolbar.tsx +21 -22
  23. package/src/components/design/canvas/react/__tests__/canvas-context-load-echo.test.tsx +199 -0
  24. package/src/components/design/canvas/react/__tests__/hooks.test.tsx +17 -3
  25. package/src/components/design/canvas/react/canvas-context.tsx +47 -26
  26. package/src/components/design/canvas/react/hooks.ts +39 -34
  27. package/src/components/design/editor/layout/__tests__/collab-param-wiring.test.tsx +17 -0
  28. package/src/components/design/editor/layout/__tests__/context-menu.test.tsx +17 -1
  29. package/src/components/design/editor/layout/__tests__/no-page-save-toast.test.tsx +17 -0
  30. package/src/components/design/editor/layout/__tests__/persistence-restore.test.tsx +17 -1
  31. package/src/components/design/editor/layout/__tests__/ssr-localstorage-smoke.test.tsx +17 -1
  32. package/src/components/design/editor/layout/design-editor-layout.tsx +13 -2
  33. package/src/components/design/editor/layout/hooks/use-design-auto-save.ts +3 -19
  34. package/src/components/devtools/devtools-panel.tsx +56 -0
  35. package/src/components/forms/__tests__/create-app-form.test.tsx +131 -0
  36. package/src/components/forms/app-field-error.tsx +21 -1
  37. package/src/components/forms/app-field.tsx +8 -8
  38. package/src/components/forms/app-submit.tsx +12 -9
  39. package/src/components/forms/create-app-form.tsx +124 -0
  40. package/src/components/forms/index.ts +2 -3
  41. package/src/components/harness/__tests__/gallery-behavior.test.tsx +37 -1
  42. package/src/components/harness/__tests__/harness-workbench-first-send.test.tsx +4 -1
  43. package/src/components/harness/__tests__/message-actions.test.tsx +7 -6
  44. package/src/components/harness/__tests__/use-video-generation.test.tsx +165 -0
  45. package/src/components/harness/ai-chat-history/index.tsx +5 -3
  46. package/src/components/harness/gallery/gallery-card.tsx +57 -35
  47. package/src/components/harness/gallery/gallery-grid.tsx +32 -15
  48. package/src/components/harness/gallery/gallery-types.ts +19 -0
  49. package/src/components/harness/gallery/gallery-video-card.tsx +54 -0
  50. package/src/components/harness/gallery/types.ts +2 -0
  51. package/src/components/harness/gallery/use-video-generation.ts +95 -0
  52. package/src/components/harness/harness-workbench-page.tsx +2 -3
  53. package/src/components/harness/message-actions.tsx +5 -3
  54. package/src/components/hotkeys/shortcuts-panel.tsx +28 -13
  55. package/src/components/iteration/__tests__/burndown-chart.test.tsx +118 -0
  56. package/src/components/iteration/burndown-chart.tsx +54 -23
  57. package/src/components/knowledge/editor/__tests__/knowledge-doc-editor-collab.test.tsx +96 -0
  58. package/src/components/knowledge/editor/knowledge-doc-editor.tsx +56 -1
  59. package/src/components/knowledge/editor/mindmap-doc-editor.tsx +12 -0
  60. package/src/components/knowledge/editor/use-auto-save-channels.ts +3 -1
  61. package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +0 -4
  62. package/src/components/layout/sidebar-03/nav-knowledge.tsx +11 -11
  63. package/src/components/layout/sidebar-03/nav-main.tsx +12 -15
  64. package/src/components/layout/sidebar-03/nav-notifications.tsx +5 -8
  65. package/src/components/layout/sidebar-03/nav-workspace.tsx +11 -9
  66. package/src/components/notifications/__tests__/settings-notifications.test.tsx +158 -0
  67. package/src/components/notifications/settings-notifications.tsx +37 -13
  68. package/src/components/notifications/team-notifications.tsx +10 -14
  69. package/src/components/settings/personal-team/account/__tests__/account-client.test.tsx +170 -0
  70. package/src/components/settings/personal-team/account/account-client.tsx +44 -7
  71. package/src/components/settings/personal-team/team/team-client.tsx +17 -19
  72. package/src/components/shared-kit/list-kit/__tests__/frozen-header.test.ts +210 -41
  73. package/src/components/shared-kit/list-kit/frozen-header.ts +134 -21
  74. package/src/components/shared-kit/list-kit/kanban/frozen-header.ts +20 -27
  75. package/src/components/story/__tests__/peek-panel-patch-sync.test.tsx +8 -0
  76. package/src/components/story/__tests__/story-list-table.test.tsx +214 -0
  77. package/src/components/story/columns.tsx +11 -2
  78. package/src/components/story/peek-panel.tsx +4 -2
  79. package/src/components/story/story-list-table.tsx +16 -1
  80. package/src/components/story/views/story-list/__tests__/frozen-headers.test.tsx +186 -0
  81. package/src/components/story/views/story-list/group.tsx +12 -1
  82. package/src/components/story/views/story-list/index.tsx +101 -68
  83. package/src/components/testcase/__tests__/case-row.test.tsx +134 -2
  84. package/src/components/testcase/__tests__/peek-panel.test.tsx +8 -0
  85. package/src/components/testcase/__tests__/step-editor.test.tsx +106 -0
  86. package/src/components/testcase/list/grouped-list.tsx +50 -22
  87. package/src/components/testcase/peek-panel.tsx +7 -4
  88. package/src/components/testcase/step-editor.tsx +196 -150
  89. package/src/components/tiptap/notion-like/editor-extensions.ts +0 -3
  90. package/src/components/tiptap/notion-like/notion-like-editor-header.tsx +1 -31
  91. package/src/components/tiptap/notion-like/notion-like-editor.tsx +32 -1
  92. package/src/components/workspace/__tests__/dialogs/create-workspace-dialog.test.tsx +2 -28
  93. package/src/components/workspace/__tests__/dialogs/edit-workspace-dialog.test.tsx +2 -28
  94. package/src/components/workspace/dialogs/create-workspace-dialog.tsx +45 -43
  95. package/src/components/workspace/dialogs/edit-workspace-dialog.tsx +38 -36
  96. package/src/design-engine/engine/engine-decor.ts +2 -0
  97. package/src/design-engine/engine/engine-path.ts +2 -0
  98. package/src/hooks/__tests__/use-unsaved-guard.test.tsx +180 -0
  99. package/src/hooks/use-notifications.ts +9 -16
  100. package/src/hooks/use-unsaved-guard.tsx +87 -0
  101. package/src/lib/__tests__/pacer.test.ts +33 -96
  102. package/src/lib/__tests__/query-defaults.test.ts +169 -0
  103. package/src/lib/__tests__/query-global-error.test.ts +256 -0
  104. package/src/lib/anyhub/anyhub-store.ts +2 -2
  105. package/src/lib/changelog/sdk-versions.ts +2 -10
  106. package/src/lib/collab/presence-store.ts +5 -5
  107. package/src/lib/collab/use-presence-list-refresh.ts +2 -2
  108. package/src/lib/collab/use-share-presence.ts +2 -2
  109. package/src/lib/collections/query-keys.ts +7 -2
  110. package/src/lib/commands/__tests__/command-palette-state.test.ts +12 -14
  111. package/src/lib/commands/__tests__/registry.test.ts +6 -3
  112. package/src/lib/commands/command-palette-state.ts +8 -31
  113. package/src/lib/hotkeys/__tests__/registry.test.ts +71 -18
  114. package/src/lib/hotkeys/registry.ts +188 -85
  115. package/src/lib/hotkeys/types.ts +1 -13
  116. package/src/lib/pacer.ts +1 -79
  117. package/src/lib/queries/README.md +67 -0
  118. package/src/lib/queries/__tests__/agents-queries.test.ts +101 -0
  119. package/src/lib/queries/__tests__/compliance-queries.test.ts +136 -0
  120. package/src/lib/queries/__tests__/notifications-queries.test.ts +175 -0
  121. package/src/lib/queries/__tests__/team-queries.test.ts +87 -0
  122. package/src/lib/queries/agents.ts +40 -0
  123. package/src/lib/queries/compliance.ts +69 -0
  124. package/src/lib/queries/notifications.ts +63 -0
  125. package/src/lib/queries/team.ts +39 -0
  126. package/src/lib/query-defaults.ts +8 -0
  127. package/src/lib/query-global-error.ts +128 -0
  128. package/src/lib/remote-auth-client.ts +16 -0
  129. package/src/paraglide/README.md +1 -1
  130. package/src/paraglide/messages/_index.d.ts +11 -0
  131. package/src/paraglide/messages/_index.js +5659 -88285
  132. package/src/paraglide/messages/common_error_generic.d.ts +16 -0
  133. package/src/paraglide/messages/common_error_generic.js +29 -0
  134. package/src/paraglide/messages/common_unsaved_body.d.ts +16 -0
  135. package/src/paraglide/messages/common_unsaved_body.js +29 -0
  136. package/src/paraglide/messages/common_unsaved_leave.d.ts +16 -0
  137. package/src/paraglide/messages/common_unsaved_leave.js +29 -0
  138. package/src/paraglide/messages/common_unsaved_stay.d.ts +16 -0
  139. package/src/paraglide/messages/common_unsaved_stay.js +29 -0
  140. package/src/paraglide/messages/common_unsaved_title.d.ts +16 -0
  141. package/src/paraglide/messages/common_unsaved_title.js +29 -0
  142. package/src/paraglide/messages/gallery_videofailed1.d.ts +17 -0
  143. package/src/paraglide/messages/gallery_videofailed1.js +30 -0
  144. package/src/paraglide/messages/gallery_videogenerating1.d.ts +17 -0
  145. package/src/paraglide/messages/gallery_videogenerating1.js +30 -0
  146. package/src/paraglide/messages/gallery_videomissingprompt2.d.ts +17 -0
  147. package/src/paraglide/messages/gallery_videomissingprompt2.js +30 -0
  148. package/src/paraglide/messages/gallery_videoprogress1.d.ts +19 -0
  149. package/src/paraglide/messages/gallery_videoprogress1.js +30 -0
  150. package/src/paraglide/messages/gallery_videostop1.d.ts +17 -0
  151. package/src/paraglide/messages/gallery_videostop1.js +30 -0
  152. package/src/paraglide/messages/story_table_resizecolumn1.d.ts +17 -0
  153. package/src/paraglide/messages/story_table_resizecolumn1.js +30 -0
  154. package/src/providers/index.tsx +5 -50
  155. package/src/routeTree.gen.ts +21 -0
  156. package/src/router.tsx +21 -8
  157. package/src/routes/__root.tsx +2 -1
  158. package/src/routes/__tests__/-edition-routing.test.tsx +6 -1
  159. package/src/routes/_login/forgot-password/route.lazy.tsx +7 -6
  160. package/src/routes/_login/login/route.lazy.tsx +10 -9
  161. package/src/routes/_login/register/route.lazy.tsx +19 -18
  162. package/src/routes/_login/reset-password/route.lazy.tsx +10 -9
  163. package/src/routes/_nav/knowledge/compliance/$id.lazy.tsx +9 -12
  164. package/src/routes/_nav/knowledge/compliance/index.lazy.tsx +16 -13
  165. package/src/routes/_nav/knowledge/laws/$id.lazy.tsx +1 -1
  166. package/src/routes/_nav/notifications/route.tsx +6 -16
  167. package/src/routes/_nav/settings/-settings-tabs.tsx +9 -12
  168. package/src/routes/api/generate/__tests__/media.test.ts +378 -0
  169. package/src/routes/api/generate/media.ts +179 -0
  170. package/src/server/__tests__/agent-client-graph.test.ts +0 -1
  171. package/src/server/__tests__/agent-crud.test.ts +37 -4
  172. package/src/server/__tests__/bug-groups-two-level.test.ts +34 -1
  173. package/src/server/__tests__/chat-conversations.test.ts +37 -4
  174. package/src/server/__tests__/chat-list-messages.test.ts +40 -7
  175. package/src/server/__tests__/csv-export.test.ts +36 -3
  176. package/src/server/__tests__/desktop-bootstrap.test.ts +189 -0
  177. package/src/server/__tests__/remote-auth.test.ts +72 -0
  178. package/src/server/__tests__/skill-crud.test.ts +37 -4
  179. package/src/server/__tests__/sso-domain.test.ts +1 -1
  180. package/src/server/__tests__/sso-rate-limit-singleton.test.ts +123 -0
  181. package/src/server/__tests__/task-serverfn-crud.test.ts +35 -2
  182. package/src/server/__tests__/task-serverfn-queries.test.ts +35 -2
  183. package/src/server/__tests__/testcase-board.test.ts +38 -5
  184. package/src/server/__tests__/testcase-bulk.test.ts +38 -5
  185. package/src/server/__tests__/testcase-list-scope.test.ts +38 -5
  186. package/src/server/ai-gateway/__tests__/ai-output-guard.test.ts +126 -2
  187. package/src/server/ai-gateway/__tests__/chat-stream-builder-code-mode.test.ts +4 -0
  188. package/src/server/ai-gateway/__tests__/chat-stream-builder-memory.test.ts +4 -0
  189. package/src/server/ai-gateway/__tests__/chat-stream-builder-sandbox.test.ts +4 -0
  190. package/src/server/ai-gateway/__tests__/chat-stream-builder-skills.test.ts +4 -0
  191. package/src/server/ai-gateway/__tests__/pgvector-memory-adapter.test.ts +30 -0
  192. package/src/server/ai-gateway/__tests__/pgvector-memory-conformance.test.ts +92 -0
  193. package/src/server/ai-gateway/__tests__/source-mode.test.ts +49 -8
  194. package/src/server/ai-gateway/ai-output-guard.ts +50 -2
  195. package/src/server/ai-gateway/chat-stream-builder.ts +3 -0
  196. package/src/server/ai-gateway/media/index.ts +36 -29
  197. package/src/server/ai-gateway/media/trim-input.ts +52 -0
  198. package/src/server/ai-gateway/memory/pgvector-adapter.ts +27 -13
  199. package/src/server/ai-gateway/sandbox/__tests__/harness-sandbox.test.ts +252 -41
  200. package/src/server/ai-gateway/sandbox/harness-sandbox.ts +6 -67
  201. package/src/server/ai-gateway/source-mode.ts +24 -8
  202. package/src/server/desktop-bootstrap.ts +74 -0
  203. package/src/server/lib/__tests__/rate-limit.test.ts +0 -1
  204. package/src/server/lib/rate-limit.ts +0 -2
  205. package/src/server/middlewares/__tests__/auth-context.test.ts +156 -0
  206. package/src/server/middlewares/auth-context.ts +62 -0
  207. package/src/server/remote-auth.ts +58 -31
  208. package/src/server/serverFns/__tests__/ai-hosted-models.test.ts +47 -15
  209. package/src/server/serverFns/__tests__/ai-source-mode.test.ts +45 -15
  210. package/src/server/serverFns/__tests__/billing.test.ts +37 -2
  211. package/src/server/serverFns/__tests__/collab-snapshot.test.ts +41 -14
  212. package/src/server/serverFns/__tests__/collab.test.ts +41 -14
  213. package/src/server/serverFns/__tests__/compliance.test.ts +33 -1
  214. package/src/server/serverFns/__tests__/email-verification.test.ts +35 -1
  215. package/src/server/serverFns/__tests__/knowledge-breadcrumb.test.ts +46 -6
  216. package/src/server/serverFns/__tests__/knowledge-delete.test.ts +42 -14
  217. package/src/server/serverFns/__tests__/knowledge-kb-projects.test.ts +46 -6
  218. package/src/server/serverFns/__tests__/knowledge-public-compliance.test.ts +36 -2
  219. package/src/server/serverFns/__tests__/knowledge-readable-kb.test.ts +42 -10
  220. package/src/server/serverFns/__tests__/knowledge-search.test.ts +45 -9
  221. package/src/server/serverFns/__tests__/legal-code-canonical.test.ts +36 -2
  222. package/src/server/serverFns/__tests__/link-preview.test.ts +38 -4
  223. package/src/server/serverFns/__tests__/notifications-list.test.ts +36 -3
  224. package/src/server/serverFns/__tests__/ownership-merge.test.ts +38 -4
  225. package/src/server/serverFns/__tests__/project-delete-transaction.test.ts +39 -2
  226. package/src/server/serverFns/__tests__/project-kb.test.ts +34 -1
  227. package/src/server/serverFns/__tests__/project-member-idor.test.ts +34 -1
  228. package/src/server/serverFns/__tests__/project-team-assign.test.ts +34 -1
  229. package/src/server/serverFns/__tests__/read-write-merge.test.ts +38 -4
  230. package/src/server/serverFns/__tests__/story-comments.test.ts +38 -4
  231. package/src/server/serverFns/__tests__/story-crud-archive-detail.test.ts +38 -4
  232. package/src/server/serverFns/__tests__/story-crud-labels-users.test.ts +37 -3
  233. package/src/server/serverFns/__tests__/story-crud.test.ts +36 -3
  234. package/src/server/serverFns/__tests__/story-links.test.ts +38 -4
  235. package/src/server/serverFns/__tests__/story-reactions.test.ts +38 -4
  236. package/src/server/serverFns/__tests__/story-tree.test.ts +38 -4
  237. package/src/server/serverFns/__tests__/team-accept.test.ts +34 -1
  238. package/src/server/serverFns/__tests__/team-invitation.test.ts +34 -1
  239. package/src/server/serverFns/__tests__/team-join-request.test.ts +34 -1
  240. package/src/server/serverFns/__tests__/team-members.test.ts +34 -1
  241. package/src/server/serverFns/__tests__/team-ownership-merge.test.ts +34 -1
  242. package/src/server/serverFns/__tests__/update-bug-guard.test.ts +37 -3
  243. package/src/server/serverFns/__tests__/update-document-guard.test.ts +38 -4
  244. package/src/server/serverFns/__tests__/update-page-guard.test.ts +40 -4
  245. package/src/server/serverFns/__tests__/update-seats.test.ts +33 -1
  246. package/src/server/serverFns/__tests__/update-story-guard.test.ts +36 -3
  247. package/src/server/serverFns/__tests__/update-testcase-guard.test.ts +37 -3
  248. package/src/server/serverFns/__tests__/workspace-kb-idor.test.ts +36 -2
  249. package/src/server/serverFns/__tests__/workspace-kb.test.ts +36 -2
  250. package/src/server/serverFns/__tests__/workspace.test.ts +34 -1
  251. package/src/server/serverFns/activity.ts +10 -14
  252. package/src/server/serverFns/agents/agent-crud.ts +16 -15
  253. package/src/server/serverFns/agents/bindings.ts +12 -13
  254. package/src/server/serverFns/agents/skill-crud.ts +16 -15
  255. package/src/server/serverFns/ai/code-mode-readiness.ts +3 -4
  256. package/src/server/serverFns/ai/search-capability.ts +3 -4
  257. package/src/server/serverFns/anyhub/app.ts +10 -14
  258. package/src/server/serverFns/anyhub/index.ts +7 -10
  259. package/src/server/serverFns/anyhub/install.ts +4 -4
  260. package/src/server/serverFns/anyhub/permissions.ts +10 -14
  261. package/src/server/serverFns/anyhub/proxy.ts +4 -6
  262. package/src/server/serverFns/auth/email-verification.ts +4 -5
  263. package/src/server/serverFns/auth/session.ts +9 -0
  264. package/src/server/serverFns/billing.ts +18 -28
  265. package/src/server/serverFns/blob-upload.ts +6 -9
  266. package/src/server/serverFns/bug/__tests__/bug-access-tokens.test.ts +34 -1
  267. package/src/server/serverFns/bug/__tests__/bug-activities.test.ts +34 -1
  268. package/src/server/serverFns/bug/__tests__/bug-comments.test.ts +34 -1
  269. package/src/server/serverFns/bug/__tests__/bug-crud.test.ts +34 -1
  270. package/src/server/serverFns/bug/__tests__/bug-fix.test.ts +34 -1
  271. package/src/server/serverFns/bug/__tests__/bug-groups.test.ts +34 -1
  272. package/src/server/serverFns/bug/__tests__/bug-reactions.test.ts +34 -1
  273. package/src/server/serverFns/bug/__tests__/bug-reorder.test.ts +39 -5
  274. package/src/server/serverFns/bug/__tests__/bug-repro.test.ts +34 -1
  275. package/src/server/serverFns/bug/__tests__/bug-story.test.ts +34 -1
  276. package/src/server/serverFns/bug/__tests__/bug-webhooks.test.ts +34 -1
  277. package/src/server/serverFns/bug/bug-access-tokens.ts +16 -22
  278. package/src/server/serverFns/bug/bug-activities.ts +4 -6
  279. package/src/server/serverFns/bug/bug-collect-tokens.ts +13 -18
  280. package/src/server/serverFns/bug/bug-comments.ts +13 -18
  281. package/src/server/serverFns/bug/bug-crud/detail.ts +10 -14
  282. package/src/server/serverFns/bug/bug-crud.ts +28 -38
  283. package/src/server/serverFns/bug/bug-display-options.ts +8 -11
  284. package/src/server/serverFns/bug/bug-export.ts +5 -7
  285. package/src/server/serverFns/bug/bug-fix.ts +10 -14
  286. package/src/server/serverFns/bug/bug-groups.ts +10 -14
  287. package/src/server/serverFns/bug/bug-reactions.ts +10 -14
  288. package/src/server/serverFns/bug/bug-reorder.ts +4 -6
  289. package/src/server/serverFns/bug/bug-repro.ts +8 -11
  290. package/src/server/serverFns/bug/bug-story-bulk.ts +13 -18
  291. package/src/server/serverFns/bug/bug-story.ts +10 -14
  292. package/src/server/serverFns/bug/bug-views.ts +12 -18
  293. package/src/server/serverFns/bug/bug-webhooks.ts +13 -18
  294. package/src/server/serverFns/bug/test-bug-confirm.ts +7 -10
  295. package/src/server/serverFns/changelog/__tests__/fetch.test.ts +40 -1
  296. package/src/server/serverFns/changelog/fetch.ts +4 -6
  297. package/src/server/serverFns/chat/conversations.ts +17 -28
  298. package/src/server/serverFns/chat/list-messages.ts +4 -7
  299. package/src/server/serverFns/collab-presence.ts +11 -13
  300. package/src/server/serverFns/collab-snapshot.ts +6 -7
  301. package/src/server/serverFns/collab.ts +7 -7
  302. package/src/server/serverFns/compliance.ts +8 -8
  303. package/src/server/serverFns/design/__tests__/comments.test.ts +54 -10
  304. package/src/server/serverFns/design/__tests__/designs.test.ts +39 -16
  305. package/src/server/serverFns/design/__tests__/page-versions.test.ts +52 -14
  306. package/src/server/serverFns/design/__tests__/shares-password.test.ts +44 -14
  307. package/src/server/serverFns/design/__tests__/shares.test.ts +44 -14
  308. package/src/server/serverFns/design/__tests__/versions.test.ts +44 -14
  309. package/src/server/serverFns/design/ai/__tests__/agent-conversations.test.ts +50 -21
  310. package/src/server/serverFns/design/ai/__tests__/providers.test.ts +42 -8
  311. package/src/server/serverFns/design/ai/agent-conversations.ts +19 -26
  312. package/src/server/serverFns/design/ai/fill-annotation.ts +5 -7
  313. package/src/server/serverFns/design/ai/generate-prototype.ts +5 -7
  314. package/src/server/serverFns/design/ai/hosted-models.ts +4 -5
  315. package/src/server/serverFns/design/ai/providers.ts +30 -39
  316. package/src/server/serverFns/design/ai/source-mode.ts +10 -12
  317. package/src/server/serverFns/design/comments.ts +14 -13
  318. package/src/server/serverFns/design/designs.ts +13 -19
  319. package/src/server/serverFns/design/page-versions.ts +17 -18
  320. package/src/server/serverFns/design/pages.ts +17 -18
  321. package/src/server/serverFns/design/reviews.ts +8 -9
  322. package/src/server/serverFns/design/shares-password.ts +13 -19
  323. package/src/server/serverFns/design/shares.ts +22 -31
  324. package/src/server/serverFns/design/versions.ts +8 -9
  325. package/src/server/serverFns/harness/__tests__/attach-prototype.spec.ts +52 -2
  326. package/src/server/serverFns/harness/__tests__/attach-to-kb.spec.ts +52 -2
  327. package/src/server/serverFns/harness/__tests__/attach-to-story-ssrf.spec.ts +52 -2
  328. package/src/server/serverFns/harness/__tests__/attach-to-story.spec.ts +52 -2
  329. package/src/server/serverFns/harness/__tests__/cancel-run.test.ts +40 -15
  330. package/src/server/serverFns/harness/__tests__/extract-attachment.spec.ts +52 -2
  331. package/src/server/serverFns/harness/__tests__/gallery.spec.ts +76 -12
  332. package/src/server/serverFns/harness/__tests__/github-workspace.spec.ts +38 -6
  333. package/src/server/serverFns/harness/__tests__/media-prototype.spec.ts +50 -6
  334. package/src/server/serverFns/harness/__tests__/media.spec.ts +55 -11
  335. package/src/server/serverFns/harness/__tests__/sandbox-policy.test.ts +41 -16
  336. package/src/server/serverFns/harness/__tests__/workspace-files.test.ts +41 -16
  337. package/src/server/serverFns/harness/attach-prototype.ts +4 -4
  338. package/src/server/serverFns/harness/attach-to-kb.ts +4 -4
  339. package/src/server/serverFns/harness/attach-to-story.ts +4 -4
  340. package/src/server/serverFns/harness/cancel-run.ts +4 -6
  341. package/src/server/serverFns/harness/extract-attachment.ts +3 -4
  342. package/src/server/serverFns/harness/gallery.ts +19 -23
  343. package/src/server/serverFns/harness/github-workspace.ts +14 -21
  344. package/src/server/serverFns/harness/media.ts +18 -117
  345. package/src/server/serverFns/harness/sandbox-policy.ts +11 -10
  346. package/src/server/serverFns/harness/search-mention-candidates.ts +4 -6
  347. package/src/server/serverFns/harness/workspace-files.ts +19 -26
  348. package/src/server/serverFns/iteration/__tests__/iteration-auto-schedule.test.ts +35 -3
  349. package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +35 -3
  350. package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +35 -3
  351. package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +35 -3
  352. package/src/server/serverFns/iteration/iteration-auto-schedule.ts +7 -10
  353. package/src/server/serverFns/iteration/iteration-burndown.ts +4 -6
  354. package/src/server/serverFns/iteration/iteration-crud.ts +26 -35
  355. package/src/server/serverFns/iteration/iteration-stories.ts +10 -14
  356. package/src/server/serverFns/iteration/iteration-transfer.ts +4 -6
  357. package/src/server/serverFns/knowledge/public/bookmarks/categories.ts +11 -15
  358. package/src/server/serverFns/knowledge/public/bookmarks/create.ts +4 -6
  359. package/src/server/serverFns/knowledge/public/bookmarks/delete.ts +5 -7
  360. package/src/server/serverFns/knowledge/public/bookmarks/detail.ts +4 -6
  361. package/src/server/serverFns/knowledge/public/bookmarks/favorite.ts +4 -6
  362. package/src/server/serverFns/knowledge/public/bookmarks/list.ts +4 -6
  363. package/src/server/serverFns/knowledge/public/bookmarks/refresh-favicon.ts +4 -6
  364. package/src/server/serverFns/knowledge/public/bookmarks/share.ts +4 -6
  365. package/src/server/serverFns/knowledge/public/bookmarks/sync-github.ts +5 -7
  366. package/src/server/serverFns/knowledge/public/bookmarks/unshare.ts +4 -6
  367. package/src/server/serverFns/knowledge/public/bookmarks/update.ts +6 -8
  368. package/src/server/serverFns/knowledge/public/compliance/create.ts +4 -6
  369. package/src/server/serverFns/knowledge/public/compliance/delete.ts +5 -7
  370. package/src/server/serverFns/knowledge/public/compliance/detail.ts +4 -6
  371. package/src/server/serverFns/knowledge/public/compliance/list.ts +4 -6
  372. package/src/server/serverFns/knowledge/public/compliance/share.ts +4 -6
  373. package/src/server/serverFns/knowledge/public/compliance/unshare.ts +4 -6
  374. package/src/server/serverFns/knowledge/public/compliance/update.ts +5 -7
  375. package/src/server/serverFns/knowledge/public/dictionary/create.ts +4 -6
  376. package/src/server/serverFns/knowledge/public/dictionary/delete.ts +5 -7
  377. package/src/server/serverFns/knowledge/public/dictionary/detail.ts +4 -6
  378. package/src/server/serverFns/knowledge/public/dictionary/list.ts +4 -6
  379. package/src/server/serverFns/knowledge/public/dictionary/share.ts +4 -6
  380. package/src/server/serverFns/knowledge/public/dictionary/unshare.ts +4 -6
  381. package/src/server/serverFns/knowledge/public/dictionary/update.ts +5 -7
  382. package/src/server/serverFns/knowledge/public/entity-tags.ts +4 -6
  383. package/src/server/serverFns/knowledge/public/favorite.ts +7 -10
  384. package/src/server/serverFns/knowledge/public/legal/create.ts +4 -6
  385. package/src/server/serverFns/knowledge/public/legal/delete.ts +5 -7
  386. package/src/server/serverFns/knowledge/public/legal/detail.ts +4 -6
  387. package/src/server/serverFns/knowledge/public/legal/list.ts +4 -6
  388. package/src/server/serverFns/knowledge/public/legal/share.ts +4 -6
  389. package/src/server/serverFns/knowledge/public/legal/unshare.ts +4 -6
  390. package/src/server/serverFns/knowledge/public/legal/update.ts +5 -7
  391. package/src/server/serverFns/knowledge-index.ts +12 -16
  392. package/src/server/serverFns/knowledge.ts +50 -67
  393. package/src/server/serverFns/notifications.ts +25 -34
  394. package/src/server/serverFns/plugins/harness-plugins.ts +16 -23
  395. package/src/server/serverFns/project/module-overview.ts +7 -10
  396. package/src/server/serverFns/project.ts +48 -68
  397. package/src/server/serverFns/references/create.ts +4 -6
  398. package/src/server/serverFns/references/resolve.ts +4 -6
  399. package/src/server/serverFns/references/search.ts +4 -6
  400. package/src/server/serverFns/share/design/[designId]/events.ts +4 -6
  401. package/src/server/serverFns/sso.ts +1 -1
  402. package/src/server/serverFns/story/__tests__/bulk.test.ts +34 -1
  403. package/src/server/serverFns/story/__tests__/story-assoc.test.ts +34 -1
  404. package/src/server/serverFns/story/__tests__/story-attachments.test.ts +34 -1
  405. package/src/server/serverFns/story/__tests__/story-crud-date-key.test.ts +42 -17
  406. package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +34 -1
  407. package/src/server/serverFns/story/__tests__/story-gantt-deps.test.ts +34 -1
  408. package/src/server/serverFns/story/__tests__/story-groups.test.ts +34 -1
  409. package/src/server/serverFns/story/__tests__/story-read-path-authz.test.ts +40 -4
  410. package/src/server/serverFns/story/__tests__/story-search.test.ts +34 -1
  411. package/src/server/serverFns/story/__tests__/story-transfer.test.ts +34 -1
  412. package/src/server/serverFns/story/link-preview.ts +5 -7
  413. package/src/server/serverFns/story/story-assoc.ts +28 -38
  414. package/src/server/serverFns/story/story-attachments.ts +7 -10
  415. package/src/server/serverFns/story/story-children.ts +4 -6
  416. package/src/server/serverFns/story/story-comments.ts +13 -18
  417. package/src/server/serverFns/story/story-crud/activities.ts +4 -6
  418. package/src/server/serverFns/story/story-crud/archive.ts +7 -10
  419. package/src/server/serverFns/story/story-crud/bulk.ts +10 -14
  420. package/src/server/serverFns/story/story-crud/detail.ts +7 -10
  421. package/src/server/serverFns/story/story-crud/label-suggest.ts +4 -6
  422. package/src/server/serverFns/story/story-crud/labels.ts +6 -9
  423. package/src/server/serverFns/story/story-crud/user-brief.ts +4 -6
  424. package/src/server/serverFns/story/story-crud.ts +13 -18
  425. package/src/server/serverFns/story/story-dependencies.ts +16 -22
  426. package/src/server/serverFns/story/story-groups.ts +10 -14
  427. package/src/server/serverFns/story/story-links.ts +13 -18
  428. package/src/server/serverFns/story/story-reactions.ts +13 -18
  429. package/src/server/serverFns/story/story-search.ts +7 -10
  430. package/src/server/serverFns/story/story-transfer.ts +10 -14
  431. package/src/server/serverFns/story/story-tree.ts +7 -10
  432. package/src/server/serverFns/story-display-options.ts +7 -8
  433. package/src/server/serverFns/story-view.ts +12 -18
  434. package/src/server/serverFns/tasks/task-crud.ts +13 -17
  435. package/src/server/serverFns/tasks/task-queries.ts +12 -18
  436. package/src/server/serverFns/team.ts +46 -66
  437. package/src/server/serverFns/testcase/__tests__/automation.test.ts +38 -5
  438. package/src/server/serverFns/testcase/__tests__/evidence-upload.test.ts +37 -3
  439. package/src/server/serverFns/testcase/__tests__/generation.test.ts +38 -5
  440. package/src/server/serverFns/testcase/__tests__/plans.test.ts +36 -3
  441. package/src/server/serverFns/testcase/__tests__/regression.test.ts +36 -3
  442. package/src/server/serverFns/testcase/__tests__/run-cases.test.ts +36 -3
  443. package/src/server/serverFns/testcase/__tests__/runs.test.ts +36 -3
  444. package/src/server/serverFns/testcase/__tests__/stats.test.ts +36 -3
  445. package/src/server/serverFns/testcase/__tests__/testcase-board.test.ts +39 -5
  446. package/src/server/serverFns/testcase/__tests__/testcase-crud.test.ts +36 -3
  447. package/src/server/serverFns/testcase/__tests__/testcase-folders.test.ts +36 -3
  448. package/src/server/serverFns/testcase/__tests__/testcase-groups.test.ts +38 -5
  449. package/src/server/serverFns/testcase/automation.ts +13 -17
  450. package/src/server/serverFns/testcase/bulk.ts +10 -14
  451. package/src/server/serverFns/testcase/evidence-upload.ts +5 -7
  452. package/src/server/serverFns/testcase/generation.ts +10 -13
  453. package/src/server/serverFns/testcase/plans.ts +23 -31
  454. package/src/server/serverFns/testcase/regression.ts +7 -10
  455. package/src/server/serverFns/testcase/run-cases.ts +13 -18
  456. package/src/server/serverFns/testcase/runs.ts +13 -18
  457. package/src/server/serverFns/testcase/stats.ts +13 -18
  458. package/src/server/serverFns/testcase/testcase-activities.ts +4 -6
  459. package/src/server/serverFns/testcase/testcase-board.ts +4 -6
  460. package/src/server/serverFns/testcase/testcase-crud.ts +20 -27
  461. package/src/server/serverFns/testcase/testcase-display-options.ts +8 -11
  462. package/src/server/serverFns/testcase/testcase-export.ts +5 -7
  463. package/src/server/serverFns/testcase/testcase-filter-views.ts +12 -18
  464. package/src/server/serverFns/testcase/testcase-folders.ts +16 -22
  465. package/src/server/serverFns/testcase/testcase-groups.ts +4 -6
  466. package/src/server/serverFns/testcase/testcase-reorder.ts +4 -6
  467. package/src/server/serverFns/translation.ts +4 -6
  468. package/src/server/serverFns/user/avatar.ts +4 -6
  469. package/src/server/serverFns/user/settings.ts +4 -6
  470. package/src/server/serverFns/user/update-locale.ts +4 -6
  471. package/src/server/serverFns/workspace-kb.ts +26 -35
  472. package/src/server/serverFns/workspace.ts +43 -59
  473. package/src/server/serverFns/ws.ts +4 -6
  474. package/src/server/serverfn-public-allowlist.ts +117 -0
  475. package/src/server/story/types.ts +0 -1
  476. package/src/server/sync/__tests__/scopes.test.ts +64 -0
  477. package/src/server/sync/scopes.ts +7 -0
  478. package/src/server/testcase/__tests__/auto-bug.test.ts +44 -12
  479. package/src/__tests__/bench/baselines/format-timestamp.json +0 -44
  480. package/src/__typeprobe__.d.ts +0 -4
  481. package/src/components/forms/app-form-dialog.tsx +0 -126
  482. package/src/components/forms/hooks/use-form-submit.ts +0 -57
  483. package/src/components/forms/types.ts +0 -34
  484. package/src/components/harness/code/workspace-picker.tsx +0 -239
  485. package/src/components/tiptap/extensions/local-file/insert-local-file.ts +0 -35
  486. package/src/components/tiptap/extensions/local-file/local-file-node.ts +0 -71
  487. package/src/devframe-docks/ai-chat-iframe.tsx +0 -29
  488. package/src/devframe-docks/session-iframe.tsx +0 -90
  489. package/src/lib/__tests__/desktop-detect.test.ts +0 -19
  490. package/src/lib/collections/index.ts +0 -22
  491. package/src/lib/collections/knowledge/index.ts +0 -18
  492. package/src/lib/collections/knowledge/knowledge-bases-collection.ts +0 -34
  493. package/src/lib/collections/reference/bookmark-categories-collection.ts +0 -31
  494. package/src/lib/collections/reference/labels-collection.ts +0 -31
  495. package/src/lib/collections/reference/projects-collection.ts +0 -31
  496. package/src/lib/collections/reference/users-brief-collection.ts +0 -35
  497. package/src/lib/collections/reference/workspaces-collection.ts +0 -34
  498. package/src/lib/desktop-detect.ts +0 -29
  499. package/src/paraglide/messages/en.js +0 -28260
  500. package/src/paraglide/messages/zh.js +0 -28258
@@ -65,9 +65,8 @@ import {
65
65
  LibraryBig,
66
66
  PinOff,
67
67
  } from "lucide-react";
68
- import { Link, useLocation } from "@tanstack/react-router";
68
+ import { Link } from "@tanstack/react-router";
69
69
  import { cn } from "@/lib/utils";
70
- import { isParentActive } from "@/lib/sidebar-utils";
71
70
  import { useEffect, useState } from "react";
72
71
  import { useQuery, useQueryClient } from "@tanstack/react-query";
73
72
  import { KNOWLEDGE_BASES_QUERY_KEY } from "@/lib/collections/query-keys";
@@ -184,7 +183,7 @@ function UnpinButton({ kb }: { kb: KnowledgeItem }) {
184
183
  );
185
184
  }
186
185
 
187
- function KnowledgeBaseLinks({ pathname }: { pathname: string }) {
186
+ function KnowledgeBaseLinks() {
188
187
  // useQuery 直读 per-request queryClient(_nav loader 已 SSR 预取),
189
188
  // 列表随 SSR HTML 直出,刷新无需等待;避免 module-level collection 的跨请求泄漏
190
189
  const { data: items } = useQuery({
@@ -202,12 +201,13 @@ function KnowledgeBaseLinks({ pathname }: { pathname: string }) {
202
201
  <SidebarMenu className="space-y-1">
203
202
  {pinnedItems.map((item) => {
204
203
  const Icon = ICON_MAP[item.icon] || NotebookPen;
205
- const itemActive = pathname.startsWith(`/knowledge/${item.id}`);
206
204
  return (
207
205
  <SidebarMenuItem key={item.id} className="group/kb relative">
206
+ {/* 默认模糊匹配(段边界前缀)等价原 pathname.startsWith(`/knowledge/${item.id}`),
207
+ 且不误伤 id 互为字符串前缀的知识库;activeProps data-active 走 Link 自身激活判定 */}
208
208
  <SidebarMenuButton render={<Link to={"/knowledge/$kbId" as any}
209
209
  params={{ kbId: item.id } as any}
210
- className="flex w-full items-center" />} tooltip={item.name} isActive={itemActive} className="flex w-full items-center rounded-lg px-2">
210
+ className="flex w-full items-center" activeProps={{ 'data-active': '' }} activeOptions={{ includeSearch: false }} />} tooltip={item.name} className="flex w-full items-center rounded-lg px-2">
211
211
  <Icon className="size-4 mr-2 shrink-0" />
212
212
  <span className="text-sm font-medium truncate">{item.name}</span>
213
213
  </SidebarMenuButton>
@@ -221,12 +221,10 @@ function KnowledgeBaseLinks({ pathname }: { pathname: string }) {
221
221
 
222
222
  export function NavKnowledge() {
223
223
  const { state } = useSidebar();
224
- const { pathname } = useLocation();
225
224
  const isCollapsed = state === "collapsed";
226
225
  const [open, setOpen] = useState(false);
227
226
  const [createOpen, setCreateOpen] = useState(false);
228
227
  const queryClient = useQueryClient();
229
- const active = isParentActive(pathname, "/knowledge");
230
228
 
231
229
  useEffect(() => {
232
230
  const refresh = () => {
@@ -268,8 +266,10 @@ export function NavKnowledge() {
268
266
  <SidebarMenu>
269
267
  <SidebarMenuItem>
270
268
  <div className="relative">
269
+ {/* 精确匹配(activeOptions.exact)保持原 isParentActive(pathname === "/knowledge")口径;
270
+ activeProps data-active 走 Link 自身激活判定,includeSearch:false 保持纯 pathname 口径 */}
271
271
  <SidebarMenuButton render={<Link to={("/knowledge" as any)}
272
- className="flex w-full items-center" />} tooltip={m.common_sidebar_knowledge_title()} isActive={active} className={cn(
272
+ className="flex w-full items-center" activeProps={{ 'data-active': '' }} activeOptions={{ exact: true, includeSearch: false }} />} tooltip={m.common_sidebar_knowledge_title()} className={cn(
273
273
  "flex w-full items-center rounded-lg px-2 pr-9",
274
274
  isCollapsed && "justify-center"
275
275
  )}>
@@ -302,11 +302,11 @@ export function NavKnowledge() {
302
302
  <SidebarMenuSub className="my-1 ml-3.5">
303
303
  {systemRoutes.map((route) => {
304
304
  const Icon = route.icon;
305
- const subActive = pathname === route.link;
306
305
  return (
307
306
  <SidebarMenuSubItem key={route.id} className="h-auto">
307
+ {/* 精确匹配(activeOptions.exact)保持原 pathname === route.link 口径 */}
308
308
  <SidebarMenuSubButton render={<Link to={route.link}
309
- className="flex items-center rounded-md w-50 py-1.5 text-sm font-medium" />} isActive={subActive}>
309
+ className="flex items-center rounded-md w-50 py-1.5 text-sm font-medium" activeProps={{ 'data-active': '' }} activeOptions={{ exact: true, includeSearch: false }} />}>
310
310
  <Icon className="size-4 mr-2" />
311
311
  {route.title()}
312
312
  {route.badge && (
@@ -324,7 +324,7 @@ export function NavKnowledge() {
324
324
  </Collapsible>
325
325
 
326
326
  {!isCollapsed && (
327
- <KnowledgeBaseLinks pathname={pathname} />
327
+ <KnowledgeBaseLinks />
328
328
  )}
329
329
 
330
330
  <KnowledgeFormDialog
@@ -46,9 +46,7 @@ import {
46
46
  } from "lucide-react";
47
47
  import { Link } from "@tanstack/react-router";
48
48
  import { useState } from "react";
49
- import { useLocation } from "@tanstack/react-router";
50
49
  import type { Route } from "@/config/sidebar-routes";
51
- import { isParentActive } from "@/lib/sidebar-utils";
52
50
  import { MODULE_ROUTES } from "@/lib/collab/module-map";
53
51
 
54
52
  import { AnimatedGradientText } from "@/components/magicui/animated-gradient-text";
@@ -95,19 +93,21 @@ function getIconComponent(iconName: string | undefined) {
95
93
  import { CreateDesignDialog } from "@/components/design/editor/dashboard/create-design-dialog";
96
94
  import { m } from "@/paraglide/messages.js";
97
95
 
96
+ // 激活态交由 Link 自身判定(activeProps data-active 替代 SidebarMenuButton isActive,
97
+ // Base UI state.active=true 同样落 data-active="",CSS data-active: 变体不受影响)。
98
+ // 口径保持:/dashboard 与有子级的父行 = 精确匹配(原 isParentActive / pathname=== 特例),
99
+ // 其余 = 默认模糊匹配(段边界前缀,等价原 startsWith 且不误伤 /bugs-x 同前缀路由);
100
+ // includeSearch:false 保持纯 pathname 口径(否则带查询串的 URL 不命中激活态)。
101
+ const EXACT_ACTIVE_OPTIONS = { exact: true, includeSearch: false } as const;
102
+ const FUZZY_ACTIVE_OPTIONS = { includeSearch: false } as const;
103
+ const LINK_ACTIVE_PROPS = { "data-active": "" } as const;
104
+
98
105
  export default function DashboardNavigation({ routes }: { routes: Route[] }) {
99
106
  const { state } = useSidebar();
100
- const { pathname } = useLocation();
101
107
  const isCollapsed = state === "collapsed";
102
108
  const [openCollapsible, setOpenCollapsible] = useState<string | null>(null);
103
109
  const [createDesignOpen, setCreateDesignOpen] = useState(false);
104
110
 
105
- const isActive = (link: string) => {
106
- if (link === "/dashboard") return pathname === "/dashboard";
107
- if (link === "#") return false;
108
- return pathname.startsWith(link);
109
- };
110
-
111
111
  return (
112
112
  <>
113
113
  <SidebarMenu className="space-y-1">
@@ -115,9 +115,6 @@ export default function DashboardNavigation({ routes }: { routes: Route[] }) {
115
115
  const hasSubRoutes = !!route.subs?.length;
116
116
  const isDesignRoute = route.id === "prd";
117
117
  const isOpen = hasSubRoutes && !isCollapsed && openCollapsible === route.id;
118
- const active = hasSubRoutes
119
- ? isParentActive(pathname, route.link)
120
- : isActive(route.link);
121
118
  // route.id ≠ presence module key(home→dashboard 等),MODULE_ROUTES 查表(§8.2 交接锚点 F)
122
119
  const presenceModule = MODULE_ROUTES.find(
123
120
  (mr) => mr.id === route.id,
@@ -135,7 +132,7 @@ export default function DashboardNavigation({ routes }: { routes: Route[] }) {
135
132
  >
136
133
  <div className="relative">
137
134
  <SidebarMenuButton render={<Link to={route.link}
138
- className="flex w-full items-center" />} isActive={active} className={cn(
135
+ className="flex w-full items-center" activeProps={LINK_ACTIVE_PROPS} activeOptions={hasSubRoutes ? EXACT_ACTIVE_OPTIONS : FUZZY_ACTIVE_OPTIONS} />} className={cn(
139
136
  "flex w-full items-center rounded-lg px-2",
140
137
  !isCollapsed && (isDesignRoute || hasSubRoutes) && "pr-16",
141
138
  isCollapsed && "justify-center"
@@ -202,7 +199,7 @@ export default function DashboardNavigation({ routes }: { routes: Route[] }) {
202
199
  className="h-auto"
203
200
  >
204
201
  <SidebarMenuSubButton render={<Link to={subRoute.link}
205
- className="flex items-center rounded-md px-2.5 py-1.5 text-sm font-medium" />} isActive={isActive(subRoute.link)}>
202
+ className="flex items-center rounded-md px-2.5 py-1.5 text-sm font-medium" activeProps={LINK_ACTIVE_PROPS} activeOptions={subRoute.link === "/dashboard" ? EXACT_ACTIVE_OPTIONS : FUZZY_ACTIVE_OPTIONS} />}>
206
203
  {getIconComponent(subRoute.icon as string)}
207
204
  <span className="truncate min-w-0">{subRoute.title()}</span>
208
205
  {/* {subRoute.badge?.trim() && (
@@ -224,7 +221,7 @@ export default function DashboardNavigation({ routes }: { routes: Route[] }) {
224
221
  "flex items-center rounded-lg px-2",
225
222
  !isCollapsed && presenceModule && route.presence && "pr-12",
226
223
  isCollapsed && "justify-center"
227
- )} />} tooltip={route.title()} isActive={active}>
224
+ )} activeProps={LINK_ACTIVE_PROPS} activeOptions={route.link === "/dashboard" ? EXACT_ACTIVE_OPTIONS : FUZZY_ACTIVE_OPTIONS} />} tooltip={route.title()}>
228
225
  {route.icon}
229
226
  {!isCollapsed && (
230
227
  <span className="ml-2 text-sm font-medium">
@@ -32,12 +32,12 @@ import {
32
32
  PopoverTrigger,
33
33
  } from '@/components/ui/popover'
34
34
  import { CATEGORY_ICONS, formatRelativeTime } from '@/components/notifications/team-notifications'
35
+ import { useNotifications } from '@/hooks/use-notifications'
35
36
  import {
36
- useNotifications,
37
- NOTIFICATION_POPOVER_KEY,
38
37
  NOTIFICATION_ROOT_KEY,
39
- } from '@/hooks/use-notifications'
40
- import { listNotificationsFn, markAllNotificationsReadFn, markNotificationReadFn } from '@/server/serverFns/notifications'
38
+ notificationsPopoverUnreadQueryOptions,
39
+ } from '@/lib/queries/notifications'
40
+ import { markAllNotificationsReadFn, markNotificationReadFn } from '@/server/serverFns/notifications'
41
41
  import type { NotificationItem } from '@allbluecn/kernel'
42
42
  import { m } from "@/paraglide/messages.js";
43
43
 
@@ -50,10 +50,7 @@ export function NotificationsBell() {
50
50
  const [open, setOpen] = useState(false)
51
51
 
52
52
  // ⚠ 与详情页 useInfiniteQuery 的分页缓存形态不同,必须使用独立 queryKey
53
- const { data } = useQuery({
54
- queryKey: [...NOTIFICATION_POPOVER_KEY, 'status', 'unread'],
55
- queryFn: () => listNotificationsFn({ data: { status: 'unread' } }),
56
- })
53
+ const { data } = useQuery(notificationsPopoverUnreadQueryOptions())
57
54
  const items = (data?.items ?? []).slice(0, POPOVER_ITEM_LIMIT)
58
55
 
59
56
  const invalidateNotificationData = () => {
@@ -15,7 +15,7 @@
15
15
  // You should have received a copy of the GNU Affero General Public License
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
- import { Link, useLocation } from "@tanstack/react-router";
18
+ import { Link } from "@tanstack/react-router";
19
19
  import { useEffect, useState } from "react";
20
20
  import { useQuery, useQueryClient } from "@tanstack/react-query";
21
21
  import { Layers, Plus, PinOff } from "lucide-react";
@@ -28,7 +28,6 @@ import {
28
28
  } from "@/components/ui/sidebar";
29
29
  import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
30
30
  import { cn } from "@/lib/utils";
31
- import { isParentActive } from "@/lib/sidebar-utils";
32
31
  import { LucideIconByName } from "@/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name";
33
32
  import { CreateWorkspaceDialog } from "@/components/workspace/dialogs/create-workspace-dialog";
34
33
  import { WORKSPACES_QUERY_KEY } from "@/lib/collections/query-keys";
@@ -39,7 +38,7 @@ import {
39
38
  import { m } from "@/paraglide/messages.js";
40
39
  import { onEvent, emitEvent, AppEvents } from "@/lib/events";
41
40
 
42
- function WorkspaceLinks({ pathname }: { pathname: string }) {
41
+ function WorkspaceLinks() {
43
42
  // useQuery 直读 per-request queryClient(_nav loader 已 SSR 预取),
44
43
  // 列表随 SSR HTML 直出,刷新无需等待;避免 module-level collection 的跨请求泄漏
45
44
  const { data } = useQuery({
@@ -52,18 +51,22 @@ function WorkspaceLinks({ pathname }: { pathname: string }) {
52
51
  return (
53
52
  <SidebarMenu className="space-y-1">
54
53
  <SidebarMenuItem>
55
- <SidebarMenuButton render={<Link to="/workspaces" className="flex w-full items-center" />} tooltip={m.common_sidebar_workspace_home()} isActive={pathname === "/workspaces"} className="flex w-full items-center rounded-lg px-2 font-medium data-active:text-sidebar-foreground">
54
+ {/* 精确匹配(activeOptions.exact)保持原 pathname === "/workspaces" 口径;
55
+ includeSearch:false 保持纯 pathname 口径(activeProps data-active 走 Link 自身
56
+ 激活判定,替代 SidebarMenuButton isActive,data-active 值同为空串) */}
57
+ <SidebarMenuButton render={<Link to="/workspaces" className="flex w-full items-center" activeProps={{ 'data-active': '' }} activeOptions={{ exact: true, includeSearch: false }} />} tooltip={m.common_sidebar_workspace_home()} className="flex w-full items-center rounded-lg px-2 font-medium data-active:text-sidebar-foreground">
56
58
  <Layers className="size-4" />
57
59
  <span className="ml-2 text-sm font-medium">{m.common_sidebar_workspace_home()}</span>
58
60
  </SidebarMenuButton>
59
61
  </SidebarMenuItem>
60
62
  {workspacesList.map((ws) => {
61
- const active = pathname.startsWith(`/workspaces/${ws.id}`);
62
63
  return (
63
64
  <SidebarMenuItem key={ws.id} className="group/ws relative">
65
+ {/* 默认模糊匹配(段边界前缀)等价原 pathname.startsWith(`/workspaces/${ws.id}`),
66
+ 且不误伤 id 互为字符串前缀的工作区 */}
64
67
  <SidebarMenuButton render={<Link to="/workspaces/$workspaceId"
65
68
  params={{ workspaceId: ws.id }}
66
- className="flex w-full items-center" />} tooltip={ws.name} isActive={active} className="flex w-full items-center rounded-lg px-2 font-medium pr-8 data-active:text-sidebar-foreground">
69
+ className="flex w-full items-center" activeProps={{ 'data-active': '' }} activeOptions={{ includeSearch: false }} />} tooltip={ws.name} className="flex w-full items-center rounded-lg px-2 font-medium pr-8 data-active:text-sidebar-foreground">
67
70
  <LucideIconByName
68
71
  name={ws.icon}
69
72
  size={16}
@@ -95,7 +98,6 @@ function WorkspaceLinks({ pathname }: { pathname: string }) {
95
98
 
96
99
  export function NavWorkspace() {
97
100
  const { state } = useSidebar();
98
- const { pathname } = useLocation();
99
101
  const isCollapsed = state === "collapsed";
100
102
  const [createOpen, setCreateOpen] = useState(false);
101
103
  const queryClient = useQueryClient();
@@ -118,7 +120,7 @@ export function NavWorkspace() {
118
120
  <SidebarGroup className="p-0">
119
121
  <SidebarMenu>
120
122
  <SidebarMenuItem>
121
- <SidebarMenuButton render={<Link to="/workspaces" className="flex items-center" />} tooltip={m.common_sidebar_workspace_home()} isActive={isParentActive(pathname, "/workspaces")}>
123
+ <SidebarMenuButton render={<Link to="/workspaces" className="flex items-center" activeProps={{ 'data-active': '' }} activeOptions={{ exact: true, includeSearch: false }} />} tooltip={m.common_sidebar_workspace_home()}>
122
124
  <Layers className="size-4" />
123
125
  </SidebarMenuButton>
124
126
  </SidebarMenuItem>
@@ -145,7 +147,7 @@ export function NavWorkspace() {
145
147
  <Plus className="size-3.5" />
146
148
  </button>
147
149
  </div>
148
- <WorkspaceLinks pathname={pathname} />
150
+ <WorkspaceLinks />
149
151
  <CreateWorkspaceDialog open={createOpen} onOpenChange={setCreateOpen} />
150
152
  </SidebarGroup>
151
153
  );
@@ -0,0 +1,158 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ //
3
+ // settings-notifications 偏好保存竞态回归(TanStack 优化任务 12)。
4
+ //
5
+ // 旧实现在保存 serverFn 成功后才 setQueryData 写缓存,且成功路径无 invalidate、
6
+ // 无 cancelQueries:mutation 前已在途的 refetch 若在成功写缓存之后才 resolve
7
+ // (其响应内容是保存提交前的服务端旧偏好),会把缓存写回旧值——组件 useEffect
8
+ // 同步本地状态后,用户的开关被永久弹回(直到下一次无关 refetch 才纠偏)。
9
+ // 迁移为官方三段式后:onMutate cancelQueries + 快照 + 乐观写(取消在途 fetch,
10
+ // 旧值永不落缓存)、onError 回滚 + toast、onSettled invalidate 对齐服务端真相。
11
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
12
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
13
+ import { act, cleanup, fireEvent, render, waitFor } from '@testing-library/react'
14
+
15
+ vi.mock('@/server/serverFns/notifications', () => ({
16
+ getNotificationPreferencesFn: vi.fn(),
17
+ getUnreadCountFn: vi.fn(),
18
+ listNotificationsFn: vi.fn(),
19
+ updateNotificationPreferencesFn: vi.fn(),
20
+ }))
21
+ vi.mock('@/server/serverFns/auth/email-verification', () => ({
22
+ getEmailVerifiedStatusFn: vi.fn(),
23
+ resendVerificationEmailFn: vi.fn(),
24
+ }))
25
+ vi.mock('@/components/shared-kit/auth/auth-client', () => ({
26
+ useSession: vi.fn(() => ({ data: null })),
27
+ }))
28
+
29
+ import { SettingsNotifications } from '../settings-notifications'
30
+ import { NOTIFICATION_PREFERENCES_KEY } from '@/lib/queries/notifications'
31
+ import {
32
+ getNotificationPreferencesFn,
33
+ updateNotificationPreferencesFn,
34
+ } from '@/server/serverFns/notifications'
35
+ import type { NotificationPreferences } from '@allbluecn/kernel'
36
+
37
+ const prefsDndOff: NotificationPreferences = {
38
+ categories: { team: 'realtime', collaboration: 'realtime', system: 'realtime', billing: 'realtime', security: 'realtime' },
39
+ emailCategories: { team: true, collaboration: true, system: true, billing: true, security: true },
40
+ dndEnabled: false,
41
+ dndStart: '22:00',
42
+ dndEnd: '07:00',
43
+ }
44
+ const prefsDndOn: NotificationPreferences = { ...prefsDndOff, dndEnabled: true }
45
+
46
+ function deferred<T>() {
47
+ let resolve!: (value: T) => void
48
+ let reject!: (reason?: unknown) => void
49
+ const promise = new Promise<T>((res, rej) => {
50
+ resolve = res
51
+ reject = rej
52
+ })
53
+ return { promise, resolve, reject }
54
+ }
55
+
56
+ const clients: QueryClient[] = []
57
+
58
+ function renderPanel() {
59
+ const client = new QueryClient({
60
+ defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
61
+ })
62
+ clients.push(client)
63
+ render(
64
+ <QueryClientProvider client={client}>
65
+ <SettingsNotifications />
66
+ </QueryClientProvider>,
67
+ )
68
+ return client
69
+ }
70
+
71
+ const readDnd = (client: QueryClient) =>
72
+ client.getQueryData<NotificationPreferences>(NOTIFICATION_PREFERENCES_KEY)?.dndEnabled
73
+
74
+ const dndSwitch = () => document.getElementById('dnd-enabled') as HTMLButtonElement
75
+
76
+ beforeEach(() => {
77
+ // shouldAdvanceTime:让 RTL waitFor 的轮询 timer 走真实节奏;调度确定性仍由显式 advance 控制
78
+ vi.useFakeTimers({ shouldAdvanceTime: true })
79
+ vi.mocked(getNotificationPreferencesFn).mockResolvedValue(prefsDndOff)
80
+ vi.mocked(updateNotificationPreferencesFn).mockResolvedValue(undefined as never)
81
+ })
82
+
83
+ afterEach(() => {
84
+ cleanup()
85
+ vi.useRealTimers()
86
+ // reset 而非 clear:mockClear 不清 mockImplementationOnce 队列,会泄漏到下一用例
87
+ vi.resetAllMocks()
88
+ clients.length = 0
89
+ })
90
+
91
+ describe('SettingsNotifications 偏好保存乐观更新(官方 onMutate 通道)', () => {
92
+ it('保存成功后在途 refetch 迟到返回旧偏好不覆盖缓存(cancelQueries 防线),终态为新值', async () => {
93
+ const inFlight = deferred<NotificationPreferences>()
94
+ const mutation = deferred<{ success: true }>()
95
+ const afterInvalidate = deferred<NotificationPreferences>()
96
+
97
+ vi.mocked(getNotificationPreferencesFn)
98
+ .mockImplementationOnce(() => Promise.resolve(prefsDndOff)) // 首屏
99
+ .mockImplementationOnce(() => inFlight.promise) // mutation 前触发的在途 refetch
100
+ .mockImplementationOnce(() => afterInvalidate.promise) // onSettled invalidate
101
+ vi.mocked(updateNotificationPreferencesFn).mockImplementation(() => mutation.promise)
102
+
103
+ const client = renderPanel()
104
+ await waitFor(() => expect(dndSwitch()).toBeTruthy())
105
+
106
+ // mutation(防抖保存)前触发一次 refetch,延迟返回旧偏好
107
+ void client.invalidateQueries({ queryKey: NOTIFICATION_PREFERENCES_KEY })
108
+ await waitFor(() => expect(getNotificationPreferencesFn).toHaveBeenCalledTimes(2))
109
+
110
+ // 开启 DND → 本地 state 立即切换,500ms 防抖后发起保存
111
+ fireEvent.click(dndSwitch())
112
+ await act(async () => {
113
+ await vi.advanceTimersByTimeAsync(500)
114
+ })
115
+ await waitFor(() => expect(updateNotificationPreferencesFn).toHaveBeenCalledTimes(1))
116
+ // 乐观写先落地(缓存已含新值,不等服务端返回)
117
+ await waitFor(() => expect(readDnd(client)).toBe(true))
118
+
119
+ // 服务端确认 → onSettled invalidate → 拉取服务端真相
120
+ await act(async () => {
121
+ mutation.resolve({ success: true })
122
+ await vi.advanceTimersByTimeAsync(50)
123
+ })
124
+ await waitFor(() => expect(getNotificationPreferencesFn).toHaveBeenCalledTimes(3))
125
+
126
+ // 竞态核心断言:在途 refetch 此刻才 resolve 保存前的旧偏好(canceled,不得覆盖)
127
+ await act(async () => {
128
+ inFlight.resolve(prefsDndOff)
129
+ await vi.advanceTimersByTimeAsync(50)
130
+ })
131
+ expect(readDnd(client)).toBe(true)
132
+
133
+ afterInvalidate.resolve(prefsDndOn)
134
+ await waitFor(() => {
135
+ expect(client.getQueryData<NotificationPreferences>(NOTIFICATION_PREFERENCES_KEY)).toEqual(prefsDndOn)
136
+ })
137
+ })
138
+
139
+ it('保存失败回滚快照并 invalidate 纠偏(开关不被乐观值残留误导)', async () => {
140
+ const truth = deferred<NotificationPreferences>()
141
+ vi.mocked(getNotificationPreferencesFn)
142
+ .mockImplementationOnce(() => Promise.resolve(prefsDndOff)) // 首屏
143
+ .mockImplementationOnce(() => truth.promise) // onSettled invalidate
144
+ vi.mocked(updateNotificationPreferencesFn).mockRejectedValue(new Error('boom'))
145
+
146
+ const client = renderPanel()
147
+ await waitFor(() => expect(dndSwitch()).toBeTruthy())
148
+
149
+ fireEvent.click(dndSwitch())
150
+ await act(async () => {
151
+ await vi.advanceTimersByTimeAsync(500)
152
+ })
153
+ // 失败 → onError 回滚快照 + onSettled invalidate 拉取服务端真相
154
+ truth.resolve(prefsDndOff)
155
+ await waitFor(() => expect(readDnd(client)).toBe(false))
156
+ expect(readDnd(client)).toBe(false)
157
+ })
158
+ })
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
2
  // Adapted from @hextaui/settings-notifications (https://www.hextaui.com)
3
3
  import { useEffect, useState } from 'react'
4
- import { useQuery, useQueryClient } from '@tanstack/react-query'
4
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
5
5
  import { toast } from '@/components/ui/toast'
6
6
  import { Bell, Mail } from 'lucide-react'
7
7
  import { Field, FieldContent, FieldDescription, FieldLabel } from '@/components/ui/field'
@@ -16,7 +16,11 @@ import {
16
16
  import { Separator } from '@/components/ui/separator'
17
17
  import { Switch } from '@/components/ui/switch'
18
18
  import { useDebouncedCallback } from '@/lib/pacer'
19
- import { getNotificationPreferencesFn, updateNotificationPreferencesFn } from '@/server/serverFns/notifications'
19
+ import { updateNotificationPreferencesFn } from '@/server/serverFns/notifications'
20
+ import {
21
+ NOTIFICATION_PREFERENCES_KEY,
22
+ notificationPreferencesQueryOptions,
23
+ } from '@/lib/queries/notifications'
20
24
  import { getEmailVerifiedStatusFn } from '@/server/serverFns/auth/email-verification'
21
25
  import { useSession } from '@/components/shared-kit/auth/auth-client'
22
26
  import type {
@@ -52,10 +56,7 @@ export function SettingsNotifications() {
52
56
  const { data: session } = useSession()
53
57
  const userId = session?.user?.id ?? null
54
58
 
55
- const { data: prefsData, isLoading } = useQuery({
56
- queryKey: ['notifications', 'preferences'],
57
- queryFn: () => getNotificationPreferencesFn({ data: {} }),
58
- })
59
+ const { data: prefsData, isLoading } = useQuery(notificationPreferencesQueryOptions())
59
60
 
60
61
  const { data: emailVerified = null } = useQuery({
61
62
  queryKey: ['email-verified-status', userId ?? 'anonymous'],
@@ -73,14 +74,37 @@ export function SettingsNotifications() {
73
74
  if (prefsData) setPrefs(prefsData)
74
75
  }, [prefsData])
75
76
 
76
- const debouncedSave = useDebouncedCallback(async (next: NotificationPreferences) => {
77
- try {
78
- await updateNotificationPreferencesFn({ data: next })
79
- queryClient.setQueryData(['notifications', 'preferences'], next)
80
- } catch {
77
+ // 官方乐观更新三段式(TanStack Query Optimistic Updates 指南):
78
+ // onMutate —— cancelQueries 取消在途 refetch(防其迟到返回保存前的旧偏好覆盖
79
+ // 刚写入的值——旧实现成功写缓存后无 invalidate,被覆盖即成永久回退)+
80
+ // 快照 + 乐观写;onError —— 回滚快照 + toast;onSettled —— invalidate
81
+ // 对齐服务端真相(失败路径同样兜底纠偏)。
82
+ const savePrefsMutation = useMutation({
83
+ mutationFn: (next: NotificationPreferences) =>
84
+ updateNotificationPreferencesFn({ data: next }),
85
+ onMutate: async (next) => {
86
+ await queryClient.cancelQueries({ queryKey: NOTIFICATION_PREFERENCES_KEY })
87
+ const previous = queryClient.getQueryData<NotificationPreferences>(
88
+ NOTIFICATION_PREFERENCES_KEY,
89
+ )
90
+ queryClient.setQueryData(NOTIFICATION_PREFERENCES_KEY, next)
91
+ return { previous }
92
+ },
93
+ onError: (_err, _next, ctx) => {
94
+ if (ctx?.previous !== undefined) {
95
+ queryClient.setQueryData(NOTIFICATION_PREFERENCES_KEY, ctx.previous)
96
+ }
81
97
  toast.error(m.notifications_preferences_saveFailed())
82
- queryClient.invalidateQueries({ queryKey: ['notifications', 'preferences'] })
83
- }
98
+ },
99
+ onSettled: () => {
100
+ void queryClient.invalidateQueries({ queryKey: NOTIFICATION_PREFERENCES_KEY })
101
+ },
102
+ })
103
+
104
+ const debouncedSave = useDebouncedCallback((next: NotificationPreferences) => {
105
+ void savePrefsMutation.mutateAsync(next).catch(() => {
106
+ // onError 已 toast + 回滚,rejection 到此终结
107
+ })
84
108
  }, { wait: PREFERENCES_SAVE_DEBOUNCE_MS })
85
109
 
86
110
  const patch = (partial: Partial<NotificationPreferences>) => {
@@ -47,12 +47,15 @@ import {
47
47
  } from '@/components/ui/select'
48
48
  import { Separator } from '@/components/ui/separator'
49
49
  import { cn } from '@/lib/utils'
50
- import { useNotifications, NOTIFICATION_LIST_KEY, NOTIFICATION_ROOT_KEY } from '@/hooks/use-notifications'
50
+ import { useNotifications } from '@/hooks/use-notifications'
51
+ import {
52
+ NOTIFICATION_ROOT_KEY,
53
+ notificationsListInfiniteQueryOptions,
54
+ } from '@/lib/queries/notifications'
51
55
  import type { NotificationCategory, NotificationItem } from '@allbluecn/kernel'
52
56
  import {
53
57
  clearNotificationsFn,
54
58
  deleteNotificationFn,
55
- listNotificationsFn,
56
59
  markAllNotificationsReadFn,
57
60
  markNotificationReadFn,
58
61
  } from '@/server/serverFns/notifications'
@@ -106,18 +109,11 @@ export function TeamNotificationsPanel() {
106
109
  const [categoryFilter, setCategoryFilter] = useState<'all' | NotificationCategory>('all')
107
110
  const [statusFilter, setStatusFilter] = useState<StatusFilter>('all')
108
111
 
109
- const { data, isLoading, fetchNextPage, isFetchingNextPage, hasNextPage } = useInfiniteQuery({
110
- queryKey: [...NOTIFICATION_LIST_KEY, categoryFilter === 'all' ? undefined : categoryFilter],
111
- queryFn: async ({ pageParam }) => {
112
- const base = categoryFilter === 'all' ? {} : { category: categoryFilter }
113
- return listNotificationsFn({
114
- data: pageParam ? { ...base, cursor: pageParam } : base,
115
- })
116
- },
117
- initialPageParam: undefined as string | undefined,
118
- getNextPageParam: (lastPage) =>
119
- lastPage.hasMore ? lastPage.items[lastPage.items.length - 1].id : undefined,
120
- })
112
+ const { data, isLoading, fetchNextPage, isFetchingNextPage, hasNextPage } = useInfiniteQuery(
113
+ notificationsListInfiniteQueryOptions(
114
+ categoryFilter === 'all' ? undefined : categoryFilter,
115
+ ),
116
+ )
121
117
 
122
118
  const items = useMemo(() => data?.pages.flatMap((page) => page.items) ?? [], [data])
123
119