@allbluecn/web-app 0.19.5 → 0.21.0

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 (235) hide show
  1. package/package.json +3 -3
  2. package/src/components/bug/__tests__/board-subgroups.test.tsx +1 -1
  3. package/src/components/bug/__tests__/board-view.test.tsx +1 -1
  4. package/src/components/bug/__tests__/comment-reactions.test.tsx +1 -1
  5. package/src/components/bug/__tests__/create-bug-dialog.test.tsx +2 -2
  6. package/src/components/bug/__tests__/filters-compat.test.tsx +1 -1
  7. package/src/components/bug/__tests__/fix-request-race.test.tsx +1 -1
  8. package/src/components/bug/__tests__/peek-modes.test.tsx +1 -1
  9. package/src/components/bug/__tests__/use-paged-bug-groups.test.tsx +1 -1
  10. package/src/components/bug/__tests__/view-type.test.tsx +1 -1
  11. package/src/components/bug/bug-list-view.tsx +2 -2
  12. package/src/components/bug/bug-toolbar.tsx +1 -1
  13. package/src/components/bug/create-bug-dialog.tsx +3 -3
  14. package/src/components/bug/detail/__tests__/activity-timeline.test.tsx +1 -1
  15. package/src/components/bug/detail/__tests__/bug-detail-collab.test.tsx +1 -1
  16. package/src/components/bug/detail/__tests__/repro-report-card.test.tsx +1 -1
  17. package/src/components/bug/detail/activity-tabs.tsx +34 -3
  18. package/src/components/bug/detail/activity-timeline.tsx +1 -1
  19. package/src/components/bug/detail/bug-detail-body.tsx +3 -3
  20. package/src/components/bug/detail/comment-input.tsx +1 -1
  21. package/src/components/bug/detail/comment-item.tsx +1 -1
  22. package/src/components/bug/detail/comment-reactions.tsx +1 -1
  23. package/src/components/bug/detail/comment-thread.tsx +1 -1
  24. package/src/components/bug/detail/props-section.tsx +2 -2
  25. package/src/components/bug/detail/repro-report-card.tsx +1 -1
  26. package/src/components/bug/detail/reproduce-steps-view.tsx +1 -1
  27. package/src/components/bug/display/use-paged-bug-groups.ts +2 -2
  28. package/src/components/bug/filters/field-configs.tsx +1 -1
  29. package/src/components/bug/inline-editors/assignee-editor.tsx +1 -1
  30. package/src/components/bug/peek-panel.tsx +2 -2
  31. package/src/components/bug/reproduce-steps-editor.tsx +1 -1
  32. package/src/components/bug/story-bugs-panel.tsx +10 -6
  33. package/src/components/bug/views/board-view.tsx +2 -2
  34. package/src/components/bug/views/bug-list/batch-toolbar.tsx +1 -1
  35. package/src/components/bug/views/bug-list/group.tsx +1 -1
  36. package/src/components/bug/views/bug-list/index.tsx +1 -1
  37. package/src/components/bug/views/bug-list/inline-bug-creator.tsx +1 -1
  38. package/src/components/bug/views/bug-list/row.tsx +2 -2
  39. package/src/components/bug/views/create-view-dialog.tsx +1 -1
  40. package/src/components/design/editor/versions/hooks/use-design-versions.ts +11 -4
  41. package/src/components/design/editor/versions/page-version-panel.tsx +30 -11
  42. package/src/components/harness/code/workspace-context.tsx +5 -1
  43. package/src/components/harness/code/workspace-picker.tsx +19 -6
  44. package/src/components/harness/composer/harness-composer.tsx +25 -3
  45. package/src/components/harness/composer/mention/mention-resolvers.ts +2 -0
  46. package/src/components/harness/composer/mention/mention-suggestion-menu.tsx +6 -3
  47. package/src/components/harness/composer/mention/use-mention-sources.ts +1 -1
  48. package/src/components/iteration/iteration-progress-bar.tsx +2 -2
  49. package/src/components/knowledge/public/legal-compliance-editor.tsx +1 -1
  50. package/src/components/layout/sidebar-03/nav-knowledge.tsx +1 -1
  51. package/src/components/layout/sidebar-03/nav-workspace.tsx +1 -1
  52. package/src/components/project/invite-member-dialog.tsx +2 -2
  53. package/src/components/project/project-member-manager.tsx +0 -3
  54. package/src/components/shared-kit/list-kit/kanban/__tests__/instance-key.test.ts +1 -1
  55. package/src/components/story/__tests__/constants.test.ts +1 -1
  56. package/src/components/story/__tests__/dependency-type-badge.test.ts +1 -1
  57. package/src/components/story/__tests__/peek-panel-patch-sync.test.tsx +1 -1
  58. package/src/components/story/__tests__/story-list-view-peek-sync.test.tsx +1 -1
  59. package/src/components/story/ai/story-ai-panel.tsx +1 -1
  60. package/src/components/story/ai/story-ai-preview-list.tsx +2 -2
  61. package/src/components/story/ai/use-story-ai-model.ts +17 -4
  62. package/src/components/story/columns.tsx +2 -2
  63. package/src/components/story/create-story-dialog.tsx +4 -4
  64. package/src/components/story/detail/__tests__/activity-timeline.test.tsx +1 -1
  65. package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +1 -1
  66. package/src/components/story/detail/__tests__/story-collab.test.tsx +1 -1
  67. package/src/components/story/detail/activity-tabs.tsx +1 -1
  68. package/src/components/story/detail/activity-timeline.tsx +36 -17
  69. package/src/components/story/detail/comment-input.tsx +1 -1
  70. package/src/components/story/detail/comment-item.tsx +1 -1
  71. package/src/components/story/detail/comment-thread.tsx +1 -1
  72. package/src/components/story/detail/reaction-bar.tsx +1 -1
  73. package/src/components/story/detail/story-detail-body.tsx +4 -4
  74. package/src/components/story/detail/story-detail-toolbar/__tests__/attachment-cache.test.ts +83 -0
  75. package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +50 -9
  76. package/src/components/story/detail/story-detail-toolbar/children.tsx +3 -3
  77. package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +9 -8
  78. package/src/components/story/detail/story-detail-toolbar/index.tsx +8 -8
  79. package/src/components/story/detail/story-detail-toolbar/related-test-cases.tsx +2 -2
  80. package/src/components/story/detail/story-properties.tsx +2 -2
  81. package/src/components/story/display/__tests__/flatten-story-groups.test.ts +1 -1
  82. package/src/components/story/display/__tests__/group-fields.test.ts +1 -1
  83. package/src/components/story/display/__tests__/group-resolver.test.ts +2 -2
  84. package/src/components/story/display/__tests__/order-resolver.test.ts +1 -1
  85. package/src/components/story/display/__tests__/use-grouped-stories.test.ts +1 -1
  86. package/src/components/story/display/__tests__/use-paged-story-groups.test.ts +1 -1
  87. package/src/components/story/display/flatten-story-groups.ts +1 -1
  88. package/src/components/story/display/group-fields.ts +2 -2
  89. package/src/components/story/display/group-resolver.tsx +4 -4
  90. package/src/components/story/display/order-resolver.ts +3 -3
  91. package/src/components/story/display/types.ts +5 -4
  92. package/src/components/story/display/use-grouped-stories.ts +5 -5
  93. package/src/components/story/display/use-paged-story-groups.ts +11 -4
  94. package/src/components/story/entity-overlay.ts +24 -0
  95. package/src/components/story/filters/field-configs.tsx +2 -2
  96. package/src/components/story/inline-editors/assignee-editor.tsx +1 -1
  97. package/src/components/story/inline-editors/dependency-type-select.tsx +1 -1
  98. package/src/components/story/inline-editors/parent-story-editor.tsx +2 -2
  99. package/src/components/story/inline-editors/priority-editor.tsx +2 -2
  100. package/src/components/story/inline-editors/state-editor.tsx +2 -2
  101. package/src/components/story/inline-editors/story-select-dialog.tsx +2 -2
  102. package/src/components/story/peek-panel.tsx +6 -5
  103. package/src/components/story/priority-icon.tsx +2 -2
  104. package/src/components/story/relations/__tests__/attachment-manage-dialog.test.tsx +1 -1
  105. package/src/components/story/relations/__tests__/design-select-dialog.test.tsx +1 -1
  106. package/src/components/story/relations/__tests__/knowledge-select-dialog.test.tsx +1 -1
  107. package/src/components/story/relations/__tests__/link-manage-dialog.test.tsx +1 -1
  108. package/src/components/story/relations/attachment-manage-dialog.tsx +4 -4
  109. package/src/components/story/relations/design-select-dialog.tsx +2 -2
  110. package/src/components/story/relations/knowledge-select-dialog.tsx +2 -2
  111. package/src/components/story/relations/link-manage-dialog.tsx +2 -2
  112. package/src/components/story/relations/story-ref-row.tsx +3 -3
  113. package/src/components/story/story-list-table.tsx +1 -1
  114. package/src/components/story/story-list-view.tsx +1 -1
  115. package/src/components/story/story-toolbar.tsx +1 -1
  116. package/src/components/story/transfer-story-popover.tsx +3 -2
  117. package/src/components/story/views/__tests__/calendar-view.test.tsx +1 -1
  118. package/src/components/story/views/__tests__/kanban-drop-subgroup.test.ts +1 -1
  119. package/src/components/story/views/__tests__/kanban-peek-sync.test.tsx +2 -2
  120. package/src/components/story/views/__tests__/kanban-view-membership-sync.test.tsx +1 -1
  121. package/src/components/story/views/__tests__/timeline-view.test.tsx +1 -1
  122. package/src/components/story/views/calendar/__tests__/mappers.test.ts +1 -1
  123. package/src/components/story/views/calendar/mappers.ts +1 -1
  124. package/src/components/story/views/calendar-view.tsx +1 -1
  125. package/src/components/story/views/create-view-dialog.tsx +1 -1
  126. package/src/components/story/views/kanban-view.tsx +2 -2
  127. package/src/components/story/views/story-list/batch-preview-dialog.tsx +2 -2
  128. package/src/components/story/views/story-list/batch-toolbar.tsx +3 -3
  129. package/src/components/story/views/story-list/group.tsx +18 -5
  130. package/src/components/story/views/story-list/index.tsx +4 -2
  131. package/src/components/story/views/story-list/inline-story-creator.tsx +2 -2
  132. package/src/components/story/views/story-list/row.tsx +2 -2
  133. package/src/components/story/views/story-list/use-flat-rows.ts +1 -1
  134. package/src/components/story/views/timeline/__tests__/mappers.test.ts +2 -2
  135. package/src/components/story/views/timeline/mappers.ts +2 -2
  136. package/src/components/story/views/timeline/use-date-commit.ts +1 -1
  137. package/src/components/story/views/timeline-view.tsx +2 -2
  138. package/src/components/testcase/__tests__/case-row.test.tsx +1 -1
  139. package/src/components/testcase/__tests__/create-dialog-scope.test.tsx +1 -1
  140. package/src/components/testcase/__tests__/display-options.test.tsx +1 -1
  141. package/src/components/testcase/__tests__/export-entry.test.tsx +1 -1
  142. package/src/components/testcase/__tests__/list-view.test.tsx +1 -1
  143. package/src/components/testcase/__tests__/step-editor.test.tsx +4 -2
  144. package/src/components/testcase/__tests__/toolbar.test.tsx +1 -1
  145. package/src/components/testcase/create-testcase-dialog.tsx +6 -5
  146. package/src/components/testcase/detail/case-detail-body.tsx +1 -1
  147. package/src/components/testcase/detail/properties.tsx +1 -1
  148. package/src/components/testcase/display/display-dropdown.tsx +2 -2
  149. package/src/components/testcase/display/use-testcase-display-options.ts +1 -1
  150. package/src/components/testcase/filters/field-configs.tsx +1 -1
  151. package/src/components/testcase/generation/draft-card.tsx +2 -2
  152. package/src/components/testcase/inline-editors/assignee-editor.tsx +1 -1
  153. package/src/components/testcase/list/case-row.tsx +1 -1
  154. package/src/components/testcase/list/grouped-list.tsx +1 -1
  155. package/src/components/testcase/list-view.tsx +2 -2
  156. package/src/components/testcase/runs/convert-bug-button.tsx +2 -2
  157. package/src/components/testcase/step-editor.tsx +13 -2
  158. package/src/components/testcase/testcase-toolbar.tsx +2 -2
  159. package/src/components/testcase/views/batch-toolbar.tsx +1 -1
  160. package/src/components/testcase/views/board-view.tsx +2 -2
  161. package/src/lib/changelog/sdk-versions.ts +2 -2
  162. package/src/lib/collections/knowledge/knowledge-bases-collection.ts +2 -1
  163. package/src/lib/collections/query-keys.ts +18 -0
  164. package/src/lib/collections/reference/users-brief-collection.ts +2 -1
  165. package/src/lib/collections/reference/workspaces-collection.ts +2 -1
  166. package/src/lib/story/__tests__/activity-grouper.test.ts +1 -1
  167. package/src/lib/story/activity-grouper.ts +1 -1
  168. package/src/paraglide/messages/_index.d.ts +2 -0
  169. package/src/paraglide/messages/_index.js +88269 -5642
  170. package/src/paraglide/messages/en.js +28255 -0
  171. package/src/paraglide/messages/story_group_loadfailed1.d.ts +17 -0
  172. package/src/paraglide/messages/story_group_loadfailed1.js +30 -0
  173. package/src/paraglide/messages/story_group_retry.d.ts +16 -0
  174. package/src/paraglide/messages/story_group_retry.js +29 -0
  175. package/src/paraglide/messages/zh.js +28253 -0
  176. package/src/routes/_nav/bugs/$bugId/route.lazy.tsx +2 -2
  177. package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.lazy.tsx +1 -1
  178. package/src/routes/_nav/route.tsx +1 -2
  179. package/src/routes/_nav/stories/$storyId/route.lazy.tsx +2 -2
  180. package/src/routes/_nav/stories/$storyId/route.tsx +1 -1
  181. package/src/routes/api/design/prototype/__tests__/route.test.ts +4 -1
  182. package/src/server/__tests__/task-executor.test.ts +7 -0
  183. package/src/server/ai-gateway/__tests__/ai-output-guard.test.ts +4 -4
  184. package/src/server/ai-gateway/__tests__/tools-authorization.test.ts +25 -0
  185. package/src/server/ai-gateway/tools/server.ts +24 -15
  186. package/src/server/knowledge/reindex-guard.ts +35 -0
  187. package/src/server/lib/__tests__/cron-auth.test.ts +1 -1
  188. package/src/server/serverFns/__tests__/legal-code-canonical.test.ts +169 -0
  189. package/src/server/serverFns/__tests__/story-tree.test.ts +0 -1
  190. package/src/server/serverFns/__tests__/story-types.test.ts +2 -1
  191. package/src/server/serverFns/__tests__/update-bug-guard.test.ts +1 -1
  192. package/src/server/serverFns/bug/__tests__/bug-comments.test.ts +95 -44
  193. package/src/server/serverFns/bug/__tests__/bug-crud.test.ts +46 -18
  194. package/src/server/serverFns/bug/__tests__/bug-groups.test.ts +1 -1
  195. package/src/server/serverFns/bug/bug-activities.ts +2 -2
  196. package/src/server/serverFns/bug/bug-comments.ts +44 -19
  197. package/src/server/serverFns/bug/bug-crud/detail-select.ts +1 -1
  198. package/src/server/serverFns/bug/bug-crud/detail.ts +2 -2
  199. package/src/server/serverFns/bug/bug-crud.ts +142 -110
  200. package/src/server/serverFns/bug/bug-group-query.ts +1 -1
  201. package/src/server/serverFns/bug/bug-groups.ts +2 -2
  202. package/src/server/serverFns/bug/bug-story-utils.ts +1 -1
  203. package/src/server/serverFns/chat/conversations.ts +27 -13
  204. package/src/server/serverFns/harness/github-workspace.ts +32 -25
  205. package/src/server/serverFns/harness/search-mention-candidates.ts +3 -1
  206. package/src/server/serverFns/iteration/iteration-crud.ts +1 -1
  207. package/src/server/serverFns/knowledge/public/legal/create.ts +3 -0
  208. package/src/server/serverFns/knowledge/public/legal/update.ts +8 -8
  209. package/src/server/serverFns/knowledge-index.ts +12 -5
  210. package/src/server/serverFns/project.ts +5 -11
  211. package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +15 -4
  212. package/src/server/serverFns/story/__tests__/story-transfer.test.ts +20 -0
  213. package/src/server/serverFns/story/link-preview.ts +3 -2
  214. package/src/server/serverFns/story/story-children.ts +1 -1
  215. package/src/server/serverFns/story/story-comments.ts +1 -1
  216. package/src/server/serverFns/story/story-crud/bulk.ts +1 -1
  217. package/src/server/serverFns/story/story-crud.ts +1 -1
  218. package/src/server/serverFns/story/story-dependencies.ts +61 -69
  219. package/src/server/serverFns/story/story-groups.ts +1 -1
  220. package/src/server/serverFns/story/story-transfer.ts +8 -20
  221. package/src/server/serverFns/testcase/__tests__/automation.test.ts +21 -0
  222. package/src/server/serverFns/testcase/__tests__/testcase-groups.test.ts +1 -1
  223. package/src/server/serverFns/testcase/automation.ts +24 -1
  224. package/src/server/serverFns/testcase/testcase-board.ts +1 -1
  225. package/src/server/serverFns/testcase/testcase-crud.ts +1 -1
  226. package/src/server/serverFns/testcase/testcase-display-options.ts +1 -1
  227. package/src/server/serverFns/testcase/testcase-filter-views.ts +1 -1
  228. package/src/server/serverFns/testcase/types.ts +1 -1
  229. package/src/server/story/types.ts +7 -3
  230. package/src/shared/bug/__tests__/constants.test.ts +1 -1
  231. package/src/{components → shared}/bug/types.ts +4 -3
  232. package/src/{components → shared}/story/constants.ts +1 -1
  233. package/src/{components → shared}/story/types.ts +1 -7
  234. package/src/types/project.ts +1 -2
  235. /package/src/{components/testcase/display → shared/testcase}/field-configs.ts +0 -0
@@ -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 { useState, useRef, useLayoutEffect, useEffect } from "react";
18
+ import { useState, useRef, useLayoutEffect, useEffect, useMemo } from "react";
19
19
  import type { ReactNode } from "react";
20
20
  import { UserAvatar } from "@/components/shared-kit";
21
21
  import {
@@ -28,10 +28,10 @@ import {
28
28
  PRIORITY_ICON,
29
29
  type StoryState,
30
30
  type StoryPriority,
31
- } from "@/components/story/constants";
31
+ } from "@/shared/story/constants";
32
32
  import { LucideIconByName } from "@/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name";
33
33
  import { listProjectsBriefFn } from "@/server/serverFns/project";
34
- import type { ActivityItem, UserBrief } from "@/components/story/types";
34
+ import type { ActivityItem, UserBrief } from "@/shared/story/types";
35
35
  import { groupActivities, type ActivityGroup } from "@/lib/story/activity-grouper";
36
36
  import { ArrowUpFromDot, ArrowRight, ChevronDown, Dot, Sparkles, Layers, UserRound } from "lucide-react";
37
37
  import { sanitize } from "@/lib/sanitize";
@@ -422,18 +422,37 @@ interface DiffState {
422
422
 
423
423
  export function ActivityTimeline({ activities, users }: ActivityTimelineProps) {
424
424
  const [diffStates, setDiffStates] = useState<Record<string, DiffState>>({});
425
- const [projectIconByName, setProjectIconByName] = useState<Record<string, string>>({});
425
+ const [projectBriefs, setProjectBriefs] = useState<Array<{ id: string; name: string; icon: string }>>([]);
426
426
 
427
427
  useEffect(() => {
428
428
  listProjectsBriefFn()
429
- .then((rows) => {
430
- const map: Record<string, string> = {};
431
- for (const r of rows) map[r.name] = r.icon;
432
- setProjectIconByName(map);
433
- })
429
+ .then(setProjectBriefs)
434
430
  .catch(() => { });
435
431
  }, []);
436
432
 
433
+ // S13:project_transferred 的 value 已改为存项目 id;id map 供新值解析名称/图标,
434
+ // name map 兼容存量行(value=项目名)回退命中——重名错配仅残留于存量数据
435
+ const { projectById, projectIconByName } = useMemo(() => {
436
+ const byId = new Map<string, { name: string; icon: string }>();
437
+ const byName = new Map<string, string>();
438
+ for (const p of projectBriefs) {
439
+ byId.set(p.id, { name: p.name, icon: p.icon });
440
+ if (!byName.has(p.name)) byName.set(p.name, p.icon);
441
+ }
442
+ return { projectById: byId, projectIconByName: byName };
443
+ }, [projectBriefs]);
444
+
445
+ // 仅 project_transferred 走 id→名 解析(其余动作值不进 id map,防误命中)
446
+ const displayValue = (action: string, value: string | null): string | null => {
447
+ if (action !== "project_transferred" || !value) return value;
448
+ return projectById.get(value)?.name ?? value;
449
+ };
450
+ const resolveProjectIcon = (action: string, value: string | null | undefined): string | undefined => {
451
+ if (!value) return undefined;
452
+ if (action === "project_transferred") return projectById.get(value)?.icon ?? projectIconByName.get(value);
453
+ return projectIconByName.get(value);
454
+ };
455
+
437
456
  if (activities.length === 0) {
438
457
  return (
439
458
  <p className="py-8 text-center text-sm text-muted-foreground">
@@ -548,16 +567,16 @@ export function ActivityTimeline({ activities, users }: ActivityTimelineProps) {
548
567
  </div>
549
568
  {hasDiff ? (
550
569
  <DiffCompare
551
- oldValue={formatDiffValue(a.action, g.displayOldValue!)}
552
- newValue={formatDiffValue(a.action, g.displayNewValue!)}
553
- oldDecorator={getDiffDecorator(a.action, g.displayOldValue, projectIconByName[g.displayOldValue ?? ""], users?.find((u) => u.username === g.displayOldValue))}
554
- newDecorator={getDiffDecorator(a.action, g.displayNewValue, projectIconByName[g.displayNewValue ?? ""], users?.find((u) => u.username === g.displayNewValue))}
570
+ oldValue={formatDiffValue(a.action, displayValue(a.action, g.displayOldValue)!)}
571
+ newValue={formatDiffValue(a.action, displayValue(a.action, g.displayNewValue)!)}
572
+ oldDecorator={getDiffDecorator(a.action, displayValue(a.action, g.displayOldValue), resolveProjectIcon(a.action, g.displayOldValue), users?.find((u) => u.username === g.displayOldValue))}
573
+ newDecorator={getDiffDecorator(a.action, displayValue(a.action, g.displayNewValue), resolveProjectIcon(a.action, g.displayNewValue), users?.find((u) => u.username === g.displayNewValue))}
555
574
  plain={a.action !== "description_changed"}
556
575
  blocks={isBlocks ? g.items.map((it) => ({
557
- oldValue: formatDiffValue(a.action, it.oldValue),
558
- newValue: formatDiffValue(a.action, it.newValue),
559
- oldDecorator: getDiffDecorator(a.action, it.oldValue, projectIconByName[it.oldValue ?? ""], users?.find((u) => u.username === it.oldValue)),
560
- newDecorator: getDiffDecorator(a.action, it.newValue, projectIconByName[it.newValue ?? ""], users?.find((u) => u.username === it.newValue)),
576
+ oldValue: formatDiffValue(a.action, displayValue(a.action, it.oldValue)),
577
+ newValue: formatDiffValue(a.action, displayValue(a.action, it.newValue)),
578
+ oldDecorator: getDiffDecorator(a.action, displayValue(a.action, it.oldValue), resolveProjectIcon(a.action, it.oldValue), users?.find((u) => u.username === it.oldValue)),
579
+ newDecorator: getDiffDecorator(a.action, displayValue(a.action, it.newValue), resolveProjectIcon(a.action, it.newValue), users?.find((u) => u.username === it.newValue)),
561
580
  })) : undefined}
562
581
  collapsed={state.collapsed}
563
582
  isTall={state.isTall}
@@ -21,7 +21,7 @@ import { Button } from "@/components/ui/button";
21
21
  import { Textarea } from "@/components/ui/textarea";
22
22
  import { createCommentFn } from "@/server/serverFns/story";
23
23
  import { toast } from "@/components/ui/toast";
24
- import type { CommentWithReplies } from "../types";
24
+ import type { CommentWithReplies } from "@/shared/story/types";
25
25
  import { m } from "@/paraglide/messages.js";
26
26
 
27
27
  interface CommentInputProps {
@@ -29,7 +29,7 @@ import {
29
29
  } from "@/components/ui/dropdown-menu";
30
30
  import { deleteCommentFn, updateCommentFn } from "@/server/serverFns/story";
31
31
  import { toast } from "@/components/ui/toast";
32
- import type { CommentWithReplies } from "../types";
32
+ import type { CommentWithReplies } from "@/shared/story/types";
33
33
  import { m } from "@/paraglide/messages.js";
34
34
 
35
35
  interface CommentItemProps {
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { CommentItem } from "./comment-item";
19
19
  import { CommentInput } from "./comment-input";
20
- import type { CommentWithReplies } from "../types";
20
+ import type { CommentWithReplies } from "@/shared/story/types";
21
21
  import { m } from '@/paraglide/messages.js'
22
22
  import { formatTimestamp } from "@/lib/datetime";
23
23
 
@@ -19,7 +19,7 @@ import { Plus } from "lucide-react";
19
19
  import { Button } from "@/components/ui/button";
20
20
  import { cn } from "@/lib/utils";
21
21
  import { EmojiPicker } from "@/components/shared-kit/comments/emoji-picker";
22
- import type { ReactionGroup } from "@/components/story/types";
22
+ import type { ReactionGroup } from "@/shared/story/types";
23
23
  import { m } from "@/paraglide/messages.js";
24
24
 
25
25
  interface ReactionBarProps {
@@ -35,9 +35,9 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
35
35
  import { updateStoryFn, listStoryReactionsFn, toggleStoryReactionFn } from "@/server/serverFns/story";
36
36
  import { formatTimestamp } from "@/lib/datetime";
37
37
  import { toast } from "@/components/ui/toast";
38
- import type { StoryDetail, UserBrief, CommentWithReplies, ActivityItem, ReactionGroup, RelationsData } from "../types";
39
- import { parseStoryDescription, serializeStoryDescription } from "../types";
40
- import type { StoryState } from "../constants";
38
+ import type { StoryDetail, UserBrief, CommentWithReplies, ActivityItem, ReactionGroup, RelationsData } from "@/shared/story/types";
39
+ import { parseStoryDescription, serializeStoryDescription } from "@/shared/story/types";
40
+ import type { StoryState } from "@/shared/story/constants";
41
41
  import type { StoryPatch } from "../display/group-fields";
42
42
  import { ActivityTabs } from "./activity-tabs";
43
43
  import { EmojiPicker } from "@/components/shared-kit/comments/emoji-picker";
@@ -233,7 +233,7 @@ export interface StoryDetailBodyProps {
233
233
  /** 字段落库成功后的补丁回调(PeekPanel 回写列表/看板做乐观更新) */
234
234
  onStoryPatch?: (storyId: string, patch: StoryPatch) => void;
235
235
  onNavigateInPanel?: (targetId: string, targetIdentifier?: string, relationKind?: "child" | "dependency") => void;
236
- onOpenEntity?: (entity: import("../types").StoryEntityOverlay) => void;
236
+ onOpenEntity?: (entity: import("@/components/story/entity-overlay").StoryEntityOverlay) => void;
237
237
  /** 用例/BUG 行的面板内叠加打开(peek 传入;缺省整页跳转) */
238
238
  /** 内嵌面板导航面包屑(在面板内跳转时替代单层 identifier 显示) */
239
239
  breadcrumbItems?: Array<{ identifier?: string; projectId?: string; label: string; onClick?: () => void }>;
@@ -0,0 +1,83 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ // S12(审计 A7x-14 ⚪)附件面板级缓存回归:
19
+ // - 同 storageUrl 并发/跨实例只 fetch 一次(网格 + 全屏弹窗复用);
20
+ // - LRU 上限淘汰最旧条目;
21
+ // - 超大附件不驻留内存;
22
+ // - 失败不缓存,下次重试。
23
+ // 注:jsdom 的 Response 会把 Blob 体当字符串化,测试统一用文本体控制字节数。
24
+
25
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
26
+ import { loadAttachmentFile } from "../associated-attachments-panel";
27
+ import type { StoryAttachmentItem } from "@/shared/story/types";
28
+
29
+ const attachment = (over: Partial<StoryAttachmentItem> = {}): StoryAttachmentItem => ({
30
+ id: "att-1",
31
+ fileName: "a.png",
32
+ fileSize: 10,
33
+ mimeType: "image/png",
34
+ storageUrl: "https://cdn.test/att-1",
35
+ ...over,
36
+ });
37
+
38
+ const fetchMock = vi.fn(async () => new Response("{}", { status: 200 }));
39
+
40
+ beforeEach(() => {
41
+ vi.stubGlobal("fetch", fetchMock);
42
+ });
43
+
44
+ afterEach(() => {
45
+ vi.unstubAllGlobals();
46
+ vi.restoreAllMocks();
47
+ });
48
+
49
+ describe("loadAttachmentFile(S12 面板级缓存)", () => {
50
+ it("同 storageUrl 两次调用只发一次 fetch,File 名/类型正确", async () => {
51
+ const [f1, f2] = await Promise.all([
52
+ loadAttachmentFile(attachment()),
53
+ loadAttachmentFile(attachment()),
54
+ ]);
55
+
56
+ expect(fetchMock).toHaveBeenCalledTimes(1);
57
+ expect(f1.name).toBe("a.png");
58
+ expect(f1.type).toBe("image/png");
59
+ expect(f2).toBe(f1);
60
+ });
61
+
62
+ it("超过 20MB 的大附件不驻留缓存(下次重拉)", async () => {
63
+ const big = attachment({ id: "att-big", storageUrl: "https://cdn.test/big" });
64
+ const BIG = "x".repeat(20 * 1024 * 1024 + 1);
65
+ fetchMock.mockImplementation(async () => new Response(BIG, { status: 200 }));
66
+
67
+ await loadAttachmentFile(big);
68
+ await loadAttachmentFile(big);
69
+
70
+ expect(fetchMock).toHaveBeenCalledTimes(2);
71
+ });
72
+
73
+ it("失败不缓存: rejected 后再次调用重新 fetch", async () => {
74
+ const bad = attachment({ id: "att-bad", storageUrl: "https://cdn.test/bad" });
75
+ fetchMock.mockImplementationOnce(async () => new Response("no", { status: 404 }));
76
+ fetchMock.mockImplementationOnce(async () => new Response("abcd", { status: 200 }));
77
+
78
+ await expect(loadAttachmentFile(bad)).rejects.toThrow("加载失败");
79
+ const ok = await loadAttachmentFile(bad);
80
+ expect(ok.size).toBe(4);
81
+ expect(fetchMock).toHaveBeenCalledTimes(2);
82
+ });
83
+ });
@@ -49,7 +49,7 @@ import { sanitize } from "@/lib/sanitize";
49
49
  import { formatFileSize, getFaviconUrl, getFileTypeIcon, extractLinkTitle, getMimeTypeLabel, getMimeTypeLabelKey, getMimeTypeLabelByKey, MIME_TYPE_LABEL_ORDER, type MimeTypeLabelKey } from "../../relations/format";
50
50
  import { generalGroupLabel } from "../../relations/general-groups";
51
51
  import { LucideIconByName } from "@/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name";
52
- import type { AssociatedItem, RelationsData, StoryLinkItem, StoryAttachmentItem } from "../../types";
52
+ import type { AssociatedItem, RelationsData, StoryLinkItem, StoryAttachmentItem } from "@/shared/story/types";
53
53
  import { m } from "@/paraglide/messages.js";
54
54
  import { toastError } from "@/lib/toast-error";
55
55
  import { useStoryPanelOpen } from "@/lib/hooks/use-story-panel-open";
@@ -61,7 +61,7 @@ export interface AssociatedAttachmentsPanelProps {
61
61
  data: RelationsData;
62
62
  onRefresh: (next: (prev: RelationsData) => RelationsData) => void;
63
63
  onOpenDialog: (kind: AssociatedDialogKind, editLink?: StoryLinkItem, editAttachment?: StoryAttachmentItem) => void;
64
- onAppendActivities?: (items: import("../../types").ActivityItem[]) => void;
64
+ onAppendActivities?: (items: import("@/shared/story/types").ActivityItem[]) => void;
65
65
  }
66
66
 
67
67
  // 记住列表/预览显示方式
@@ -70,20 +70,61 @@ const VIEW_MODE_STORAGE_KEY = "associated-attachments-view-mode";
70
70
  // 全屏浮窗尺寸与 peek-panel 面板的 full-screen 模式一致(四边 inset-4)
71
71
  const FULLSCREEN_CONTENT_CLASS = "inset-4 w-auto h-auto sm:max-w-none";
72
72
 
73
+ // ===== 附件面板级缓存(S12,审计 A7x-14 ⚪)=====
74
+ // 网格视图与全屏弹窗各自挂 AttachmentFileView,此前对同一附件重复 fetch。
75
+ // 以 storageUrl 为键缓存 in-flight/已完成 的 File Promise:并发去重 + 跨实例复用。
76
+ // 内存策略:LRU 上限 12 条;超过 20MB 的大附件不驻留(走浏览器 HTTP 缓存),
77
+ // 失败的 Promise 不缓存(下次挂载可重试)。File 由 GC 回收,无需手动 revoke。
78
+ const MAX_CACHED_ATTACHMENTS = 12;
79
+ const MAX_CACHEABLE_BYTES = 20 * 1024 * 1024;
80
+
81
+ const attachmentFileCache = new Map<string, Promise<File>>();
82
+
83
+ async function fetchAttachmentFile(attachment: StoryAttachmentItem): Promise<File> {
84
+ const res = await fetch(attachment.storageUrl);
85
+ if (!res.ok) throw new Error("加载失败");
86
+ const blob = await res.blob();
87
+ return new File([blob], attachment.fileName, { type: attachment.mimeType });
88
+ }
89
+
90
+ export function loadAttachmentFile(attachment: StoryAttachmentItem): Promise<File> {
91
+ const hit = attachmentFileCache.get(attachment.storageUrl);
92
+ if (hit) {
93
+ // LRU touch:删除重插维护插入序
94
+ attachmentFileCache.delete(attachment.storageUrl);
95
+ attachmentFileCache.set(attachment.storageUrl, hit);
96
+ return hit;
97
+ }
98
+ // 先以 in-flight Promise 占位(并发去重),完成后按大小决定去留
99
+ const promise = fetchAttachmentFile(attachment).then((file) => {
100
+ if (file.size > MAX_CACHEABLE_BYTES) {
101
+ // 大附件不驻留:本次直返,后续重拉走浏览器 HTTP 缓存
102
+ attachmentFileCache.delete(attachment.storageUrl);
103
+ } else {
104
+ attachmentFileCache.set(attachment.storageUrl, promise);
105
+ while (attachmentFileCache.size > MAX_CACHED_ATTACHMENTS) {
106
+ const oldest = attachmentFileCache.keys().next().value;
107
+ if (oldest === undefined) break;
108
+ attachmentFileCache.delete(oldest);
109
+ }
110
+ }
111
+ return file;
112
+ });
113
+ attachmentFileCache.set(attachment.storageUrl, promise);
114
+ promise.catch(() => attachmentFileCache.delete(attachment.storageUrl));
115
+ return promise;
116
+ }
117
+
73
118
  function useAttachmentFile(attachment: StoryAttachmentItem) {
74
119
  const [file, setFile] = React.useState<File | null>(null);
75
120
  const [error, setError] = React.useState<string | null>(null);
76
121
 
77
122
  React.useEffect(() => {
78
123
  let cancelled = false;
79
- fetch(attachment.storageUrl)
80
- .then((res) => {
81
- if (!res.ok) throw new Error("加载失败");
82
- return res.blob();
83
- })
84
- .then((blob) => {
124
+ loadAttachmentFile(attachment)
125
+ .then((f) => {
85
126
  if (cancelled) return;
86
- setFile(new File([blob], attachment.fileName, { type: attachment.mimeType }));
127
+ setFile(f);
87
128
  })
88
129
  .catch((e) => {
89
130
  if (cancelled) return;
@@ -47,7 +47,7 @@ import { VirtualGroupHeader } from "../../views/story-list/group";
47
47
  import { DisplayOptionsBody } from "../../display/display-dropdown";
48
48
  import { StoryRefRow } from "../../relations/story-ref-row";
49
49
  import type { DisplayOptions } from "../../display/types";
50
- import type { RelationsData, StoryListItem, UserBrief } from "../../types";
50
+ import type { RelationsData, StoryListItem, UserBrief } from "@/shared/story/types";
51
51
  import type { StoryPatch } from "../../display/group-fields";
52
52
  import { m } from "@/paraglide/messages.js";
53
53
  import { toastError } from "@/lib/toast-error";
@@ -284,9 +284,9 @@ export function StoryDetailChildrenPanel({
284
284
  }
285
285
  };
286
286
 
287
- const handleTransfer = (updatedStories?: Array<{ id: string; identifier: string; projectId: string; parentId: string | null }>) => {
287
+ const handleTransfer = (updatedStories?: Array<{ id: string; identifier: string; projectId: string | null; parentId: string | null }>) => {
288
288
  if (!updatedStories?.length) return;
289
- const updatedMap = new Map<string, { identifier: string; projectId: string }>();
289
+ const updatedMap = new Map<string, { identifier: string; projectId: string | null }>();
290
290
  for (const s of updatedStories) {
291
291
  updatedMap.set(s.id, { identifier: s.identifier, projectId: s.projectId });
292
292
  }
@@ -29,7 +29,7 @@ import {
29
29
  bulkDeleteStoriesFn,
30
30
  bulkUpdateStoriesFn,
31
31
  } from "@/server/serverFns/story";
32
- import { DEPENDENCY_TYPE_OPTIONS, toDependencyEdge, type DependencyTypeOption } from "../../constants";
32
+ import { DEPENDENCY_TYPE_OPTIONS, toDependencyEdge, type DependencyTypeOption } from "@/shared/story/constants";
33
33
  import { BatchToolbar } from "../../views/story-list/batch-toolbar";
34
34
  import { useGroupedStories } from "../../display/use-grouped-stories";
35
35
  import { VirtualGroupHeader } from "../../views/story-list/group";
@@ -37,7 +37,8 @@ import { DisplayOptionsBody } from "../../display/display-dropdown";
37
37
  import { StoryRefRow } from "../../relations/story-ref-row";
38
38
  import type { DisplayOptions, GroupNode } from "../../display/types";
39
39
  import type { StoryPatch } from "../../display/group-fields";
40
- import type { RelationsData, UserBrief, DependencyRow } from "../../types";
40
+ import type { StoryRelationPatch } from "../../relations/story-ref-row";
41
+ import type { RelationsData, UserBrief, DependencyRow } from "@/shared/story/types";
41
42
  import { m } from "@/paraglide/messages.js";
42
43
  import { toastError } from "@/lib/toast-error";
43
44
  import {
@@ -77,7 +78,7 @@ export interface StoryDetailDependencyPanelProps {
77
78
  projects?: { id: string; name: string; icon: string; displayId: string | null }[];
78
79
  labels?: { id: string; name: string; color: string }[];
79
80
  onOpenAdd: () => void;
80
- onAppendActivities?: (items: import("../../types").ActivityItem[]) => void;
81
+ onAppendActivities?: (items: import("@/shared/story/types").ActivityItem[]) => void;
81
82
  onNavigateInPanel?: (targetId: string, targetIdentifier?: string, relationKind?: "child" | "dependency") => void;
82
83
  }
83
84
 
@@ -377,13 +378,13 @@ export function StoryDetailDependencyPanel({
377
378
  return count > 0 ? "indeterminate" : false;
378
379
  };
379
380
 
380
- const handleStoryRefresh = (rowId: string, patch: Record<string, unknown>) => {
381
+ const handleStoryRefresh = (rowId: string, patch: StoryRelationPatch) => {
381
382
  for (const group of ["dependsOn", "dependents"] as const) {
382
383
  if (patch.deleted) {
383
384
  applyRowsUpdate((rows) => rows.filter((r) => r.id !== rowId), group);
384
385
  } else {
385
386
  applyRowsUpdate(
386
- (rows) => rows.map((r) => (r.id === rowId ? ({ ...r, ...patch } as DependencyRow) : r)),
387
+ (rows) => rows.map((r) => (r.id === rowId ? { ...r, ...patch } : r)),
387
388
  group,
388
389
  );
389
390
  }
@@ -420,7 +421,7 @@ export function StoryDetailDependencyPanel({
420
421
  />
421
422
  );
422
423
 
423
- const renderGroupItems = (node: GroupNode, group: DependencySection, depth: number, collapseKey: string) => {
424
+ const renderGroupItems = (node: GroupNode<DependencyRow>, group: DependencySection, depth: number, collapseKey: string) => {
424
425
  const isCollapsed = collapsed[collapseKey] ?? false;
425
426
  const ids = node.items.map((s) => s.id);
426
427
  return (
@@ -461,7 +462,7 @@ export function StoryDetailDependencyPanel({
461
462
  ),
462
463
  )
463
464
  ) : (
464
- node.items.map((s) => renderRow(s as DependencyRow, group))
465
+ node.items.map((s) => renderRow(s, group))
465
466
  )}
466
467
  </React.Fragment>
467
468
  )}
@@ -473,7 +474,7 @@ export function StoryDetailDependencyPanel({
473
474
  titleKey: string,
474
475
  sectionKey: DependencySection,
475
476
  rows: DependencyRow[],
476
- groups: GroupNode[],
477
+ groups: GroupNode<DependencyRow>[],
477
478
  ) => {
478
479
  const isCollapsed = collapsed[`${sectionKey}:root`] ?? false;
479
480
  return (
@@ -50,9 +50,9 @@ import { getLinksFn, getAttachmentsFn, getLinkedDocsFn, getLinkedDesignsFn } fro
50
50
  import { StorySelectDialog } from "../../inline-editors/story-select-dialog";
51
51
  import { CreateStoryDialog } from "../../create-story-dialog";
52
52
  import { addDependenciesFn, getDependenciesFn, listChildStoriesFn } from "@/server/serverFns/story";
53
- import { DEPENDENCY_TYPE_OPTIONS, toDependencyEdge, type DependencyTypeOption } from "../../constants";
53
+ import { DEPENDENCY_TYPE_OPTIONS, toDependencyEdge, type DependencyTypeOption } from "@/shared/story/constants";
54
54
  import { useToolbarOverflow } from "../../ai/use-toolbar-overflow";
55
- import type { RelationsData, UserBrief } from "../../types";
55
+ import type { RelationsData, UserBrief } from "@/shared/story/types";
56
56
  import { m } from "@/paraglide/messages.js";
57
57
  import { toastError } from "@/lib/toast-error";
58
58
 
@@ -72,13 +72,13 @@ interface StoryDetailToolbarProps {
72
72
  data: RelationsData;
73
73
  onRefresh: (next: (prev: RelationsData) => RelationsData) => void;
74
74
  /** 关联类操作产生的活动即时追加到时间线 */
75
- onAppendActivities?: (items: import("../../types").ActivityItem[]) => void;
75
+ onAppendActivities?: (items: import("@/shared/story/types").ActivityItem[]) => void;
76
76
  users?: UserBrief[];
77
77
  projects?: { id: string; name: string; icon: string; displayId: string | null }[];
78
78
  labels?: { id: string; name: string; color: string }[];
79
79
  onNavigateInPanel?: (targetId: string, targetIdentifier?: string, relationKind?: "child" | "dependency") => void;
80
80
  /** 用例/BUG 行的面板内叠加打开(peek 传入;缺省整页跳转) */
81
- onOpenEntity?: (entity: import("../../types").StoryEntityOverlay) => void;
81
+ onOpenEntity?: (entity: import("@/components/story/entity-overlay").StoryEntityOverlay) => void;
82
82
  }
83
83
 
84
84
  export function StoryDetailToolbar({ storyId, storyIdentifier, projectId, storyUpdatedAt, data, onRefresh, onAppendActivities, users, projects, labels, onNavigateInPanel, onOpenEntity }: StoryDetailToolbarProps) {
@@ -86,8 +86,8 @@ export function StoryDetailToolbar({ storyId, storyIdentifier, projectId, storyU
86
86
  const [dependencyDialogOpen, setDependencyDialogOpen] = React.useState(false);
87
87
  const [assocDialogOpen, setAssocDialogOpen] = React.useState<AssociatedDialogKind | null>(null);
88
88
  const [designDialog, setDesignDialog] = React.useState<{ open: boolean; mode: DesignSelectMode }>({ open: false, mode: "design" });
89
- const [linkInitialEdit, setLinkInitialEdit] = React.useState<import("../../types").StoryLinkItem | null>(null);
90
- const [editAttachment, setEditAttachment] = React.useState<import("../../types").StoryAttachmentItem | null>(null);
89
+ const [linkInitialEdit, setLinkInitialEdit] = React.useState<import("@/shared/story/types").StoryLinkItem | null>(null);
90
+ const [editAttachment, setEditAttachment] = React.useState<import("@/shared/story/types").StoryAttachmentItem | null>(null);
91
91
  const [dependencyType, setDependencyType] = React.useState<DependencyTypeOption["key"] | undefined>(undefined);
92
92
  // 关联测试用例提交成功 → bump 刷新测试用例面板(面板数据自持,refreshKey 变化重拉)
93
93
  const [casesRefreshKey, setCasesRefreshKey] = React.useState(0);
@@ -226,7 +226,7 @@ export function StoryDetailToolbar({ storyId, storyIdentifier, projectId, storyU
226
226
 
227
227
  const handleCreateSuccess = (
228
228
  _story: { id: string; name: string; identifier?: string },
229
- activities?: import("../../types").ActivityItem[],
229
+ activities?: import("@/shared/story/types").ActivityItem[],
230
230
  ) => {
231
231
  setActionOpen(null);
232
232
  refreshChildren();
@@ -273,7 +273,7 @@ export function StoryDetailToolbar({ storyId, storyIdentifier, projectId, storyU
273
273
  setDesignDialog({ open: true, mode });
274
274
  };
275
275
 
276
- const handleOpenAssocDialog = (kind: AssociatedDialogKind, editLink?: import("../../types").StoryLinkItem, editAttachmentItem?: import("../../types").StoryAttachmentItem) => {
276
+ const handleOpenAssocDialog = (kind: AssociatedDialogKind, editLink?: import("@/shared/story/types").StoryLinkItem, editAttachmentItem?: import("@/shared/story/types").StoryAttachmentItem) => {
277
277
  if (kind === "design") {
278
278
  handleOpenDesignDialog("design");
279
279
  return;
@@ -75,7 +75,7 @@ import { TEST_CASE_PRIORITY_META } from "@/components/testcase/constants";
75
75
  import { extractTextColors } from "@/components/shared-kit/list-kit/level-badge";
76
76
  import { GenerateTestCasesDialog } from "@/components/testcase/ai/generate-dialog";
77
77
  import { SelectCasesDialog } from "@/components/testcase/detail/select-cases-dialog";
78
- import type { UserBrief } from "../../types";
78
+ import type { UserBrief } from "@/shared/story/types";
79
79
  import { m } from "@/paraglide/messages.js";
80
80
  import { toast } from "@/components/ui/toast";
81
81
  import { toastError } from "@/lib/toast-error";
@@ -256,7 +256,7 @@ export function StoryDetailTestCases({
256
256
  /** 需求最近更新时间:变化即重判 stale(编辑保存后即时刷新提示) */
257
257
  storyUpdatedAt?: string | null;
258
258
  /** peek 内叠加打开(传入时点击行在 peek 内打开,缺省整页跳转) */
259
- onOpenEntity?: (entity: import("../../types").StoryEntityOverlay) => void;
259
+ onOpenEntity?: (entity: import("@/components/story/entity-overlay").StoryEntityOverlay) => void;
260
260
  }) {
261
261
  const [panelOpen, setPanelOpen] = useStoryPanelOpen(storyId, "testcases");
262
262
  const navigate = useNavigate();
@@ -18,8 +18,8 @@
18
18
  import { GitPullRequestArrow, Tag, CalendarRange } from "lucide-react";
19
19
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
20
20
  import { ChevronDown } from "lucide-react";
21
- import type { StoryState } from "../constants";
22
- import type { ActivityItem } from "../types";
21
+ import type { StoryState } from "@/shared/story/constants";
22
+ import type { ActivityItem } from "@/shared/story/types";
23
23
  import { ParentStoryEditor } from "../inline-editors/parent-story-editor";
24
24
  import { LabelsEditor } from "../inline-editors/labels-editor";
25
25
  import { IterationEditor } from "@/components/iteration/iteration-editor";
@@ -18,7 +18,7 @@
18
18
  // 回归:peek prev/next 须按当前视图展示顺序(分组 → 子组)导航,而非平坦 stories.items。
19
19
  import { describe, expect, it } from "vitest";
20
20
  import { flattenStoryGroups } from "../flatten-story-groups";
21
- import type { StoryListItem } from "../../types";
21
+ import type { StoryListItem } from "@/shared/story/types";
22
22
 
23
23
  const s = (id: string) => ({ id } as StoryListItem);
24
24
 
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { describe, it, expect } from "vitest";
19
19
  import { GROUP_FIELD, toGroupFieldPatch, applyStoryPatchToItem } from "../group-fields";
20
- import type { StoryListItem, UserBrief } from "../../types";
20
+ import type { StoryListItem, UserBrief } from "@/shared/story/types";
21
21
 
22
22
  describe("GROUP_FIELD", () => {
23
23
  it("state 映射到 state", () => {
@@ -24,9 +24,9 @@ import {
24
24
  PRIORITY_COLUMN_TINT,
25
25
  STATE_COLUMN_TINT,
26
26
  } from "../group-resolver";
27
- import { STATE_COLOR } from "../../constants";
27
+ import { STATE_COLOR } from "@/shared/story/constants";
28
28
  import { PRIORITY_COLOR } from "../../priority-icon";
29
- import type { StoryListItem } from "../../types";
29
+ import type { StoryListItem } from "@/shared/story/types";
30
30
 
31
31
  const makeStory = (over: Partial<StoryListItem> & { id: string }): StoryListItem => ({
32
32
  identifier: "STR-1", name: "t", description: null, state: "todo", priority: "medium", archived: false,
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { describe, it, expect } from "vitest";
19
19
  import { applyOrder } from "../order-resolver";
20
- import type { StoryListItem } from "../../types";
20
+ import type { StoryListItem } from "@/shared/story/types";
21
21
 
22
22
  const makeStory = (over: Partial<StoryListItem>): StoryListItem => ({
23
23
  id: "id1", identifier: "STR-1", name: "test", description: null,
@@ -18,7 +18,7 @@
18
18
  import { describe, it, expect } from "vitest";
19
19
  import { renderHook } from "@testing-library/react";
20
20
  import { useGroupedStories } from "../use-grouped-stories";
21
- import type { StoryListItem } from "../../types";
21
+ import type { StoryListItem } from "@/shared/story/types";
22
22
  import { DEFAULT_DISPLAY_OPTIONS } from "../types";
23
23
 
24
24
  const makeStory = (over: Partial<StoryListItem> & { id: string }): StoryListItem => ({
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { describe, it, expect, vi, beforeEach } from "vitest";
19
19
  import { renderHook, waitFor, act } from "@testing-library/react";
20
- import type { StoryListItem } from "../../types";
20
+ import type { StoryListItem } from "@/shared/story/types";
21
21
  import { DEFAULT_DISPLAY_OPTIONS } from "../types";
22
22
  import type { PagedStoriesArgs } from "../use-paged-story-groups";
23
23
 
@@ -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 type { StoryListItem } from "../types";
18
+ import type { StoryListItem } from "@/shared/story/types";
19
19
 
20
20
  /** 顶层/子层分组都只需 items 与 subGroups(GroupNode / PagedGroupNode 结构兼容) */
21
21
  type GroupLike = { items: StoryListItem[]; subGroups?: { items: StoryListItem[] }[] };
@@ -15,9 +15,9 @@
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 type { StoryState, StoryPriority } from "../constants";
18
+ import type { StoryState, StoryPriority } from "@/shared/story/constants";
19
19
  import type { GroupKey } from "./types";
20
- import type { StoryListItem, UserBrief } from "../types";
20
+ import type { StoryListItem, UserBrief } from "@/shared/story/types";
21
21
 
22
22
  /** 列表行可更新的字段(与 updateStoryFn 入参保持一致),消除动态字段拼接的 as never。
23
23
  * name 由详情面板(PeekPanel / 详情页)改动后回写视图,供列表/看板实时乐观更新。 */
@@ -19,9 +19,9 @@ import { Sprout, CircleUser } from "lucide-react";
19
19
  import { UserAvatar } from "@/components/shared-kit";
20
20
  import { LucideIconByName } from "@/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name";
21
21
  import { cn } from "@/lib/utils";
22
- import { STORY_STATES, STATE_LABEL, STATE_COLOR, STATE_ICON, STORY_PRIORITIES, PRIORITY_LABEL, PRIORITY_ICON } from "../constants";
22
+ import { STORY_STATES, STATE_LABEL, STATE_COLOR, STATE_ICON, STORY_PRIORITIES, PRIORITY_LABEL, PRIORITY_ICON } from "@/shared/story/constants";
23
23
  import { PRIORITY_COLOR } from "../priority-icon";
24
- import type { StoryListItem } from "../types";
24
+ import type { StoryListItem } from "@/shared/story/types";
25
25
  import type { GroupContext, GroupKey, GroupNode } from "./types";
26
26
  import { GROUP_FIELD } from "./group-fields";
27
27
  import { m } from "@/paraglide/messages.js";
@@ -43,7 +43,7 @@ const RESOLVERS: Record<Exclude<GroupKey, "none" | "iteration">, KeyResolver> =
43
43
  project: (item) => [item.projectId ?? "no-project"],
44
44
  };
45
45
 
46
- export function groupBy(items: StoryListItem[], key: GroupKey, ctx: GroupContext, includeEmpty = false): GroupNode[] {
46
+ export function groupBy<T extends StoryListItem = StoryListItem>(items: T[], key: GroupKey, ctx: GroupContext, includeEmpty = false): GroupNode<T>[] {
47
47
  if (key === "none") {
48
48
  return [{ key: "all", label: m.story_group_all(), items: [...items] }];
49
49
  }
@@ -53,7 +53,7 @@ export function groupBy(items: StoryListItem[], key: GroupKey, ctx: GroupContext
53
53
  return [{ key: "iteration", label: m.iterations_title(), items: [...items] }];
54
54
  }
55
55
  const resolver = RESOLVERS[key];
56
- const buckets = new Map<string, StoryListItem[]>();
56
+ const buckets = new Map<string, T[]>();
57
57
  const order: string[] = [];
58
58
  for (const item of items) {
59
59
  const keys = resolver(item);