@allbluecn/web-app 0.5.0 → 0.6.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 (170) hide show
  1. package/package.json +239 -30
  2. package/src/components/iteration/auto-schedule-dialog.tsx +150 -0
  3. package/src/components/iteration/burndown-chart.tsx +63 -0
  4. package/src/components/iteration/constants.ts +28 -0
  5. package/src/components/iteration/create-iteration-dialog.tsx +140 -0
  6. package/src/components/iteration/iteration-card.tsx +60 -0
  7. package/src/components/iteration/iteration-editor.tsx +116 -0
  8. package/src/components/iteration/iteration-progress-bar.tsx +44 -0
  9. package/src/components/iteration/iteration-status-badge.tsx +33 -0
  10. package/src/components/knowledge/dialog/knowledge-form-dialog.tsx +21 -30
  11. package/src/components/knowledge/knowledge-toolbar.tsx +124 -1
  12. package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +2 -2
  13. package/src/components/layout/sidebar-03/nav-workspace.tsx +2 -2
  14. package/src/components/project/create-project-dialog.tsx +44 -57
  15. package/src/components/shared/__tests__/lucide-curated-icons.test.ts +83 -0
  16. package/src/components/shared/__tests__/lucide-icon-data.test.ts +131 -0
  17. package/src/components/shared/__tests__/lucide-icon-picker.test.tsx +197 -0
  18. package/src/components/shared/lucide-icon-picker/curated-icons.ts +478 -0
  19. package/src/components/shared/lucide-icon-picker/icon-data.ts +109 -0
  20. package/src/components/shared/lucide-icon-picker/index.tsx +82 -0
  21. package/src/components/shared/lucide-icon-picker/lucide-icon-by-name.tsx +78 -0
  22. package/src/components/shared/lucide-icon-picker/panel.tsx +163 -0
  23. package/src/components/story/__tests__/constants.test.ts +255 -0
  24. package/src/components/story/ai/story-ai-panel.tsx +23 -15
  25. package/src/components/story/ai/story-ai-preview-list.tsx +19 -0
  26. package/src/components/story/constants.ts +43 -1
  27. package/src/components/story/create-story-dialog.tsx +42 -16
  28. package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +114 -0
  29. package/src/components/story/detail/__tests__/categories.test.tsx +62 -0
  30. package/src/components/story/detail/__tests__/dependencies-button.test.tsx +49 -0
  31. package/src/components/story/detail/activity-timeline.tsx +26 -4
  32. package/src/components/story/detail/story-detail-body.tsx +90 -38
  33. package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +857 -0
  34. package/src/components/story/detail/story-detail-toolbar/categories.tsx +23 -18
  35. package/src/components/story/detail/story-detail-toolbar/children.tsx +9 -4
  36. package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +95 -27
  37. package/src/components/story/detail/story-detail-toolbar.tsx +159 -8
  38. package/src/components/story/detail/story-properties.tsx +21 -4
  39. package/src/components/story/display/__tests__/group-fields.test.ts +107 -0
  40. package/src/components/story/display/group-resolver.tsx +2 -2
  41. package/src/components/story/inline-editors/__tests__/dependency-type-select.test.tsx +155 -0
  42. package/src/components/story/inline-editors/__tests__/labels-editor.test.tsx +155 -0
  43. package/src/components/story/inline-editors/__tests__/priority-editor.test.tsx +173 -0
  44. package/src/components/story/inline-editors/__tests__/state-editor.test.tsx +174 -0
  45. package/src/components/story/inline-editors/dependency-type-select.tsx +180 -0
  46. package/src/components/story/inline-editors/index.ts +2 -1
  47. package/src/components/story/inline-editors/labels-editor.tsx +159 -3
  48. package/src/components/story/inline-editors/story-select-dialog.tsx +41 -63
  49. package/src/components/story/peek-panel.tsx +121 -21
  50. package/src/components/story/relations/__tests__/attachment-manage-dialog.test.tsx +171 -0
  51. package/src/components/story/relations/__tests__/knowledge-select-dialog.test.tsx +174 -0
  52. package/src/components/story/relations/__tests__/link-manage-dialog.test.tsx +93 -0
  53. package/src/components/story/relations/attachment-manage-dialog.tsx +490 -0
  54. package/src/components/story/relations/file-preview.tsx +173 -0
  55. package/src/components/story/relations/format.ts +120 -0
  56. package/src/components/story/relations/general-groups.ts +32 -0
  57. package/src/components/story/relations/knowledge-select-dialog.tsx +403 -0
  58. package/src/components/story/relations/link-manage-dialog.tsx +192 -0
  59. package/src/components/story/relations/story-ref-row.tsx +17 -27
  60. package/src/components/story/story-panel-header.tsx +25 -19
  61. package/src/components/story/story-toolbar.tsx +3 -3
  62. package/src/components/story/transfer-story-popover.tsx +2 -2
  63. package/src/components/story/types.ts +14 -0
  64. package/src/components/story/views/create-view-dialog.tsx +3 -3
  65. package/src/components/story/views/story-list/__tests__/group-header.test.tsx +219 -0
  66. package/src/components/story/views/story-list/group.tsx +1 -1
  67. package/src/components/story/views/story-list/inline-story-creator.tsx +2 -2
  68. package/src/components/story/views/story-list/row.tsx +1 -1
  69. package/src/components/workspace/create-workspace-dialog.tsx +41 -49
  70. package/src/lib/__tests__/project-display-id.test.ts +8 -5
  71. package/src/lib/changelog/sdk-versions.ts +20 -20
  72. package/src/lib/iteration/__tests__/status.test.ts +41 -0
  73. package/src/lib/iteration/status.ts +28 -0
  74. package/src/lib/project-display-id.ts +14 -5
  75. package/src/lib/project-module-meta.ts +77 -0
  76. package/src/lib/story/__tests__/video-embed.test.ts +77 -0
  77. package/src/lib/story/video-embed.ts +59 -0
  78. package/src/lib/utils.d.ts +1 -1
  79. package/src/plugins/.gen-manifest.json +13 -0
  80. package/src/plugins/modules.gen.ts +20 -0
  81. package/src/plugins/plugins.gen.ts +20 -0
  82. package/src/plugins/ui.gen.ts +8 -0
  83. package/src/providers/index.tsx +6 -0
  84. package/src/routeTree.gen.ts +96 -0
  85. package/src/routes/__root.tsx +0 -8
  86. package/src/routes/_nav/inspiration/route.tsx +8 -0
  87. package/src/routes/_nav/knowledge/index.lazy.tsx +2 -2
  88. package/src/routes/_nav/prd/$id/route.tsx +9 -0
  89. package/src/routes/_nav/prd/index.tsx +9 -0
  90. package/src/routes/_nav/prd/route.tsx +8 -0
  91. package/src/routes/_nav/projects/$projectId/$storyId/route.lazy.tsx +52 -25
  92. package/src/routes/_nav/projects/$projectId/$storyId/route.tsx +6 -3
  93. package/src/routes/_nav/projects/$projectId/index.lazy.tsx +32 -15
  94. package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.lazy.tsx +289 -0
  95. package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.tsx +27 -0
  96. package/src/routes/_nav/projects/$projectId/iterations/index.lazy.tsx +111 -0
  97. package/src/routes/_nav/projects/$projectId/iterations/index.tsx +24 -0
  98. package/src/routes/_nav/projects/$projectId/route.tsx +4 -2
  99. package/src/routes/_nav/projects/$projectId/settings/route.lazy.tsx +78 -7
  100. package/src/routes/_nav/projects/$projectId/settings/route.tsx +6 -2
  101. package/src/routes/_nav/stories/$storyId/route.lazy.tsx +5 -1
  102. package/src/routes/_nav/stories/$storyId/route.tsx +4 -0
  103. package/src/routes/_nav/tags/$id/route.tsx +8 -0
  104. package/src/routes/_nav/tags/index.tsx +8 -0
  105. package/src/routes/_nav/tags/route.tsx +8 -0
  106. package/src/routes/_nav/tags/settings/route.tsx +9 -0
  107. package/src/routes/_nav/workspaces/$workspaceId/route.lazy.tsx +3 -3
  108. package/src/routes/_nav/workspaces/$workspaceId/settings/route.lazy.tsx +4 -4
  109. package/src/routes/_nav/workspaces/index.lazy.tsx +3 -3
  110. package/src/routes/api/attachments/$id/content.ts +60 -0
  111. package/src/routes/api/attachments/upload.ts +148 -0
  112. package/src/routes/share/prd/$prdId/route.tsx +10 -0
  113. package/src/server/serverFns/__tests__/project-kb.test.ts +147 -0
  114. package/src/server/serverFns/iteration/__tests__/auto-schedule-core.test.ts +62 -0
  115. package/src/server/serverFns/iteration/__tests__/burndown-core.test.ts +90 -0
  116. package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +211 -0
  117. package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +155 -0
  118. package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +136 -0
  119. package/src/server/serverFns/iteration/auto-schedule-core.ts +54 -0
  120. package/src/server/serverFns/iteration/auto-schedule-internal.ts +88 -0
  121. package/src/server/serverFns/iteration/burndown-core.ts +60 -0
  122. package/src/server/serverFns/iteration/index.ts +22 -0
  123. package/src/server/serverFns/iteration/iteration-auto-schedule.ts +66 -0
  124. package/src/server/serverFns/iteration/iteration-burndown.ts +70 -0
  125. package/src/server/serverFns/iteration/iteration-crud.ts +208 -0
  126. package/src/server/serverFns/iteration/iteration-stories.ts +111 -0
  127. package/src/server/serverFns/iteration/iteration-transfer.ts +42 -0
  128. package/src/server/serverFns/iteration/schemas.ts +58 -0
  129. package/src/server/serverFns/iteration/transfer-internal.ts +62 -0
  130. package/src/server/serverFns/knowledge.ts +50 -4
  131. package/src/server/serverFns/project/module-overview.ts +109 -0
  132. package/src/server/serverFns/project.ts +45 -2
  133. package/src/server/serverFns/story/__tests__/kb-scope.test.ts +56 -0
  134. package/src/server/serverFns/story/__tests__/label-suggest-fn.test.ts +100 -0
  135. package/src/server/serverFns/story/__tests__/labels.test.ts +67 -0
  136. package/src/server/serverFns/story/__tests__/story-assoc.test.ts +222 -0
  137. package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +132 -2
  138. package/src/server/serverFns/story/index.ts +9 -4
  139. package/src/server/serverFns/story/kb-general.ts +39 -0
  140. package/src/server/serverFns/story/kb-scope.ts +52 -0
  141. package/src/server/serverFns/story/link-preview.ts +115 -0
  142. package/src/server/serverFns/story/story-assoc.ts +372 -51
  143. package/src/server/serverFns/story/story-attachments.ts +32 -45
  144. package/src/{components/project/project-icon.tsx → server/serverFns/story/story-crud/label-palette.ts} +16 -19
  145. package/src/server/serverFns/story/story-crud/label-suggest-core.ts +88 -0
  146. package/src/server/serverFns/story/story-crud/label-suggest.ts +38 -0
  147. package/src/{components/knowledge/knowledge-icon.tsx → server/serverFns/story/story-crud/label-upsert.ts} +13 -19
  148. package/src/server/serverFns/story/story-crud/labels.ts +3 -5
  149. package/src/server/serverFns/story/story-crud.ts +53 -6
  150. package/src/server/serverFns/story/story-dependencies.ts +158 -5
  151. package/src/server/serverFns/story/story-links.ts +43 -7
  152. package/src/server/serverFns/story/story-transfer.ts +5 -4
  153. package/src/server/serverFns/story/story-tree.ts +28 -1
  154. package/src/server/serverFns/story/types.ts +38 -0
  155. package/src/styles/globals.css +25 -0
  156. package/src/types/project.ts +5 -5
  157. package/vite.shared.ts +2 -0
  158. package/src/components/project/project-animated-icon.tsx +0 -73
  159. package/src/components/shared/icon-packs/index.ts +0 -1
  160. package/src/components/shared/icon-packs/knowledge.ts +0 -1
  161. package/src/components/shared/icon-packs/prd.ts +0 -1
  162. package/src/components/shared/icon-packs/project.ts +0 -1
  163. package/src/components/shared/icon-picker.tsx +0 -1
  164. package/src/components/story/detail/story-detail-toolbar/attachments.tsx +0 -73
  165. package/src/components/story/detail/story-detail-toolbar/knowledge.tsx +0 -73
  166. package/src/components/story/detail/story-detail-toolbar/links.tsx +0 -73
  167. package/src/components/story/relations/attachments-panel.tsx +0 -179
  168. package/src/components/story/relations/knowledge-panel.tsx +0 -284
  169. package/src/components/story/relations/links-panel.tsx +0 -240
  170. package/src/lib/project-mock.ts +0 -156
@@ -50,24 +50,29 @@ export function StoryDetailCategories() {
50
50
  return (
51
51
  <>
52
52
  {CATEGORIES.map((cat) => (
53
- <Popover key={cat.key}>
54
- <PopoverTrigger asChild>
55
- <Button
56
- variant="ghost"
57
- size="sm"
58
- className="h-7 gap-1.5 px-2.5 text-xs font-normal hover:bg-muted rounded-md"
59
- title={t(cat.tooltip)}
60
- >
61
- <cat.icon className="size-3.5 text-muted-foreground" />
62
- {t(cat.label)}
63
- </Button>
64
- </PopoverTrigger>
65
- <PopoverContent align="start" className="w-[320px] p-4">
66
- <div className="text-sm text-muted-foreground">
67
- {t(cat.placeholder)}
68
- </div>
69
- </PopoverContent>
70
- </Popover>
53
+ <React.Fragment key={cat.key}>
54
+ {cat.key === "bug" && (
55
+ <span className="mx-0.5 h-4 w-px shrink-0 bg-muted-foreground/20" />
56
+ )}
57
+ <Popover>
58
+ <PopoverTrigger asChild>
59
+ <Button
60
+ variant="ghost"
61
+ size="sm"
62
+ className="h-7 gap-1.5 px-2.5 text-xs font-normal hover:bg-muted rounded-md"
63
+ title={t(cat.tooltip)}
64
+ >
65
+ <cat.icon className="size-3.5 text-muted-foreground" />
66
+ {t(cat.label)}
67
+ </Button>
68
+ </PopoverTrigger>
69
+ <PopoverContent align="start" className="w-[320px] p-4">
70
+ <div className="text-sm text-muted-foreground">
71
+ {t(cat.placeholder)}
72
+ </div>
73
+ </PopoverContent>
74
+ </Popover>
75
+ </React.Fragment>
71
76
  ))}
72
77
  </>
73
78
  );
@@ -16,7 +16,6 @@
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
18
  import * as React from "react";
19
- import { useNavigate } from "@tanstack/react-router";
20
19
  import { useTranslation } from "react-i18next";
21
20
  import {
22
21
  Plus,
@@ -66,6 +65,7 @@ export interface StoryDetailChildrenPanelProps {
66
65
  projects?: { id: string; name: string; icon: string; displayId: string | null }[];
67
66
  labels?: { id: string; name: string; color: string }[];
68
67
  onAction: (action: "create" | "select") => void;
68
+ onNavigateInPanel?: (targetId: string, targetIdentifier?: string, relationKind?: "child" | "dependency") => void;
69
69
  }
70
70
 
71
71
  export function StoryDetailChildrenButton({ onAction }: StoryDetailChildrenButtonProps) {
@@ -131,9 +131,9 @@ export function StoryDetailChildrenPanel({
131
131
  projects,
132
132
  labels,
133
133
  onAction,
134
+ onNavigateInPanel,
134
135
  }: StoryDetailChildrenPanelProps) {
135
136
  const { t } = useTranslation("story");
136
- const navigate = useNavigate();
137
137
  const childrenList = data.children as StoryListItem[];
138
138
  const [selectedIds, setSelectedIds] = React.useState<Set<string>>(() => new Set());
139
139
  const [busy, setBusy] = React.useState(false);
@@ -176,7 +176,10 @@ export function StoryDetailChildrenPanel({
176
176
  };
177
177
 
178
178
  const handleOpenStory = (id: string) => {
179
- navigate({ to: "/stories/$storyId", params: { storyId: id } });
179
+ if (onNavigateInPanel) {
180
+ const story = childrenList.find((s) => s.id === id);
181
+ onNavigateInPanel(id, story?.identifier);
182
+ }
180
183
  };
181
184
 
182
185
  const handleRemoveChild = async (childId: string) => {
@@ -321,7 +324,7 @@ export function StoryDetailChildrenPanel({
321
324
  <Collapsible defaultOpen>
322
325
  <CollapsibleTrigger
323
326
  asChild
324
- className="group flex w-full cursor-pointer items-center gap-2 pb-3 pt-4 text-xs font-semibold hover:text-foreground transition-colors"
327
+ className="group flex w-full cursor-pointer select-none items-center gap-2 pb-3 pt-4 text-xs font-semibold hover:text-foreground transition-colors"
325
328
  >
326
329
  <div
327
330
  role="button"
@@ -448,6 +451,7 @@ export function StoryDetailChildrenPanel({
448
451
  children: prev.hierarchy.children.map((c) => c.id === storyId ? { ...c, ...patch } : c),
449
452
  },
450
453
  }));
454
+ refreshChildren();
451
455
  }
452
456
  }}
453
457
  />
@@ -495,6 +499,7 @@ export function StoryDetailChildrenPanel({
495
499
  children: prev.hierarchy.children.map((c) => c.id === storyId ? { ...c, ...patch } : c),
496
500
  },
497
501
  }));
502
+ refreshChildren();
498
503
  }
499
504
  }}
500
505
  />
@@ -16,7 +16,6 @@
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
18
  import * as React from "react";
19
- import { useNavigate } from "@tanstack/react-router";
20
19
  import { useTranslation } from "react-i18next";
21
20
  import { Plus, ChevronDown, ChartNoAxesGantt } from "lucide-react";
22
21
  import { Button } from "@/components/ui/button";
@@ -26,10 +25,12 @@ import { useServerFn } from "@tanstack/react-start";
26
25
  import { toast } from "sonner";
27
26
  import {
28
27
  removeDependencyFn,
28
+ updateDependencyFn,
29
29
  bulkArchiveStoriesFn,
30
30
  bulkDeleteStoriesFn,
31
31
  bulkUpdateStoriesFn,
32
32
  } from "@/server/serverFns/story";
33
+ import { DEPENDENCY_TYPE_OPTIONS, toDependencyEdge, type DependencyTypeOption } from "../../constants";
33
34
  import { BatchToolbar } from "../../views/story-list/batch-toolbar";
34
35
  import { useGroupedStories } from "../../display/use-grouped-stories";
35
36
  import { VirtualGroupHeader } from "../../views/story-list/group";
@@ -59,6 +60,8 @@ export interface StoryDetailDependencyPanelProps {
59
60
  projects?: { id: string; name: string; icon: string; displayId: string | null }[];
60
61
  labels?: { id: string; name: string; color: string }[];
61
62
  onOpenAdd: () => void;
63
+ onAppendActivities?: (items: import("../../types").ActivityItem[]) => void;
64
+ onNavigateInPanel?: (targetId: string, targetIdentifier?: string, relationKind?: "child" | "dependency") => void;
62
65
  }
63
66
 
64
67
  export function StoryDetailDependencyPanel({
@@ -72,10 +75,12 @@ export function StoryDetailDependencyPanel({
72
75
  projects,
73
76
  labels,
74
77
  onOpenAdd,
78
+ onAppendActivities,
79
+ onNavigateInPanel,
75
80
  }: StoryDetailDependencyPanelProps) {
76
81
  const { t } = useTranslation("story");
77
- const navigate = useNavigate();
78
82
  const removeDependency = useServerFn(removeDependencyFn);
83
+ const updateDependency = useServerFn(updateDependencyFn);
79
84
  const [selectedIds, setSelectedIds] = React.useState<Set<string>>(() => new Set());
80
85
  const [busy, setBusy] = React.useState(false);
81
86
  const [collapsed, setCollapsed] = React.useState<Record<string, boolean>>({});
@@ -135,7 +140,10 @@ export function StoryDetailDependencyPanel({
135
140
  };
136
141
 
137
142
  const handleOpenStory = (id: string) => {
138
- navigate({ to: "/stories/$storyId", params: { storyId: id } });
143
+ if (onNavigateInPanel) {
144
+ const story = allRows.find((r) => r.id === id);
145
+ onNavigateInPanel(id, story?.identifier, "dependency");
146
+ }
139
147
  };
140
148
 
141
149
  const applyRowsUpdate = (
@@ -155,7 +163,12 @@ export function StoryDetailDependencyPanel({
155
163
  ? "dependsOn"
156
164
  : "dependents";
157
165
  try {
158
- await removeDependency({ data: { depId: row.depId } });
166
+ const res = await removeDependency({ data: { depId: row.depId } });
167
+ // 两端各有一条记录,只追加当前需求的
168
+ const mine = (res?.activities ?? [])
169
+ .filter((a) => a.storyId === storyId)
170
+ .map(({ storyId: _sid, ...rest }) => rest);
171
+ if (mine.length > 0) onAppendActivities?.(mine);
159
172
  toast.success(t("dependency.removed"));
160
173
  applyRowsUpdate((rows) => rows.filter((r) => r.id !== rowId), group);
161
174
  setSelectedIds((prev) => {
@@ -168,13 +181,59 @@ export function StoryDetailDependencyPanel({
168
181
  }
169
182
  };
170
183
 
184
+ const handleDependencyTypeChange = async (
185
+ otherStoryId: string,
186
+ key: DependencyTypeOption["key"],
187
+ ) => {
188
+ const row = rowById.get(otherStoryId);
189
+ const option = DEPENDENCY_TYPE_OPTIONS.find((o) => o.key === key);
190
+ if (!row || !option) return;
191
+ const edge = toDependencyEdge(option, storyId, otherStoryId);
192
+ try {
193
+ const res = await updateDependency({ data: { depId: row.depId, ...edge } });
194
+ if (!res.ok) {
195
+ toast.warning(t(`dependency.errors.${res.reason}`));
196
+ return;
197
+ }
198
+ const mine = (res.activities ?? [])
199
+ .filter((a) => a.storyId === storyId)
200
+ .map(({ storyId: _sid, ...rest }) => rest);
201
+ if (mine.length > 0) onAppendActivities?.(mine);
202
+ toast.success(t("dependency.updated"));
203
+ // 行的 group 跟随 option 方向:同 group 原地更新,跨 group 移动到另一 section
204
+ const oldGroup: DependencySection = data.dependsOn.some((r) => r.id === otherStoryId)
205
+ ? "dependsOn"
206
+ : "dependents";
207
+ if (option.group === oldGroup) {
208
+ applyRowsUpdate(
209
+ (rows) => rows.map((r) => (r.id === otherStoryId ? { ...r, depType: option.edge } : r)),
210
+ oldGroup,
211
+ );
212
+ } else {
213
+ const moved = { ...row, depType: option.edge };
214
+ applyRowsUpdate((rows) => rows.filter((r) => r.id !== otherStoryId), oldGroup);
215
+ applyRowsUpdate((rows) => [moved, ...rows], option.group);
216
+ }
217
+ } catch (e) {
218
+ toast.error(
219
+ isNetworkError(e) ? t("dependency.networkFailed") : (e as Error)?.message || t("dependency.updateFailed"),
220
+ );
221
+ }
222
+ };
223
+
171
224
  const handleBulkRemoveRelations = async (storyIds: string[]) => {
172
225
  if (storyIds.length === 0) return;
173
226
  setBusy(true);
174
227
  try {
175
228
  for (const id of storyIds) {
176
229
  const row = rowById.get(id);
177
- if (row) await removeDependency({ data: { depId: row.depId } });
230
+ if (row) {
231
+ const res = await removeDependency({ data: { depId: row.depId } });
232
+ const mine = (res?.activities ?? [])
233
+ .filter((a) => a.storyId === storyId)
234
+ .map(({ storyId: _sid, ...rest }) => rest);
235
+ if (mine.length > 0) onAppendActivities?.(mine);
236
+ }
178
237
  }
179
238
  toast.success(t("dependency.removed"));
180
239
  setSelectedIds(new Set());
@@ -288,6 +347,8 @@ export function StoryDetailDependencyPanel({
288
347
  next.delete(rowId);
289
348
  return next;
290
349
  });
350
+ } else {
351
+ refreshDependencies();
291
352
  }
292
353
  };
293
354
 
@@ -306,6 +367,7 @@ export function StoryDetailDependencyPanel({
306
367
  onRemove={handleRemoveRow}
307
368
  removeMenuLabel={t("dependency.remove")}
308
369
  dependencyMeta={{ depType: row.depType, group }}
370
+ onDependencyTypeChange={handleDependencyTypeChange}
309
371
  currentIdentifier={storyIdentifier}
310
372
  onStoryRefresh={handleStoryRefresh}
311
373
  />
@@ -354,35 +416,41 @@ export function StoryDetailDependencyPanel({
354
416
 
355
417
  const renderSection = (
356
418
  titleKey: string,
357
- group: DependencySection,
419
+ sectionKey: DependencySection,
358
420
  rows: DependencyRow[],
359
421
  groups: GroupNode[],
360
- ) => (
361
- <div>
362
- <div className="flex h-9 items-center gap-1.5 bg-muted/50 px-2">
363
- <span className="text-sm font-medium">{t(titleKey)}</span>
364
- <span className="inline-flex h-4 shrink-0 items-center rounded border px-1.5 text-[10px] font-normal text-muted-foreground whitespace-nowrap">
365
- {rows.length}
366
- </span>
367
- </div>
368
- {rows.length === 0 ? (
369
- <p className="flex items-center gap-1.5 px-2 py-1.5 text-xs text-muted-foreground">
370
- <span className="w-4 shrink-0" aria-hidden />
371
- {t("relation.none")}
372
- </p>
373
- ) : filters.groupBy === "none" ? (
374
- rows.map((row) => renderRow(row, group))
375
- ) : (
376
- groups.map((node) => renderGroupItems(node, group, 0, `${group}:${node.key}`))
377
- )}
378
- </div>
379
- );
422
+ ) => {
423
+ const isCollapsed = collapsed[`${sectionKey}:root`] ?? false;
424
+ return (
425
+ <Collapsible open={!isCollapsed} onOpenChange={(open) => setCollapsed((p) => ({ ...p, [`${sectionKey}:root`]: !open }))}>
426
+ <CollapsibleTrigger asChild>
427
+ <div className="flex h-9 cursor-pointer select-none items-center gap-1.5 bg-muted/50 px-3 text-sm font-medium hover:text-foreground transition-colors">
428
+ <ChevronDown className={`size-3 shrink-0 text-muted-foreground transition-transform ${isCollapsed ? "-rotate-90" : "rotate-0"}`} />
429
+ <span>{t(titleKey)}</span>
430
+ <span className="ml-1 text-xs text-muted-foreground">{rows.length}</span>
431
+ </div>
432
+ </CollapsibleTrigger>
433
+ <CollapsibleContent>
434
+ {rows.length === 0 ? (
435
+ <p className="flex items-center gap-1.5 px-2 py-1.5 text-xs text-muted-foreground">
436
+ <span className="w-4 shrink-0" aria-hidden />
437
+ {t("relation.none")}
438
+ </p>
439
+ ) : filters.groupBy === "none" ? (
440
+ rows.map((row) => renderRow(row, sectionKey))
441
+ ) : (
442
+ groups.map((node) => renderGroupItems(node, sectionKey, 0, `${sectionKey}:${node.key}`))
443
+ )}
444
+ </CollapsibleContent>
445
+ </Collapsible>
446
+ );
447
+ };
380
448
 
381
449
  return (
382
450
  <Collapsible defaultOpen>
383
451
  <CollapsibleTrigger
384
452
  asChild
385
- className="group flex w-full cursor-pointer items-center gap-2 pb-3 pt-4 text-xs font-semibold hover:text-foreground transition-colors"
453
+ className="group flex w-full cursor-pointer select-none items-center gap-2 pb-3 pt-4 text-xs font-semibold hover:text-foreground transition-colors"
386
454
  >
387
455
  <div
388
456
  role="button"
@@ -19,13 +19,20 @@ import * as React from "react";
19
19
  import { useTranslation } from "react-i18next";
20
20
  import { useServerFn } from "@tanstack/react-start";
21
21
  import { toast } from "sonner";
22
+ import { LinkIcon, Paperclip, BookOpenText } from "lucide-react";
23
+ import { Button } from "@/components/ui/button";
22
24
  import { StoryDetailChildrenButton, StoryDetailChildrenPanel } from "./story-detail-toolbar/children";
23
25
  import { StoryDetailDependencies } from "./story-detail-toolbar/dependencies";
24
26
  import { StoryDetailDependencyPanel } from "./story-detail-toolbar/dependency-panel";
25
- import { StoryDetailLinks } from "./story-detail-toolbar/links";
26
- import { StoryDetailAttachments } from "./story-detail-toolbar/attachments";
27
- import { StoryDetailKnowledge } from "./story-detail-toolbar/knowledge";
28
27
  import { StoryDetailCategories } from "./story-detail-toolbar/categories";
28
+ import {
29
+ AssociatedAttachmentsPanel,
30
+ type AssociatedDialogKind,
31
+ } from "./story-detail-toolbar/associated-attachments-panel";
32
+ import { LinkManageDialog } from "../relations/link-manage-dialog";
33
+ import { AttachmentManageDialog } from "../relations/attachment-manage-dialog";
34
+ import { KnowledgeSelectDialog } from "../relations/knowledge-select-dialog";
35
+ import { getLinksFn, getAttachmentsFn, getLinkedDocsFn } from "@/server/serverFns/story";
29
36
  import { StorySelectDialog } from "../inline-editors/story-select-dialog";
30
37
  import { CreateStoryDialog } from "../create-story-dialog";
31
38
  import { addDependenciesFn, getDependenciesFn, listChildStoriesFn } from "@/server/serverFns/story";
@@ -39,15 +46,21 @@ interface StoryDetailToolbarProps {
39
46
  projectId: string | null;
40
47
  data: RelationsData;
41
48
  onRefresh: (next: (prev: RelationsData) => RelationsData) => void;
49
+ /** 关联类操作产生的活动即时追加到时间线 */
50
+ onAppendActivities?: (items: import("../types").ActivityItem[]) => void;
42
51
  users?: UserBrief[];
43
52
  projects?: { id: string; name: string; icon: string; displayId: string | null }[];
44
53
  labels?: { id: string; name: string; color: string }[];
54
+ onNavigateInPanel?: (targetId: string, targetIdentifier?: string, relationKind?: "child" | "dependency") => void;
45
55
  }
46
56
 
47
- export function StoryDetailToolbar({ storyId, storyIdentifier, projectId, data, onRefresh, users, projects, labels }: StoryDetailToolbarProps) {
57
+ export function StoryDetailToolbar({ storyId, storyIdentifier, projectId, data, onRefresh, onAppendActivities, users, projects, labels, onNavigateInPanel }: StoryDetailToolbarProps) {
48
58
  const { t } = useTranslation("story");
49
59
  const [actionOpen, setActionOpen] = React.useState<"create" | "select" | null>(null);
50
60
  const [dependencyDialogOpen, setDependencyDialogOpen] = React.useState(false);
61
+ const [assocDialogOpen, setAssocDialogOpen] = React.useState<AssociatedDialogKind | null>(null);
62
+ const [linkInitialEdit, setLinkInitialEdit] = React.useState<import("../types").StoryLinkItem | null>(null);
63
+ const [editAttachment, setEditAttachment] = React.useState<import("../types").StoryAttachmentItem | null>(null);
51
64
  const [dependencyType, setDependencyType] = React.useState<DependencyTypeOption["key"] | undefined>(undefined);
52
65
  const addDependencies = useServerFn(addDependenciesFn);
53
66
 
@@ -78,6 +91,10 @@ export function StoryDetailToolbar({ storyId, storyIdentifier, projectId, data,
78
91
  const res = await addDependencies({ data: { items } });
79
92
  if (res.created.length > 0) {
80
93
  toast.success(t("dependency.added", { count: res.created.length }));
94
+ const mine = res.activities
95
+ .filter((a) => a.storyId === storyId)
96
+ .map(({ storyId: _sid, ...rest }) => rest);
97
+ if (mine.length > 0) onAppendActivities?.(mine);
81
98
  }
82
99
  if (res.failed.length > 0) {
83
100
  const reasons = [...new Set(res.failed.map((f) => t(`dependency.errors.${f.reason}`)))].join("、");
@@ -117,9 +134,46 @@ export function StoryDetailToolbar({ storyId, storyIdentifier, projectId, data,
117
134
  }
118
135
  };
119
136
 
120
- const handleCreateSuccess = () => {
137
+ const handleCreateSuccess = (
138
+ _story: { id: string; name: string; identifier?: string },
139
+ activities?: import("../types").ActivityItem[],
140
+ ) => {
121
141
  setActionOpen(null);
122
142
  refreshChildren();
143
+ if (activities && activities.length > 0) onAppendActivities?.(activities);
144
+ };
145
+
146
+ const refreshLinks = async () => {
147
+ try {
148
+ const links = await getLinksFn({ data: { storyId } });
149
+ onRefresh((prev) => ({ ...prev, links }));
150
+ } catch {
151
+ // 静默兜底
152
+ }
153
+ };
154
+
155
+ const refreshAttachments = async () => {
156
+ try {
157
+ const attachments = await getAttachmentsFn({ data: { storyId } });
158
+ onRefresh((prev) => ({ ...prev, attachments }));
159
+ } catch {
160
+ // 静默兜底
161
+ }
162
+ };
163
+
164
+ const refreshKnowledge = async () => {
165
+ try {
166
+ const docs = await getLinkedDocsFn({ data: { storyId } });
167
+ onRefresh((prev) => ({ ...prev, knowledge: { ...prev.knowledge, docs } }));
168
+ } catch {
169
+ // 静默兜底
170
+ }
171
+ };
172
+
173
+ const handleOpenAssocDialog = (kind: AssociatedDialogKind, editLink?: import("../types").StoryLinkItem, editAttachmentItem?: import("../types").StoryAttachmentItem) => {
174
+ setLinkInitialEdit(editLink ?? null);
175
+ setEditAttachment(editAttachmentItem ?? null);
176
+ setAssocDialogOpen(kind);
123
177
  };
124
178
 
125
179
  return (
@@ -133,9 +187,48 @@ export function StoryDetailToolbar({ storyId, storyIdentifier, projectId, data,
133
187
 
134
188
  <span className="mx-0.5 h-4 w-px shrink-0 bg-muted-foreground/20" />
135
189
 
136
- <StoryDetailLinks storyId={storyId} data={data} onRefresh={onRefresh} />
137
- <StoryDetailAttachments storyId={storyId} data={data} onRefresh={onRefresh} />
138
- <StoryDetailKnowledge storyId={storyId} data={data} onRefresh={onRefresh} />
190
+ <Button
191
+ variant="ghost"
192
+ size="sm"
193
+ className="h-7 gap-1.5 px-2.5 text-xs font-normal hover:bg-muted rounded-md"
194
+ onClick={() => handleOpenAssocDialog("link")}
195
+ >
196
+ <LinkIcon className="size-3.5 text-muted-foreground" />
197
+ {t("linkDialog.button")}
198
+ {data.links.length > 0 && (
199
+ <span className="ml-0.5 inline-flex min-w-[18px] items-center justify-center rounded-full bg-primary px-1 text-[10px] font-medium text-primary-foreground">
200
+ {data.links.length}
201
+ </span>
202
+ )}
203
+ </Button>
204
+ <Button
205
+ variant="ghost"
206
+ size="sm"
207
+ className="h-7 gap-1.5 px-2.5 text-xs font-normal hover:bg-muted rounded-md"
208
+ onClick={() => handleOpenAssocDialog("attachment")}
209
+ >
210
+ <Paperclip className="size-3.5 text-muted-foreground" />
211
+ {t("attachmentDialog.button")}
212
+ {data.attachments.length > 0 && (
213
+ <span className="ml-0.5 inline-flex min-w-[18px] items-center justify-center rounded-full bg-primary px-1 text-[10px] font-medium text-primary-foreground">
214
+ {data.attachments.length}
215
+ </span>
216
+ )}
217
+ </Button>
218
+ <Button
219
+ variant="ghost"
220
+ size="sm"
221
+ className="h-7 gap-1.5 px-2.5 text-xs font-normal hover:bg-muted rounded-md"
222
+ onClick={() => handleOpenAssocDialog("knowledge")}
223
+ >
224
+ <BookOpenText className="size-3.5 text-muted-foreground" />
225
+ {t("knowledgeDialog.button")}
226
+ {data.knowledge.docs.length > 0 && (
227
+ <span className="ml-0.5 inline-flex min-w-[18px] items-center justify-center rounded-full bg-primary px-1 text-[10px] font-medium text-primary-foreground">
228
+ {data.knowledge.docs.length}
229
+ </span>
230
+ )}
231
+ </Button>
139
232
 
140
233
  <span className="mx-0.5 h-4 w-px shrink-0 bg-muted-foreground/20" />
141
234
 
@@ -152,6 +245,7 @@ export function StoryDetailToolbar({ storyId, storyIdentifier, projectId, data,
152
245
  projects={projects}
153
246
  labels={labels}
154
247
  onAction={handleChildAction}
248
+ onNavigateInPanel={onNavigateInPanel}
155
249
  />
156
250
 
157
251
  <StoryDetailDependencyPanel
@@ -165,6 +259,16 @@ export function StoryDetailToolbar({ storyId, storyIdentifier, projectId, data,
165
259
  projects={projects}
166
260
  labels={labels}
167
261
  onOpenAdd={() => setDependencyDialogOpen(true)}
262
+ onAppendActivities={onAppendActivities}
263
+ onNavigateInPanel={onNavigateInPanel}
264
+ />
265
+
266
+ <AssociatedAttachmentsPanel
267
+ storyId={storyId}
268
+ data={data}
269
+ onRefresh={onRefresh}
270
+ onOpenDialog={handleOpenAssocDialog}
271
+ onAppendActivities={onAppendActivities}
168
272
  />
169
273
 
170
274
  <StorySelectDialog
@@ -216,6 +320,53 @@ export function StoryDetailToolbar({ storyId, storyIdentifier, projectId, data,
216
320
  initialProjectId={projectId ?? undefined}
217
321
  onSuccess={handleCreateSuccess}
218
322
  />
323
+
324
+ <LinkManageDialog
325
+ open={assocDialogOpen === "link"}
326
+ onOpenChange={(v) => {
327
+ if (!v) {
328
+ setAssocDialogOpen(null);
329
+ setLinkInitialEdit(null);
330
+ }
331
+ }}
332
+ storyId={storyId}
333
+ data={data.links}
334
+ initialEdit={linkInitialEdit}
335
+ onActivity={(a) => onAppendActivities?.([a])}
336
+ onUpdate={(links) => {
337
+ onRefresh((prev) => ({ ...prev, links }));
338
+ void refreshLinks();
339
+ }}
340
+ />
341
+
342
+ <AttachmentManageDialog
343
+ open={assocDialogOpen === "attachment"}
344
+ onOpenChange={(v) => {
345
+ if (!v) { setAssocDialogOpen(null); setEditAttachment(null); }
346
+ }}
347
+ storyId={storyId}
348
+ data={data.attachments}
349
+ editItem={editAttachment}
350
+ onActivity={(a) => onAppendActivities?.([a])}
351
+ onUpdate={(attachments) => {
352
+ onRefresh((prev) => ({ ...prev, attachments }));
353
+ void refreshAttachments();
354
+ }}
355
+ />
356
+
357
+ <KnowledgeSelectDialog
358
+ open={assocDialogOpen === "knowledge"}
359
+ onOpenChange={(v) => {
360
+ if (!v) setAssocDialogOpen(null);
361
+ }}
362
+ storyId={storyId}
363
+ linkedDocs={data.knowledge.docs}
364
+ onActivity={(a) => onAppendActivities?.([a])}
365
+ onUpdate={(docs) => {
366
+ onRefresh((prev) => ({ ...prev, knowledge: { ...prev.knowledge, docs } }));
367
+ void refreshKnowledge();
368
+ }}
369
+ />
219
370
  </div>
220
371
  );
221
372
  }
@@ -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 { GitPullRequestArrow, Tag } from "lucide-react";
18
+ import { GitPullRequestArrow, Tag, CalendarRange } from "lucide-react";
19
19
  import { useTranslation } from "react-i18next";
20
20
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
21
21
  import { ChevronDown } from "lucide-react";
@@ -23,6 +23,8 @@ import type { StoryState } from "../constants";
23
23
  import type { ActivityItem } from "../types";
24
24
  import { ParentStoryEditor } from "../inline-editors/parent-story-editor";
25
25
  import { LabelsEditor } from "../inline-editors/labels-editor";
26
+ import { IterationEditor } from "@/components/iteration/iteration-editor";
27
+ import type { IterationSummary } from "@/server/serverFns/iteration/schemas";
26
28
 
27
29
  interface PropertyItemProps {
28
30
  icon: React.ReactNode;
@@ -45,15 +47,20 @@ function PropertyItem({ icon, label, children }: PropertyItemProps) {
45
47
  interface StoryPropertiesProps {
46
48
  story: {
47
49
  id: string;
50
+ name: string;
51
+ description?: string | null;
48
52
  parentId: string | null;
49
53
  parent: { id: string; title: string; status: StoryState; identifier?: string } | null;
50
54
  labels: { id: string; name: string; color: string }[];
51
55
  projectId?: string | null;
56
+ iteration?: { id: string; name: string } | null;
52
57
  };
53
58
  labels?: { id: string; name: string; color: string }[];
54
59
  projects?: { id: string; name: string; icon: string; displayId?: string | null }[];
60
+ iterations?: IterationSummary[];
55
61
  onParentChange: (parentId: string | null) => void;
56
62
  onLabelsChange: (labelIds: string[]) => void;
63
+ onIterationChange?: (iteration: { id: string; name: string } | null) => void;
57
64
  onActivities?: (activities: ActivityItem[]) => void;
58
65
  }
59
66
 
@@ -61,8 +68,10 @@ export function StoryProperties({
61
68
  story,
62
69
  labels,
63
70
  projects,
71
+ iterations,
64
72
  onParentChange,
65
73
  onLabelsChange,
74
+ onIterationChange,
66
75
  onActivities,
67
76
  }: StoryPropertiesProps) {
68
77
  const { t } = useTranslation("story");
@@ -71,7 +80,7 @@ export function StoryProperties({
71
80
  <div className="px-6 py-4 text-sm font-semibold">{t("properties.title")}</div>
72
81
  <CollapsibleContent className="px-6 pb-2 space-y-2">
73
82
  <Collapsible defaultOpen>
74
- <CollapsibleTrigger className="flex items-center gap-2 pb-4 text-xs font-semibold hover:text-foreground transition-colors">
83
+ <CollapsibleTrigger className="flex select-none items-center gap-2 pb-4 text-xs font-semibold hover:text-foreground transition-colors">
75
84
  {t("properties.basic")}
76
85
  <ChevronDown className="size-3" />
77
86
  </CollapsibleTrigger>
@@ -98,18 +107,26 @@ export function StoryProperties({
98
107
  labels={labels}
99
108
  onChange={onLabelsChange}
100
109
  variant="button"
110
+ aiContext={{ name: () => story.name, description: () => story.description ?? undefined }}
101
111
  />
102
112
  </PropertyItem>
103
113
  )}
104
114
  </CollapsibleContent>
105
115
  </Collapsible>
106
116
  <Collapsible>
107
- <CollapsibleTrigger className="flex items-center gap-2 pb-4 text-xs font-semibold hover:text-foreground transition-colors">
117
+ <CollapsibleTrigger className="flex select-none items-center gap-2 pb-4 text-xs font-semibold hover:text-foreground transition-colors">
108
118
  {t("properties.iteration")}
109
119
  <ChevronDown className="size-3" />
110
120
  </CollapsibleTrigger>
111
121
  <CollapsibleContent className="pb-2">
112
- {/* 迭代属性区域 - 预留 */}
122
+ <PropertyItem icon={<CalendarRange className="size-3.5" />} label={t("properties.iteration")}>
123
+ <IterationEditor
124
+ storyId={story.id}
125
+ current={story.iteration ?? null}
126
+ options={iterations ?? []}
127
+ onChanged={(it) => onIterationChange?.(it)}
128
+ />
129
+ </PropertyItem>
113
130
  </CollapsibleContent>
114
131
  </Collapsible>
115
132
  </CollapsibleContent>