@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
@@ -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, CircleFadingArrowUp, Repeat2, CalendarPlus2, Hash, MessageCircleCheck, RefreshCcwDot, CircleFadingPlus, CircleArrowUp, CircleArrowRight, CircleArrowDown, CircleSlash, CircleDashed, CircleCheckBig, LoaderCircle, CheckCircle2, XCircle, FilePlus, FilePenLine, MessageSquareText, Flag, UserRound, Text, Calendar, CalendarCheck, Tags, TextInitial, Building2, GitBranch, type LucideIcon } from "lucide-react";
18
+ import { GitPullRequestArrow, CircleFadingArrowUp, Repeat, Repeat2, CalendarPlus2, Hash, MessageCircleCheck, RefreshCcwDot, CircleFadingPlus, CircleArrowUp, CircleArrowRight, CircleArrowDown, CircleSlash, CircleDashed, CircleCheckBig, LoaderCircle, CheckCircle2, XCircle, FilePlus, FilePenLine, MessageSquareText, Flag, UserRound, Text, Calendar, CalendarCheck, Tags, TextInitial, Building2, GitBranch, ListTree, ArrowLeftRight, ArrowDownToDot, Link2, Paperclip, BookOpen, Unlink, Trash2, BookX, FileX, ArrowRightFromLine, ArrowLeftToLine, ArrowUpFromDot, type LucideIcon } from "lucide-react";
19
19
  import type { DependencyEdgeType, DependencyGroup } from "./types";
20
20
 
21
21
  export type StoryState = "backlog" | "todo" | "in_progress" | "done" | "cancelled";
@@ -101,6 +101,19 @@ export const ACTIVITY_LABEL: Record<string, string> = {
101
101
  project_transferred: "activity.project_transferred",
102
102
  parent_changed: "activity.parent_changed",
103
103
  parent_removed: "activity.parent_removed",
104
+ child_added: "activity.child_added",
105
+ dependency_added: "activity.dependency_added",
106
+ dependency_removed: "activity.dependency_removed",
107
+ link_added: "activity.link_added",
108
+ link_updated: "activity.link_updated",
109
+ link_removed: "activity.link_removed",
110
+ attachment_added: "activity.attachment_added",
111
+ attachment_updated: "activity.attachment_updated",
112
+ attachment_removed: "activity.attachment_removed",
113
+ kb_linked: "activity.kb_linked",
114
+ kb_unlinked: "activity.kb_unlinked",
115
+ prd_unlinked: "activity.prd_unlinked",
116
+ iteration_changed: "activity.iteration_changed",
104
117
  };
105
118
 
106
119
  export const ACTIVITY_ICON: Record<string, LucideIcon> = {
@@ -119,6 +132,19 @@ startDate_changed: CalendarPlus2,
119
132
  project_transferred: Repeat2,
120
133
  parent_changed: GitPullRequestArrow,
121
134
  parent_removed: GitPullRequestArrow,
135
+ child_added: ListTree,
136
+ dependency_added: ArrowLeftRight,
137
+ dependency_removed: Unlink,
138
+ link_added: Link2,
139
+ link_updated: Link2,
140
+ link_removed: Unlink,
141
+ attachment_added: Paperclip,
142
+ attachment_updated: FilePenLine,
143
+ attachment_removed: Trash2,
144
+ kb_linked: BookOpen,
145
+ kb_unlinked: BookX,
146
+ prd_unlinked: FileX,
147
+ iteration_changed: Repeat,
122
148
  };
123
149
 
124
150
  // ====== 依赖关系常量(对齐 Plane,新模型) ======
@@ -144,6 +170,10 @@ export interface DependencyTypeOption {
144
170
  edge: DependencyEdgeType;
145
171
  /** 与 group 等价的方向标志:true = 存 (Y, X, edge) */
146
172
  reversed: boolean;
173
+ /** 选择器/行内标签显示的图标(lucide 组件引用) */
174
+ icon: LucideIcon;
175
+ /** 图标旋转角度(deg),每个图标方向语义不同可单独设定;0 或不填不旋转 */
176
+ iconRotation: number;
147
177
  }
148
178
 
149
179
  export const DEPENDENCY_TYPE_OPTIONS: DependencyTypeOption[] = [
@@ -155,6 +185,8 @@ export const DEPENDENCY_TYPE_OPTIONS: DependencyTypeOption[] = [
155
185
  tooltipLabelKey: "dependency.tooltips.blocked_by",
156
186
  edge: "BLOCKS",
157
187
  reversed: true,
188
+ icon: ArrowRightFromLine,
189
+ iconRotation: 0,
158
190
  },
159
191
  {
160
192
  key: "starts_after",
@@ -164,6 +196,8 @@ export const DEPENDENCY_TYPE_OPTIONS: DependencyTypeOption[] = [
164
196
  tooltipLabelKey: "dependency.tooltips.starts_after",
165
197
  edge: "STARTS_BEFORE",
166
198
  reversed: true,
199
+ icon: ArrowUpFromDot,
200
+ iconRotation: 90,
167
201
  },
168
202
  {
169
203
  key: "finishes_after",
@@ -173,6 +207,8 @@ export const DEPENDENCY_TYPE_OPTIONS: DependencyTypeOption[] = [
173
207
  tooltipLabelKey: "dependency.tooltips.finishes_after",
174
208
  edge: "FINISHES_BEFORE",
175
209
  reversed: true,
210
+ icon: ArrowLeftToLine,
211
+ iconRotation: 0,
176
212
  },
177
213
  {
178
214
  key: "blocking",
@@ -182,6 +218,8 @@ export const DEPENDENCY_TYPE_OPTIONS: DependencyTypeOption[] = [
182
218
  tooltipLabelKey: "dependency.tooltips.blocking",
183
219
  edge: "BLOCKS",
184
220
  reversed: false,
221
+ icon: ArrowRightFromLine,
222
+ iconRotation: 0,
185
223
  },
186
224
  {
187
225
  key: "starts_before",
@@ -191,6 +229,8 @@ export const DEPENDENCY_TYPE_OPTIONS: DependencyTypeOption[] = [
191
229
  tooltipLabelKey: "dependency.tooltips.starts_before",
192
230
  edge: "STARTS_BEFORE",
193
231
  reversed: false,
232
+ icon: ArrowUpFromDot,
233
+ iconRotation: 90,
194
234
  },
195
235
  {
196
236
  key: "finishes_before",
@@ -200,6 +240,8 @@ export const DEPENDENCY_TYPE_OPTIONS: DependencyTypeOption[] = [
200
240
  tooltipLabelKey: "dependency.tooltips.finishes_before",
201
241
  edge: "FINISHES_BEFORE",
202
242
  reversed: false,
243
+ icon: ArrowLeftToLine,
244
+ iconRotation: 0,
203
245
  },
204
246
  ];
205
247
 
@@ -20,7 +20,7 @@ import { CalendarCheck, CalendarPlus2, Sprout } from "lucide-react"
20
20
  import { UserAvatar } from "@allbluecn/ui"
21
21
  import { useForm } from "@tanstack/react-form"
22
22
  import { useTranslation } from "react-i18next"
23
- import { ProjectIconDisplay } from "@/components/project/project-icon"
23
+ import { LucideIconByName } from "@/components/shared/lucide-icon-picker/lucide-icon-by-name"
24
24
  import { Button } from "@/components/ui/button"
25
25
  import { Input } from "@/components/ui/input"
26
26
 
@@ -63,7 +63,10 @@ interface CreateStoryDialogProps {
63
63
  initialState?: string
64
64
  initialParentId?: string
65
65
  initialProjectId?: string
66
- onSuccess?: (story: { id: string; name: string; identifier?: string }) => void
66
+ onSuccess?: (
67
+ story: { id: string; name: string; identifier?: string },
68
+ activities?: import("./types").ActivityItem[],
69
+ ) => void
67
70
  }
68
71
 
69
72
  export function CreateStoryDialog({
@@ -120,10 +123,12 @@ export function CreateStoryDialog({
120
123
  form.setFieldValue("startDate", initialStartDate ?? "")
121
124
  form.setFieldValue("targetDate", "")
122
125
  form.setFieldValue("labelIds", [])
123
- if (initialProjectId) {
124
- form.setFieldValue("projectId", initialProjectId)
125
- } else {
126
- ensureBuiltinProjectFn().then((p) => form.setFieldValue("projectId", p.id)).catch(() => {})
126
+ if (!continuousCreate) {
127
+ if (initialProjectId) {
128
+ form.setFieldValue("projectId", initialProjectId)
129
+ } else {
130
+ ensureBuiltinProjectFn().then((p) => form.setFieldValue("projectId", p.id)).catch(() => {})
131
+ }
127
132
  }
128
133
  setDescription({ html: "", markdown: "" })
129
134
  setIsEditorFocused(false)
@@ -173,14 +178,35 @@ export function CreateStoryDialog({
173
178
  labelIds: values.labelIds,
174
179
  },
175
180
  })
176
- form.reset()
177
- setDescription({ html: "", markdown: "" })
181
+ if (tiptapEditorRef.current) {
182
+ tiptapEditorRef.current.commands.clearContent()
183
+ }
184
+ if (continuousCreate) {
185
+ const currentProjectId = form.state.values.projectId
186
+ form.setFieldValue("name", "")
187
+ setDescription({ html: "", markdown: "" })
188
+ form.setFieldValue("state", initialState ?? "backlog")
189
+ form.setFieldValue("priority", "none")
190
+ form.setFieldValue("assigneeId", "none")
191
+ form.setFieldValue("startDate", initialStartDate ?? "")
192
+ form.setFieldValue("targetDate", "")
193
+ form.setFieldValue("labelIds", [])
194
+ if (currentProjectId) {
195
+ form.setFieldValue("projectId", currentProjectId)
196
+ }
197
+ } else {
198
+ form.reset()
199
+ setDescription({ html: "", markdown: "" })
200
+ }
178
201
  if (!continuousCreate) onOpenChange(false)
179
- onSuccess?.({
180
- id: res.id,
181
- name: res.name,
182
- identifier: res.identifier,
183
- })
202
+ onSuccess?.(
203
+ {
204
+ id: res.id,
205
+ name: res.name,
206
+ identifier: res.identifier,
207
+ },
208
+ res.activities,
209
+ )
184
210
  } catch (err) {
185
211
  setError(err instanceof Error ? err.message : t("create.failed"))
186
212
  } finally {
@@ -214,7 +240,7 @@ export function CreateStoryDialog({
214
240
  className="h-8 gap-1.5 border-0 bg-muted/50 px-2.5 text-sm font-medium"
215
241
  >
216
242
  {selectedProject ? (
217
- <ProjectIconDisplay name={selectedProject.icon} size={16} />
243
+ <LucideIconByName name={selectedProject.icon} size={16} />
218
244
  ) : (
219
245
  <Sprout className="size-4 text-muted-foreground" />
220
246
  )}
@@ -230,7 +256,7 @@ export function CreateStoryDialog({
230
256
  className={`flex w-full items-center justify-between gap-2 rounded px-2 py-1.5 text-sm ${field.value === p.id ? "bg-accent font-medium" : "hover:bg-muted"}`}
231
257
  >
232
258
  <span className="flex min-w-0 items-center gap-2">
233
- <ProjectIconDisplay name={p.icon} size={16} />
259
+ <LucideIconByName name={p.icon} size={16} />
234
260
  <span className="truncate">{p.name}</span>
235
261
  </span>
236
262
  {p.displayId && (
@@ -318,7 +344,7 @@ export function CreateStoryDialog({
318
344
  <DateEditor value={field.value || null} onChange={(v) => field.handleChange(v ?? "")} label={t("create.dueDate")} icon={CalendarCheck} />
319
345
  )} />
320
346
  <form.Field name="labelIds" children={(field) => (
321
- <LabelsEditor labelIds={field.value as string[]} labels={labels} onChange={(v) => field.handleChange(v as string[])} variant="button" />
347
+ <LabelsEditor labelIds={field.value as string[]} labels={labels} onChange={(v) => field.handleChange(v as string[])} variant="button" aiContext={{ name: () => ((form.state.values.name as string) ?? "").trim() }} />
322
348
  )} />
323
349
  </div>
324
350
 
@@ -0,0 +1,114 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { describe, it, expect, vi, beforeEach } from "vitest";
19
+ import React from "react";
20
+ import { render, screen, fireEvent } from "@testing-library/react";
21
+
22
+ vi.mock("@tanstack/react-start", () => ({
23
+ useServerFn: (fn: any) => vi.fn().mockImplementation((args: any) => fn(args)),
24
+ }));
25
+ vi.mock("@/server/serverFns/story", () => ({
26
+ removeLinkFn: vi.fn(async () => ({})),
27
+ removeAttachmentFn: vi.fn(async () => ({})),
28
+ unlinkDocFn: vi.fn(async () => ({})),
29
+ checkLinkEmbeddableFn: vi.fn(async () => ({ embeddable: true })),
30
+ }));
31
+ vi.mock("@/server/serverFns/knowledge", () => ({
32
+ getDocumentFn: vi.fn(async () => ({ content: { html: "<p>hello</p>", markdown: "hello" } })),
33
+ }));
34
+ vi.mock("sonner", () => ({
35
+ toast: { success: vi.fn(), error: vi.fn() },
36
+ }));
37
+ vi.mock("@tanstack/react-router", () => ({
38
+ Link: ({ children, to, params }: any) => React.createElement("a", { href: to, "data-to": to, ...params }, children),
39
+ }));
40
+
41
+ import { AssociatedAttachmentsPanel } from "../story-detail-toolbar/associated-attachments-panel";
42
+ import type { RelationsData } from "../../types";
43
+
44
+ const data = {
45
+ links: [{ id: "l1", url: "https://a.com", title: "站点A", description: null }],
46
+ attachments: [
47
+ { id: "a1", fileName: "文件.png", fileSize: 2048, mimeType: "image/png", storageUrl: "/m/1" },
48
+ ],
49
+ knowledge: {
50
+ prds: [],
51
+ docs: [
52
+ { id: "d1", title: "知识页", kbId: "kb1", kbName: "团队库", kbIcon: "notebook", docIcon: null, parentId: null },
53
+ ],
54
+ },
55
+ } as unknown as RelationsData;
56
+
57
+ function setup(props?: { data: RelationsData }) {
58
+ const onOpenDialog = vi.fn();
59
+ render(
60
+ <AssociatedAttachmentsPanel
61
+ storyId="s1"
62
+ data={props?.data ?? data}
63
+ onRefresh={vi.fn()}
64
+ onOpenDialog={onOpenDialog}
65
+ />,
66
+ );
67
+ return { onOpenDialog };
68
+ }
69
+
70
+ describe("AssociatedAttachmentsPanel", () => {
71
+ it("三类资源合并渲染,计数 badge 为总数 3,默认列表视图", () => {
72
+ setup();
73
+ expect(screen.getByText("关联附件")).toBeInTheDocument();
74
+ expect(screen.getByText("3")).toBeInTheDocument();
75
+ expect(screen.getByText("站点A")).toBeInTheDocument();
76
+ expect(screen.getByText("文件.png")).toBeInTheDocument();
77
+ expect(screen.getByText("知识页")).toBeInTheDocument();
78
+ expect(screen.getByText("团队库")).toBeInTheDocument();
79
+ });
80
+
81
+ it("点击预览按钮切换到预览模式,渲染网格布局", () => {
82
+ setup();
83
+ fireEvent.click(screen.getByRole("button", { name: "预览" }));
84
+ expect(screen.getByTestId("associated-items").getAttribute("data-mode")).toBe("preview");
85
+ });
86
+
87
+ it("空数据整面板不渲染", () => {
88
+ const empty = {
89
+ links: [],
90
+ attachments: [],
91
+ knowledge: { prds: [], docs: [] },
92
+ } as unknown as RelationsData;
93
+ const { container } = render(
94
+ <AssociatedAttachmentsPanel
95
+ storyId="s1"
96
+ data={empty}
97
+ onRefresh={vi.fn()}
98
+ onOpenDialog={vi.fn()}
99
+ />,
100
+ );
101
+ expect(container.querySelector("[data-testid='associated-panel']")).toBeNull();
102
+ });
103
+
104
+ it("点击编辑链接回调 onOpenDialog('link', link)", () => {
105
+ const { onOpenDialog } = setup();
106
+ const editBtns = screen.getAllByRole("button", { name: "编辑" });
107
+ // 第一个是链接的编辑按钮,第二个是附件的编辑按钮
108
+ fireEvent.click(editBtns[0]);
109
+ expect(onOpenDialog).toHaveBeenCalledWith(
110
+ "link",
111
+ expect.objectContaining({ id: "l1" }),
112
+ );
113
+ });
114
+ });
@@ -0,0 +1,62 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { describe, it, expect } from "vitest";
19
+ import { render, screen } from "@testing-library/react";
20
+ import { TooltipProvider } from "@/components/ui/tooltip";
21
+ import { StoryDetailCategories } from "../story-detail-toolbar/categories";
22
+
23
+ vi.mock("react-i18next", () => ({
24
+ useTranslation: () => ({ t: (key: string) => key }),
25
+ }));
26
+
27
+ const renderWithProviders = (ui: React.ReactElement) =>
28
+ render(<TooltipProvider>{ui}</TooltipProvider>);
29
+
30
+ describe("StoryDetailCategories", () => {
31
+ it("渲染 6 个 category button", () => {
32
+ renderWithProviders(<StoryDetailCategories />);
33
+ const buttons = screen.getAllByRole("button");
34
+ expect(buttons).toHaveLength(6);
35
+ });
36
+
37
+ it("渲染 6 个 category 标签", () => {
38
+ renderWithProviders(<StoryDetailCategories />);
39
+ expect(screen.getByText("categories.prd.label")).toBeTruthy();
40
+ expect(screen.getByText("categories.ui.label")).toBeTruthy();
41
+ expect(screen.getByText("categories.bug.label")).toBeTruthy();
42
+ expect(screen.getByText("categories.testCase.label")).toBeTruthy();
43
+ expect(screen.getByText("categories.acceptance.label")).toBeTruthy();
44
+ expect(screen.getByText("categories.retro.label")).toBeTruthy();
45
+ });
46
+
47
+ it("bug 和 test-case 之间有一个分隔线", () => {
48
+ renderWithProviders(<StoryDetailCategories />);
49
+ const spans = document.querySelectorAll("span");
50
+ const separator = Array.from(spans).find((s) =>
51
+ s.className.includes("w-px") && s.className.includes("h-4"),
52
+ );
53
+ expect(separator).toBeTruthy();
54
+ });
55
+
56
+ it("bug 前后各有一个 button", () => {
57
+ renderWithProviders(<StoryDetailCategories />);
58
+ const bugBtn = screen.getByText("categories.bug.label")
59
+ .closest("button")!;
60
+ expect(bugBtn).toBeTruthy();
61
+ });
62
+ });
@@ -0,0 +1,49 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { describe, it, expect, vi } from "vitest";
19
+ import { render, screen, fireEvent } from "@testing-library/react";
20
+ import { TooltipProvider } from "@/components/ui/tooltip";
21
+ import { StoryDetailDependencies } from "../story-detail-toolbar/dependencies";
22
+
23
+ vi.mock("react-i18next", () => ({
24
+ useTranslation: () => ({ t: (key: string) => key }),
25
+ }));
26
+
27
+ const renderWithProviders = (ui: React.ReactElement) =>
28
+ render(<TooltipProvider>{ui}</TooltipProvider>);
29
+
30
+ describe("StoryDetailDependencies", () => {
31
+ it("渲染 button,包含 Waypoints 图标和文本", () => {
32
+ renderWithProviders(
33
+ <StoryDetailDependencies onOpenAdd={vi.fn()} />,
34
+ );
35
+ const btn = screen.getByRole("button");
36
+ expect(btn).toBeTruthy();
37
+ expect(screen.getByText("relations.addDependency")).toBeTruthy();
38
+ expect(btn).toHaveAttribute("title", "relations.dependencyTitle");
39
+ });
40
+
41
+ it("点击 button 触发 onOpenAdd", () => {
42
+ const onOpenAdd = vi.fn();
43
+ renderWithProviders(
44
+ <StoryDetailDependencies onOpenAdd={onOpenAdd} />,
45
+ );
46
+ fireEvent.click(screen.getByRole("button"));
47
+ expect(onOpenAdd).toHaveBeenCalledTimes(1);
48
+ });
49
+ });
@@ -30,11 +30,11 @@ import {
30
30
  type StoryState,
31
31
  type StoryPriority,
32
32
  } from "@/components/story/constants";
33
- import { ProjectIconDisplay } from "@/components/project/project-icon";
33
+ import { LucideIconByName } from "@/components/shared/lucide-icon-picker/lucide-icon-by-name";
34
34
  import { listProjectsBriefFn } from "@/server/serverFns/project";
35
35
  import type { ActivityItem, UserBrief } from "@/components/story/types";
36
36
  import { groupActivities, type ActivityGroup } from "@/lib/story/activity-grouper";
37
- import { ArrowUpFromDot, ChevronDown, Dot, Sparkles, Layers, UserRound } from "lucide-react";
37
+ import { ArrowUpFromDot, ArrowRight, ChevronDown, Dot, Sparkles, Layers, UserRound } from "lucide-react";
38
38
  import { sanitize } from "@/lib/sanitize";
39
39
 
40
40
  function getDiffDecorator(
@@ -55,7 +55,7 @@ function getDiffDecorator(
55
55
  return <Icon className="size-3 shrink-0 text-muted-foreground" />;
56
56
  }
57
57
  if (action === "project_transferred") {
58
- return <ProjectIconDisplay name={projectIconName} size={12} className="shrink-0" />;
58
+ return <LucideIconByName name={projectIconName} size={12} className="shrink-0" />;
59
59
  }
60
60
  if (action === "assignee_changed") {
61
61
  // 优先用用户头像,找不到时降级为通用图标
@@ -250,7 +250,7 @@ function DiffCompare({ oldValue, newValue, oldDecorator, newDecorator, plain, bl
250
250
  </div>
251
251
  </div>
252
252
  <div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-10 flex size-5 items-center justify-center rounded-full bg-background border">
253
- <ArrowUpFromDot className="size-2.5 text-muted-foreground rotate-90" />
253
+ <ArrowRight className="size-2.5 text-muted-foreground" />
254
254
  </div>
255
255
  <div className="w-1/2 bg-green-50/40 dark:bg-green-950/20 p-2 pl-4">
256
256
  <div className="flex items-center gap-1.5">
@@ -322,11 +322,33 @@ interface ActivityTimelineProps {
322
322
  users?: UserBrief[];
323
323
  }
324
324
 
325
+ /** 关联类 action 集合:描述文案带 {{value}} 插值(对象名或数量) */
326
+ const OBJECT_ACTIVITY_ACTIONS: Record<string, true> = {
327
+ child_added: true,
328
+ dependency_added: true,
329
+ dependency_removed: true,
330
+ link_added: true,
331
+ link_updated: true,
332
+ link_removed: true,
333
+ attachment_added: true,
334
+ attachment_updated: true,
335
+ attachment_removed: true,
336
+ kb_linked: true,
337
+ kb_unlinked: true,
338
+ prd_unlinked: true,
339
+ };
340
+
325
341
  function describeActivity(a: ActivityItem, t: TFunction): string {
326
342
  if (a.action === "project_transferred" || a.action === "identifier_changed") {
327
343
  return t(ACTIVITY_LABEL[a.action] ?? a.action);
328
344
  }
329
345
 
346
+ // 关联类动作(添加子需求/依赖/链接/附件/知识库):newValue 为对象描述或数量,走插值文案
347
+ if (a.action in OBJECT_ACTIVITY_ACTIONS) {
348
+ const key = ACTIVITY_LABEL[a.action];
349
+ return t(key, { value: a.newValue ?? "" });
350
+ }
351
+
330
352
  const fieldLabel = ACTIVITY_LABEL[a.action] ?? a.action;
331
353
 
332
354
  if (!a.field) {