@allbluecn/web-app 0.5.1 → 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.
- package/package.json +235 -30
- package/src/components/iteration/auto-schedule-dialog.tsx +150 -0
- package/src/components/iteration/burndown-chart.tsx +63 -0
- package/src/components/iteration/constants.ts +28 -0
- package/src/components/iteration/create-iteration-dialog.tsx +140 -0
- package/src/components/iteration/iteration-card.tsx +60 -0
- package/src/components/iteration/iteration-editor.tsx +116 -0
- package/src/components/iteration/iteration-progress-bar.tsx +44 -0
- package/src/components/iteration/iteration-status-badge.tsx +33 -0
- package/src/components/knowledge/dialog/knowledge-form-dialog.tsx +21 -30
- package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +2 -2
- package/src/components/layout/sidebar-03/nav-workspace.tsx +2 -2
- package/src/components/project/create-project-dialog.tsx +44 -57
- package/src/components/shared/__tests__/lucide-curated-icons.test.ts +83 -0
- package/src/components/shared/__tests__/lucide-icon-data.test.ts +131 -0
- package/src/components/shared/__tests__/lucide-icon-picker.test.tsx +197 -0
- package/src/components/shared/lucide-icon-picker/curated-icons.ts +478 -0
- package/src/components/shared/lucide-icon-picker/icon-data.ts +109 -0
- package/src/components/shared/lucide-icon-picker/index.tsx +82 -0
- package/src/components/shared/lucide-icon-picker/lucide-icon-by-name.tsx +78 -0
- package/src/components/shared/lucide-icon-picker/panel.tsx +163 -0
- package/src/components/story/__tests__/constants.test.ts +255 -0
- package/src/components/story/ai/story-ai-panel.tsx +23 -15
- package/src/components/story/ai/story-ai-preview-list.tsx +19 -0
- package/src/components/story/constants.ts +33 -1
- package/src/components/story/create-story-dialog.tsx +30 -10
- package/src/components/story/detail/__tests__/categories.test.tsx +62 -0
- package/src/components/story/detail/__tests__/dependencies-button.test.tsx +49 -0
- package/src/components/story/detail/activity-timeline.tsx +11 -4
- package/src/components/story/detail/story-detail-body.tsx +89 -38
- package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +118 -9
- package/src/components/story/detail/story-detail-toolbar/children.tsx +7 -4
- package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +67 -7
- package/src/components/story/detail/story-detail-toolbar.tsx +6 -1
- package/src/components/story/detail/story-properties.tsx +21 -4
- package/src/components/story/display/__tests__/group-fields.test.ts +107 -0
- package/src/components/story/display/group-resolver.tsx +2 -2
- package/src/components/story/inline-editors/__tests__/dependency-type-select.test.tsx +155 -0
- package/src/components/story/inline-editors/__tests__/labels-editor.test.tsx +155 -0
- package/src/components/story/inline-editors/__tests__/priority-editor.test.tsx +173 -0
- package/src/components/story/inline-editors/__tests__/state-editor.test.tsx +174 -0
- package/src/components/story/inline-editors/dependency-type-select.tsx +180 -0
- package/src/components/story/inline-editors/index.ts +2 -1
- package/src/components/story/inline-editors/labels-editor.tsx +159 -3
- package/src/components/story/inline-editors/story-select-dialog.tsx +22 -59
- package/src/components/story/peek-panel.tsx +121 -21
- package/src/components/story/relations/link-manage-dialog.tsx +2 -1
- package/src/components/story/relations/story-ref-row.tsx +17 -27
- package/src/components/story/story-panel-header.tsx +25 -19
- package/src/components/story/story-toolbar.tsx +3 -3
- package/src/components/story/transfer-story-popover.tsx +2 -2
- package/src/components/story/types.ts +1 -0
- package/src/components/story/views/create-view-dialog.tsx +3 -3
- package/src/components/story/views/story-list/__tests__/group-header.test.tsx +219 -0
- package/src/components/story/views/story-list/group.tsx +1 -1
- package/src/components/story/views/story-list/inline-story-creator.tsx +2 -2
- package/src/components/story/views/story-list/row.tsx +1 -1
- package/src/components/workspace/create-workspace-dialog.tsx +41 -49
- package/src/lib/__tests__/project-display-id.test.ts +8 -5
- package/src/lib/changelog/sdk-versions.ts +20 -20
- package/src/lib/iteration/__tests__/status.test.ts +41 -0
- package/src/lib/iteration/status.ts +28 -0
- package/src/lib/project-display-id.ts +14 -5
- package/src/lib/project-module-meta.ts +77 -0
- package/src/lib/utils.d.ts +1 -1
- package/src/providers/index.tsx +6 -0
- package/src/routeTree.gen.ts +54 -0
- package/src/routes/__root.tsx +0 -8
- package/src/routes/_nav/knowledge/index.lazy.tsx +2 -2
- package/src/routes/_nav/projects/$projectId/$storyId/route.lazy.tsx +52 -25
- package/src/routes/_nav/projects/$projectId/$storyId/route.tsx +6 -3
- package/src/routes/_nav/projects/$projectId/index.lazy.tsx +32 -15
- package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.lazy.tsx +289 -0
- package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.tsx +27 -0
- package/src/routes/_nav/projects/$projectId/iterations/index.lazy.tsx +111 -0
- package/src/routes/_nav/projects/$projectId/iterations/index.tsx +24 -0
- package/src/routes/_nav/projects/$projectId/route.tsx +4 -2
- package/src/routes/_nav/projects/$projectId/settings/route.lazy.tsx +4 -4
- package/src/routes/_nav/stories/$storyId/route.lazy.tsx +5 -1
- package/src/routes/_nav/stories/$storyId/route.tsx +4 -0
- package/src/routes/_nav/workspaces/$workspaceId/route.lazy.tsx +3 -3
- package/src/routes/_nav/workspaces/$workspaceId/settings/route.lazy.tsx +4 -4
- package/src/routes/_nav/workspaces/index.lazy.tsx +3 -3
- package/src/server/serverFns/iteration/__tests__/auto-schedule-core.test.ts +62 -0
- package/src/server/serverFns/iteration/__tests__/burndown-core.test.ts +90 -0
- package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +211 -0
- package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +155 -0
- package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +136 -0
- package/src/server/serverFns/iteration/auto-schedule-core.ts +54 -0
- package/src/server/serverFns/iteration/auto-schedule-internal.ts +88 -0
- package/src/server/serverFns/iteration/burndown-core.ts +60 -0
- package/src/server/serverFns/iteration/index.ts +22 -0
- package/src/server/serverFns/iteration/iteration-auto-schedule.ts +66 -0
- package/src/server/serverFns/iteration/iteration-burndown.ts +70 -0
- package/src/server/serverFns/iteration/iteration-crud.ts +208 -0
- package/src/server/serverFns/iteration/iteration-stories.ts +111 -0
- package/src/server/serverFns/iteration/iteration-transfer.ts +42 -0
- package/src/server/serverFns/iteration/schemas.ts +58 -0
- package/src/server/serverFns/iteration/transfer-internal.ts +62 -0
- package/src/server/serverFns/project/module-overview.ts +109 -0
- package/src/server/serverFns/project.ts +2 -2
- package/src/server/serverFns/story/__tests__/label-suggest-fn.test.ts +100 -0
- package/src/server/serverFns/story/__tests__/labels.test.ts +67 -0
- package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +106 -2
- package/src/server/serverFns/story/index.ts +3 -0
- package/src/server/serverFns/story/story-assoc.ts +36 -4
- package/src/server/serverFns/story/story-attachments.ts +20 -2
- package/src/{components/project/project-icon.tsx → server/serverFns/story/story-crud/label-palette.ts} +16 -19
- package/src/server/serverFns/story/story-crud/label-suggest-core.ts +88 -0
- package/src/server/serverFns/story/story-crud/label-suggest.ts +38 -0
- package/src/{components/knowledge/knowledge-icon.tsx → server/serverFns/story/story-crud/label-upsert.ts} +13 -19
- package/src/server/serverFns/story/story-crud/labels.ts +3 -5
- package/src/server/serverFns/story/story-crud.ts +32 -2
- package/src/server/serverFns/story/story-dependencies.ts +123 -2
- package/src/server/serverFns/story/story-links.ts +30 -6
- package/src/server/serverFns/story/story-transfer.ts +5 -4
- package/src/server/serverFns/story/types.ts +1 -0
- package/src/types/project.ts +5 -5
- package/src/components/project/project-animated-icon.tsx +0 -73
- package/src/components/shared/icon-packs/index.ts +0 -1
- package/src/components/shared/icon-packs/knowledge.ts +0 -1
- package/src/components/shared/icon-packs/prd.ts +0 -1
- package/src/components/shared/icon-packs/project.ts +0 -1
- package/src/components/shared/icon-picker.tsx +0 -1
- 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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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
|
-
<
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
)}
|