@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.
- package/package.json +239 -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/knowledge/knowledge-toolbar.tsx +124 -1
- 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 +43 -1
- package/src/components/story/create-story-dialog.tsx +42 -16
- package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +114 -0
- 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 +26 -4
- package/src/components/story/detail/story-detail-body.tsx +90 -38
- package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +857 -0
- package/src/components/story/detail/story-detail-toolbar/categories.tsx +23 -18
- package/src/components/story/detail/story-detail-toolbar/children.tsx +9 -4
- package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +95 -27
- package/src/components/story/detail/story-detail-toolbar.tsx +159 -8
- 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 +41 -63
- package/src/components/story/peek-panel.tsx +121 -21
- package/src/components/story/relations/__tests__/attachment-manage-dialog.test.tsx +171 -0
- package/src/components/story/relations/__tests__/knowledge-select-dialog.test.tsx +174 -0
- package/src/components/story/relations/__tests__/link-manage-dialog.test.tsx +93 -0
- package/src/components/story/relations/attachment-manage-dialog.tsx +490 -0
- package/src/components/story/relations/file-preview.tsx +173 -0
- package/src/components/story/relations/format.ts +120 -0
- package/src/components/story/relations/general-groups.ts +32 -0
- package/src/components/story/relations/knowledge-select-dialog.tsx +403 -0
- package/src/components/story/relations/link-manage-dialog.tsx +192 -0
- 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 +14 -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/story/__tests__/video-embed.test.ts +77 -0
- package/src/lib/story/video-embed.ts +59 -0
- package/src/lib/utils.d.ts +1 -1
- package/src/plugins/.gen-manifest.json +13 -0
- package/src/plugins/modules.gen.ts +20 -0
- package/src/plugins/plugins.gen.ts +20 -0
- package/src/plugins/ui.gen.ts +8 -0
- package/src/providers/index.tsx +6 -0
- package/src/routeTree.gen.ts +96 -0
- package/src/routes/__root.tsx +0 -8
- package/src/routes/_nav/inspiration/route.tsx +8 -0
- package/src/routes/_nav/knowledge/index.lazy.tsx +2 -2
- package/src/routes/_nav/prd/$id/route.tsx +9 -0
- package/src/routes/_nav/prd/index.tsx +9 -0
- package/src/routes/_nav/prd/route.tsx +8 -0
- 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 +78 -7
- package/src/routes/_nav/projects/$projectId/settings/route.tsx +6 -2
- 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/tags/$id/route.tsx +8 -0
- package/src/routes/_nav/tags/index.tsx +8 -0
- package/src/routes/_nav/tags/route.tsx +8 -0
- package/src/routes/_nav/tags/settings/route.tsx +9 -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/routes/api/attachments/$id/content.ts +60 -0
- package/src/routes/api/attachments/upload.ts +148 -0
- package/src/routes/share/prd/$prdId/route.tsx +10 -0
- package/src/server/serverFns/__tests__/project-kb.test.ts +147 -0
- 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/knowledge.ts +50 -4
- package/src/server/serverFns/project/module-overview.ts +109 -0
- package/src/server/serverFns/project.ts +45 -2
- package/src/server/serverFns/story/__tests__/kb-scope.test.ts +56 -0
- 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-assoc.test.ts +222 -0
- package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +132 -2
- package/src/server/serverFns/story/index.ts +9 -4
- package/src/server/serverFns/story/kb-general.ts +39 -0
- package/src/server/serverFns/story/kb-scope.ts +52 -0
- package/src/server/serverFns/story/link-preview.ts +115 -0
- package/src/server/serverFns/story/story-assoc.ts +372 -51
- package/src/server/serverFns/story/story-attachments.ts +32 -45
- 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 +53 -6
- package/src/server/serverFns/story/story-dependencies.ts +158 -5
- package/src/server/serverFns/story/story-links.ts +43 -7
- package/src/server/serverFns/story/story-transfer.ts +5 -4
- package/src/server/serverFns/story/story-tree.ts +28 -1
- package/src/server/serverFns/story/types.ts +38 -0
- package/src/styles/globals.css +25 -0
- package/src/types/project.ts +5 -5
- package/vite.shared.ts +2 -0
- 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/components/story/detail/story-detail-toolbar/attachments.tsx +0 -73
- package/src/components/story/detail/story-detail-toolbar/knowledge.tsx +0 -73
- package/src/components/story/detail/story-detail-toolbar/links.tsx +0 -73
- package/src/components/story/relations/attachments-panel.tsx +0 -179
- package/src/components/story/relations/knowledge-panel.tsx +0 -284
- package/src/components/story/relations/links-panel.tsx +0 -240
- package/src/lib/project-mock.ts +0 -156
|
@@ -0,0 +1,192 @@
|
|
|
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 * as React from "react";
|
|
19
|
+
import { useTranslation } from "react-i18next";
|
|
20
|
+
import { useServerFn } from "@tanstack/react-start";
|
|
21
|
+
import { toast } from "sonner";
|
|
22
|
+
import { Button } from "@/components/ui/button";
|
|
23
|
+
import { Input } from "@/components/ui/input";
|
|
24
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
25
|
+
import {
|
|
26
|
+
Dialog,
|
|
27
|
+
DialogContent,
|
|
28
|
+
DialogHeader,
|
|
29
|
+
DialogTitle,
|
|
30
|
+
} from "@/components/ui/dialog";
|
|
31
|
+
import { addLinkFn, updateLinkFn } from "@/server/serverFns/story";
|
|
32
|
+
import type { StoryLinkItem } from "../types";
|
|
33
|
+
|
|
34
|
+
export interface LinkManageDialogProps {
|
|
35
|
+
open: boolean;
|
|
36
|
+
onOpenChange: (open: boolean) => void;
|
|
37
|
+
storyId: string;
|
|
38
|
+
data: StoryLinkItem[];
|
|
39
|
+
onUpdate: (links: StoryLinkItem[]) => void;
|
|
40
|
+
/** 添加链接产生的活动,供时间线即时追加 */
|
|
41
|
+
onActivity?: (activity: import("../types").ActivityItem) => void;
|
|
42
|
+
/** 非空时进入编辑态(由合并面板「编辑」触发) */
|
|
43
|
+
initialEdit?: StoryLinkItem | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function LinkManageDialog({
|
|
47
|
+
open,
|
|
48
|
+
onOpenChange,
|
|
49
|
+
storyId,
|
|
50
|
+
data,
|
|
51
|
+
onUpdate,
|
|
52
|
+
onActivity,
|
|
53
|
+
initialEdit = null,
|
|
54
|
+
}: LinkManageDialogProps) {
|
|
55
|
+
const { t } = useTranslation("story");
|
|
56
|
+
const addLink = useServerFn(addLinkFn);
|
|
57
|
+
const updateLink = useServerFn(updateLinkFn);
|
|
58
|
+
const [editing, setEditing] = React.useState<StoryLinkItem | null>(null);
|
|
59
|
+
const [form, setForm] = React.useState({ url: "", title: "", description: "" });
|
|
60
|
+
const [submitting, setSubmitting] = React.useState(false);
|
|
61
|
+
|
|
62
|
+
React.useEffect(() => {
|
|
63
|
+
if (open) {
|
|
64
|
+
setEditing(initialEdit);
|
|
65
|
+
setForm(
|
|
66
|
+
initialEdit
|
|
67
|
+
? { url: initialEdit.url, title: initialEdit.title ?? "", description: initialEdit.description ?? "" }
|
|
68
|
+
: { url: "", title: "", description: "" },
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}, [open, initialEdit]);
|
|
72
|
+
|
|
73
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
if (submitting) return;
|
|
76
|
+
setSubmitting(true);
|
|
77
|
+
try {
|
|
78
|
+
if (editing) {
|
|
79
|
+
const updated = await updateLink({
|
|
80
|
+
data: {
|
|
81
|
+
linkId: editing.id,
|
|
82
|
+
url: form.url,
|
|
83
|
+
title: form.title || undefined,
|
|
84
|
+
description: form.description || undefined,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
toast.success(t("linkDialog.updated"));
|
|
88
|
+
if (updated.activity) onActivity?.(updated.activity);
|
|
89
|
+
onUpdate(
|
|
90
|
+
data.map((l) =>
|
|
91
|
+
l.id === editing.id
|
|
92
|
+
? { ...l, url: form.url, title: form.title || null, description: form.description || null }
|
|
93
|
+
: l,
|
|
94
|
+
),
|
|
95
|
+
);
|
|
96
|
+
} else {
|
|
97
|
+
const created = await addLink({
|
|
98
|
+
data: {
|
|
99
|
+
storyId,
|
|
100
|
+
url: form.url,
|
|
101
|
+
title: form.title || undefined,
|
|
102
|
+
description: form.description || undefined,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
toast.success(t("linkDialog.added"));
|
|
106
|
+
if (created.activity) onActivity?.(created.activity);
|
|
107
|
+
onUpdate([
|
|
108
|
+
...data,
|
|
109
|
+
{
|
|
110
|
+
id: created.id,
|
|
111
|
+
url: created.url,
|
|
112
|
+
title: created.title,
|
|
113
|
+
description: created.description,
|
|
114
|
+
},
|
|
115
|
+
]);
|
|
116
|
+
setForm({ url: "", title: "", description: "" });
|
|
117
|
+
}
|
|
118
|
+
setEditing(null);
|
|
119
|
+
onOpenChange(false);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
toast.error((err as Error)?.message || t("linkDialog.saveFailed"));
|
|
122
|
+
} finally {
|
|
123
|
+
setSubmitting(false);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
129
|
+
<DialogContent className="sm:max-w-130">
|
|
130
|
+
<DialogHeader>
|
|
131
|
+
<DialogTitle className="text-xl">
|
|
132
|
+
{t(editing ? "linkDialog.editTitle" : "linkDialog.title")}
|
|
133
|
+
</DialogTitle>
|
|
134
|
+
</DialogHeader>
|
|
135
|
+
<form onSubmit={handleSubmit} className="flex h-96 flex-col gap-4 overflow-y-auto px-1 pb-1">
|
|
136
|
+
<div className="space-y-1.5">
|
|
137
|
+
<label htmlFor="link-url" className="text-xs font-medium text-muted-foreground">
|
|
138
|
+
URL
|
|
139
|
+
</label>
|
|
140
|
+
<Input
|
|
141
|
+
id="link-url"
|
|
142
|
+
type="url"
|
|
143
|
+
required
|
|
144
|
+
placeholder={t("linkDialog.urlPlaceholder")}
|
|
145
|
+
value={form.url}
|
|
146
|
+
onChange={(e) => setForm({ ...form, url: e.target.value })}
|
|
147
|
+
className="h-10 text-base"
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="space-y-1.5">
|
|
151
|
+
<label htmlFor="link-title" className="text-xs font-medium text-muted-foreground">
|
|
152
|
+
{t("linkDialog.titleOptional")}
|
|
153
|
+
</label>
|
|
154
|
+
<Input
|
|
155
|
+
id="link-title"
|
|
156
|
+
placeholder={t("linkDialog.titlePlaceholder")}
|
|
157
|
+
value={form.title}
|
|
158
|
+
onChange={(e) => setForm({ ...form, title: e.target.value })}
|
|
159
|
+
className="h-10"
|
|
160
|
+
/>
|
|
161
|
+
</div>
|
|
162
|
+
<div className="flex min-h-0 flex-1 flex-col gap-1.5">
|
|
163
|
+
<label htmlFor="link-desc" className="text-xs font-medium text-muted-foreground">
|
|
164
|
+
{t("linkDialog.descOptional")}
|
|
165
|
+
</label>
|
|
166
|
+
<Textarea
|
|
167
|
+
id="link-desc"
|
|
168
|
+
placeholder={t("linkDialog.descPlaceholder")}
|
|
169
|
+
value={form.description}
|
|
170
|
+
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
|
171
|
+
className="min-h-20 flex-1 resize-none"
|
|
172
|
+
/>
|
|
173
|
+
</div>
|
|
174
|
+
<div className="flex shrink-0 justify-end gap-2 pt-1">
|
|
175
|
+
{editing && (
|
|
176
|
+
<Button
|
|
177
|
+
type="button"
|
|
178
|
+
variant="outline"
|
|
179
|
+
onClick={() => onOpenChange(false)}
|
|
180
|
+
>
|
|
181
|
+
{t("linkDialog.cancel")}
|
|
182
|
+
</Button>
|
|
183
|
+
)}
|
|
184
|
+
<Button type="submit" disabled={submitting}>
|
|
185
|
+
{t(editing ? "linkDialog.save" : "linkDialog.add")}
|
|
186
|
+
</Button>
|
|
187
|
+
</div>
|
|
188
|
+
</form>
|
|
189
|
+
</DialogContent>
|
|
190
|
+
</Dialog>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
@@ -36,11 +36,10 @@ import {
|
|
|
36
36
|
unarchiveStoryFn,
|
|
37
37
|
deleteStoryFn,
|
|
38
38
|
} from "@/server/serverFns/story";
|
|
39
|
-
import { PriorityEditor, StateEditor, AssigneeEditor, DateEditor, LabelsEditor } from "../inline-editors";
|
|
39
|
+
import { PriorityEditor, StateEditor, AssigneeEditor, DateEditor, LabelsEditor, DependencyTypeSelect } from "../inline-editors";
|
|
40
40
|
import { NameConfirmDialog } from "@/components/shared/name-confirm-dialog";
|
|
41
41
|
import { cn } from "@/lib/utils";
|
|
42
|
-
import {
|
|
43
|
-
import { Badge } from "@/components/ui/badge";
|
|
42
|
+
import { LucideIconByName } from "@/components/shared/lucide-icon-picker/lucide-icon-by-name";
|
|
44
43
|
import { dependencyTypeOption } from "../constants";
|
|
45
44
|
import type { DisplayProperty } from "../display/types";
|
|
46
45
|
import type { StoryPatch } from "../display/group-fields";
|
|
@@ -50,6 +49,7 @@ import type {
|
|
|
50
49
|
DependencyEdgeType,
|
|
51
50
|
DependencyGroup,
|
|
52
51
|
} from "../types";
|
|
52
|
+
import type { DependencyTypeOption } from "../constants";
|
|
53
53
|
|
|
54
54
|
/** 行内编辑/归档/删除后回传给面板的补丁载荷(StoryPatch 字段 + 面板控制信号) */
|
|
55
55
|
export type StoryRelationPatch = StoryPatch & {
|
|
@@ -72,8 +72,10 @@ export interface StoryRefRowProps {
|
|
|
72
72
|
/** 移除关系:子需求=移除子需求,依赖=移除依赖 */
|
|
73
73
|
onRemove: (storyId: string) => void;
|
|
74
74
|
removeMenuLabel: string;
|
|
75
|
-
/** 依赖行类型标签:传 depType + group
|
|
75
|
+
/** 依赖行类型标签:传 depType + group 时行内渲染可编辑的类型选择器 */
|
|
76
76
|
dependencyMeta?: { depType: DependencyEdgeType; group: keyof DependencyGroup };
|
|
77
|
+
/** 依赖类型就地编辑回调(依赖列表场景;children 列表不传) */
|
|
78
|
+
onDependencyTypeChange?: (otherStoryId: string, key: DependencyTypeOption["key"]) => void;
|
|
77
79
|
/** 当前需求编号(tooltip 中以编号替代「我」指代当前需求) */
|
|
78
80
|
currentIdentifier?: string;
|
|
79
81
|
}
|
|
@@ -92,6 +94,7 @@ export function StoryRefRow({
|
|
|
92
94
|
onRemove,
|
|
93
95
|
removeMenuLabel,
|
|
94
96
|
dependencyMeta,
|
|
97
|
+
onDependencyTypeChange,
|
|
95
98
|
currentIdentifier,
|
|
96
99
|
}: StoryRefRowProps) {
|
|
97
100
|
const { t } = useTranslation("story");
|
|
@@ -190,7 +193,7 @@ export function StoryRefRow({
|
|
|
190
193
|
className="shrink-0"
|
|
191
194
|
style={{ color: "oklch(80.517% 0.20237 152.675 / 0.909)" }}
|
|
192
195
|
>
|
|
193
|
-
<
|
|
196
|
+
<LucideIconByName name={storyProjectInfo.icon} size={14} />
|
|
194
197
|
</span>
|
|
195
198
|
</TooltipTrigger>
|
|
196
199
|
<TooltipContent side="top" sideOffset={6} className="flex flex-col items-start gap-0.5 text-xs">
|
|
@@ -199,7 +202,7 @@ export function StoryRefRow({
|
|
|
199
202
|
</TooltipContent>
|
|
200
203
|
</Tooltip>
|
|
201
204
|
) : storyProjectInfo ? (
|
|
202
|
-
<
|
|
205
|
+
<LucideIconByName
|
|
203
206
|
name={storyProjectInfo.icon}
|
|
204
207
|
size={14}
|
|
205
208
|
className="shrink-0 text-muted-foreground"
|
|
@@ -224,26 +227,13 @@ export function StoryRefRow({
|
|
|
224
227
|
<span className="font-mono text-xs text-muted-foreground shrink-0">{storyState.identifier}</span>
|
|
225
228
|
)}
|
|
226
229
|
{dependencyMeta && (
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
</TooltipTrigger>
|
|
235
|
-
<TooltipContent side="top" className="text-xs">
|
|
236
|
-
{t(
|
|
237
|
-
currentIdentifier
|
|
238
|
-
? dependencyTypeOption(dependencyMeta.group, dependencyMeta.depType).tooltipLabelKey
|
|
239
|
-
: dependencyTypeOption(dependencyMeta.group, dependencyMeta.depType).badgeLabelKey,
|
|
240
|
-
{
|
|
241
|
-
identifier: storyState.identifier,
|
|
242
|
-
me: currentIdentifier,
|
|
243
|
-
},
|
|
244
|
-
)}
|
|
245
|
-
</TooltipContent>
|
|
246
|
-
</Tooltip>
|
|
230
|
+
<DependencyTypeSelect
|
|
231
|
+
variant="badge"
|
|
232
|
+
value={dependencyTypeOption(dependencyMeta.group, dependencyMeta.depType).key}
|
|
233
|
+
onChange={(key) => onDependencyTypeChange?.(storyState.id, key)}
|
|
234
|
+
identifier={storyState.identifier}
|
|
235
|
+
currentIdentifier={currentIdentifier}
|
|
236
|
+
/>
|
|
247
237
|
)}
|
|
248
238
|
<span className="min-w-0 flex-1 truncate font-medium">{storyState.name}</span>
|
|
249
239
|
</button>
|
|
@@ -263,7 +253,7 @@ export function StoryRefRow({
|
|
|
263
253
|
<DateEditor value={storyState.targetDate} onChange={(v) => updateField({ targetDate: v })} label={t("children.dueDate")} icon={CalendarCheck} overdue={overdue} />
|
|
264
254
|
)}
|
|
265
255
|
{properties.includes("labels") && (
|
|
266
|
-
<LabelsEditor labelIds={currentLabelIds} labels={labels} onChange={(v) => updateField({ labelIds: v })} variant="button" />
|
|
256
|
+
<LabelsEditor labelIds={currentLabelIds} labels={labels} onChange={(v) => updateField({ labelIds: v })} variant="button" aiContext={{ name: () => storyState.name }} />
|
|
267
257
|
)}
|
|
268
258
|
<DropdownMenu>
|
|
269
259
|
<DropdownMenuTrigger asChild>
|
|
@@ -20,9 +20,9 @@ import type { ReactNode } from "react";
|
|
|
20
20
|
import { useTranslation } from "react-i18next";
|
|
21
21
|
import { Link } from "@tanstack/react-router";
|
|
22
22
|
import {
|
|
23
|
+
Archive,
|
|
23
24
|
ArrowLeft,
|
|
24
25
|
ArrowRight,
|
|
25
|
-
Archive,
|
|
26
26
|
ChevronDown,
|
|
27
27
|
ChevronUp,
|
|
28
28
|
Link2,
|
|
@@ -59,6 +59,14 @@ export const PEEK_MODE_OPTIONS: { value: PeekMode; label: string; icon: typeof P
|
|
|
59
59
|
];
|
|
60
60
|
|
|
61
61
|
export type PanelCloseType = "collapse" | "close" | "back";
|
|
62
|
+
export type PeekRelationKind = "child" | "dependency";
|
|
63
|
+
|
|
64
|
+
export interface PeekBreadcrumbItem {
|
|
65
|
+
identifier?: string;
|
|
66
|
+
projectId?: string;
|
|
67
|
+
label: string;
|
|
68
|
+
onClick?: () => void;
|
|
69
|
+
}
|
|
62
70
|
|
|
63
71
|
export interface StoryPanelHeaderProps {
|
|
64
72
|
closeType?: PanelCloseType;
|
|
@@ -86,6 +94,7 @@ export interface StoryPanelHeaderProps {
|
|
|
86
94
|
hideCopyLink?: boolean;
|
|
87
95
|
hideMoreMenu?: boolean;
|
|
88
96
|
hideTransfer?: boolean;
|
|
97
|
+
breadcrumbItems?: PeekBreadcrumbItem[];
|
|
89
98
|
}
|
|
90
99
|
|
|
91
100
|
export function StoryPanelHeader({
|
|
@@ -114,6 +123,7 @@ export function StoryPanelHeader({
|
|
|
114
123
|
hideCopyLink = false,
|
|
115
124
|
hideMoreMenu = false,
|
|
116
125
|
hideTransfer = false,
|
|
126
|
+
breadcrumbItems,
|
|
117
127
|
}: StoryPanelHeaderProps) {
|
|
118
128
|
const { t } = useTranslation("story");
|
|
119
129
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
@@ -121,28 +131,24 @@ export function StoryPanelHeader({
|
|
|
121
131
|
|
|
122
132
|
const currentMode = PEEK_MODE_OPTIONS.find((o) => o.value === mode) ?? PEEK_MODE_OPTIONS[0];
|
|
123
133
|
const CurrentModeIcon = currentMode.icon;
|
|
134
|
+
const closeTooltip = closeType === "back" ? t("panel.back") : closeType === "collapse" ? t("panel.collapse") : t("panel.close");
|
|
135
|
+
// 形变动画(MorphIcon)因主线程阻塞掉帧卡顿已移除,静态切换图标
|
|
136
|
+
const CloseIcon = closeType === "back" ? ArrowLeft : closeType === "collapse" ? ArrowRight : X;
|
|
124
137
|
|
|
125
|
-
const closeButton =
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
aria-label={closeType === "collapse" ? t("panel.collapse") : t("panel.close")}
|
|
137
|
-
>
|
|
138
|
-
{closeType === "collapse" ? <ArrowRight className="size-4" /> : <X className="size-4" />}
|
|
139
|
-
</Button>
|
|
140
|
-
);
|
|
138
|
+
const closeButton = (
|
|
139
|
+
<Button
|
|
140
|
+
variant="ghost"
|
|
141
|
+
size="icon"
|
|
142
|
+
className="size-8 panel-close-btn"
|
|
143
|
+
onClick={onClose}
|
|
144
|
+
aria-label={closeTooltip}
|
|
145
|
+
>
|
|
146
|
+
<CloseIcon className="size-4" />
|
|
147
|
+
</Button>
|
|
148
|
+
);
|
|
141
149
|
|
|
142
150
|
const closeButtonWithLink = backTo ? <Link to={backTo}>{closeButton}</Link> : closeButton;
|
|
143
151
|
|
|
144
|
-
const closeTooltip = closeType === "back" ? t("panel.back") : closeType === "collapse" ? t("panel.collapse") : t("panel.close");
|
|
145
|
-
|
|
146
152
|
const handleCopyLink = async () => {
|
|
147
153
|
const url = storyId ? `${window.location.origin}/stories/${storyId}` : window.location.href;
|
|
148
154
|
try {
|
|
@@ -35,7 +35,7 @@ import type { SavedView } from "./views/view-types";
|
|
|
35
35
|
import { DisplayDropdown } from "./display/display-dropdown";
|
|
36
36
|
import type { DisplayOptions } from "./display/types";
|
|
37
37
|
import { StoryFilterToggle, StoryFilterRow, AppliedFilters, type FilterCondition, type FilterKey } from "./filters";
|
|
38
|
-
import {
|
|
38
|
+
import { LucideIconByName } from "@/components/shared/lucide-icon-picker/lucide-icon-by-name";
|
|
39
39
|
import { CreateProjectDialog } from "@/components/project/create-project-dialog";
|
|
40
40
|
import type { UserBrief } from "./types";
|
|
41
41
|
|
|
@@ -121,7 +121,7 @@ export function StoryToolbar({ filters, filterConditions, onFilterChange, onFilt
|
|
|
121
121
|
{filters.projectId === "__archived__" ? (
|
|
122
122
|
<Archive className="size-4 text-muted-foreground" />
|
|
123
123
|
) : filters.projectId ? (
|
|
124
|
-
<
|
|
124
|
+
<LucideIconByName name={projects.find((p) => p.id === filters.projectId)?.icon ?? ""} size={16} className="text-muted-foreground" />
|
|
125
125
|
) : (
|
|
126
126
|
<Trees className="size-4 text-muted-foreground" />
|
|
127
127
|
)}
|
|
@@ -149,7 +149,7 @@ export function StoryToolbar({ filters, filterConditions, onFilterChange, onFilt
|
|
|
149
149
|
className={`flex w-full items-center justify-between gap-2 rounded px-2 py-1.5 text-sm ${filters.projectId === p.id ? "bg-accent font-medium" : "hover:bg-muted"}`}
|
|
150
150
|
>
|
|
151
151
|
<span className="flex min-w-0 items-center gap-2">
|
|
152
|
-
<
|
|
152
|
+
<LucideIconByName name={p.icon} size={16} className="text-muted-foreground" />
|
|
153
153
|
<span className="truncate">{p.name}</span>
|
|
154
154
|
</span>
|
|
155
155
|
{p.displayId && (
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { useState } from "react";
|
|
19
19
|
import { useTranslation } from "react-i18next";
|
|
20
20
|
import { GitCompareArrows, GitPullRequest, Check } from "lucide-react";
|
|
21
|
-
import {
|
|
21
|
+
import { LucideIconByName } from "@/components/shared/lucide-icon-picker/lucide-icon-by-name";
|
|
22
22
|
import { Button } from "@/components/ui/button";
|
|
23
23
|
import { Separator } from "@/components/ui/separator";
|
|
24
24
|
import {
|
|
@@ -194,7 +194,7 @@ export function TransferStoryPopover({
|
|
|
194
194
|
isSelected ? "bg-accent" : "hover:bg-muted"
|
|
195
195
|
}`}
|
|
196
196
|
>
|
|
197
|
-
<
|
|
197
|
+
<LucideIconByName name={p.icon} size={16} />
|
|
198
198
|
<span className="min-w-0 flex-1 truncate">{p.name}</span>
|
|
199
199
|
{p.displayId && (
|
|
200
200
|
<span className="shrink-0 rounded-sm bg-muted px-1.5 py-0.5 text-[10px] font-normal text-muted-foreground">
|
|
@@ -51,6 +51,7 @@ export interface StoryListItem {
|
|
|
51
51
|
labels: { id: string; name: string; color: string }[];
|
|
52
52
|
createdAt: string;
|
|
53
53
|
updatedAt: string;
|
|
54
|
+
iteration?: { id: string; name: string } | null;
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
export interface StoryDetail extends StoryListItem {
|
|
@@ -145,7 +146,9 @@ export interface StoryAttachmentItem {
|
|
|
145
146
|
fileName: string;
|
|
146
147
|
fileSize: number;
|
|
147
148
|
mimeType: string;
|
|
149
|
+
description?: string | null;
|
|
148
150
|
storageUrl: string;
|
|
151
|
+
fileHash?: string | null;
|
|
149
152
|
}
|
|
150
153
|
|
|
151
154
|
// PRD 简要信息
|
|
@@ -158,6 +161,11 @@ export interface PrdBrief {
|
|
|
158
161
|
export interface DocBrief {
|
|
159
162
|
id: string;
|
|
160
163
|
title: string;
|
|
164
|
+
kbId: string;
|
|
165
|
+
kbName: string;
|
|
166
|
+
kbIcon: string;
|
|
167
|
+
docIcon: string | null;
|
|
168
|
+
parentId: string | null;
|
|
161
169
|
}
|
|
162
170
|
|
|
163
171
|
// 关联 Tab 数据汇总
|
|
@@ -176,6 +184,12 @@ export interface RelationsData {
|
|
|
176
184
|
};
|
|
177
185
|
}
|
|
178
186
|
|
|
187
|
+
/** 关联附件面板统一数据类型 */
|
|
188
|
+
export type AssociatedItem =
|
|
189
|
+
| { kind: "link"; data: StoryLinkItem }
|
|
190
|
+
| { kind: "attachment"; data: StoryAttachmentItem }
|
|
191
|
+
| { kind: "doc"; data: DocBrief };
|
|
192
|
+
|
|
179
193
|
// ====== 依赖关系(对齐 Plane,新模型) ======
|
|
180
194
|
|
|
181
195
|
// 规范化存储类型:语义统一为「source 领先于 target」
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
PopoverTrigger,
|
|
32
32
|
} from "@/components/ui/popover";
|
|
33
33
|
import { Button } from "@/components/ui/button";
|
|
34
|
-
import {
|
|
34
|
+
import { LucideIconByName } from "@/components/shared/lucide-icon-picker/lucide-icon-by-name";
|
|
35
35
|
import { cn } from "@/lib/utils";
|
|
36
36
|
import { StoryFilterRow } from "../filters/filter-row";
|
|
37
37
|
import { createDefaultCondition, type FilterCondition } from "../filters/filter-types";
|
|
@@ -160,7 +160,7 @@ export function CreateViewDialog({ open, onOpenChange, mode, initialValues, view
|
|
|
160
160
|
className="h-8 gap-1.5 border-0 bg-muted/50 px-2.5 text-sm font-medium"
|
|
161
161
|
>
|
|
162
162
|
{selectedProject ? (
|
|
163
|
-
<
|
|
163
|
+
<LucideIconByName name={selectedProject.icon} size={16} />
|
|
164
164
|
) : (
|
|
165
165
|
<Trees className="size-4 text-muted-foreground" />
|
|
166
166
|
)}
|
|
@@ -190,7 +190,7 @@ export function CreateViewDialog({ open, onOpenChange, mode, initialValues, view
|
|
|
190
190
|
className={`flex w-full items-center justify-between gap-2 rounded px-2 py-1.5 text-sm ${projectId === p.id ? "bg-accent font-medium" : "hover:bg-muted"}`}
|
|
191
191
|
>
|
|
192
192
|
<span className="flex min-w-0 items-center gap-2">
|
|
193
|
-
<
|
|
193
|
+
<LucideIconByName name={p.icon} size={16} />
|
|
194
194
|
<span className="truncate">{p.name}</span>
|
|
195
195
|
</span>
|
|
196
196
|
{p.displayId && (
|