@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
@@ -0,0 +1,170 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ //
3
+ // account-client 头像持久化竞态回归(TanStack 优化任务 12)。
4
+ //
5
+ // 旧实现在 updateAvatarFn 成功后才 setQueryData(['session']) 合并头像字段,
6
+ // 且成功路径无 invalidate、无 cancelQueries:mutation 前已在途的 session
7
+ // refetch(useSession staleTime 0 + refetchOnMount,挂载/聚焦频发)若在成功
8
+ // 写缓存之后才 resolve,会把会话缓存写回保存前的旧值——头像字段丢失成永久
9
+ // 回退。迁移为官方三段式后:onMutate cancelQueries + 快照 + 乐观合并、
10
+ // onError 回滚 + toast、onSettled invalidate 对齐服务端真相。
11
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
12
+ import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'
13
+ import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
14
+
15
+ vi.mock('@/server/serverFns/user/avatar', () => ({
16
+ updateAvatarFn: vi.fn(),
17
+ }))
18
+ vi.mock('@/server/serverFns/auth/email-verification', () => ({
19
+ getEmailVerifiedStatusFn: vi.fn(),
20
+ resendVerificationEmailFn: vi.fn(),
21
+ }))
22
+ vi.mock('@/components/shared-kit/auth/auth-client', () => ({
23
+ useSession: vi.fn(() => ({ data: { user: { id: 'u1' } } })),
24
+ }))
25
+
26
+ import AccountClient from '../account-client'
27
+ import { updateAvatarFn } from '@/server/serverFns/user/avatar'
28
+ import { getEmailVerifiedStatusFn } from '@/server/serverFns/auth/email-verification'
29
+ import { m } from '@/paraglide/messages.js'
30
+
31
+ type SessionValue = { user?: Record<string, unknown> } | null
32
+
33
+ const sessionBase: SessionValue = { user: { name: '旧名字', email: 'u@example.com' } }
34
+
35
+ function deferred<T>() {
36
+ let resolve!: (value: T) => void
37
+ let reject!: (reason?: unknown) => void
38
+ const promise = new Promise<T>((res, rej) => {
39
+ resolve = res
40
+ reject = rej
41
+ })
42
+ return { promise, resolve, reject }
43
+ }
44
+
45
+ /** 与 AccountClient 同 provider 的探针:制造可被 mutation 取消的在途 session refetch */
46
+ function SessionProbe({ queryFn }: { queryFn: () => Promise<SessionValue> }) {
47
+ useQuery({ queryKey: ['session'], queryFn })
48
+ return null
49
+ }
50
+
51
+ const clients: QueryClient[] = []
52
+
53
+ function renderAccount(queryFn: () => Promise<SessionValue>) {
54
+ const client = new QueryClient({
55
+ defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
56
+ })
57
+ clients.push(client)
58
+ render(
59
+ <QueryClientProvider client={client}>
60
+ <SessionProbe queryFn={queryFn} />
61
+ <AccountClient
62
+ initialAvatarConfig={null}
63
+ initialBgShape="square"
64
+ initialBgColor="#7c3aed"
65
+ user={{ name: '旧名字', email: 'u@example.com', createdAt: null }}
66
+ />
67
+ </QueryClientProvider>,
68
+ )
69
+ return client
70
+ }
71
+
72
+ const readAvatar = (client: QueryClient) =>
73
+ (client.getQueryData<SessionValue>(['session'])?.user?.avatarConfig ?? null) as
74
+ | Record<string, unknown>
75
+ | null
76
+
77
+ beforeEach(() => {
78
+ // shouldAdvanceTime:让 RTL waitFor 的轮询 timer 走真实节奏;调度确定性仍由显式 advance 控制
79
+ vi.useFakeTimers({ shouldAdvanceTime: true })
80
+ vi.mocked(updateAvatarFn).mockResolvedValue(undefined as never)
81
+ vi.mocked(getEmailVerifiedStatusFn).mockResolvedValue({ verified: true })
82
+ })
83
+
84
+ afterEach(() => {
85
+ cleanup()
86
+ vi.useRealTimers()
87
+ // reset 而非 clear:mockClear 不清 mockImplementationOnce 队列,会泄漏到下一用例
88
+ vi.resetAllMocks()
89
+ clients.length = 0
90
+ })
91
+
92
+ describe('AccountClient 头像会话乐观更新(官方 onMutate 通道)', () => {
93
+ it('保存成功后在途 session refetch 迟到返回旧会话不覆盖头像字段,终态保留新值', async () => {
94
+ let sessionCalls = 0
95
+ const call2 = deferred<SessionValue>()
96
+ type UpdateAvatarResult = Awaited<ReturnType<typeof updateAvatarFn>>
97
+ const mutation = deferred<UpdateAvatarResult>()
98
+ const call3 = deferred<SessionValue>()
99
+
100
+ vi.mocked(updateAvatarFn).mockImplementation(() => mutation.promise)
101
+
102
+ const client = renderAccount(() => {
103
+ sessionCalls++
104
+ if (sessionCalls === 1) return Promise.resolve(sessionBase)
105
+ if (sessionCalls === 2) return call2.promise
106
+ return call3.promise
107
+ })
108
+
109
+ await waitFor(() => expect(client.getQueryData(['session'])).toEqual(sessionBase))
110
+
111
+ // mutation 前触发一次 session refetch(在途、延迟返回保存前的旧会话)
112
+ void client.invalidateQueries({ queryKey: ['session'] })
113
+ await waitFor(() => expect(sessionCalls).toBe(2))
114
+
115
+ // 保存资料 → persistAvatar → 乐观合并头像字段进 session 缓存
116
+ fireEvent.click(screen.getByRole('button', { name: m.settings_profile_saveChanges() }))
117
+ await waitFor(() => expect(updateAvatarFn).toHaveBeenCalledTimes(1))
118
+ await waitFor(() => expect(readAvatar(client)).not.toBeNull())
119
+
120
+ // 服务端确认 → onSettled invalidate → 拉取服务端真相
121
+ await act(async () => {
122
+ mutation.resolve({ avatarConfig: null, bgShape: null, bgColor: null })
123
+ await vi.advanceTimersByTimeAsync(50)
124
+ })
125
+ await waitFor(() => expect(sessionCalls).toBe(3))
126
+
127
+ // 竞态核心断言:在途 refetch 此刻才 resolve 保存前的旧会话(canceled,不得覆盖)
128
+ await act(async () => {
129
+ call2.resolve(sessionBase)
130
+ await vi.advanceTimersByTimeAsync(50)
131
+ })
132
+ expect(readAvatar(client)).not.toBeNull()
133
+
134
+ call3.resolve({ user: { ...sessionBase.user, avatarConfig: { seed: 'x' }, bgColor: '#7c3aed' } })
135
+ await waitFor(() => {
136
+ expect(client.getQueryData<SessionValue>(['session'])?.user?.avatarConfig).toEqual({ seed: 'x' })
137
+ })
138
+ })
139
+
140
+ it('保存失败回滚快照(session 缓存不残留乐观头像字段)', async () => {
141
+ let sessionCalls = 0
142
+ const mutation = deferred<never>()
143
+ const truth = deferred<SessionValue>()
144
+ vi.mocked(updateAvatarFn).mockImplementation(() => mutation.promise as never)
145
+
146
+ const client = renderAccount(() => {
147
+ sessionCalls++
148
+ if (sessionCalls === 1) return Promise.resolve(sessionBase)
149
+ return truth.promise
150
+ })
151
+
152
+ await waitFor(() => expect(client.getQueryData(['session'])).toEqual(sessionBase))
153
+
154
+ fireEvent.click(screen.getByRole('button', { name: m.settings_profile_saveChanges() }))
155
+ await waitFor(() => expect(updateAvatarFn).toHaveBeenCalledTimes(1))
156
+ // onMutate 已乐观合并头像字段(mutation 挂起期间可观测)
157
+ await waitFor(() => expect(readAvatar(client)).not.toBeNull())
158
+
159
+ // 失败 → onError 回滚快照,乐观字段不留残留
160
+ mutation.reject(new Error('boom'))
161
+ await waitFor(() => expect(readAvatar(client)).toBeNull())
162
+
163
+ // onSettled invalidate 以服务端真相收尾
164
+ await waitFor(() => expect(sessionCalls).toBe(2))
165
+ truth.resolve(sessionBase)
166
+ await waitFor(() => {
167
+ expect(client.getQueryData<SessionValue>(['session'])).toEqual(sessionBase)
168
+ })
169
+ })
170
+ })
@@ -29,7 +29,7 @@ import { useState, useCallback } from "react"
29
29
  import { type AvatarConfig, getRandomConfig } from "@/lib/avatar/avatar-config"
30
30
  import type { ShapeType } from "@allbluecn/kernel"
31
31
  import { toAvatarShape } from "@/components/shared-kit/avatars/avatar-shape"
32
- import { useQueryClient } from "@tanstack/react-query"
32
+ import { useMutation, useQueryClient } from "@tanstack/react-query"
33
33
  import { updateAvatarFn } from "@/server/serverFns/user/avatar"
34
34
  import { toast } from '@/components/ui/toast'
35
35
  import { formatDate } from "@/lib/datetime"
@@ -59,17 +59,54 @@ export default function AccountClient({
59
59
  const [bgColor, setBgColor] = useState(initialBgColor)
60
60
  const queryClient = useQueryClient()
61
61
  const timezone = useTimezone()
62
- const persistAvatar = useCallback(async (config: AvatarConfig, shape: string, color: string) => {
63
- try {
64
- await updateAvatarFn({ data: { avatarConfig: config, bgShape: toAvatarShape(shape) ?? "square", bgColor: color } })
62
+ // 官方乐观更新三段式(TanStack Query Optimistic Updates 指南):
63
+ // onMutate —— cancelQueries 取消在途 session refetch(useSession staleTime 0 且
64
+ // refetchOnMount,挂载/聚焦常触发;防其迟到返回保存前的旧会话把刚写入
65
+ // 的头像字段冲掉——旧实现成功写缓存后无 invalidate,被覆盖即成永久回退)+
66
+ // 快照 + 乐观写;onError —— 回滚快照 + toast;onSettled —— invalidate
67
+ // 对齐服务端真相(失败路径同样兜底纠偏)。
68
+ // ['session'] 内联字面量与 auth-client.ts 的 useSession / user-menu 的 resetQueries 同键,
69
+ // 该 key 尚无 queryOptions 工厂(收敛属查询键治理任务,不在本任务范围)。
70
+ const persistAvatarMutation = useMutation({
71
+ mutationFn: (vars: { config: AvatarConfig; shape: string; color: string }) =>
72
+ updateAvatarFn({
73
+ data: {
74
+ avatarConfig: vars.config,
75
+ bgShape: toAvatarShape(vars.shape) ?? "square",
76
+ bgColor: vars.color,
77
+ },
78
+ }),
79
+ onMutate: async (vars) => {
80
+ await queryClient.cancelQueries({ queryKey: ['session'] })
81
+ const previousSession = queryClient.getQueryData<{
82
+ user?: Record<string, unknown>
83
+ } | null>(['session'])
65
84
  queryClient.setQueryData(['session'], (old: { user?: Record<string, unknown> } | null) =>
66
- old ? { ...old, user: { ...old.user, avatarConfig: config, bgShape: shape, bgColor: color } } : old,
85
+ old
86
+ ? { ...old, user: { ...old.user, avatarConfig: vars.config, bgShape: vars.shape, bgColor: vars.color } }
87
+ : old,
67
88
  )
68
- } catch (err) {
89
+ return { previousSession }
90
+ },
91
+ onError: (err, _vars, ctx) => {
92
+ if (ctx?.previousSession !== undefined) {
93
+ queryClient.setQueryData(['session'], ctx.previousSession)
94
+ }
69
95
  console.error("Failed to persist avatar:", err instanceof Error ? err.message : String(err))
70
96
  toast.error(m.settings_profile_avatarSaveFailed())
97
+ },
98
+ onSettled: () => {
99
+ void queryClient.invalidateQueries({ queryKey: ['session'] })
100
+ },
101
+ })
102
+
103
+ const persistAvatar = useCallback(async (config: AvatarConfig, shape: string, color: string) => {
104
+ try {
105
+ await persistAvatarMutation.mutateAsync({ config, shape, color })
106
+ } catch {
107
+ // onError 已 toast + 回滚;维持既有「保存失败不阻断后续成功 toast」行为
71
108
  }
72
- }, [queryClient])
109
+ }, [persistAvatarMutation])
73
110
 
74
111
  const handleSaveAvatar = async (config: AvatarConfig, newBgShape: string, newBgColor: string) => {
75
112
  setAvatarConfig(config)
@@ -20,9 +20,6 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
20
20
  import { toast } from "@/components/ui/toast"
21
21
  import { useResource } from "@/components/billing/use-resource"
22
22
  import {
23
- listTeamMembersFn,
24
- listTeamInvitationsFn,
25
- listTeamUsersFn,
26
23
  createTeamInvitationFn,
27
24
  refreshTeamInvitationLinkFn,
28
25
  revokeTeamInvitationFn,
@@ -36,6 +33,14 @@ import {
36
33
  leaveTeamFn,
37
34
  clearExpiredInvitationsFn,
38
35
  } from "@/server/serverFns/team"
36
+ import {
37
+ TEAM_INVITATIONS_QUERY_KEY,
38
+ TEAM_MEMBERS_QUERY_KEY,
39
+ TEAM_USERS_QUERY_KEY,
40
+ teamInvitationsQueryOptions,
41
+ teamMembersQueryOptions,
42
+ teamUsersQueryOptions,
43
+ } from "@/lib/queries/team"
39
44
  import { Button } from "@/components/ui/button"
40
45
  import {
41
46
  Card,
@@ -85,22 +90,15 @@ export default function TeamClient({ user }: TeamClientProps) {
85
90
  const seats = useResource("seats")
86
91
  const guestSeats = useResource("guestSeats")
87
92
 
88
- const { data: members = [], isLoading: membersLoading } = useQuery({
89
- queryKey: ["teamMembers"],
90
- queryFn: () => listTeamMembersFn(),
91
- })
93
+ const { data: members = [], isLoading: membersLoading } = useQuery(teamMembersQueryOptions())
92
94
 
93
95
  const { data: inviteData } = useQuery({
94
- queryKey: ["teamInvitations"],
95
- queryFn: () => listTeamInvitationsFn(),
96
+ ...teamInvitationsQueryOptions(),
96
97
  enabled: members.length > 0,
97
98
  })
98
99
  const invitations = inviteData?.invitations ?? []
99
100
 
100
- const { data: teamUsers = [] } = useQuery({
101
- queryKey: ["teamUsers"],
102
- queryFn: () => listTeamUsersFn(),
103
- })
101
+ const { data: teamUsers = [] } = useQuery(teamUsersQueryOptions())
104
102
 
105
103
  const isOwner = members.length > 0
106
104
  const teamOwner = teamUsers.find((u) => u.seatType === "owner")
@@ -109,9 +107,9 @@ export default function TeamClient({ user }: TeamClientProps) {
109
107
  const isInAnyTeam = isOwner || isMember
110
108
 
111
109
  const invalidate = () => {
112
- void qc.invalidateQueries({ queryKey: ["teamMembers"] })
113
- void qc.invalidateQueries({ queryKey: ["teamInvitations"] })
114
- void qc.invalidateQueries({ queryKey: ["teamUsers"] })
110
+ void qc.invalidateQueries({ queryKey: TEAM_MEMBERS_QUERY_KEY })
111
+ void qc.invalidateQueries({ queryKey: TEAM_INVITATIONS_QUERY_KEY })
112
+ void qc.invalidateQueries({ queryKey: TEAM_USERS_QUERY_KEY })
115
113
  }
116
114
 
117
115
  const createInviteMutation = useMutation({
@@ -278,8 +276,8 @@ export default function TeamClient({ user }: TeamClientProps) {
278
276
  const handleRefreshLink = async (expiresInDays?: number) => {
279
277
  const days = expiresInDays ?? 7
280
278
  const result = await refreshTeamInvitationLinkFn({ data: { expiresInDays: days } })
281
- void qc.invalidateQueries({ queryKey: ["teamInvitations"] })
282
- void qc.invalidateQueries({ queryKey: ["teamMembers"] })
279
+ void qc.invalidateQueries({ queryKey: TEAM_INVITATIONS_QUERY_KEY })
280
+ void qc.invalidateQueries({ queryKey: TEAM_MEMBERS_QUERY_KEY })
283
281
  return {
284
282
  id: result.invitationId,
285
283
  email: undefined,
@@ -369,7 +367,7 @@ export default function TeamClient({ user }: TeamClientProps) {
369
367
 
370
368
  useEffect(() => {
371
369
  if (isMember) {
372
- void qc.invalidateQueries({ queryKey: ["teamUsers"] })
370
+ void qc.invalidateQueries({ queryKey: TEAM_USERS_QUERY_KEY })
373
371
  }
374
372
  }, [isMember, qc])
375
373
 
@@ -18,72 +18,241 @@
18
18
  import { describe, it, expect } from "vitest";
19
19
  import { computeFrozenHeader } from "../frozen-header";
20
20
 
21
- // 平铺行布局(index → 行类型 → start):
22
- // 0 grp0 0
23
- // 1 sub0 36
24
- // 2 story 80
25
- // 3 grp1 200
26
- // 4 sub1 236
21
+ // 双层扁平行布局(index → 行类型 → start/size),组头/子组头高 36:
22
+ // 0 G1 0/36 ← 内容盒坐标系原点
23
+ // 1 S1 36/36
24
+ // 2 row 72/84
25
+ // 3 row 156/84
26
+ // 4 G2 240/36
27
+ // 5 S2 276/36
28
+ // 6 row 312/84
27
29
  const measurements = [
28
- { start: 0 },
29
- { start: 36 },
30
- { start: 80 },
31
- { start: 200 },
32
- { start: 236 },
30
+ { start: 0, size: 36 },
31
+ { start: 36, size: 36 },
32
+ { start: 72, size: 84 },
33
+ { start: 156, size: 84 },
34
+ { start: 240, size: 36 },
35
+ { start: 276, size: 36 },
36
+ { start: 312, size: 84 },
33
37
  ];
34
- const groupHeaderIndexes = [0, 3];
35
- const subGroupHeaderIndexes = [1, 4];
36
- const parentGroupOfSub = new Map<number, number>([
38
+ const groupIndexes = [0, 4];
39
+ const subIndexes = [1, 5];
40
+ const parentOfSub = new Map<number, number>([
37
41
  [1, 0],
38
- [4, 3],
42
+ [5, 4],
39
43
  ]);
40
44
 
41
45
  const compute = (scrollTop: number) =>
42
- computeFrozenHeader(scrollTop, measurements, groupHeaderIndexes, subGroupHeaderIndexes, parentGroupOfSub);
46
+ computeFrozenHeader(scrollTop, measurements, {
47
+ groupIndexes,
48
+ subIndexes,
49
+ parentGroupOfSub: parentOfSub,
50
+ pad: 0,
51
+ subPinTop: 36,
52
+ });
53
+
54
+ describe("computeFrozenHeader(双层:主组头 + 子组头)", () => {
55
+ it("首屏(scrollTop 0):首个主组头容器坐标恰为 0,边界不留容差地吸附", () => {
56
+ // 与 kanban 五轮裁决一致:容器坐标恰好 0 时吸附层与在流组头像素重合(零跳变)
57
+ expect(compute(0)).toEqual({
58
+ group: { index: 0, offset: 0 },
59
+ sub: { index: 1, offset: 0 },
60
+ });
61
+ });
62
+
63
+ it("组内滚动未出区段:双层均吸附 offset 0", () => {
64
+ expect(compute(50)).toEqual({
65
+ group: { index: 0, offset: 0 },
66
+ sub: { index: 1, offset: 0 },
67
+ });
68
+ });
43
69
 
44
- describe("computeFrozenHeader", () => {
45
- it("首屏(未滚动)激活第一个主组、无子组", () => {
46
- expect(compute(0)).toEqual({ groupIdx: 0, subIdx: null });
70
+ it("子组头吸附位在主组头正下方(subPinTop=36):坐标 37 未越过不吸附", () => {
71
+ // 子组头 start=36 在 scrollTop=-1 时容器坐标 37 > 36 —— 只测 scrollTop>=0 域:
72
+ // 用一个 start 更大的布局验证「未到吸附位不渲染副本」
73
+ const far = [
74
+ { start: 0, size: 36 },
75
+ { start: 100, size: 36 },
76
+ ];
77
+ const r = (scrollTop: number) =>
78
+ computeFrozenHeader(scrollTop, far, {
79
+ groupIndexes: [0],
80
+ subIndexes: [1],
81
+ parentGroupOfSub: new Map([[1, 0]]),
82
+ pad: 0,
83
+ subPinTop: 36,
84
+ });
85
+ // 子组头容器坐标 = 100 - st + 0;吸附位 36 → st = 64 越过
86
+ expect(r(63)?.sub).toBeNull();
87
+ expect(r(63)?.group).toEqual({ index: 0, offset: 0 });
88
+ expect(r(64)?.sub).toEqual({ index: 1, offset: 0 });
47
89
  });
48
90
 
49
- it("组内滚动但未到子组头:仍激活主组、无子组", () => {
50
- expect(compute(20)).toEqual({ groupIdx: 0, subIdx: null });
91
+ it("子组头被下一个子组头逐像素顶出(offset = 下一个头上缘容器坐标 - 72)", () => {
92
+ // 单组布局:G(0) S1(36) S2(200) —— S1 被 S2 顶出
93
+ const one = [
94
+ { start: 0, size: 36 },
95
+ { start: 36, size: 36 },
96
+ { start: 200, size: 36 },
97
+ ];
98
+ const r = (scrollTop: number) =>
99
+ computeFrozenHeader(scrollTop, one, {
100
+ groupIndexes: [0],
101
+ subIndexes: [1, 2],
102
+ parentGroupOfSub: new Map([
103
+ [1, 0],
104
+ [2, 0],
105
+ ]),
106
+ pad: 0,
107
+ subPinTop: 36,
108
+ });
109
+ // S2 上缘容器坐标 = 200 - st;触及 S1 底边(36+36=72)开始上推 → st = 128
110
+ expect(r(100)?.sub).toEqual({ index: 1, offset: 0 });
111
+ expect(r(128)?.sub).toEqual({ index: 1, offset: 0 });
112
+ expect(r(136)?.sub).toEqual({ index: 1, offset: -8 });
113
+ // S2 越过吸附位(st=164)时 S1 恰被完全顶出(-36,隐入主组头层之下)
114
+ expect(r(164)?.sub).toEqual({ index: 2, offset: 0 });
115
+ });
116
+
117
+ it("组切换边界:旧组头被顶出至恰好完全离场(offset=-36),新组头零跳变就位", () => {
118
+ // G2.start=240:scrollTop=240 时 G2 越过容器顶边成为活跃组头
119
+ expect(compute(240)).toEqual({
120
+ group: { index: 4, offset: 0 },
121
+ sub: { index: 5, offset: 0 },
122
+ });
123
+ // 切换前一瞬(scrollTop=239):G1 被顶出 offset = 240-239-36 = -35
124
+ expect(compute(239)?.group).toEqual({ index: 0, offset: -35 });
125
+ // 子组头随区段结束边界一起滑出:offset = 240-239-72 = -71
126
+ expect(compute(239)?.sub).toEqual({ index: 1, offset: -71 });
51
127
  });
52
128
 
53
- it("滚动到子组头:激活该子组", () => {
54
- expect(compute(36)).toEqual({ groupIdx: 0, subIdx: 1 });
129
+ it("主组头被下一个主组头顶出(offset = G2 上缘容器坐标 - 36)", () => {
130
+ // G2 上缘容器坐标 = 240 - st;触及 G1 底边(36)开始上推 → st = 204
131
+ expect(compute(204)?.group).toEqual({ index: 0, offset: 0 });
132
+ expect(compute(212)?.group).toEqual({ index: 0, offset: -8 });
133
+ expect(compute(230)?.group).toEqual({ index: 0, offset: -26 });
55
134
  });
56
135
 
57
- it("start == scrollTop + 1 边界:+1 容差视为已到达", () => {
58
- expect(compute(35)).toEqual({ groupIdx: 0, subIdx: 1 });
136
+ it("子组头随区段结束(下一个主组头)滑出,与主组头同步离场", () => {
137
+ // scrollTop=212:G2 上缘容器坐标 28;G1 顶出 -8;S1 顶出 28-72 = -44
138
+ expect(compute(212)?.sub).toEqual({ index: 1, offset: -44 });
59
139
  });
60
140
 
61
- it("跨到第二个主组:子组重置为 null(不属于该主组)", () => {
62
- expect(compute(200)).toEqual({ groupIdx: 3, subIdx: null });
141
+ it("主组头切换后子组头归属门:新组头下方不得钉旧组子组头", () => {
142
+ // scrollTop=241:G2/S2 均活跃;旧 S1(start 36)测量上早已越过但归属 G1
143
+ const r = compute(241);
144
+ expect(r.group).toEqual({ index: 4, offset: 0 });
145
+ expect(r.sub).toEqual({ index: 5, offset: 0 });
63
146
  });
64
147
 
65
- it("第二主组内滚动到其子组头:激活对应子组", () => {
66
- expect(compute(236)).toEqual({ groupIdx: 3, subIdx: 4 });
148
+ it("新组头尚无越过吸附位的子组头:sub 为 null(不钉旧组的子组头)", () => {
149
+ // 构造 G2 与其子组头间距大的布局:G2 越过时 S2 未到吸附位
150
+ const sparse = [
151
+ { start: 0, size: 36 },
152
+ { start: 36, size: 36 },
153
+ { start: 240, size: 36 },
154
+ { start: 400, size: 36 },
155
+ ];
156
+ const r = computeFrozenHeader(241, sparse, {
157
+ groupIndexes: [0, 2],
158
+ subIndexes: [1, 3],
159
+ parentGroupOfSub: new Map([
160
+ [1, 0],
161
+ [3, 2],
162
+ ]),
163
+ pad: 0,
164
+ subPinTop: 36,
165
+ });
166
+ expect(r.group).toEqual({ index: 2, offset: 0 });
167
+ // 旧 S1(index 1)归属 G0 ≠ 活跃 G2 → 排除;新 S3(index 3,start 400)未越过
168
+ expect(r.sub).toBeNull();
67
169
  });
68
170
 
69
- it("滚过末尾:冻结组头停在最后一个主组及其子组", () => {
70
- expect(compute(10000)).toEqual({ groupIdx: 3, subIdx: 4 });
171
+ it("滚过末尾:双层吸附头停在最后一层(无下一个顶出源,offset 恒 0)", () => {
172
+ expect(compute(10000)).toEqual({
173
+ group: { index: 4, offset: 0 },
174
+ sub: { index: 5, offset: 0 },
175
+ });
71
176
  });
72
177
 
73
- it("子组头属于其他主组时不激活(parent 判定生效)", () => {
74
- // st=236 时索引 1(start 36)虽已越过,但它属于 group0,不应被选
75
- expect(compute(236)?.subIdx).toBe(4);
178
+ it("负数 scrollTop(overscroll)钳制为 0:首个组头吸附 offset 0", () => {
179
+ expect(compute(-5)).toEqual({
180
+ group: { index: 0, offset: 0 },
181
+ sub: { index: 1, offset: 0 },
182
+ });
76
183
  });
77
184
 
78
- it("测量缺失的组头被忽略;全部缺失时返回 null", () => {
79
- expect(computeFrozenHeader(100, [{ start: 0 }], [0, 3], [1, 4], parentGroupOfSub)).toEqual({
80
- groupIdx: 0,
81
- subIdx: null,
185
+ it("测量缺失的组头被跳过;全部缺失时两层均为 null", () => {
186
+ const sparse = [{ start: 0, size: 36 }];
187
+ const r = computeFrozenHeader(10000, sparse, {
188
+ groupIndexes: [0, 4],
189
+ subIndexes: [1, 5],
190
+ parentGroupOfSub: parentOfSub,
191
+ pad: 0,
192
+ subPinTop: 36,
82
193
  });
83
- expect(computeFrozenHeader(0, [], [0, 3], [1, 4], parentGroupOfSub)).toBeNull();
194
+ expect(r.group).toEqual({ index: 0, offset: 0 });
195
+ // index 1 无测量 → 跳过;index 5 无测量 → 跳过
196
+ expect(r.sub).toBeNull();
197
+ expect(
198
+ computeFrozenHeader(0, [], { groupIndexes: [0], subIndexes: [1], parentGroupOfSub: new Map() }),
199
+ ).toEqual({ group: null, sub: null });
200
+ });
201
+
202
+ it("无任何组头(groupIndexes/subIndexes 空):两层均 null", () => {
203
+ expect(
204
+ computeFrozenHeader(100, measurements, {
205
+ groupIndexes: [],
206
+ subIndexes: [],
207
+ parentGroupOfSub: new Map(),
208
+ }),
209
+ ).toEqual({ group: null, sub: null });
84
210
  });
85
211
 
86
- it("负数 scrollTop(overscroll)不崩且激活首个主组", () => {
87
- expect(compute(-5)).toEqual({ groupIdx: 0, subIdx: null });
212
+ it("pad 参数(看板 p-2 口径):阈值整体平移,S≤7 不吸附、S=8 吸附", () => {
213
+ // kanban 列内布局:首个子组头 start 0(内容盒原点),pad 8
214
+ const kanban = [
215
+ { start: 0, size: 28 },
216
+ { start: 28, size: 84 },
217
+ { start: 200, size: 28 },
218
+ ];
219
+ const r = (scrollTop: number) =>
220
+ computeFrozenHeader(scrollTop, kanban, {
221
+ groupIndexes: [],
222
+ subIndexes: [0, 2],
223
+ parentGroupOfSub: new Map(),
224
+ pad: 8,
225
+ subPinTop: 0,
226
+ });
227
+ for (let s = 0; s <= 7; s++) expect(r(s)).toEqual({ group: null, sub: null });
228
+ expect(r(8)).toEqual({ group: null, sub: { index: 0, offset: 0 } });
229
+ expect(r(50)).toEqual({ group: null, sub: { index: 0, offset: 0 } });
230
+ });
231
+
232
+ it("顶出量逐像素变化:offset 随 scrollTop 线性变化(消费方 state 比较必须含 offset)", () => {
233
+ const o1 = compute(210).group?.offset;
234
+ const o2 = compute(211).group?.offset;
235
+ expect(o1).toBe(-6);
236
+ expect(o2).toBe(-7);
237
+ expect(o1).not.toBe(o2);
238
+ });
239
+
240
+ it("subPinTop 缺省 0(单层直接吸附容器顶边,看板口径)", () => {
241
+ const r = computeFrozenHeader(40, measurements, {
242
+ groupIndexes: [],
243
+ subIndexes: [1],
244
+ parentGroupOfSub: new Map(),
245
+ pad: 0,
246
+ });
247
+ // 子组头 start 36 <= scrollTop 40 → 吸附 offset 0
248
+ expect(r).toEqual({ group: null, sub: { index: 1, offset: 0 } });
249
+ expect(
250
+ computeFrozenHeader(35, measurements, {
251
+ groupIndexes: [],
252
+ subIndexes: [1],
253
+ parentGroupOfSub: new Map(),
254
+ pad: 0,
255
+ }).sub,
256
+ ).toBeNull();
88
257
  });
89
258
  });