@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
|
@@ -35,11 +35,14 @@ import {
|
|
|
35
35
|
ShieldOff,
|
|
36
36
|
Trash2,
|
|
37
37
|
Unlink,
|
|
38
|
+
ChartNoAxesGantt,
|
|
38
39
|
} from "lucide-react";
|
|
39
40
|
import { Button } from "@/components/ui/button";
|
|
40
41
|
import { Badge } from "@/components/ui/badge";
|
|
41
42
|
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
|
42
43
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
44
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
45
|
+
import { cn } from "@/lib/utils";
|
|
43
46
|
import { FilePreview } from "@/components/story/relations/file-preview";
|
|
44
47
|
import { resolveVideoEmbed } from "@/lib/story/video-embed";
|
|
45
48
|
import { removeLinkFn, removeAttachmentFn, unlinkDocFn, checkLinkEmbeddableFn } from "@/server/serverFns/story";
|
|
@@ -47,7 +50,7 @@ import { getDocumentFn } from "@/server/serverFns/knowledge";
|
|
|
47
50
|
import { sanitize } from "@/lib/sanitize";
|
|
48
51
|
import { formatFileSize, getFaviconUrl, getFileTypeIcon, extractLinkTitle, getMimeTypeLabel } from "../../relations/format";
|
|
49
52
|
import { generalGroupLabel } from "../../relations/general-groups";
|
|
50
|
-
import {
|
|
53
|
+
import { LucideIconByName } from "@/components/shared/lucide-icon-picker/lucide-icon-by-name";
|
|
51
54
|
import type { AssociatedItem, RelationsData, StoryLinkItem, StoryAttachmentItem } from "../../types";
|
|
52
55
|
|
|
53
56
|
export type AssociatedDialogKind = "link" | "attachment" | "knowledge";
|
|
@@ -57,6 +60,7 @@ export interface AssociatedAttachmentsPanelProps {
|
|
|
57
60
|
data: RelationsData;
|
|
58
61
|
onRefresh: (next: (prev: RelationsData) => RelationsData) => void;
|
|
59
62
|
onOpenDialog: (kind: AssociatedDialogKind, editLink?: StoryLinkItem, editAttachment?: StoryAttachmentItem) => void;
|
|
63
|
+
onAppendActivities?: (items: import("../../types").ActivityItem[]) => void;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
// 记住列表/预览显示方式
|
|
@@ -297,6 +301,7 @@ export function AssociatedAttachmentsPanel({
|
|
|
297
301
|
data,
|
|
298
302
|
onRefresh,
|
|
299
303
|
onOpenDialog,
|
|
304
|
+
onAppendActivities,
|
|
300
305
|
}: AssociatedAttachmentsPanelProps) {
|
|
301
306
|
const { t } = useTranslation("story");
|
|
302
307
|
const removeLink = useServerFn(removeLinkFn);
|
|
@@ -305,6 +310,35 @@ export function AssociatedAttachmentsPanel({
|
|
|
305
310
|
const [mode, setMode] = React.useState<"list" | "preview">("list");
|
|
306
311
|
const [groupCollapsed, setGroupCollapsed] = React.useState<Record<string, boolean>>({});
|
|
307
312
|
const [fullscreenAttachment, setFullscreenAttachment] = React.useState<StoryAttachmentItem | null>(null);
|
|
313
|
+
const [activeTypeFilters, setActiveTypeFilters] = React.useState<Set<string>>(new Set());
|
|
314
|
+
|
|
315
|
+
// 附件类型分组定义
|
|
316
|
+
const attachmentTypeGroups = React.useMemo(() => {
|
|
317
|
+
const groups: Array<{ key: string; label: string; count: number }> = [];
|
|
318
|
+
const typeMap = new Map<string, number>();
|
|
319
|
+
|
|
320
|
+
for (const item of data.attachments) {
|
|
321
|
+
const type = getMimeTypeLabel(item.mimeType, item.fileName);
|
|
322
|
+
typeMap.set(type, (typeMap.get(type) ?? 0) + 1);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const order = ["图片", "视频", "音频", "PDF", "压缩包", "文本", "Word", "表格", "演示", "电子书", "未识别文件"];
|
|
326
|
+
for (const type of order) {
|
|
327
|
+
const count = typeMap.get(type) ?? 0;
|
|
328
|
+
if (count > 0) groups.push({ key: type, label: type, count });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return groups;
|
|
332
|
+
}, [data.attachments]);
|
|
333
|
+
|
|
334
|
+
// 根据筛选条件过滤附件
|
|
335
|
+
const filteredAttachments = React.useMemo(() => {
|
|
336
|
+
if (activeTypeFilters.size === 0) return data.attachments;
|
|
337
|
+
return data.attachments.filter((a) => {
|
|
338
|
+
const type = getMimeTypeLabel(a.mimeType, a.fileName);
|
|
339
|
+
return activeTypeFilters.has(type);
|
|
340
|
+
});
|
|
341
|
+
}, [data.attachments, activeTypeFilters]);
|
|
308
342
|
|
|
309
343
|
// 下次进入时恢复上次的显示方式(挂载后读取,避免 SSR hydration 不匹配)
|
|
310
344
|
React.useEffect(() => {
|
|
@@ -333,16 +367,26 @@ export function AssociatedAttachmentsPanel({
|
|
|
333
367
|
const linkItems = items.filter((i) => i.kind === "link");
|
|
334
368
|
const attachmentItems = items.filter((i) => i.kind === "attachment");
|
|
335
369
|
const docItems = items.filter((i) => i.kind === "doc");
|
|
370
|
+
|
|
371
|
+
// 如果启用了附件类型筛选,则过滤 attachmentItems
|
|
372
|
+
const filteredAttachmentItems = activeTypeFilters.size > 0
|
|
373
|
+
? attachmentItems.filter((item) => {
|
|
374
|
+
const type = getMimeTypeLabel(item.data.mimeType, item.data.fileName);
|
|
375
|
+
return activeTypeFilters.has(type);
|
|
376
|
+
})
|
|
377
|
+
: attachmentItems;
|
|
378
|
+
|
|
336
379
|
return [
|
|
337
380
|
{ kind: "link" as const, label: t("associatedAttachments.groupLink"), count: linkItems.length, items: linkItems },
|
|
338
|
-
{ kind: "attachment" as const, label: t("associatedAttachments.groupAttachment"), count:
|
|
381
|
+
{ kind: "attachment" as const, label: t("associatedAttachments.groupAttachment"), count: filteredAttachmentItems.length, items: filteredAttachmentItems },
|
|
339
382
|
{ kind: "doc" as const, label: t("associatedAttachments.groupKb"), count: docItems.length, items: docItems },
|
|
340
383
|
].filter((g) => g.count > 0);
|
|
341
|
-
}, [items, t]);
|
|
384
|
+
}, [items, t, activeTypeFilters]);
|
|
342
385
|
|
|
343
386
|
const handleRemoveLink = async (id: string) => {
|
|
344
387
|
try {
|
|
345
|
-
await removeLink({ data: { linkId: id } });
|
|
388
|
+
const res = await removeLink({ data: { linkId: id } });
|
|
389
|
+
if (res?.activity) onAppendActivities?.([res.activity]);
|
|
346
390
|
onRefresh((prev) => ({ ...prev, links: prev.links.filter((l) => l.id !== id) }));
|
|
347
391
|
} catch (err) {
|
|
348
392
|
toast.error((err as Error)?.message || t("linkDialog.removeFailed"));
|
|
@@ -351,7 +395,8 @@ export function AssociatedAttachmentsPanel({
|
|
|
351
395
|
|
|
352
396
|
const handleRemoveAttachment = async (id: string) => {
|
|
353
397
|
try {
|
|
354
|
-
await removeAttachment({ data: { attachmentId: id } });
|
|
398
|
+
const res = await removeAttachment({ data: { attachmentId: id } });
|
|
399
|
+
if (res?.activity) onAppendActivities?.([res.activity]);
|
|
355
400
|
onRefresh((prev) => ({
|
|
356
401
|
...prev,
|
|
357
402
|
attachments: prev.attachments.filter((a) => a.id !== id),
|
|
@@ -363,7 +408,8 @@ export function AssociatedAttachmentsPanel({
|
|
|
363
408
|
|
|
364
409
|
const handleUnlinkDoc = async (docId: string) => {
|
|
365
410
|
try {
|
|
366
|
-
await unlinkDoc({ data: { storyId, docId } });
|
|
411
|
+
const res = await unlinkDoc({ data: { storyId, docId } });
|
|
412
|
+
if (res?.activity) onAppendActivities?.([res.activity]);
|
|
367
413
|
onRefresh((prev) => ({
|
|
368
414
|
...prev,
|
|
369
415
|
knowledge: { ...prev.knowledge, docs: prev.knowledge.docs.filter((d) => d.id !== docId) },
|
|
@@ -558,7 +604,7 @@ export function AssociatedAttachmentsPanel({
|
|
|
558
604
|
<span className="w-4 shrink-0" aria-hidden />
|
|
559
605
|
<BookOpenText className="size-4 shrink-0 text-muted-foreground" />
|
|
560
606
|
<span className="min-w-0 flex-1 truncate text-sm text-foreground">{item.data.title}</span>
|
|
561
|
-
<
|
|
607
|
+
<LucideIconByName name={item.data.kbIcon} size={14} className="shrink-0 text-muted-foreground" />
|
|
562
608
|
<span className="shrink-0 text-xs text-muted-foreground">{item.data.kbId.startsWith("ext:") ? generalGroupLabel(t, item.data.kbName) : item.data.kbName}</span>
|
|
563
609
|
<div className="flex items-center gap-0.5 shrink-0">{renderActionsInline(item)}</div>
|
|
564
610
|
</div>
|
|
@@ -619,7 +665,7 @@ export function AssociatedAttachmentsPanel({
|
|
|
619
665
|
<span className="min-w-0 truncate text-sm font-medium text-foreground">{item.data.title}</span>
|
|
620
666
|
</div>
|
|
621
667
|
<div className="flex items-center gap-1.5">
|
|
622
|
-
<
|
|
668
|
+
<LucideIconByName name={item.data.kbIcon} size={12} className="shrink-0 text-muted-foreground" />
|
|
623
669
|
<span className="truncate text-xs text-muted-foreground">{item.data.kbId.startsWith("ext:") ? generalGroupLabel(t, item.data.kbName) : item.data.kbName}</span>
|
|
624
670
|
</div>
|
|
625
671
|
<div className="absolute right-2 top-2 hidden gap-0.5 rounded-md border bg-background p-0.5 group-hover:flex">
|
|
@@ -693,7 +739,70 @@ export function AssociatedAttachmentsPanel({
|
|
|
693
739
|
<ChevronDown className={`size-3 shrink-0 text-muted-foreground transition-transform ${isCollapsed ? "-rotate-90" : "rotate-0"}`} />
|
|
694
740
|
<span>{group.label}</span>
|
|
695
741
|
<span className="ml-1 text-xs text-muted-foreground">{group.count}</span>
|
|
696
|
-
<div className="ml-auto flex items-center" onClick={(e) => e.stopPropagation()}>
|
|
742
|
+
<div className="ml-auto flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
|
743
|
+
{group.kind === "attachment" && (
|
|
744
|
+
<Popover>
|
|
745
|
+
<PopoverTrigger asChild>
|
|
746
|
+
<Button
|
|
747
|
+
variant="ghost"
|
|
748
|
+
size="sm"
|
|
749
|
+
className={`size-6 p-0 ${activeTypeFilters.size > 0 ? "bg-muted text-foreground" : ""}`}
|
|
750
|
+
aria-label={t("associatedAttachments.filter")}
|
|
751
|
+
title={t("associatedAttachments.filter")}
|
|
752
|
+
>
|
|
753
|
+
<ChartNoAxesGantt className="size-3.5" />
|
|
754
|
+
{activeTypeFilters.size > 0 && (
|
|
755
|
+
<span className="absolute -right-1 -top-1 size-1.5 rounded-full bg-primary" />
|
|
756
|
+
)}
|
|
757
|
+
</Button>
|
|
758
|
+
</PopoverTrigger>
|
|
759
|
+
<PopoverContent align="end" className="w-52 p-2">
|
|
760
|
+
<div className="text-xs font-medium text-muted-foreground my-1 px-1">
|
|
761
|
+
{t("associatedAttachments.filterByType")}
|
|
762
|
+
</div>
|
|
763
|
+
<div className="flex flex-wrap gap-1">
|
|
764
|
+
<button
|
|
765
|
+
type="button"
|
|
766
|
+
onClick={() => {
|
|
767
|
+
if (activeTypeFilters.size === attachmentTypeGroups.length) {
|
|
768
|
+
setActiveTypeFilters(new Set());
|
|
769
|
+
} else {
|
|
770
|
+
setActiveTypeFilters(new Set(attachmentTypeGroups.map((g) => g.key)));
|
|
771
|
+
}
|
|
772
|
+
}}
|
|
773
|
+
className={cn(
|
|
774
|
+
"rounded border px-2 py-0.5 text-xs transition-all",
|
|
775
|
+
activeTypeFilters.size === attachmentTypeGroups.length && attachmentTypeGroups.length > 0
|
|
776
|
+
? "border-primary bg-primary text-primary-foreground"
|
|
777
|
+
: "border-border text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
778
|
+
)}
|
|
779
|
+
>
|
|
780
|
+
{t("associatedAttachments.filterAll")}
|
|
781
|
+
</button>
|
|
782
|
+
{attachmentTypeGroups.map((g) => (
|
|
783
|
+
<button
|
|
784
|
+
type="button"
|
|
785
|
+
key={g.key}
|
|
786
|
+
onClick={() => {
|
|
787
|
+
const next = new Set(activeTypeFilters);
|
|
788
|
+
if (next.has(g.key)) next.delete(g.key);
|
|
789
|
+
else next.add(g.key);
|
|
790
|
+
setActiveTypeFilters(next);
|
|
791
|
+
}}
|
|
792
|
+
className={cn(
|
|
793
|
+
"rounded border px-2 py-0.5 text-xs transition-all",
|
|
794
|
+
activeTypeFilters.has(g.key)
|
|
795
|
+
? "border-primary bg-primary text-primary-foreground"
|
|
796
|
+
: "border-border text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
797
|
+
)}
|
|
798
|
+
>
|
|
799
|
+
{g.label}({g.count})
|
|
800
|
+
</button>
|
|
801
|
+
))}
|
|
802
|
+
</div>
|
|
803
|
+
</PopoverContent>
|
|
804
|
+
</Popover>
|
|
805
|
+
)}
|
|
697
806
|
<Button
|
|
698
807
|
variant="ghost"
|
|
699
808
|
size="sm"
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import * as React from "react";
|
|
19
|
-
import { useNavigate } from "@tanstack/react-router";
|
|
20
19
|
import { useTranslation } from "react-i18next";
|
|
21
20
|
import {
|
|
22
21
|
Plus,
|
|
@@ -66,6 +65,7 @@ export interface StoryDetailChildrenPanelProps {
|
|
|
66
65
|
projects?: { id: string; name: string; icon: string; displayId: string | null }[];
|
|
67
66
|
labels?: { id: string; name: string; color: string }[];
|
|
68
67
|
onAction: (action: "create" | "select") => void;
|
|
68
|
+
onNavigateInPanel?: (targetId: string, targetIdentifier?: string, relationKind?: "child" | "dependency") => void;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
export function StoryDetailChildrenButton({ onAction }: StoryDetailChildrenButtonProps) {
|
|
@@ -131,9 +131,9 @@ export function StoryDetailChildrenPanel({
|
|
|
131
131
|
projects,
|
|
132
132
|
labels,
|
|
133
133
|
onAction,
|
|
134
|
+
onNavigateInPanel,
|
|
134
135
|
}: StoryDetailChildrenPanelProps) {
|
|
135
136
|
const { t } = useTranslation("story");
|
|
136
|
-
const navigate = useNavigate();
|
|
137
137
|
const childrenList = data.children as StoryListItem[];
|
|
138
138
|
const [selectedIds, setSelectedIds] = React.useState<Set<string>>(() => new Set());
|
|
139
139
|
const [busy, setBusy] = React.useState(false);
|
|
@@ -176,7 +176,10 @@ export function StoryDetailChildrenPanel({
|
|
|
176
176
|
};
|
|
177
177
|
|
|
178
178
|
const handleOpenStory = (id: string) => {
|
|
179
|
-
|
|
179
|
+
if (onNavigateInPanel) {
|
|
180
|
+
const story = childrenList.find((s) => s.id === id);
|
|
181
|
+
onNavigateInPanel(id, story?.identifier);
|
|
182
|
+
}
|
|
180
183
|
};
|
|
181
184
|
|
|
182
185
|
const handleRemoveChild = async (childId: string) => {
|
|
@@ -321,7 +324,7 @@ export function StoryDetailChildrenPanel({
|
|
|
321
324
|
<Collapsible defaultOpen>
|
|
322
325
|
<CollapsibleTrigger
|
|
323
326
|
asChild
|
|
324
|
-
className="group flex w-full cursor-pointer items-center gap-2 pb-3 pt-4 text-xs font-semibold hover:text-foreground transition-colors"
|
|
327
|
+
className="group flex w-full cursor-pointer select-none items-center gap-2 pb-3 pt-4 text-xs font-semibold hover:text-foreground transition-colors"
|
|
325
328
|
>
|
|
326
329
|
<div
|
|
327
330
|
role="button"
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import * as React from "react";
|
|
19
|
-
import { useNavigate } from "@tanstack/react-router";
|
|
20
19
|
import { useTranslation } from "react-i18next";
|
|
21
20
|
import { Plus, ChevronDown, ChartNoAxesGantt } from "lucide-react";
|
|
22
21
|
import { Button } from "@/components/ui/button";
|
|
@@ -26,10 +25,12 @@ import { useServerFn } from "@tanstack/react-start";
|
|
|
26
25
|
import { toast } from "sonner";
|
|
27
26
|
import {
|
|
28
27
|
removeDependencyFn,
|
|
28
|
+
updateDependencyFn,
|
|
29
29
|
bulkArchiveStoriesFn,
|
|
30
30
|
bulkDeleteStoriesFn,
|
|
31
31
|
bulkUpdateStoriesFn,
|
|
32
32
|
} from "@/server/serverFns/story";
|
|
33
|
+
import { DEPENDENCY_TYPE_OPTIONS, toDependencyEdge, type DependencyTypeOption } from "../../constants";
|
|
33
34
|
import { BatchToolbar } from "../../views/story-list/batch-toolbar";
|
|
34
35
|
import { useGroupedStories } from "../../display/use-grouped-stories";
|
|
35
36
|
import { VirtualGroupHeader } from "../../views/story-list/group";
|
|
@@ -59,6 +60,8 @@ export interface StoryDetailDependencyPanelProps {
|
|
|
59
60
|
projects?: { id: string; name: string; icon: string; displayId: string | null }[];
|
|
60
61
|
labels?: { id: string; name: string; color: string }[];
|
|
61
62
|
onOpenAdd: () => void;
|
|
63
|
+
onAppendActivities?: (items: import("../../types").ActivityItem[]) => void;
|
|
64
|
+
onNavigateInPanel?: (targetId: string, targetIdentifier?: string, relationKind?: "child" | "dependency") => void;
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
export function StoryDetailDependencyPanel({
|
|
@@ -72,10 +75,12 @@ export function StoryDetailDependencyPanel({
|
|
|
72
75
|
projects,
|
|
73
76
|
labels,
|
|
74
77
|
onOpenAdd,
|
|
78
|
+
onAppendActivities,
|
|
79
|
+
onNavigateInPanel,
|
|
75
80
|
}: StoryDetailDependencyPanelProps) {
|
|
76
81
|
const { t } = useTranslation("story");
|
|
77
|
-
const navigate = useNavigate();
|
|
78
82
|
const removeDependency = useServerFn(removeDependencyFn);
|
|
83
|
+
const updateDependency = useServerFn(updateDependencyFn);
|
|
79
84
|
const [selectedIds, setSelectedIds] = React.useState<Set<string>>(() => new Set());
|
|
80
85
|
const [busy, setBusy] = React.useState(false);
|
|
81
86
|
const [collapsed, setCollapsed] = React.useState<Record<string, boolean>>({});
|
|
@@ -135,7 +140,10 @@ export function StoryDetailDependencyPanel({
|
|
|
135
140
|
};
|
|
136
141
|
|
|
137
142
|
const handleOpenStory = (id: string) => {
|
|
138
|
-
|
|
143
|
+
if (onNavigateInPanel) {
|
|
144
|
+
const story = allRows.find((r) => r.id === id);
|
|
145
|
+
onNavigateInPanel(id, story?.identifier, "dependency");
|
|
146
|
+
}
|
|
139
147
|
};
|
|
140
148
|
|
|
141
149
|
const applyRowsUpdate = (
|
|
@@ -155,7 +163,12 @@ export function StoryDetailDependencyPanel({
|
|
|
155
163
|
? "dependsOn"
|
|
156
164
|
: "dependents";
|
|
157
165
|
try {
|
|
158
|
-
await removeDependency({ data: { depId: row.depId } });
|
|
166
|
+
const res = await removeDependency({ data: { depId: row.depId } });
|
|
167
|
+
// 两端各有一条记录,只追加当前需求的
|
|
168
|
+
const mine = (res?.activities ?? [])
|
|
169
|
+
.filter((a) => a.storyId === storyId)
|
|
170
|
+
.map(({ storyId: _sid, ...rest }) => rest);
|
|
171
|
+
if (mine.length > 0) onAppendActivities?.(mine);
|
|
159
172
|
toast.success(t("dependency.removed"));
|
|
160
173
|
applyRowsUpdate((rows) => rows.filter((r) => r.id !== rowId), group);
|
|
161
174
|
setSelectedIds((prev) => {
|
|
@@ -168,13 +181,59 @@ export function StoryDetailDependencyPanel({
|
|
|
168
181
|
}
|
|
169
182
|
};
|
|
170
183
|
|
|
184
|
+
const handleDependencyTypeChange = async (
|
|
185
|
+
otherStoryId: string,
|
|
186
|
+
key: DependencyTypeOption["key"],
|
|
187
|
+
) => {
|
|
188
|
+
const row = rowById.get(otherStoryId);
|
|
189
|
+
const option = DEPENDENCY_TYPE_OPTIONS.find((o) => o.key === key);
|
|
190
|
+
if (!row || !option) return;
|
|
191
|
+
const edge = toDependencyEdge(option, storyId, otherStoryId);
|
|
192
|
+
try {
|
|
193
|
+
const res = await updateDependency({ data: { depId: row.depId, ...edge } });
|
|
194
|
+
if (!res.ok) {
|
|
195
|
+
toast.warning(t(`dependency.errors.${res.reason}`));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const mine = (res.activities ?? [])
|
|
199
|
+
.filter((a) => a.storyId === storyId)
|
|
200
|
+
.map(({ storyId: _sid, ...rest }) => rest);
|
|
201
|
+
if (mine.length > 0) onAppendActivities?.(mine);
|
|
202
|
+
toast.success(t("dependency.updated"));
|
|
203
|
+
// 行的 group 跟随 option 方向:同 group 原地更新,跨 group 移动到另一 section
|
|
204
|
+
const oldGroup: DependencySection = data.dependsOn.some((r) => r.id === otherStoryId)
|
|
205
|
+
? "dependsOn"
|
|
206
|
+
: "dependents";
|
|
207
|
+
if (option.group === oldGroup) {
|
|
208
|
+
applyRowsUpdate(
|
|
209
|
+
(rows) => rows.map((r) => (r.id === otherStoryId ? { ...r, depType: option.edge } : r)),
|
|
210
|
+
oldGroup,
|
|
211
|
+
);
|
|
212
|
+
} else {
|
|
213
|
+
const moved = { ...row, depType: option.edge };
|
|
214
|
+
applyRowsUpdate((rows) => rows.filter((r) => r.id !== otherStoryId), oldGroup);
|
|
215
|
+
applyRowsUpdate((rows) => [moved, ...rows], option.group);
|
|
216
|
+
}
|
|
217
|
+
} catch (e) {
|
|
218
|
+
toast.error(
|
|
219
|
+
isNetworkError(e) ? t("dependency.networkFailed") : (e as Error)?.message || t("dependency.updateFailed"),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
171
224
|
const handleBulkRemoveRelations = async (storyIds: string[]) => {
|
|
172
225
|
if (storyIds.length === 0) return;
|
|
173
226
|
setBusy(true);
|
|
174
227
|
try {
|
|
175
228
|
for (const id of storyIds) {
|
|
176
229
|
const row = rowById.get(id);
|
|
177
|
-
if (row)
|
|
230
|
+
if (row) {
|
|
231
|
+
const res = await removeDependency({ data: { depId: row.depId } });
|
|
232
|
+
const mine = (res?.activities ?? [])
|
|
233
|
+
.filter((a) => a.storyId === storyId)
|
|
234
|
+
.map(({ storyId: _sid, ...rest }) => rest);
|
|
235
|
+
if (mine.length > 0) onAppendActivities?.(mine);
|
|
236
|
+
}
|
|
178
237
|
}
|
|
179
238
|
toast.success(t("dependency.removed"));
|
|
180
239
|
setSelectedIds(new Set());
|
|
@@ -308,6 +367,7 @@ export function StoryDetailDependencyPanel({
|
|
|
308
367
|
onRemove={handleRemoveRow}
|
|
309
368
|
removeMenuLabel={t("dependency.remove")}
|
|
310
369
|
dependencyMeta={{ depType: row.depType, group }}
|
|
370
|
+
onDependencyTypeChange={handleDependencyTypeChange}
|
|
311
371
|
currentIdentifier={storyIdentifier}
|
|
312
372
|
onStoryRefresh={handleStoryRefresh}
|
|
313
373
|
/>
|
|
@@ -364,7 +424,7 @@ export function StoryDetailDependencyPanel({
|
|
|
364
424
|
return (
|
|
365
425
|
<Collapsible open={!isCollapsed} onOpenChange={(open) => setCollapsed((p) => ({ ...p, [`${sectionKey}:root`]: !open }))}>
|
|
366
426
|
<CollapsibleTrigger asChild>
|
|
367
|
-
<div className="flex h-9 cursor-pointer items-center gap-1.5 bg-muted/50 px-3 text-sm font-medium hover:text-foreground transition-colors">
|
|
427
|
+
<div className="flex h-9 cursor-pointer select-none items-center gap-1.5 bg-muted/50 px-3 text-sm font-medium hover:text-foreground transition-colors">
|
|
368
428
|
<ChevronDown className={`size-3 shrink-0 text-muted-foreground transition-transform ${isCollapsed ? "-rotate-90" : "rotate-0"}`} />
|
|
369
429
|
<span>{t(titleKey)}</span>
|
|
370
430
|
<span className="ml-1 text-xs text-muted-foreground">{rows.length}</span>
|
|
@@ -390,7 +450,7 @@ export function StoryDetailDependencyPanel({
|
|
|
390
450
|
<Collapsible defaultOpen>
|
|
391
451
|
<CollapsibleTrigger
|
|
392
452
|
asChild
|
|
393
|
-
className="group flex w-full cursor-pointer items-center gap-2 pb-3 pt-4 text-xs font-semibold hover:text-foreground transition-colors"
|
|
453
|
+
className="group flex w-full cursor-pointer select-none items-center gap-2 pb-3 pt-4 text-xs font-semibold hover:text-foreground transition-colors"
|
|
394
454
|
>
|
|
395
455
|
<div
|
|
396
456
|
role="button"
|
|
@@ -51,9 +51,10 @@ interface StoryDetailToolbarProps {
|
|
|
51
51
|
users?: UserBrief[];
|
|
52
52
|
projects?: { id: string; name: string; icon: string; displayId: string | null }[];
|
|
53
53
|
labels?: { id: string; name: string; color: string }[];
|
|
54
|
+
onNavigateInPanel?: (targetId: string, targetIdentifier?: string, relationKind?: "child" | "dependency") => void;
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
export function StoryDetailToolbar({ storyId, storyIdentifier, projectId, data, onRefresh, onAppendActivities, users, projects, labels }: StoryDetailToolbarProps) {
|
|
57
|
+
export function StoryDetailToolbar({ storyId, storyIdentifier, projectId, data, onRefresh, onAppendActivities, users, projects, labels, onNavigateInPanel }: StoryDetailToolbarProps) {
|
|
57
58
|
const { t } = useTranslation("story");
|
|
58
59
|
const [actionOpen, setActionOpen] = React.useState<"create" | "select" | null>(null);
|
|
59
60
|
const [dependencyDialogOpen, setDependencyDialogOpen] = React.useState(false);
|
|
@@ -244,6 +245,7 @@ export function StoryDetailToolbar({ storyId, storyIdentifier, projectId, data,
|
|
|
244
245
|
projects={projects}
|
|
245
246
|
labels={labels}
|
|
246
247
|
onAction={handleChildAction}
|
|
248
|
+
onNavigateInPanel={onNavigateInPanel}
|
|
247
249
|
/>
|
|
248
250
|
|
|
249
251
|
<StoryDetailDependencyPanel
|
|
@@ -257,6 +259,8 @@ export function StoryDetailToolbar({ storyId, storyIdentifier, projectId, data,
|
|
|
257
259
|
projects={projects}
|
|
258
260
|
labels={labels}
|
|
259
261
|
onOpenAdd={() => setDependencyDialogOpen(true)}
|
|
262
|
+
onAppendActivities={onAppendActivities}
|
|
263
|
+
onNavigateInPanel={onNavigateInPanel}
|
|
260
264
|
/>
|
|
261
265
|
|
|
262
266
|
<AssociatedAttachmentsPanel
|
|
@@ -264,6 +268,7 @@ export function StoryDetailToolbar({ storyId, storyIdentifier, projectId, data,
|
|
|
264
268
|
data={data}
|
|
265
269
|
onRefresh={onRefresh}
|
|
266
270
|
onOpenDialog={handleOpenAssocDialog}
|
|
271
|
+
onAppendActivities={onAppendActivities}
|
|
267
272
|
/>
|
|
268
273
|
|
|
269
274
|
<StorySelectDialog
|
|
@@ -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 { GitPullRequestArrow, Tag } from "lucide-react";
|
|
18
|
+
import { GitPullRequestArrow, Tag, CalendarRange } from "lucide-react";
|
|
19
19
|
import { useTranslation } from "react-i18next";
|
|
20
20
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
21
21
|
import { ChevronDown } from "lucide-react";
|
|
@@ -23,6 +23,8 @@ import type { StoryState } from "../constants";
|
|
|
23
23
|
import type { ActivityItem } from "../types";
|
|
24
24
|
import { ParentStoryEditor } from "../inline-editors/parent-story-editor";
|
|
25
25
|
import { LabelsEditor } from "../inline-editors/labels-editor";
|
|
26
|
+
import { IterationEditor } from "@/components/iteration/iteration-editor";
|
|
27
|
+
import type { IterationSummary } from "@/server/serverFns/iteration/schemas";
|
|
26
28
|
|
|
27
29
|
interface PropertyItemProps {
|
|
28
30
|
icon: React.ReactNode;
|
|
@@ -45,15 +47,20 @@ function PropertyItem({ icon, label, children }: PropertyItemProps) {
|
|
|
45
47
|
interface StoryPropertiesProps {
|
|
46
48
|
story: {
|
|
47
49
|
id: string;
|
|
50
|
+
name: string;
|
|
51
|
+
description?: string | null;
|
|
48
52
|
parentId: string | null;
|
|
49
53
|
parent: { id: string; title: string; status: StoryState; identifier?: string } | null;
|
|
50
54
|
labels: { id: string; name: string; color: string }[];
|
|
51
55
|
projectId?: string | null;
|
|
56
|
+
iteration?: { id: string; name: string } | null;
|
|
52
57
|
};
|
|
53
58
|
labels?: { id: string; name: string; color: string }[];
|
|
54
59
|
projects?: { id: string; name: string; icon: string; displayId?: string | null }[];
|
|
60
|
+
iterations?: IterationSummary[];
|
|
55
61
|
onParentChange: (parentId: string | null) => void;
|
|
56
62
|
onLabelsChange: (labelIds: string[]) => void;
|
|
63
|
+
onIterationChange?: (iteration: { id: string; name: string } | null) => void;
|
|
57
64
|
onActivities?: (activities: ActivityItem[]) => void;
|
|
58
65
|
}
|
|
59
66
|
|
|
@@ -61,8 +68,10 @@ export function StoryProperties({
|
|
|
61
68
|
story,
|
|
62
69
|
labels,
|
|
63
70
|
projects,
|
|
71
|
+
iterations,
|
|
64
72
|
onParentChange,
|
|
65
73
|
onLabelsChange,
|
|
74
|
+
onIterationChange,
|
|
66
75
|
onActivities,
|
|
67
76
|
}: StoryPropertiesProps) {
|
|
68
77
|
const { t } = useTranslation("story");
|
|
@@ -71,7 +80,7 @@ export function StoryProperties({
|
|
|
71
80
|
<div className="px-6 py-4 text-sm font-semibold">{t("properties.title")}</div>
|
|
72
81
|
<CollapsibleContent className="px-6 pb-2 space-y-2">
|
|
73
82
|
<Collapsible defaultOpen>
|
|
74
|
-
<CollapsibleTrigger className="flex items-center gap-2 pb-4 text-xs font-semibold hover:text-foreground transition-colors">
|
|
83
|
+
<CollapsibleTrigger className="flex select-none items-center gap-2 pb-4 text-xs font-semibold hover:text-foreground transition-colors">
|
|
75
84
|
{t("properties.basic")}
|
|
76
85
|
<ChevronDown className="size-3" />
|
|
77
86
|
</CollapsibleTrigger>
|
|
@@ -98,18 +107,26 @@ export function StoryProperties({
|
|
|
98
107
|
labels={labels}
|
|
99
108
|
onChange={onLabelsChange}
|
|
100
109
|
variant="button"
|
|
110
|
+
aiContext={{ name: () => story.name, description: () => story.description ?? undefined }}
|
|
101
111
|
/>
|
|
102
112
|
</PropertyItem>
|
|
103
113
|
)}
|
|
104
114
|
</CollapsibleContent>
|
|
105
115
|
</Collapsible>
|
|
106
116
|
<Collapsible>
|
|
107
|
-
<CollapsibleTrigger className="flex items-center gap-2 pb-4 text-xs font-semibold hover:text-foreground transition-colors">
|
|
117
|
+
<CollapsibleTrigger className="flex select-none items-center gap-2 pb-4 text-xs font-semibold hover:text-foreground transition-colors">
|
|
108
118
|
{t("properties.iteration")}
|
|
109
119
|
<ChevronDown className="size-3" />
|
|
110
120
|
</CollapsibleTrigger>
|
|
111
121
|
<CollapsibleContent className="pb-2">
|
|
112
|
-
{
|
|
122
|
+
<PropertyItem icon={<CalendarRange className="size-3.5" />} label={t("properties.iteration")}>
|
|
123
|
+
<IterationEditor
|
|
124
|
+
storyId={story.id}
|
|
125
|
+
current={story.iteration ?? null}
|
|
126
|
+
options={iterations ?? []}
|
|
127
|
+
onChanged={(it) => onIterationChange?.(it)}
|
|
128
|
+
/>
|
|
129
|
+
</PropertyItem>
|
|
113
130
|
</CollapsibleContent>
|
|
114
131
|
</Collapsible>
|
|
115
132
|
</CollapsibleContent>
|
|
@@ -0,0 +1,107 @@
|
|
|
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 { describe, it, expect } from "vitest";
|
|
19
|
+
import { GROUP_FIELD, toGroupFieldPatch } from "../group-fields";
|
|
20
|
+
|
|
21
|
+
describe("GROUP_FIELD", () => {
|
|
22
|
+
it("state 映射到 state", () => {
|
|
23
|
+
expect(GROUP_FIELD.state).toBe("state");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("priority 映射到 priority", () => {
|
|
27
|
+
expect(GROUP_FIELD.priority).toBe("priority");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("assignee 映射到 assigneeId", () => {
|
|
31
|
+
expect(GROUP_FIELD.assignee).toBe("assigneeId");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("project 映射到 projectId", () => {
|
|
35
|
+
expect(GROUP_FIELD.project).toBe("projectId");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("createdBy 为 null", () => {
|
|
39
|
+
expect(GROUP_FIELD.createdBy).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("labels 为 null", () => {
|
|
43
|
+
expect(GROUP_FIELD.labels).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("none 为 null", () => {
|
|
47
|
+
expect(GROUP_FIELD.none).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("toGroupFieldPatch", () => {
|
|
52
|
+
describe("state 字段", () => {
|
|
53
|
+
it("todo → { state: todo }", () => {
|
|
54
|
+
expect(toGroupFieldPatch("state", "todo")).toEqual({ state: "todo" });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("backlog → { state: backlog }", () => {
|
|
58
|
+
expect(toGroupFieldPatch("state", "backlog")).toEqual({ state: "backlog" });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("in_progress → { state: in_progress }", () => {
|
|
62
|
+
expect(toGroupFieldPatch("state", "in_progress")).toEqual({ state: "in_progress" });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("done → { state: done }", () => {
|
|
66
|
+
expect(toGroupFieldPatch("state", "done")).toEqual({ state: "done" });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("cancelled → { state: cancelled }", () => {
|
|
70
|
+
expect(toGroupFieldPatch("state", "cancelled")).toEqual({ state: "cancelled" });
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("priority 字段", () => {
|
|
75
|
+
it("high → { priority: high }", () => {
|
|
76
|
+
expect(toGroupFieldPatch("priority", "high")).toEqual({ priority: "high" });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("urgent → { priority: urgent }", () => {
|
|
80
|
+
expect(toGroupFieldPatch("priority", "urgent")).toEqual({ priority: "urgent" });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("none → { priority: none }", () => {
|
|
84
|
+
expect(toGroupFieldPatch("priority", "none")).toEqual({ priority: "none" });
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("assigneeId 字段", () => {
|
|
89
|
+
it("unassigned → { assigneeId: null }", () => {
|
|
90
|
+
expect(toGroupFieldPatch("assigneeId", "unassigned")).toEqual({ assigneeId: null });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("普通 userId → { assigneeId: userId }", () => {
|
|
94
|
+
expect(toGroupFieldPatch("assigneeId", "u123")).toEqual({ assigneeId: "u123" });
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("projectId 字段", () => {
|
|
99
|
+
it("no-project → { projectId: null }", () => {
|
|
100
|
+
expect(toGroupFieldPatch("projectId", "no-project")).toEqual({ projectId: null });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("普通 projectId → { projectId: projectId }", () => {
|
|
104
|
+
expect(toGroupFieldPatch("projectId", "proj1")).toEqual({ projectId: "proj1" });
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { Sprout, CircleUser } from "lucide-react";
|
|
19
19
|
import { UserAvatar } from "@allbluecn/ui";
|
|
20
|
-
import {
|
|
20
|
+
import { LucideIconByName } from "@/components/shared/lucide-icon-picker/lucide-icon-by-name";
|
|
21
21
|
import { cn } from "@/lib/utils";
|
|
22
22
|
import { STORY_STATES, STATE_LABEL, STATE_COLOR, STATE_ICON, STORY_PRIORITIES, PRIORITY_LABEL, PRIORITY_ICON } from "../constants";
|
|
23
23
|
import { PRIORITY_COLOR } from "../priority-icon";
|
|
@@ -127,7 +127,7 @@ export function resolveGroupIcon(k: string, groupKey: GroupKey, ctx: GroupContex
|
|
|
127
127
|
}
|
|
128
128
|
if (!project) return undefined;
|
|
129
129
|
const IconComponent = ({ className }: { className?: string }) => (
|
|
130
|
-
<
|
|
130
|
+
<LucideIconByName name={project.icon} size={16} className={className} />
|
|
131
131
|
);
|
|
132
132
|
return IconComponent;
|
|
133
133
|
}
|