@allbluecn/web-app 0.5.1 → 0.6.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 (125) hide show
  1. package/package.json +230 -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/layout/sidebar-03/__tests__/nav-workspace.test.tsx +2 -2
  12. package/src/components/layout/sidebar-03/nav-workspace.tsx +2 -2
  13. package/src/components/project/create-project-dialog.tsx +44 -57
  14. package/src/components/shared/__tests__/lucide-curated-icons.test.ts +83 -0
  15. package/src/components/shared/__tests__/lucide-icon-data.test.ts +131 -0
  16. package/src/components/shared/__tests__/lucide-icon-picker.test.tsx +197 -0
  17. package/src/components/shared/lucide-icon-picker/curated-icons.ts +478 -0
  18. package/src/components/shared/lucide-icon-picker/icon-data.ts +109 -0
  19. package/src/components/shared/lucide-icon-picker/index.tsx +82 -0
  20. package/src/components/shared/lucide-icon-picker/lucide-icon-by-name.tsx +78 -0
  21. package/src/components/shared/lucide-icon-picker/panel.tsx +163 -0
  22. package/src/components/story/__tests__/constants.test.ts +255 -0
  23. package/src/components/story/ai/story-ai-panel.tsx +23 -15
  24. package/src/components/story/ai/story-ai-preview-list.tsx +19 -0
  25. package/src/components/story/constants.ts +33 -1
  26. package/src/components/story/create-story-dialog.tsx +30 -10
  27. package/src/components/story/detail/__tests__/categories.test.tsx +62 -0
  28. package/src/components/story/detail/__tests__/dependencies-button.test.tsx +49 -0
  29. package/src/components/story/detail/activity-timeline.tsx +11 -4
  30. package/src/components/story/detail/story-detail-body.tsx +89 -38
  31. package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +118 -9
  32. package/src/components/story/detail/story-detail-toolbar/children.tsx +7 -4
  33. package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +67 -7
  34. package/src/components/story/detail/story-detail-toolbar.tsx +6 -1
  35. package/src/components/story/detail/story-properties.tsx +21 -4
  36. package/src/components/story/display/__tests__/group-fields.test.ts +107 -0
  37. package/src/components/story/display/group-resolver.tsx +2 -2
  38. package/src/components/story/inline-editors/__tests__/dependency-type-select.test.tsx +155 -0
  39. package/src/components/story/inline-editors/__tests__/labels-editor.test.tsx +155 -0
  40. package/src/components/story/inline-editors/__tests__/priority-editor.test.tsx +173 -0
  41. package/src/components/story/inline-editors/__tests__/state-editor.test.tsx +174 -0
  42. package/src/components/story/inline-editors/dependency-type-select.tsx +180 -0
  43. package/src/components/story/inline-editors/index.ts +2 -1
  44. package/src/components/story/inline-editors/labels-editor.tsx +159 -3
  45. package/src/components/story/inline-editors/story-select-dialog.tsx +22 -59
  46. package/src/components/story/peek-panel.tsx +121 -21
  47. package/src/components/story/relations/link-manage-dialog.tsx +2 -1
  48. package/src/components/story/relations/story-ref-row.tsx +17 -27
  49. package/src/components/story/story-panel-header.tsx +25 -19
  50. package/src/components/story/story-toolbar.tsx +3 -3
  51. package/src/components/story/transfer-story-popover.tsx +2 -2
  52. package/src/components/story/types.ts +1 -0
  53. package/src/components/story/views/create-view-dialog.tsx +3 -3
  54. package/src/components/story/views/story-list/__tests__/group-header.test.tsx +219 -0
  55. package/src/components/story/views/story-list/group.tsx +1 -1
  56. package/src/components/story/views/story-list/inline-story-creator.tsx +2 -2
  57. package/src/components/story/views/story-list/row.tsx +1 -1
  58. package/src/components/workspace/create-workspace-dialog.tsx +41 -49
  59. package/src/lib/__tests__/project-display-id.test.ts +8 -5
  60. package/src/lib/changelog/sdk-versions.ts +20 -20
  61. package/src/lib/iteration/__tests__/status.test.ts +41 -0
  62. package/src/lib/iteration/status.ts +28 -0
  63. package/src/lib/project-display-id.ts +14 -5
  64. package/src/lib/project-module-meta.ts +77 -0
  65. package/src/lib/utils.d.ts +1 -1
  66. package/src/providers/index.tsx +6 -0
  67. package/src/routeTree.gen.ts +54 -0
  68. package/src/routes/__root.tsx +0 -8
  69. package/src/routes/_nav/knowledge/index.lazy.tsx +2 -2
  70. package/src/routes/_nav/projects/$projectId/$storyId/route.lazy.tsx +52 -25
  71. package/src/routes/_nav/projects/$projectId/$storyId/route.tsx +6 -3
  72. package/src/routes/_nav/projects/$projectId/index.lazy.tsx +32 -15
  73. package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.lazy.tsx +289 -0
  74. package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.tsx +27 -0
  75. package/src/routes/_nav/projects/$projectId/iterations/index.lazy.tsx +111 -0
  76. package/src/routes/_nav/projects/$projectId/iterations/index.tsx +24 -0
  77. package/src/routes/_nav/projects/$projectId/route.tsx +4 -2
  78. package/src/routes/_nav/projects/$projectId/settings/route.lazy.tsx +4 -4
  79. package/src/routes/_nav/stories/$storyId/route.lazy.tsx +5 -1
  80. package/src/routes/_nav/stories/$storyId/route.tsx +4 -0
  81. package/src/routes/_nav/workspaces/$workspaceId/route.lazy.tsx +3 -3
  82. package/src/routes/_nav/workspaces/$workspaceId/settings/route.lazy.tsx +4 -4
  83. package/src/routes/_nav/workspaces/index.lazy.tsx +3 -3
  84. package/src/server/serverFns/iteration/__tests__/auto-schedule-core.test.ts +62 -0
  85. package/src/server/serverFns/iteration/__tests__/burndown-core.test.ts +90 -0
  86. package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +211 -0
  87. package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +155 -0
  88. package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +136 -0
  89. package/src/server/serverFns/iteration/auto-schedule-core.ts +54 -0
  90. package/src/server/serverFns/iteration/auto-schedule-internal.ts +88 -0
  91. package/src/server/serverFns/iteration/burndown-core.ts +60 -0
  92. package/src/server/serverFns/iteration/index.ts +22 -0
  93. package/src/server/serverFns/iteration/iteration-auto-schedule.ts +66 -0
  94. package/src/server/serverFns/iteration/iteration-burndown.ts +70 -0
  95. package/src/server/serverFns/iteration/iteration-crud.ts +208 -0
  96. package/src/server/serverFns/iteration/iteration-stories.ts +111 -0
  97. package/src/server/serverFns/iteration/iteration-transfer.ts +42 -0
  98. package/src/server/serverFns/iteration/schemas.ts +58 -0
  99. package/src/server/serverFns/iteration/transfer-internal.ts +62 -0
  100. package/src/server/serverFns/project/module-overview.ts +109 -0
  101. package/src/server/serverFns/project.ts +2 -2
  102. package/src/server/serverFns/story/__tests__/label-suggest-fn.test.ts +100 -0
  103. package/src/server/serverFns/story/__tests__/labels.test.ts +67 -0
  104. package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +106 -2
  105. package/src/server/serverFns/story/index.ts +3 -0
  106. package/src/server/serverFns/story/story-assoc.ts +36 -4
  107. package/src/server/serverFns/story/story-attachments.ts +20 -2
  108. package/src/{components/project/project-icon.tsx → server/serverFns/story/story-crud/label-palette.ts} +16 -19
  109. package/src/server/serverFns/story/story-crud/label-suggest-core.ts +88 -0
  110. package/src/server/serverFns/story/story-crud/label-suggest.ts +38 -0
  111. package/src/{components/knowledge/knowledge-icon.tsx → server/serverFns/story/story-crud/label-upsert.ts} +13 -19
  112. package/src/server/serverFns/story/story-crud/labels.ts +3 -5
  113. package/src/server/serverFns/story/story-crud.ts +32 -2
  114. package/src/server/serverFns/story/story-dependencies.ts +123 -2
  115. package/src/server/serverFns/story/story-links.ts +30 -6
  116. package/src/server/serverFns/story/story-transfer.ts +5 -4
  117. package/src/server/serverFns/story/types.ts +1 -0
  118. package/src/types/project.ts +5 -5
  119. package/src/components/project/project-animated-icon.tsx +0 -73
  120. package/src/components/shared/icon-packs/index.ts +0 -1
  121. package/src/components/shared/icon-packs/knowledge.ts +0 -1
  122. package/src/components/shared/icon-packs/prd.ts +0 -1
  123. package/src/components/shared/icon-packs/project.ts +0 -1
  124. package/src/components/shared/icon-picker.tsx +0 -1
  125. package/src/lib/project-mock.ts +0 -156
@@ -0,0 +1,180 @@
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 { useState } from "react";
19
+ import { useTranslation } from "react-i18next";
20
+ import { Check, Waypoints } from "lucide-react";
21
+ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
22
+ import { Button, buttonVariants } from "@/components/ui/button";
23
+ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
24
+ import { DEPENDENCY_TYPE_OPTIONS, type DependencyTypeOption } from "../constants";
25
+ import { cn } from "@/lib/utils";
26
+
27
+ const OPTION_GROUPS = ["dependsOn", "dependents"] as const;
28
+
29
+ /** 按选项配置渲染图标(旋转角度来自 constants 的 iconRotation,用内联 style 避免 JIT 动态类名) */
30
+ function OptionIcon({
31
+ option,
32
+ className,
33
+ }: {
34
+ option: DependencyTypeOption;
35
+ className?: string;
36
+ }) {
37
+ return (
38
+ <option.icon
39
+ className={cn("size-3.5 shrink-0 text-muted-foreground", className)}
40
+ style={{
41
+ transform:
42
+ option.iconRotation ? `rotate(${option.iconRotation}deg)` : undefined,
43
+ }}
44
+ />
45
+ );
46
+ }
47
+
48
+ interface DependencyTypeSelectProps {
49
+ /** 当前选中的依赖类型(受控) */
50
+ value?: DependencyTypeOption["key"];
51
+ onChange: (key: DependencyTypeOption["key"]) => void;
52
+ /** trigger=按钮形态(添加依赖弹窗底部);badge=行内标签形态(依赖列表就地编辑) */
53
+ variant?: "trigger" | "badge";
54
+ /** badge 形态:Badge 文案中插值的对方需求编号 */
55
+ identifier?: string;
56
+ /** badge 形态:tooltip 中当前需求编号(传入才使用 tooltips 文案,否则用 labels) */
57
+ currentIdentifier?: string;
58
+ /** Popover 开合(受控,Dialog 嵌套场景由外部持有以便统一外部点击关闭) */
59
+ open?: boolean;
60
+ onOpenChange?: (open: boolean) => void;
61
+ className?: string;
62
+ }
63
+
64
+ export function DependencyTypeSelect({
65
+ value,
66
+ onChange,
67
+ variant = "trigger",
68
+ identifier,
69
+ currentIdentifier,
70
+ open: openProp,
71
+ onOpenChange,
72
+ className,
73
+ }: DependencyTypeSelectProps) {
74
+ const { t } = useTranslation("story");
75
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
76
+ const open = openProp ?? uncontrolledOpen;
77
+ const handleOpenChange = (next: boolean) => {
78
+ if (openProp === undefined) setUncontrolledOpen(next);
79
+ onOpenChange?.(next);
80
+ };
81
+ const current = value
82
+ ? DEPENDENCY_TYPE_OPTIONS.find((o) => o.key === value)
83
+ : undefined;
84
+
85
+ const content = (
86
+ <PopoverContent align="start" className="w-80 p-1">
87
+ {OPTION_GROUPS.map((group, gi) => (
88
+ <div key={group} className={cn(gi > 0 && "mt-1 border-t pt-1")}>
89
+ <p className="px-2 py-1 text-xs font-medium text-muted-foreground">
90
+ {t(`dependency.optionGroups.${group}`)}
91
+ </p>
92
+ {DEPENDENCY_TYPE_OPTIONS.filter((o) => o.group === group).map((opt) => {
93
+ const checked = value === opt.key;
94
+ return (
95
+ <button
96
+ key={opt.key}
97
+ type="button"
98
+ onClick={() => {
99
+ onChange(opt.key);
100
+ handleOpenChange(false);
101
+ }}
102
+ className={cn(
103
+ "flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-sm hover:bg-muted",
104
+ checked && "font-medium",
105
+ )}
106
+ >
107
+ <OptionIcon option={opt} />
108
+ <span className="flex-1">{t(opt.optionLabelKey)}</span>
109
+ {checked && <Check className="size-3.5 shrink-0 text-primary" />}
110
+ </button>
111
+ );
112
+ })}
113
+ </div>
114
+ ))}
115
+ </PopoverContent>
116
+ );
117
+
118
+ if (variant === "badge") {
119
+ const tooltipKey = current
120
+ ? currentIdentifier
121
+ ? current.tooltipLabelKey
122
+ : current.badgeLabelKey
123
+ : null;
124
+ return (
125
+ <Popover open={open} onOpenChange={handleOpenChange}>
126
+ <Tooltip>
127
+ <TooltipTrigger asChild>
128
+ <PopoverTrigger asChild>
129
+ {/* 行打开按钮是 button,此处必须用 span 复刻 Button 样式(button 嵌 button 会 hydration 报错) */}
130
+ <span
131
+ className={cn(
132
+ buttonVariants({ variant: "outline", size: "sm" }),
133
+ "h-7 max-w-56 gap-1 rounded-md px-2 text-xs font-normal",
134
+ className,
135
+ )}
136
+ onClick={(e) => e.stopPropagation()}
137
+ >
138
+ {current && <OptionIcon option={current} />}
139
+ <span className="truncate">
140
+ {current
141
+ ? t(current.badgeLabelKey, { identifier: identifier ?? "" })
142
+ : t("dependency.typePlaceholder")}
143
+ </span>
144
+ </span>
145
+ </PopoverTrigger>
146
+ </TooltipTrigger>
147
+ {tooltipKey && (
148
+ <TooltipContent side="top" className="text-xs">
149
+ {t(tooltipKey, {
150
+ identifier: identifier ?? "",
151
+ me: currentIdentifier ?? "",
152
+ })}
153
+ </TooltipContent>
154
+ )}
155
+ </Tooltip>
156
+ {content}
157
+ </Popover>
158
+ );
159
+ }
160
+
161
+ return (
162
+ <Popover open={open} onOpenChange={handleOpenChange}>
163
+ <PopoverTrigger asChild>
164
+ <Button
165
+ variant="ghost"
166
+ size="sm"
167
+ className={cn("h-9 gap-1.5", !value && "text-muted-foreground", className)}
168
+ >
169
+ {current ? (
170
+ <OptionIcon option={current} className="text-current" />
171
+ ) : (
172
+ <Waypoints className="size-3.5" />
173
+ )}
174
+ {current ? t(current.optionLabelKey) : t("dependency.typePlaceholder")}
175
+ </Button>
176
+ </PopoverTrigger>
177
+ {content}
178
+ </Popover>
179
+ );
180
+ }
@@ -21,4 +21,5 @@ export { AssigneeEditor } from "./assignee-editor";
21
21
  export { DateEditor } from "./date-editor";
22
22
  export { LabelsEditor } from "./labels-editor";
23
23
  export { TitleEditor } from "./title-editor";
24
- export { ParentStoryEditor } from "./parent-story-editor";
24
+ export { ParentStoryEditor } from "./parent-story-editor";
25
+ export { DependencyTypeSelect } from "./dependency-type-select";
@@ -16,10 +16,14 @@
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
18
  import { useState } from "react";
19
- import { Tag, Check } from "lucide-react";
19
+ import { Tag, Check, Loader2, Sparkles } from "lucide-react";
20
20
  import { useTranslation } from "react-i18next";
21
21
  import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
22
+ import { Checkbox } from "@/components/ui/checkbox";
22
23
  import { cn } from "@/lib/utils";
24
+ import { useSession } from "@/lib/auth/auth-client";
25
+ import { toast } from "sonner";
26
+ import { createLabelFn, suggestStoryLabelsFn } from "@/server/serverFns/story";
23
27
  import { InlineTrigger } from "./inline-trigger";
24
28
 
25
29
  interface LabelsEditorProps {
@@ -28,13 +32,29 @@ interface LabelsEditorProps {
28
32
  onChange: (labelIds: string[]) => void;
29
33
  mode?: "single" | "multi";
30
34
  className?: string;
31
- /** 渲染形态:chip=有值时显示 chip 列表(默认,筛选场景),button=始终 Button(列表/详情场景) */
32
35
  variant?: "chip" | "button";
36
+ aiContext?: { name: () => string; description?: () => string | undefined };
33
37
  }
34
38
 
35
- export function LabelsEditor({ labelIds, labels, onChange, mode = "multi", className, variant = "chip" }: LabelsEditorProps) {
39
+ type AiPanel =
40
+ | { phase: "idle" }
41
+ | { phase: "loading" }
42
+ | {
43
+ phase: "done";
44
+ existingLabels: { id: string; name: string; color: string }[];
45
+ newNames: string[];
46
+ }
47
+ | { phase: "error" };
48
+
49
+ export function LabelsEditor({ labelIds, labels, onChange, mode = "multi", className, variant = "chip", aiContext }: LabelsEditorProps) {
36
50
  const { t } = useTranslation("story");
37
51
  const [open, setOpen] = useState(false);
52
+ const { data: session } = useSession();
53
+ const hasAiKey = session?.user?.hasAiKey ?? false;
54
+ const [ai, setAi] = useState<AiPanel>({ phase: "idle" });
55
+ const [selectedExisting, setSelectedExisting] = useState<string[]>([]);
56
+ const [selectedNew, setSelectedNew] = useState<string[]>([]);
57
+ const aiEnabled = !!aiContext && hasAiKey;
38
58
  const isMulti = mode === "multi";
39
59
  const showChip = variant === "chip";
40
60
 
@@ -43,6 +63,10 @@ export function LabelsEditor({ labelIds, labels, onChange, mode = "multi", class
43
63
  ? labelIds.map((id) => labels.find((l) => l.id === id)?.name).filter(Boolean).join("、")
44
64
  : t("inline.notSelected");
45
65
 
66
+ const currentNames = labelIds
67
+ .map((id) => labels.find((l) => l.id === id)?.name)
68
+ .filter(Boolean) as string[];
69
+
46
70
  const toggle = (id: string) => {
47
71
  if (isMulti) {
48
72
  const next = labelIds.includes(id)
@@ -55,6 +79,51 @@ export function LabelsEditor({ labelIds, labels, onChange, mode = "multi", class
55
79
  }
56
80
  };
57
81
 
82
+ const generateAiSuggestions = async () => {
83
+ if (!aiContext) return;
84
+ const name = aiContext.name().trim();
85
+ if (!name) {
86
+ toast.error(t("inline.aiNeedName"));
87
+ return;
88
+ }
89
+ setAi({ phase: "loading" });
90
+ try {
91
+ const result = await suggestStoryLabelsFn({
92
+ data: {
93
+ name,
94
+ description: aiContext.description?.() || undefined,
95
+ currentLabelNames: currentNames,
96
+ },
97
+ });
98
+ if (result.existingLabels.length === 0 && result.newNames.length === 0) {
99
+ setAi({ phase: "error" });
100
+ toast.error(t("inline.aiEmpty"));
101
+ return;
102
+ }
103
+ setAi({ phase: "done", ...result });
104
+ setSelectedExisting(result.existingLabels.map((l) => l.id));
105
+ setSelectedNew(result.newNames);
106
+ } catch (e) {
107
+ setAi({ phase: "error" });
108
+ toast.error(e instanceof Error ? e.message : t("inline.aiFailed"));
109
+ }
110
+ };
111
+
112
+ const applyAiSuggestions = async () => {
113
+ if (ai.phase !== "done") return;
114
+ const newIds: string[] = [];
115
+ for (const name of selectedNew) {
116
+ const created = await createLabelFn({ data: { name } });
117
+ newIds.push(created.id);
118
+ }
119
+ const merged = Array.from(new Set([...labelIds, ...selectedExisting, ...newIds]));
120
+ onChange(merged);
121
+ toast.success(t("inline.aiApplied", { count: merged.length - labelIds.length }));
122
+ setAi({ phase: "idle" });
123
+ setSelectedExisting([]);
124
+ setSelectedNew([]);
125
+ };
126
+
58
127
  return (
59
128
  <Popover open={open} onOpenChange={setOpen}>
60
129
  {showChip && labelIds.length > 0 ? (
@@ -113,6 +182,93 @@ export function LabelsEditor({ labelIds, labels, onChange, mode = "multi", class
113
182
  );
114
183
  })
115
184
  )}
185
+ {aiEnabled && (
186
+ <div className="border-t border-border mt-1 pt-1">
187
+ {ai.phase === "idle" && (
188
+ <button
189
+ type="button"
190
+ onClick={generateAiSuggestions}
191
+ className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-xs text-muted-foreground hover:bg-muted"
192
+ >
193
+ <Sparkles className="size-3.5" />
194
+ {t("inline.aiGenerate")}
195
+ </button>
196
+ )}
197
+ {ai.phase === "loading" && (
198
+ <div className="flex items-center gap-2 px-2 py-1.5 text-xs text-muted-foreground">
199
+ <Loader2 className="size-3.5 animate-spin" />
200
+ {t("inline.aiLoading")}
201
+ </div>
202
+ )}
203
+ {(ai.phase === "done" || ai.phase === "error") && (
204
+ <div className="max-h-44 overflow-y-auto p-1">
205
+ {ai.phase === "error" ? (
206
+ <div className="px-1 py-1.5 text-xs text-destructive">{t("inline.aiFailed")}</div>
207
+ ) : (
208
+ <>
209
+ {ai.existingLabels.map((l) => (
210
+ <div
211
+ key={l.id}
212
+ className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-xs hover:bg-muted cursor-pointer"
213
+ onClick={() =>
214
+ setSelectedExisting((prev) =>
215
+ prev.includes(l.id) ? prev.filter((x) => x !== l.id) : [...prev, l.id],
216
+ )
217
+ }
218
+ >
219
+ <Checkbox checked={selectedExisting.includes(l.id)} />
220
+ <span className="inline-block size-2.5 rounded-full shrink-0" style={{ backgroundColor: l.color }} />
221
+ {l.name}
222
+ </div>
223
+ ))}
224
+ {ai.newNames.map((n) => (
225
+ <div
226
+ key={n}
227
+ className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-xs hover:bg-muted cursor-pointer"
228
+ onClick={() =>
229
+ setSelectedNew((prev) =>
230
+ prev.includes(n) ? prev.filter((x) => x !== n) : [...prev, n],
231
+ )
232
+ }
233
+ >
234
+ <Checkbox checked={selectedNew.includes(n)} />
235
+ <span className="rounded-sm border border-dashed border-primary/60 px-1 text-[10px] leading-none text-primary">
236
+ {t("inline.aiNew")}
237
+ </span>
238
+ {n}
239
+ </div>
240
+ ))}
241
+ <div className="flex items-center gap-1 pt-1">
242
+ <button
243
+ type="button"
244
+ onClick={applyAiSuggestions}
245
+ className="rounded bg-primary px-2 py-1 text-xs text-primary-foreground"
246
+ >
247
+ {t("inline.aiApply")}
248
+ </button>
249
+ <button
250
+ type="button"
251
+ onClick={generateAiSuggestions}
252
+ className="rounded px-2 py-1 text-xs text-muted-foreground hover:bg-muted"
253
+ >
254
+ {t("inline.aiRegenerate")}
255
+ </button>
256
+ </div>
257
+ </>
258
+ )}
259
+ {ai.phase === "error" && (
260
+ <button
261
+ type="button"
262
+ onClick={generateAiSuggestions}
263
+ className="mt-1 w-full rounded px-2 py-1 text-xs text-muted-foreground hover:bg-muted"
264
+ >
265
+ {t("inline.aiRegenerate")}
266
+ </button>
267
+ )}
268
+ </div>
269
+ )}
270
+ </div>
271
+ )}
116
272
  </PopoverContent>
117
273
  </Popover>
118
274
  );
@@ -19,10 +19,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
19
19
  import { useTranslation } from "react-i18next";
20
20
  import { useDebouncedCallback, SEARCH_DEBOUNCE_MS } from "@/lib/pacer";
21
21
  import { cn } from "@/lib/utils";
22
- import { Search, Sprout, ChevronsUpDown, Check, Trees, ListFilter, Flag, Waypoints } from "lucide-react";
22
+ import { Search, Sprout, ChevronsUpDown, Check, Trees, ListFilter, Flag } from "lucide-react";
23
23
  import { Badge } from "@/components/ui/badge";
24
24
  import { Checkbox } from "@/components/ui/checkbox";
25
- import { ProjectIconDisplay } from "@/components/project/project-icon";
25
+ import { LucideIconByName } from "@/components/shared/lucide-icon-picker/lucide-icon-by-name";
26
26
  import { Button } from "@/components/ui/button";
27
27
  import { Input } from "@/components/ui/input";
28
28
  import {
@@ -50,11 +50,11 @@ import {
50
50
  STATE_LABEL,
51
51
  STORY_STATES,
52
52
  STORY_PRIORITIES,
53
- DEPENDENCY_TYPE_OPTIONS,
54
53
  type StoryState,
55
54
  type DependencyTypeOption,
56
55
  } from "../constants";
57
56
  import { PriorityIcon } from "../priority-icon";
57
+ import { DependencyTypeSelect } from "./dependency-type-select";
58
58
 
59
59
  interface ParentStoryItem {
60
60
  id: string;
@@ -63,6 +63,7 @@ interface ParentStoryItem {
63
63
  state: string;
64
64
  priority: string;
65
65
  projectId: string;
66
+ iteration?: { id: string; name: string } | null;
66
67
  }
67
68
 
68
69
  interface StorySelectDialogProps {
@@ -73,7 +74,7 @@ interface StorySelectDialogProps {
73
74
  initialProjectId?: string | null;
74
75
  defaultSelectedParentId?: string | null;
75
76
  defaultSelectedParentIds?: string[];
76
- mode?: "parent" | "child" | "dependency";
77
+ mode?: "parent" | "child" | "dependency" | "iteration";
77
78
  onSelected: (parentId: string | string[]) => void;
78
79
  onActivities?: (activities: ActivityItem[]) => void;
79
80
  skipTransfer?: boolean;
@@ -115,7 +116,7 @@ export function StorySelectDialog({
115
116
  }: StorySelectDialogProps) {
116
117
  const { t } = useTranslation("story");
117
118
  const resolvedTitle = title ?? t("select.defaultTitle");
118
- const isMulti = mode === "child" || mode === "dependency";
119
+ const isMulti = mode === "child" || mode === "dependency" || mode === "iteration";
119
120
  const [projectFilter, setProjectFilter] = useState<string>(ALL_PROJECTS);
120
121
  const [items, setItems] = useState<ParentStoryItem[]>([]);
121
122
  const [search, setSearch] = useState("");
@@ -480,7 +481,7 @@ export function StorySelectDialog({
480
481
  >
481
482
  {activeFilterProject ? (
482
483
  <span className="flex items-center gap-1.5">
483
- <ProjectIconDisplay name={activeFilterProject.icon} size={14} />
484
+ <LucideIconByName name={activeFilterProject.icon} size={14} />
484
485
  <span className="truncate max-w-[80px]">{activeFilterProject.name}</span>
485
486
  </span>
486
487
  ) : (
@@ -524,7 +525,7 @@ export function StorySelectDialog({
524
525
  }`}
525
526
  >
526
527
  <span className="flex min-w-0 items-center gap-2">
527
- <ProjectIconDisplay name={p.icon} size={14} />
528
+ <LucideIconByName name={p.icon} size={14} />
528
529
  <span className="min-w-0 flex-1 truncate">{p.name}</span>
529
530
  </span>
530
531
  {isActive && <Check className="size-3.5 shrink-0 text-primary" />}
@@ -621,9 +622,14 @@ export function StorySelectDialog({
621
622
  <span className="shrink-0 font-mono text-xs text-muted-foreground">
622
623
  {story.identifier}
623
624
  </span>
624
- <span className="min-w-0 flex-1 truncate">
625
- {story.name}
626
- </span>
625
+ <span className="min-w-0 flex-1 truncate">
626
+ {story.name}
627
+ </span>
628
+ {mode === "iteration" && story.iteration && (
629
+ <span className="shrink-0 text-[10px] text-muted-foreground">
630
+ {t("iterations:editor.currentIteration")}: {story.iteration.name}
631
+ </span>
632
+ )}
627
633
  {owner && (
628
634
  <span className="shrink-0 rounded-sm bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
629
635
  {owner.name}
@@ -675,55 +681,12 @@ export function StorySelectDialog({
675
681
  {selectedCount > 0 ? t("select.selectedCount", { count: selectedCount }) : t("select.pleaseSelect")}
676
682
  </span>
677
683
  {mode === "dependency" && (
678
- <Popover open={depPopoverOpen} onOpenChange={setDepPopoverOpen}>
679
- <PopoverTrigger asChild>
680
- <Button
681
- variant="ghost"
682
- size="sm"
683
- className={cn(
684
- "h-9 gap-1.5",
685
- !dependencyType && "text-muted-foreground",
686
- )}
687
- >
688
- <Waypoints className="size-3.5" />
689
- {dependencyType
690
- ? t(
691
- DEPENDENCY_TYPE_OPTIONS.find((o) => o.key === dependencyType)!
692
- .optionLabelKey,
693
- )
694
- : t("dependency.typePlaceholder")}
695
- </Button>
696
- </PopoverTrigger>
697
- <PopoverContent align="start" className="w-80 p-1">
698
- {(["dependsOn", "dependents"] as const).map((group, gi) => (
699
- <div key={group} className={cn(gi > 0 && "mt-1 border-t pt-1")}>
700
- <p className="px-2 py-1 text-xs font-medium text-muted-foreground">
701
- {t(`dependency.optionGroups.${group}`)}
702
- </p>
703
- {DEPENDENCY_TYPE_OPTIONS.filter((o) => o.group === group).map((opt) => {
704
- const checked = dependencyType === opt.key;
705
- return (
706
- <button
707
- key={opt.key}
708
- type="button"
709
- onClick={() => {
710
- onDependencyTypeChange?.(opt.key);
711
- setDepPopoverOpen(false);
712
- }}
713
- className={cn(
714
- "flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-sm hover:bg-muted",
715
- checked && "font-medium",
716
- )}
717
- >
718
- <span className="flex-1">{t(opt.optionLabelKey)}</span>
719
- {checked && <Check className="size-3.5 shrink-0 text-primary" />}
720
- </button>
721
- );
722
- })}
723
- </div>
724
- ))}
725
- </PopoverContent>
726
- </Popover>
684
+ <DependencyTypeSelect
685
+ value={dependencyType}
686
+ onChange={(key) => onDependencyTypeChange?.(key)}
687
+ open={depPopoverOpen}
688
+ onOpenChange={setDepPopoverOpen}
689
+ />
727
690
  )}
728
691
  </div>
729
692
  )}