@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.
- package/package.json +230 -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
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
|
-
import { useEffect, useState } from "react";
|
|
18
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
19
19
|
import { useTranslation } from "react-i18next";
|
|
20
20
|
import { useHotkey } from "@tanstack/react-hotkeys";
|
|
21
21
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
@@ -33,11 +33,12 @@ import {
|
|
|
33
33
|
getLinkedPrdsFn,
|
|
34
34
|
getLinkedDocsFn,
|
|
35
35
|
} from "@/server/serverFns/story";
|
|
36
|
+
import { listIterationsFn } from "@/server/serverFns/iteration";
|
|
36
37
|
import type { StoryDetail, UserBrief, CommentWithReplies, ActivityItem, RelationsData } from "./types";
|
|
37
38
|
import type { StoryState } from "./constants";
|
|
38
39
|
import { StoryDetailBody } from "./detail/story-detail-body";
|
|
39
40
|
import { StoryPanelHeader } from "./story-panel-header";
|
|
40
|
-
import type { PeekMode } from "./story-panel-header";
|
|
41
|
+
import type { PeekMode, PeekRelationKind, PeekBreadcrumbItem } from "./story-panel-header";
|
|
41
42
|
|
|
42
43
|
export type { PeekMode };
|
|
43
44
|
|
|
@@ -70,6 +71,13 @@ interface StoryPeekPanelProps {
|
|
|
70
71
|
onTransferred?: () => void;
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
interface PeekLocation {
|
|
75
|
+
storyId: string;
|
|
76
|
+
identifier?: string;
|
|
77
|
+
projectId?: string;
|
|
78
|
+
relationKind: PeekRelationKind;
|
|
79
|
+
}
|
|
80
|
+
|
|
73
81
|
interface PeekData {
|
|
74
82
|
story: StoryDetail;
|
|
75
83
|
comments: CommentWithReplies[];
|
|
@@ -77,6 +85,7 @@ interface PeekData {
|
|
|
77
85
|
labels: { id: string; name: string; color: string }[];
|
|
78
86
|
stories: { id: string; identifier: string; name: string; state: StoryState }[];
|
|
79
87
|
relationsData: RelationsData;
|
|
88
|
+
iterations: import("@/server/serverFns/iteration/schemas").IterationSummary[];
|
|
80
89
|
}
|
|
81
90
|
|
|
82
91
|
export function StoryPeekPanel({
|
|
@@ -100,25 +109,61 @@ export function StoryPeekPanel({
|
|
|
100
109
|
const { t } = useTranslation("story");
|
|
101
110
|
const [data, setData] = useState<PeekData | null>(null);
|
|
102
111
|
const [error, setError] = useState<string | null>(null);
|
|
112
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
113
|
+
const [history, setHistory] = useState<PeekLocation[]>([]);
|
|
114
|
+
const [rootIdentifier, setRootIdentifier] = useState<string | undefined>();
|
|
115
|
+
const [rootProjectId, setRootProjectId] = useState<string | undefined>();
|
|
116
|
+
|
|
117
|
+
const effectiveStoryId = history.length > 0 ? history[history.length - 1].storyId : storyId;
|
|
118
|
+
|
|
119
|
+
const onNavigateInPanel = useCallback((targetId: string, targetIdentifier?: string, relationKind: PeekRelationKind = "child") => {
|
|
120
|
+
setHistory((prev) => {
|
|
121
|
+
const currentId = prev.length > 0 ? prev[prev.length - 1].storyId : storyId;
|
|
122
|
+
return [...prev, { storyId: targetId, identifier: targetIdentifier, relationKind, parentId: currentId }];
|
|
123
|
+
});
|
|
124
|
+
}, [storyId]);
|
|
125
|
+
|
|
126
|
+
const onBackInPanel = useCallback(() => {
|
|
127
|
+
setHistory((prev) => prev.slice(0, -1));
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
setHistory([]);
|
|
132
|
+
}, [storyId]);
|
|
103
133
|
|
|
104
134
|
useEffect(() => {
|
|
105
135
|
let cancelled = false;
|
|
106
|
-
setData(null);
|
|
107
136
|
setError(null);
|
|
137
|
+
// 跳转时保留旧内容(不 setData(null)):同步卸载含 TipTap 等重节点的详情树会
|
|
138
|
+
// 阻塞主线程,导致顶部 MorphIcon 关闭/返回按钮的形变动画掉帧卡顿。
|
|
139
|
+
// 改为半透明遮罩过渡,新数据到达后经 key 变化整体替换。
|
|
140
|
+
setRefreshing(data !== null);
|
|
108
141
|
(async () => {
|
|
109
142
|
try {
|
|
110
|
-
const story = await getStoryDetailFn({ data: { storyId } });
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
143
|
+
const story = await getStoryDetailFn({ data: { storyId: effectiveStoryId } });
|
|
144
|
+
if (history.length === 0) {
|
|
145
|
+
setRootIdentifier(story.identifier);
|
|
146
|
+
setRootProjectId(story.projectId);
|
|
147
|
+
} else {
|
|
148
|
+
// 回填当前层 projectId:跨项目父子/依赖跳转时,面包屑各级需显示各自项目图标
|
|
149
|
+
setHistory((prev) => {
|
|
150
|
+
const last = prev[prev.length - 1];
|
|
151
|
+
if (!last || last.projectId === story.projectId) return prev;
|
|
152
|
+
return prev.map((h, i) => (i === prev.length - 1 ? { ...h, projectId: story.projectId } : h));
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const [comments, activities, labels, storiesResult, childrenStories, dependencies, links, attachments, prds, docs, iterationsResult] = await Promise.all([
|
|
156
|
+
listCommentsFn({ data: { storyId: effectiveStoryId } }),
|
|
157
|
+
listActivitiesFn({ data: { storyId: effectiveStoryId, take: 50 } }),
|
|
114
158
|
listLabelsFn(),
|
|
115
159
|
listStoriesFn({ data: { projectId: story.projectId } }),
|
|
116
|
-
listChildStoriesFn({ data: { parentId:
|
|
117
|
-
getDependenciesFn({ data: { storyId } }),
|
|
118
|
-
getLinksFn({ data: { storyId } }),
|
|
119
|
-
getAttachmentsFn({ data: { storyId } }),
|
|
120
|
-
getLinkedPrdsFn({ data: { storyId } }),
|
|
121
|
-
getLinkedDocsFn({ data: { storyId } }),
|
|
160
|
+
listChildStoriesFn({ data: { parentId: effectiveStoryId } }),
|
|
161
|
+
getDependenciesFn({ data: { storyId: effectiveStoryId } }),
|
|
162
|
+
getLinksFn({ data: { storyId: effectiveStoryId } }),
|
|
163
|
+
getAttachmentsFn({ data: { storyId: effectiveStoryId } }),
|
|
164
|
+
getLinkedPrdsFn({ data: { storyId: effectiveStoryId } }),
|
|
165
|
+
getLinkedDocsFn({ data: { storyId: effectiveStoryId } }),
|
|
166
|
+
story.projectId ? listIterationsFn({ data: { projectId: story.projectId, includeArchived: false } }) : Promise.resolve({ items: [] }),
|
|
122
167
|
]);
|
|
123
168
|
const relationsData: RelationsData = {
|
|
124
169
|
hierarchy: {
|
|
@@ -131,33 +176,82 @@ export function StoryPeekPanel({
|
|
|
131
176
|
attachments,
|
|
132
177
|
knowledge: { prds, docs },
|
|
133
178
|
};
|
|
134
|
-
if (!cancelled)
|
|
179
|
+
if (!cancelled) {
|
|
180
|
+
setData({ story, comments, activities: activities.items, labels: labels, stories: storiesResult.items.map((s) => ({ id: s.id, identifier: s.identifier, name: s.name, state: s.state })), relationsData, iterations: iterationsResult.items });
|
|
181
|
+
setRefreshing(false);
|
|
182
|
+
}
|
|
135
183
|
} catch (e) {
|
|
136
|
-
if (!cancelled)
|
|
184
|
+
if (!cancelled) {
|
|
185
|
+
setError(e instanceof Error ? e.message : t("peek.loadFailed"));
|
|
186
|
+
setRefreshing(false);
|
|
187
|
+
}
|
|
137
188
|
}
|
|
138
189
|
})();
|
|
139
190
|
return () => {
|
|
140
191
|
cancelled = true;
|
|
141
192
|
};
|
|
142
|
-
}, [
|
|
193
|
+
}, [effectiveStoryId]);
|
|
143
194
|
|
|
144
195
|
useHotkey("Escape", onClose, {
|
|
145
|
-
enabled: mode === "side-peek",
|
|
196
|
+
enabled: mode === "side-peek" && history.length === 0,
|
|
146
197
|
});
|
|
147
198
|
|
|
148
199
|
const canArchive = data?.story
|
|
149
200
|
? (data.story.state === "done" || data.story.state === "cancelled") && !data.story.archived
|
|
150
201
|
: false;
|
|
151
202
|
|
|
203
|
+
const closeType: "collapse" | "close" | "back" =
|
|
204
|
+
history.length > 0
|
|
205
|
+
? "back"
|
|
206
|
+
: mode === "side-peek"
|
|
207
|
+
? "collapse"
|
|
208
|
+
: "close";
|
|
209
|
+
|
|
210
|
+
const handleClose = useCallback(() => {
|
|
211
|
+
if (history.length > 0) {
|
|
212
|
+
onBackInPanel();
|
|
213
|
+
} else {
|
|
214
|
+
onClose();
|
|
215
|
+
}
|
|
216
|
+
}, [history.length, onBackInPanel, onClose]);
|
|
217
|
+
|
|
218
|
+
const breadcrumbItems: PeekBreadcrumbItem[] = useMemo(() => {
|
|
219
|
+
const rootId = rootIdentifier;
|
|
220
|
+
const currentId = data?.story.identifier;
|
|
221
|
+
const items: PeekBreadcrumbItem[] = [];
|
|
222
|
+
|
|
223
|
+
if (history.length === 0) {
|
|
224
|
+
return items;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const entry = history[history.length - 1];
|
|
228
|
+
const parentLabel = entry.relationKind === "child" ? t("peek.parentStory") : t("peek.currentStory");
|
|
229
|
+
const childLabel = entry.relationKind === "child" ? t("peek.childStory") : t("peek.dependentStory");
|
|
230
|
+
|
|
231
|
+
items.push({
|
|
232
|
+
identifier: history.length >= 2 ? history[history.length - 2].identifier ?? rootId : rootId,
|
|
233
|
+
projectId: history.length >= 2 ? history[history.length - 2].projectId ?? rootProjectId : rootProjectId,
|
|
234
|
+
label: parentLabel,
|
|
235
|
+
onClick: () => onBackInPanel(),
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
items.push({
|
|
239
|
+
identifier: currentId,
|
|
240
|
+
projectId: data?.story.projectId ?? undefined,
|
|
241
|
+
label: childLabel,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return items;
|
|
245
|
+
}, [history, rootIdentifier, rootProjectId, data?.story.identifier, data?.story.projectId, t, onBackInPanel]);
|
|
246
|
+
|
|
152
247
|
const header = (
|
|
153
248
|
<StoryPanelHeader
|
|
154
|
-
closeType={
|
|
155
|
-
onClose={
|
|
249
|
+
closeType={closeType}
|
|
250
|
+
onClose={handleClose}
|
|
156
251
|
onFullscreen={onFullscreen}
|
|
157
252
|
mode={mode}
|
|
158
253
|
onModeChange={onModeChange}
|
|
159
254
|
storyId={storyId}
|
|
160
|
-
identifier={data?.story.identifier}
|
|
161
255
|
canArchive={canArchive}
|
|
162
256
|
onPrevious={onPrevious}
|
|
163
257
|
onNext={onNext}
|
|
@@ -172,7 +266,10 @@ export function StoryPeekPanel({
|
|
|
172
266
|
);
|
|
173
267
|
|
|
174
268
|
const body = (
|
|
175
|
-
<div
|
|
269
|
+
<div
|
|
270
|
+
className={`flex min-h-0 flex-1 flex-col overflow-y-auto px-8 transition-opacity duration-200${refreshing ? " pointer-events-none opacity-50" : ""}`}
|
|
271
|
+
style={{ scrollbarGutter: "stable" } as React.CSSProperties}
|
|
272
|
+
>
|
|
176
273
|
|
|
177
274
|
{error && <div className="w-full p-6 text-sm text-destructive">{t("peek.loadFailed")}: {error}</div>}
|
|
178
275
|
{!error && !data && (
|
|
@@ -230,12 +327,15 @@ export function StoryPeekPanel({
|
|
|
230
327
|
labels={data.labels}
|
|
231
328
|
stories={data.stories}
|
|
232
329
|
projects={projects}
|
|
330
|
+
iterations={data.iterations}
|
|
233
331
|
relationsData={data.relationsData}
|
|
234
332
|
onRelationsUpdate={(next) => setData((d) => d ? { ...d, relationsData: typeof next === "function" ? next(d.relationsData) : next } : d)}
|
|
235
333
|
onAppendActivities={(items) => setData((d) => d ? { ...d, activities: [...d.activities, ...items] } : d)}
|
|
236
334
|
onAppendComment={(c) => setData((d) => d ? { ...d, comments: [...d.comments, c] } : d)}
|
|
237
335
|
onRemoveComment={(id) => setData((d) => d ? { ...d, comments: d.comments.filter((c) => c.id !== id) } : d)}
|
|
238
336
|
onStoryUpdate={(s) => setData((d) => d ? { ...d, story: s } : d)}
|
|
337
|
+
onNavigateInPanel={onNavigateInPanel}
|
|
338
|
+
breadcrumbItems={breadcrumbItems}
|
|
239
339
|
/>
|
|
240
340
|
)}
|
|
241
341
|
</div>
|
|
@@ -76,7 +76,7 @@ export function LinkManageDialog({
|
|
|
76
76
|
setSubmitting(true);
|
|
77
77
|
try {
|
|
78
78
|
if (editing) {
|
|
79
|
-
await updateLink({
|
|
79
|
+
const updated = await updateLink({
|
|
80
80
|
data: {
|
|
81
81
|
linkId: editing.id,
|
|
82
82
|
url: form.url,
|
|
@@ -85,6 +85,7 @@ export function LinkManageDialog({
|
|
|
85
85
|
},
|
|
86
86
|
});
|
|
87
87
|
toast.success(t("linkDialog.updated"));
|
|
88
|
+
if (updated.activity) onActivity?.(updated.activity);
|
|
88
89
|
onUpdate(
|
|
89
90
|
data.map((l) =>
|
|
90
91
|
l.id === editing.id
|
|
@@ -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">
|
|
@@ -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 && (
|