@allbluecn/web-app 0.4.22 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/package.json +12 -8
  2. package/src/components/knowledge/knowledge-toolbar.tsx +124 -1
  3. package/src/components/layout/sidebar-03/app-sidebar.tsx +3 -4
  4. package/src/components/layout/sidebar-03/nav-notifications.tsx +135 -52
  5. package/src/components/layout/sidebar-03/user-menu.tsx +4 -2
  6. package/src/components/notifications/settings-notifications.tsx +198 -0
  7. package/src/components/notifications/team-notifications.tsx +343 -0
  8. package/src/components/settings/personal-team/team/team-client.tsx +51 -12
  9. package/src/components/settings/personal-team/team/team-invitations.tsx +181 -181
  10. package/src/components/shared/wave-physics-loader.tsx +155 -0
  11. package/src/components/story/__tests__/dependency-type-badge.test.ts +89 -0
  12. package/src/components/story/constants.ts +124 -51
  13. package/src/components/story/create-story-dialog.tsx +12 -6
  14. package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +114 -0
  15. package/src/components/story/detail/activity-timeline.tsx +15 -0
  16. package/src/components/story/detail/story-detail-body.tsx +14 -3
  17. package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +748 -0
  18. package/src/components/story/detail/story-detail-toolbar/categories.tsx +23 -18
  19. package/src/components/story/detail/story-detail-toolbar/children.tsx +51 -274
  20. package/src/components/story/detail/story-detail-toolbar/dependencies.tsx +13 -45
  21. package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +473 -0
  22. package/src/components/story/detail/story-detail-toolbar.tsx +237 -10
  23. package/src/components/story/display/__tests__/use-paged-story-groups.test.ts +152 -1
  24. package/src/components/story/display/use-paged-story-groups.ts +370 -28
  25. package/src/components/story/inline-editors/story-select-dialog.tsx +107 -16
  26. package/src/components/story/peek-panel.tsx +8 -1
  27. package/src/components/story/relations/__tests__/attachment-manage-dialog.test.tsx +171 -0
  28. package/src/components/story/relations/__tests__/knowledge-select-dialog.test.tsx +174 -0
  29. package/src/components/story/relations/__tests__/link-manage-dialog.test.tsx +93 -0
  30. package/src/components/story/relations/attachment-manage-dialog.tsx +490 -0
  31. package/src/components/story/relations/file-preview.tsx +173 -0
  32. package/src/components/story/relations/format.ts +120 -0
  33. package/src/{config/sidebar-notifications.ts → components/story/relations/general-groups.ts} +12 -30
  34. package/src/components/story/relations/knowledge-select-dialog.tsx +403 -0
  35. package/src/components/story/relations/link-manage-dialog.tsx +191 -0
  36. package/src/components/story/relations/story-ref-row.tsx +319 -0
  37. package/src/components/story/story-list-view.tsx +3 -1
  38. package/src/components/story/story-panel-header.tsx +4 -1
  39. package/src/components/story/types.ts +30 -18
  40. package/src/components/story/views/story-list/batch-toolbar.tsx +12 -1
  41. package/src/components/story/views/story-list/index.tsx +16 -5
  42. package/src/components/story/views/story-list/row.tsx +5 -4
  43. package/src/components/ui/separator.tsx +1 -1
  44. package/src/hooks/use-notifications.ts +4 -1
  45. package/src/lib/__tests__/datetime.test.ts +16 -1
  46. package/src/lib/changelog/sdk-versions.ts +19 -19
  47. package/src/lib/datetime.ts +1 -0
  48. package/src/lib/story/__tests__/video-embed.test.ts +77 -0
  49. package/src/lib/story/video-embed.ts +59 -0
  50. package/src/routeTree.gen.ts +65 -0
  51. package/src/routes/_nav/notifications/route.lazy.tsx +22 -0
  52. package/src/routes/_nav/notifications/route.tsx +30 -0
  53. package/src/routes/_nav/projects/$projectId/settings/route.lazy.tsx +74 -3
  54. package/src/routes/_nav/projects/$projectId/settings/route.tsx +6 -2
  55. package/src/routes/_nav/settings/notifications/route.lazy.tsx +6 -5
  56. package/src/routes/api/attachments/$id/content.ts +60 -0
  57. package/src/routes/api/attachments/upload.ts +148 -0
  58. package/src/routes/api/notifications/stream.ts +4 -0
  59. package/src/server/notifications/__tests__/notification-service.test.ts +189 -0
  60. package/src/server/notifications/notification-service.ts +137 -27
  61. package/src/server/serverFns/__tests__/project-kb.test.ts +147 -0
  62. package/src/server/serverFns/__tests__/team-invitation.test.ts +73 -7
  63. package/src/server/serverFns/__tests__/team-join-request.test.ts +5 -3
  64. package/src/server/serverFns/knowledge.ts +50 -4
  65. package/src/server/serverFns/locale.ts +15 -3
  66. package/src/server/serverFns/notifications.ts +18 -4
  67. package/src/server/serverFns/project.ts +43 -0
  68. package/src/server/serverFns/story/__tests__/kb-scope.test.ts +56 -0
  69. package/src/server/serverFns/story/__tests__/story-assoc.test.ts +222 -0
  70. package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +265 -0
  71. package/src/server/serverFns/story/__tests__/story-groups.test.ts +19 -1
  72. package/src/server/serverFns/story/index.ts +7 -6
  73. package/src/server/serverFns/story/kb-general.ts +39 -0
  74. package/src/server/serverFns/story/kb-scope.ts +52 -0
  75. package/src/server/serverFns/story/link-preview.ts +115 -0
  76. package/src/server/serverFns/story/story-assoc.ts +339 -50
  77. package/src/server/serverFns/story/story-attachments.ts +12 -43
  78. package/src/server/serverFns/story/story-crud.ts +21 -4
  79. package/src/server/serverFns/story/story-dependencies.ts +138 -162
  80. package/src/server/serverFns/story/story-groups.ts +23 -3
  81. package/src/server/serverFns/story/story-links.ts +13 -1
  82. package/src/server/serverFns/story/story-transfer.ts +5 -3
  83. package/src/server/serverFns/story/story-tree.ts +28 -1
  84. package/src/server/serverFns/story/types.ts +37 -0
  85. package/src/server/serverFns/team.ts +51 -2
  86. package/src/styles/globals.css +25 -0
  87. package/vite.shared.ts +2 -0
  88. package/src/components/notifications/notification-list.tsx +0 -232
  89. package/src/components/notifications/notification-preferences.tsx +0 -156
  90. package/src/components/story/detail/story-detail-toolbar/attachments.tsx +0 -73
  91. package/src/components/story/detail/story-detail-toolbar/knowledge.tsx +0 -73
  92. package/src/components/story/detail/story-detail-toolbar/links.tsx +0 -73
  93. package/src/components/story/relation-badge.tsx +0 -57
  94. package/src/components/story/relations/attachments-panel.tsx +0 -179
  95. package/src/components/story/relations/dependency-panel.tsx +0 -190
  96. package/src/components/story/relations/hierarchy-panel.tsx +0 -180
  97. package/src/components/story/relations/knowledge-panel.tsx +0 -284
  98. package/src/components/story/relations/links-panel.tsx +0 -240
  99. package/src/components/story/relations/tab.tsx +0 -212
@@ -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
  );
@@ -23,15 +23,9 @@ import {
23
23
  ChevronDown,
24
24
  GitBranch,
25
25
  SquareDashedMousePointer,
26
- MoreHorizontal,
27
- Archive,
28
- Trash2,
29
- CalendarCheck,
30
- CalendarPlus2,
31
26
  ChartNoAxesGantt,
32
27
  } from "lucide-react";
33
28
  import { Button } from "@/components/ui/button";
34
- import { Checkbox } from "@/components/ui/checkbox";
35
29
  import {
36
30
  DropdownMenu,
37
31
  DropdownMenuContent,
@@ -42,27 +36,19 @@ import {
42
36
  import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
43
37
  import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
44
38
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
45
- import { useServerFn } from "@tanstack/react-start";
46
39
  import { toast } from "sonner";
47
40
  import {
48
- updateStoryFn,
49
- archiveStoryFn,
50
- unarchiveStoryFn,
51
- deleteStoryFn,
52
41
  removeChildFn,
53
42
  bulkArchiveStoriesFn,
54
43
  bulkDeleteStoriesFn,
55
44
  bulkUpdateStoriesFn,
56
45
  } from "@/server/serverFns/story";
57
- import { PriorityEditor, StateEditor, AssigneeEditor, DateEditor, LabelsEditor } from "../../inline-editors";
58
- import { NameConfirmDialog } from "@/components/shared/name-confirm-dialog";
59
- import { cn } from "@/lib/utils";
60
- import { ProjectIconDisplay } from "@/components/project/project-icon";
61
46
  import { BatchToolbar } from "../../views/story-list/batch-toolbar";
62
47
  import { useGroupedStories } from "../../display/use-grouped-stories";
63
48
  import { VirtualGroupHeader } from "../../views/story-list/group";
64
49
  import { DisplayOptionsBody } from "../../display/display-dropdown";
65
- import type { DisplayOptions, DisplayProperty } from "../../display/types";
50
+ import { StoryRefRow } from "../../relations/story-ref-row";
51
+ import type { DisplayOptions } from "../../display/types";
66
52
  import type { RelationsData, StoryListItem, UserBrief } from "../../types";
67
53
  import type { StoryPatch } from "../../display/group-fields";
68
54
 
@@ -135,233 +121,6 @@ function ChildrenActionDropdown({ onAction }: { onAction: (action: "create" | "s
135
121
  );
136
122
  }
137
123
 
138
- function ChildStoryRow({
139
- story,
140
- users,
141
- labels,
142
- parentProjectId,
143
- projects,
144
- selected,
145
- properties,
146
- onToggle,
147
- onOpenStory,
148
- onStoryRefresh,
149
- onRemoveChild,
150
- }: {
151
- story: StoryListItem;
152
- users: UserBrief[];
153
- labels: { id: string; name: string; color: string }[];
154
- parentProjectId: string | null;
155
- projects: { id: string; name: string; icon: string; displayId: string | null }[];
156
- selected: boolean;
157
- properties: DisplayProperty[];
158
- onToggle: (id: string) => void;
159
- onOpenStory: (id: string) => void;
160
- onStoryRefresh: (storyId: string, patch: Record<string, any>) => void;
161
- onRemoveChild: (storyId: string) => void;
162
- }) {
163
- const { t } = useTranslation("story");
164
- const [storyState, setStoryState] = React.useState(story);
165
- React.useEffect(() => {
166
- setStoryState(story);
167
- }, [story]);
168
- const updateStory = useServerFn(updateStoryFn);
169
- const archiveStory = useServerFn(archiveStoryFn);
170
- const unarchiveStory = useServerFn(unarchiveStoryFn);
171
- const deleteStory = useServerFn(deleteStoryFn);
172
- const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false);
173
-
174
- const crossProject = storyState.projectId !== parentProjectId;
175
- const storyProjectInfo = projects.find((p) => p.id === storyState.projectId) ?? null;
176
-
177
- const updateField = async (patch: Record<string, any>) => {
178
- const updated = { ...storyState, ...patch };
179
- setStoryState(updated);
180
- try {
181
- await updateStory({ data: { storyId: story.id, ...patch } });
182
- onStoryRefresh(story.id, patch);
183
- } catch (e) {
184
- setStoryState(story);
185
- toast.error((e as Error)?.message || t("children.updateFailed"));
186
- }
187
- };
188
-
189
- const currentLabelIds = storyState.labels.map((l) => l.id);
190
- const overdue = !!(
191
- storyState.targetDate &&
192
- storyState.state !== "done" &&
193
- new Date(storyState.targetDate.slice(0, 10)) < new Date(new Date().toISOString().slice(0, 10))
194
- );
195
-
196
- const canArchiveSingle = storyState.state === "done" || storyState.state === "cancelled";
197
-
198
- const handleArchive = async () => {
199
- if (!canArchiveSingle) {
200
- toast.error(t("children.archiveOnlyDone"));
201
- return;
202
- }
203
- try {
204
- await archiveStory({ data: { storyId: story.id } });
205
- onStoryRefresh(story.id, { archived: true, archivedAt: new Date().toISOString() });
206
- toast.success(t("children.archived"));
207
- } catch (e) {
208
- toast.error((e as Error)?.message || t("children.archiveFailed"));
209
- }
210
- };
211
-
212
- const handleUnarchive = async () => {
213
- try {
214
- await unarchiveStory({ data: { storyId: story.id } });
215
- onStoryRefresh(story.id, { archived: false, archivedAt: null });
216
- toast.success(t("children.unarchived"));
217
- } catch (e) {
218
- toast.error((e as Error)?.message || t("children.unarchiveFailed"));
219
- }
220
- };
221
-
222
- const handleDelete = async () => {
223
- try {
224
- await deleteStory({ data: { storyId: story.id } });
225
- onStoryRefresh(story.id, { deleted: true });
226
- toast.success(t("children.deleted"));
227
- } catch (e) {
228
- toast.error((e as Error)?.message || t("children.deleteFailed"));
229
- }
230
- };
231
-
232
- const showArchived = !storyState.archived;
233
-
234
- return (
235
- <div
236
- className={`group flex items-center gap-1.5 px-2 py-1.5 text-sm transition-colors ${selected ? "bg-muted/20" : "bg-card hover:bg-accent/40"} border-b border-border/40`}
237
- >
238
- <Checkbox
239
- checked={selected}
240
- onCheckedChange={() => onToggle(story.id)}
241
- aria-label={t("children.selectStory", { name: story.name })}
242
- className={cn(
243
- "shrink-0 transition-opacity",
244
- selected ? "opacity-100" : "opacity-0 group-hover:opacity-100",
245
- )}
246
- />
247
- <button
248
- type="button"
249
- onClick={() => onOpenStory(storyState.id)}
250
- className="flex h-8 min-w-0 flex-1 items-center gap-2 text-left"
251
- >
252
- {crossProject && storyProjectInfo ? (
253
- <Tooltip>
254
- <TooltipTrigger asChild>
255
- <span
256
- className="shrink-0"
257
- style={{ color: "oklch(80.517% 0.20237 152.675 / 0.909)" }}
258
- >
259
- <ProjectIconDisplay name={storyProjectInfo.icon} size={14} />
260
- </span>
261
- </TooltipTrigger>
262
- <TooltipContent side="top" sideOffset={6} className="flex flex-col items-start gap-0.5 text-xs">
263
- <span className="opacity-80">{t("children.crossProject")}</span>
264
- <span className="font-medium">{storyProjectInfo.name}</span>
265
- </TooltipContent>
266
- </Tooltip>
267
- ) : storyProjectInfo ? (
268
- <ProjectIconDisplay
269
- name={storyProjectInfo.icon}
270
- size={14}
271
- className="shrink-0 text-muted-foreground"
272
- />
273
- ) : null}
274
- {crossProject && storyProjectInfo ? (
275
- <Tooltip>
276
- <TooltipTrigger asChild>
277
- <span
278
- className="font-mono text-xs shrink-0"
279
- style={{ color: "oklch(80.517% 0.20237 152.675 / 0.909)" }}
280
- >
281
- {storyState.identifier}
282
- </span>
283
- </TooltipTrigger>
284
- <TooltipContent side="top" sideOffset={6} className="flex flex-col items-start gap-0.5 text-xs">
285
- <span className="opacity-80">{t("children.crossProject")}</span>
286
- <span className="font-medium">{storyProjectInfo.name}</span>
287
- </TooltipContent>
288
- </Tooltip>
289
- ) : (
290
- <span className="font-mono text-xs text-muted-foreground shrink-0">{storyState.identifier}</span>
291
- )}
292
- <span className="min-w-0 flex-1 truncate font-medium">{storyState.name}</span>
293
- </button>
294
- {properties.includes("priority") && (
295
- <PriorityEditor value={storyState.priority} onChange={(v) => updateField({ priority: v })} variant="button" />
296
- )}
297
- {properties.includes("state") && (
298
- <StateEditor value={storyState.state} onChange={(v) => updateField({ state: v })} variant="button" />
299
- )}
300
- {properties.includes("assignee") && (
301
- <AssigneeEditor assignee={storyState.assignee} users={users} onChange={(v) => updateField({ assigneeId: v })} variant="button" />
302
- )}
303
- {properties.includes("startDate") && (
304
- <DateEditor value={storyState.startDate} onChange={(v) => updateField({ startDate: v })} label={t("children.startDate")} icon={CalendarPlus2} />
305
- )}
306
- {properties.includes("dueDate") && (
307
- <DateEditor value={storyState.targetDate} onChange={(v) => updateField({ targetDate: v })} label={t("children.dueDate")} icon={CalendarCheck} overdue={overdue} />
308
- )}
309
- {properties.includes("labels") && (
310
- <LabelsEditor labelIds={currentLabelIds} labels={labels} onChange={(v) => updateField({ labelIds: v })} variant="button" />
311
- )}
312
- <DropdownMenu>
313
- <DropdownMenuTrigger asChild>
314
- <Button variant="ghost" size="icon" className="size-7 p-0 ml-auto border border-border/60 rounded hover:bg-muted">
315
- <MoreHorizontal className="size-4" />
316
- </Button>
317
- </DropdownMenuTrigger>
318
- <DropdownMenuContent align="end" className="w-40" sideOffset={4}>
319
- <DropdownMenuItem onClick={() => onRemoveChild(story.id)}>
320
- <GitBranch className="mr-2 size-4" />
321
- {t("children.removeChild")}
322
- </DropdownMenuItem>
323
- <DropdownMenuSeparator />
324
- {showArchived ? (
325
- <>
326
- {canArchiveSingle && (
327
- <>
328
- <DropdownMenuItem onClick={handleArchive}>
329
- <Archive className="mr-2 size-4" />
330
- {t("children.archive")}
331
- </DropdownMenuItem>
332
- <DropdownMenuSeparator />
333
- </>
334
- )}
335
- <DropdownMenuItem
336
- onClick={() => setDeleteDialogOpen(true)}
337
- className="text-destructive focus:text-destructive"
338
- >
339
- <Trash2 className="mr-2 size-4" />
340
- {t("children.delete")}
341
- </DropdownMenuItem>
342
- </>
343
- ) : (
344
- <DropdownMenuItem onClick={handleUnarchive}>
345
- <Archive className="mr-2 size-4" />
346
- {t("children.unarchive")}
347
- </DropdownMenuItem>
348
- )}
349
- </DropdownMenuContent>
350
- </DropdownMenu>
351
-
352
- <NameConfirmDialog
353
- open={deleteDialogOpen}
354
- onOpenChange={setDeleteDialogOpen}
355
- title={t("children.deleteTitle")}
356
- description={t("children.deleteDescription")}
357
- confirmText={t("children.delete")}
358
- expectedName={storyState.identifier ?? storyState.id ?? ""}
359
- onConfirm={handleDelete}
360
- />
361
- </div>
362
- );
363
- }
364
-
365
124
  export function StoryDetailChildrenPanel({
366
125
  storyId,
367
126
  projectId,
@@ -560,33 +319,47 @@ export function StoryDetailChildrenPanel({
560
319
  return (
561
320
  <>
562
321
  <Collapsible defaultOpen>
563
- <CollapsibleTrigger className="group flex w-full items-center gap-2 pb-3 pt-4 text-xs font-semibold hover:text-foreground transition-colors">
564
- <div className="flex items-center gap-1.5">
565
- <span>{t("children.childStories")}</span>
566
- <span className="inline-flex min-w-[18px] items-center justify-center rounded-full bg-primary px-1 text-[10px] font-medium text-primary-foreground">
567
- {childrenList.length}
568
- </span>
569
- <ChevronDown className="size-3 text-muted-foreground transition-transform group-data-[state=open]:rotate-0 group-data-[state=closed]:-rotate-90" />
570
- </div>
571
- <div className="flex items-center gap-1 ml-auto" onClick={(e) => e.stopPropagation()}>
572
- <Popover>
573
- <PopoverTrigger asChild>
574
- <Button variant="ghost" size="sm" className="size-7 p-0">
575
- <ChartNoAxesGantt className="size-3.5" />
576
- </Button>
577
- </PopoverTrigger>
578
- <PopoverContent align="end" className="w-75 p-0">
579
- <div className="max-h-[60vh] overflow-y-auto">
580
- <DisplayOptionsBody
581
- filters={filters}
582
- onChange={updateFilters}
583
- layout="list"
584
- showSubItems={false}
585
- />
586
- </div>
587
- </PopoverContent>
588
- </Popover>
589
- <ChildrenActionDropdown onAction={onAction} />
322
+ <CollapsibleTrigger
323
+ 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"
325
+ >
326
+ <div
327
+ role="button"
328
+ tabIndex={0}
329
+ onKeyDown={(e) => {
330
+ if (e.key === "Enter" || e.key === " ") {
331
+ e.preventDefault();
332
+ (e.currentTarget as HTMLDivElement).click();
333
+ }
334
+ }}
335
+ >
336
+ <div className="flex items-center gap-1.5">
337
+ <span>{t("children.childStories")}</span>
338
+ <span className="inline-flex min-w-[18px] items-center justify-center rounded-full bg-primary px-1 text-[10px] font-medium text-primary-foreground">
339
+ {childrenList.length}
340
+ </span>
341
+ <ChevronDown className="size-3 text-muted-foreground transition-transform group-data-[state=open]:rotate-0 group-data-[state=closed]:-rotate-90" />
342
+ </div>
343
+ <div className="flex items-center gap-1 ml-auto" onClick={(e) => e.stopPropagation()}>
344
+ <Popover>
345
+ <PopoverTrigger asChild>
346
+ <Button variant="ghost" size="sm" className="size-7 p-0">
347
+ <ChartNoAxesGantt className="size-3.5" />
348
+ </Button>
349
+ </PopoverTrigger>
350
+ <PopoverContent align="end" className="w-75 p-0">
351
+ <div className="max-h-[60vh] overflow-y-auto">
352
+ <DisplayOptionsBody
353
+ filters={filters}
354
+ onChange={updateFilters}
355
+ layout="list"
356
+ showSubItems={false}
357
+ />
358
+ </div>
359
+ </PopoverContent>
360
+ </Popover>
361
+ <ChildrenActionDropdown onAction={onAction} />
362
+ </div>
590
363
  </div>
591
364
  </CollapsibleTrigger>
592
365
  <CollapsibleContent className="pb-2">
@@ -637,7 +410,7 @@ export function StoryDetailChildrenPanel({
637
410
  </div>
638
411
  {!subCollapsed &&
639
412
  subGroup.items.map((story) => (
640
- <ChildStoryRow
413
+ <StoryRefRow
641
414
  key={story.id}
642
415
  story={story}
643
416
  users={users ?? []}
@@ -648,7 +421,8 @@ export function StoryDetailChildrenPanel({
648
421
  properties={filters.properties}
649
422
  onToggle={toggleStory}
650
423
  onOpenStory={handleOpenStory}
651
- onRemoveChild={handleRemoveChild}
424
+ onRemove={handleRemoveChild}
425
+ removeMenuLabel={t("children.removeChild")}
652
426
  onStoryRefresh={(storyId, patch) => {
653
427
  if (patch.deleted) {
654
428
  onRefresh((prev) => ({
@@ -674,6 +448,7 @@ export function StoryDetailChildrenPanel({
674
448
  children: prev.hierarchy.children.map((c) => c.id === storyId ? { ...c, ...patch } : c),
675
449
  },
676
450
  }));
451
+ refreshChildren();
677
452
  }
678
453
  }}
679
454
  />
@@ -683,7 +458,7 @@ export function StoryDetailChildrenPanel({
683
458
  })
684
459
  ) : (
685
460
  group.items.map((story) => (
686
- <ChildStoryRow
461
+ <StoryRefRow
687
462
  key={story.id}
688
463
  story={story}
689
464
  users={users ?? []}
@@ -694,7 +469,8 @@ export function StoryDetailChildrenPanel({
694
469
  properties={filters.properties}
695
470
  onToggle={toggleStory}
696
471
  onOpenStory={handleOpenStory}
697
- onRemoveChild={handleRemoveChild}
472
+ onRemove={handleRemoveChild}
473
+ removeMenuLabel={t("children.removeChild")}
698
474
  onStoryRefresh={(storyId, patch) => {
699
475
  if (patch.deleted) {
700
476
  onRefresh((prev) => ({
@@ -720,6 +496,7 @@ export function StoryDetailChildrenPanel({
720
496
  children: prev.hierarchy.children.map((c) => c.id === storyId ? { ...c, ...patch } : c),
721
497
  },
722
498
  }));
499
+ refreshChildren();
723
500
  }
724
501
  }}
725
502
  />
@@ -18,56 +18,24 @@
18
18
  import { Waypoints } from "lucide-react";
19
19
  import { useTranslation } from "react-i18next";
20
20
  import { Button } from "@/components/ui/button";
21
- import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
22
- import { DependencyPanel } from "../../relations/dependency-panel";
23
- import type { RelationsData } from "../../types";
24
21
 
25
22
  export function StoryDetailDependencies({
26
- storyId,
27
- data,
28
- onRefresh,
23
+ onOpenAdd,
29
24
  }: {
30
- storyId: string;
31
- data: RelationsData;
32
- onRefresh: (next: (prev: RelationsData) => RelationsData) => void;
25
+ onOpenAdd: () => void;
33
26
  }) {
34
27
  const { t } = useTranslation("story");
35
- const count = data.dependencies.blocking.length + data.dependencies.blockedBy.length + data.dependencies.relatesTo.length;
36
28
 
37
29
  return (
38
- <Popover>
39
- <PopoverTrigger asChild>
40
- <Button
41
- variant="ghost"
42
- size="sm"
43
- className="h-7 gap-1.5 px-2.5 text-xs font-normal hover:bg-muted rounded-md"
44
- title={t("relations.dependencyTitle")}
45
- >
46
- <Waypoints className="size-3.5 text-muted-foreground" />
47
- {t("relations.dependency")}
48
- {count > 0 && (
49
- <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">
50
- {count}
51
- </span>
52
- )}
53
- </Button>
54
- </PopoverTrigger>
55
- <PopoverContent align="start" className="w-[380px] p-4">
56
- <div className="flex items-center justify-between mb-3">
57
- <span className="text-sm font-semibold">{t("relations.dependency")}</span>
58
- {count > 0 && <span className="text-xs text-muted-foreground">{t("relations.itemCount", { count })}</span>}
59
- </div>
60
- <DependencyPanel
61
- storyId={storyId}
62
- data={data.dependencies}
63
- onUpdate={(updates) =>
64
- onRefresh((prev) => ({
65
- ...prev,
66
- dependencies: { ...prev.dependencies, ...updates },
67
- }))
68
- }
69
- />
70
- </PopoverContent>
71
- </Popover>
30
+ <Button
31
+ variant="ghost"
32
+ size="sm"
33
+ className="h-7 gap-1.5 px-2.5 text-xs font-normal hover:bg-muted rounded-md"
34
+ onClick={onOpenAdd}
35
+ title={t("relations.dependencyTitle")}
36
+ >
37
+ <Waypoints className="size-3.5 text-muted-foreground" />
38
+ {t("relations.addDependency")}
39
+ </Button>
72
40
  );
73
- }
41
+ }