@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
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
//
|
|
5
|
+
// This program is free software: you can redistribute it and/or modify
|
|
6
|
+
// it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
// the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
// (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// This program is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
import { useState } from "react";
|
|
19
|
+
import { useTranslation } from "react-i18next";
|
|
20
|
+
import { Button } from "@/components/ui/button";
|
|
21
|
+
import { Input } from "@/components/ui/input";
|
|
22
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
23
|
+
import {
|
|
24
|
+
Dialog,
|
|
25
|
+
DialogContent,
|
|
26
|
+
DialogFooter,
|
|
27
|
+
DialogHeader,
|
|
28
|
+
DialogTitle,
|
|
29
|
+
} from "@/components/ui/dialog";
|
|
30
|
+
import { createIterationsFn, updateIterationsFn } from "@/server/serverFns/iteration";
|
|
31
|
+
import { toast } from "@/components/ui/sonner";
|
|
32
|
+
import type { IterationSummary } from "@/server/serverFns/iteration/schemas";
|
|
33
|
+
|
|
34
|
+
interface CreateIterationDialogProps {
|
|
35
|
+
open: boolean;
|
|
36
|
+
onOpenChange: (open: boolean) => void;
|
|
37
|
+
projectId: string;
|
|
38
|
+
editing?: IterationSummary | null;
|
|
39
|
+
onSaved?: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function CreateIterationDialog({ open, onOpenChange, projectId, editing, onSaved }: CreateIterationDialogProps) {
|
|
43
|
+
const { t } = useTranslation("iterations");
|
|
44
|
+
const [name, setName] = useState(editing?.name ?? "");
|
|
45
|
+
const [startDate, setStartDate] = useState(editing?.startDate ?? "");
|
|
46
|
+
const [endDate, setEndDate] = useState(editing?.endDate ?? "");
|
|
47
|
+
const [description, setDescription] = useState(editing?.description ?? "");
|
|
48
|
+
const [loading, setLoading] = useState(false);
|
|
49
|
+
const [error, setError] = useState<string | null>(null);
|
|
50
|
+
|
|
51
|
+
async function handleSubmit() {
|
|
52
|
+
setError(null);
|
|
53
|
+
if (!name.trim()) {
|
|
54
|
+
toast.error(t("errors.nameRequired"));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (startDate && endDate && startDate > endDate) {
|
|
58
|
+
toast.error(t("errors.invalidRange"));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
setLoading(true);
|
|
62
|
+
try {
|
|
63
|
+
if (editing) {
|
|
64
|
+
await updateIterationsFn({
|
|
65
|
+
data: {
|
|
66
|
+
id: editing.id,
|
|
67
|
+
name: name.trim(),
|
|
68
|
+
description: description.trim() || undefined,
|
|
69
|
+
...(startDate ? { startDate } : {}),
|
|
70
|
+
...(endDate ? { endDate } : {}),
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
toast.success(t("actions.edit"));
|
|
74
|
+
} else {
|
|
75
|
+
await createIterationsFn({
|
|
76
|
+
data: {
|
|
77
|
+
projectId,
|
|
78
|
+
name: name.trim(),
|
|
79
|
+
description: description.trim() || undefined,
|
|
80
|
+
startDate,
|
|
81
|
+
endDate,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
toast.success(t("actions.create"));
|
|
85
|
+
}
|
|
86
|
+
onSaved?.();
|
|
87
|
+
onOpenChange(false);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
90
|
+
toast.error(e instanceof Error ? e.message : "操作失败");
|
|
91
|
+
} finally {
|
|
92
|
+
setLoading(false);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
98
|
+
<DialogContent className="sm:max-w-md">
|
|
99
|
+
<DialogHeader>
|
|
100
|
+
<DialogTitle>{editing ? t("actions.edit") : t("actions.create")}</DialogTitle>
|
|
101
|
+
</DialogHeader>
|
|
102
|
+
<div className="space-y-4 py-4">
|
|
103
|
+
<div className="space-y-2">
|
|
104
|
+
<label className="text-sm font-medium leading-none">{t("fields.name")}</label>
|
|
105
|
+
<Input
|
|
106
|
+
placeholder={t("fields.namePlaceholder")}
|
|
107
|
+
value={name}
|
|
108
|
+
onChange={(e) => setName(e.target.value)}
|
|
109
|
+
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); void handleSubmit(); } }}
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
<div className="grid grid-cols-2 gap-4">
|
|
113
|
+
<div className="space-y-2">
|
|
114
|
+
<label className="text-sm font-medium leading-none">{t("fields.startDate")}</label>
|
|
115
|
+
<Input type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} />
|
|
116
|
+
</div>
|
|
117
|
+
<div className="space-y-2">
|
|
118
|
+
<label className="text-sm font-medium leading-none">{t("fields.endDate")}</label>
|
|
119
|
+
<Input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} />
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
<div className="space-y-2">
|
|
123
|
+
<label className="text-sm font-medium leading-none">{t("fields.description")}</label>
|
|
124
|
+
<Textarea
|
|
125
|
+
placeholder={t("fields.description")}
|
|
126
|
+
value={description}
|
|
127
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
128
|
+
className="min-h-[80px]"
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
132
|
+
</div>
|
|
133
|
+
<DialogFooter>
|
|
134
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>{t("actions.cancel")}</Button>
|
|
135
|
+
<Button onClick={() => void handleSubmit()} disabled={loading}>{loading ? t("actions.saving") : t("actions.save")}</Button>
|
|
136
|
+
</DialogFooter>
|
|
137
|
+
</DialogContent>
|
|
138
|
+
</Dialog>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
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 { Link } from "@tanstack/react-router";
|
|
19
|
+
import { useTranslation } from "react-i18next";
|
|
20
|
+
import { IterationStatusBadge } from "./iteration-status-badge";
|
|
21
|
+
import { IterationProgressBar } from "./iteration-progress-bar";
|
|
22
|
+
import type { IterationSummary } from "@/server/serverFns/iteration/schemas";
|
|
23
|
+
|
|
24
|
+
export function IterationCard({ item, projectId }: { item: IterationSummary; projectId: string }) {
|
|
25
|
+
const { t } = useTranslation("iterations");
|
|
26
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
27
|
+
const remainingDays = Math.round((Date.parse(item.endDate) - Date.parse(today)) / 86400000);
|
|
28
|
+
const isOverdue = remainingDays < 0 && item.status === "active";
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Link
|
|
32
|
+
to="/projects/$projectId/iterations/$iterationId"
|
|
33
|
+
params={{ projectId, iterationId: item.id }}
|
|
34
|
+
className="block rounded-lg border bg-card p-4 transition-colors hover:border-primary/50"
|
|
35
|
+
>
|
|
36
|
+
<div className="flex items-start justify-between gap-2">
|
|
37
|
+
<div className="min-w-0">
|
|
38
|
+
<div className="flex items-center gap-2">
|
|
39
|
+
<h3 className="truncate text-sm font-semibold">{item.name}</h3>
|
|
40
|
+
<IterationStatusBadge status={item.status} />
|
|
41
|
+
</div>
|
|
42
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
43
|
+
{item.startDate} ~ {item.endDate}
|
|
44
|
+
{item.status === "active" && !isOverdue && (
|
|
45
|
+
<span className="ml-2 text-muted-foreground/80">{t("progress.remainingDays", { days: remainingDays })}</span>
|
|
46
|
+
)}
|
|
47
|
+
{isOverdue && (
|
|
48
|
+
<span className="ml-2 text-red-500">{t("progress.overdueDays", { days: Math.abs(remainingDays) })}</span>
|
|
49
|
+
)}
|
|
50
|
+
</p>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
{item.storyCount > 0 && (
|
|
54
|
+
<div className="mt-3">
|
|
55
|
+
<IterationProgressBar stateCounts={item.stateCounts} />
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
</Link>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
//
|
|
5
|
+
// This program is free software: you can redistribute it and/or modify
|
|
6
|
+
// it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
// the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
// (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// This program is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
import { useState } from "react";
|
|
19
|
+
import { ChevronDown, X } from "lucide-react";
|
|
20
|
+
import { useTranslation } from "react-i18next";
|
|
21
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
22
|
+
import { Button } from "@/components/ui/button";
|
|
23
|
+
import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from "@/components/ui/command";
|
|
24
|
+
import {
|
|
25
|
+
AlertDialog,
|
|
26
|
+
AlertDialogAction,
|
|
27
|
+
AlertDialogCancel,
|
|
28
|
+
AlertDialogContent,
|
|
29
|
+
AlertDialogDescription,
|
|
30
|
+
AlertDialogFooter,
|
|
31
|
+
AlertDialogHeader,
|
|
32
|
+
AlertDialogTitle,
|
|
33
|
+
} from "@/components/ui/alert-dialog";
|
|
34
|
+
import { setStoryIterationFn } from "@/server/serverFns/iteration";
|
|
35
|
+
import { toast } from "@/components/ui/sonner";
|
|
36
|
+
import type { IterationSummary } from "@/server/serverFns/iteration/schemas";
|
|
37
|
+
|
|
38
|
+
interface IterationEditorProps {
|
|
39
|
+
storyId: string;
|
|
40
|
+
current: { id: string; name: string } | null;
|
|
41
|
+
options: IterationSummary[];
|
|
42
|
+
onChanged: (iteration: { id: string; name: string } | null) => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function IterationEditor({ storyId, current, options, onChanged }: IterationEditorProps) {
|
|
46
|
+
const { t } = useTranslation("iterations");
|
|
47
|
+
const [open, setOpen] = useState(false);
|
|
48
|
+
const [pendingId, setPendingId] = useState<string | null>(null);
|
|
49
|
+
const joinable = options.filter((i) => i.status !== "completed");
|
|
50
|
+
|
|
51
|
+
async function executePick(id: string | null) {
|
|
52
|
+
setPendingId(null);
|
|
53
|
+
await setStoryIterationFn({ data: { storyId, iterationId: id } });
|
|
54
|
+
const next = id ? joinable.find((i) => i.id === id) ?? null : null;
|
|
55
|
+
toast.success(id && next ? t("editor.setSuccess", { name: next.name }) : t("editor.clearSuccess"));
|
|
56
|
+
onChanged(next ? { id: next.id, name: next.name } : null);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handlePick(id: string | null) {
|
|
60
|
+
setOpen(false);
|
|
61
|
+
if (current && id && current.id !== id) {
|
|
62
|
+
setPendingId(id);
|
|
63
|
+
} else {
|
|
64
|
+
void executePick(id);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<>
|
|
70
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
71
|
+
<PopoverTrigger asChild>
|
|
72
|
+
<Button variant="ghost" size="sm" className="h-6 justify-start gap-1 px-1 text-xs font-normal">
|
|
73
|
+
{current ? current.name : <span className="text-muted-foreground">{t("editor.placeholder")}</span>}
|
|
74
|
+
{current ? (
|
|
75
|
+
<X
|
|
76
|
+
className="ml-1 size-3 hover:text-destructive"
|
|
77
|
+
onClick={(e) => { e.stopPropagation(); void executePick(null); }}
|
|
78
|
+
/>
|
|
79
|
+
) : null}
|
|
80
|
+
<ChevronDown className="ml-auto size-3 text-muted-foreground" />
|
|
81
|
+
</Button>
|
|
82
|
+
</PopoverTrigger>
|
|
83
|
+
<PopoverContent className="w-56 p-0" align="start">
|
|
84
|
+
<Command>
|
|
85
|
+
<CommandList>
|
|
86
|
+
<CommandEmpty>{t("editor.none")}</CommandEmpty>
|
|
87
|
+
<CommandGroup>
|
|
88
|
+
{joinable.map((i) => (
|
|
89
|
+
<CommandItem key={i.id} value={i.id} onSelect={() => handlePick(i.id)}>
|
|
90
|
+
{i.name}
|
|
91
|
+
<span className="ml-auto text-xs text-muted-foreground">{i.startDate} ~ {i.endDate}</span>
|
|
92
|
+
</CommandItem>
|
|
93
|
+
))}
|
|
94
|
+
</CommandGroup>
|
|
95
|
+
</CommandList>
|
|
96
|
+
</Command>
|
|
97
|
+
</PopoverContent>
|
|
98
|
+
</Popover>
|
|
99
|
+
|
|
100
|
+
<AlertDialog open={pendingId !== null} onOpenChange={(v) => { if (!v) setPendingId(null); }}>
|
|
101
|
+
<AlertDialogContent>
|
|
102
|
+
<AlertDialogHeader>
|
|
103
|
+
<AlertDialogTitle>{t("editor.confirmSwitch", { name: current?.name ?? "" })}</AlertDialogTitle>
|
|
104
|
+
<AlertDialogDescription />
|
|
105
|
+
</AlertDialogHeader>
|
|
106
|
+
<AlertDialogFooter>
|
|
107
|
+
<AlertDialogCancel onClick={() => setPendingId(null)}>{t("actions.cancel")}</AlertDialogCancel>
|
|
108
|
+
<AlertDialogAction onClick={() => { if (pendingId) void executePick(pendingId); }}>
|
|
109
|
+
{t("actions.confirm")}
|
|
110
|
+
</AlertDialogAction>
|
|
111
|
+
</AlertDialogFooter>
|
|
112
|
+
</AlertDialogContent>
|
|
113
|
+
</AlertDialog>
|
|
114
|
+
</>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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 { useTranslation } from "react-i18next";
|
|
19
|
+
import { STATE_COLOR } from "@/components/story/constants";
|
|
20
|
+
import type { StoryState } from "@/components/story/constants";
|
|
21
|
+
|
|
22
|
+
const ORDER: StoryState[] = ["backlog", "todo", "in_progress", "done", "cancelled"];
|
|
23
|
+
|
|
24
|
+
export function IterationProgressBar({ stateCounts }: { stateCounts: Record<string, number> }) {
|
|
25
|
+
const { t } = useTranslation("iterations");
|
|
26
|
+
const total = ORDER.reduce((a, s) => a + (stateCounts[s] ?? 0), 0);
|
|
27
|
+
const denom = total - (stateCounts.cancelled ?? 0);
|
|
28
|
+
const percent = denom > 0 ? Math.round(((stateCounts.done ?? 0) / denom) * 100) : 0;
|
|
29
|
+
return (
|
|
30
|
+
<div className="space-y-1">
|
|
31
|
+
<div className="flex h-1.5 w-full overflow-hidden rounded-full bg-muted">
|
|
32
|
+
{ORDER.map((s) => {
|
|
33
|
+
const n = stateCounts[s] ?? 0;
|
|
34
|
+
if (!n) return null;
|
|
35
|
+
return <div key={s} className={STATE_COLOR[s]} style={{ width: `${(n / Math.max(1, total)) * 100}%` }} />;
|
|
36
|
+
})}
|
|
37
|
+
</div>
|
|
38
|
+
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
39
|
+
<span>{t("progress.total")}: {total}</span>
|
|
40
|
+
<span>{t("progress.percent")}: {percent}%</span>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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 { useTranslation } from "react-i18next";
|
|
19
|
+
import { ITERATION_STATUS_META } from "./constants";
|
|
20
|
+
import type { IterationStatus } from "@/lib/iteration/status";
|
|
21
|
+
import { cn } from "@/lib/utils";
|
|
22
|
+
|
|
23
|
+
export function IterationStatusBadge({ status }: { status: IterationStatus }) {
|
|
24
|
+
const { t } = useTranslation("iterations");
|
|
25
|
+
const meta = ITERATION_STATUS_META[status];
|
|
26
|
+
const Icon = meta.icon;
|
|
27
|
+
return (
|
|
28
|
+
<span className={cn("inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium", meta.className)}>
|
|
29
|
+
<Icon className="size-3" />
|
|
30
|
+
{t(meta.labelKey)}
|
|
31
|
+
</span>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -21,17 +21,14 @@ import { useForm } from "@tanstack/react-form";
|
|
|
21
21
|
import { useRouter } from "@tanstack/react-router";
|
|
22
22
|
import { Button } from "@/components/ui/button";
|
|
23
23
|
import { Input } from "@/components/ui/input";
|
|
24
|
-
import { Textarea } from "@/components/ui/textarea";
|
|
25
24
|
import {
|
|
26
25
|
Dialog,
|
|
27
26
|
DialogContent,
|
|
28
|
-
DialogDescription,
|
|
29
27
|
DialogFooter,
|
|
30
28
|
DialogHeader,
|
|
31
29
|
DialogTitle,
|
|
32
30
|
} from "@/components/ui/dialog";
|
|
33
|
-
import {
|
|
34
|
-
import { KNOWLEDGE_ICONS } from "@/components/shared/icon-packs/knowledge";
|
|
31
|
+
import { LucideIconPicker } from "@/components/shared/lucide-icon-picker";
|
|
35
32
|
import { createKnowledgeFn, updateKnowledgeFn } from "@/server/serverFns/knowledge";
|
|
36
33
|
import { knowledgeFormSchema, type KnowledgeFormValues } from "../schemas/knowledge-form";
|
|
37
34
|
|
|
@@ -53,7 +50,7 @@ export function KnowledgeFormDialog({ open, onOpenChange, kb, onSuccess }: Knowl
|
|
|
53
50
|
defaultValues: {
|
|
54
51
|
name: kb?.name ?? "",
|
|
55
52
|
description: kb?.description ?? "",
|
|
56
|
-
icon: kb?.icon ?? "
|
|
53
|
+
icon: kb?.icon ?? "",
|
|
57
54
|
},
|
|
58
55
|
validators: [{ run: knowledgeFormSchema as any, triggers: ['change'] }],
|
|
59
56
|
});
|
|
@@ -102,7 +99,7 @@ export function KnowledgeFormDialog({ open, onOpenChange, kb, onSuccess }: Knowl
|
|
|
102
99
|
|
|
103
100
|
const handleOpenChange = (newOpen: boolean) => {
|
|
104
101
|
if (newOpen) {
|
|
105
|
-
form.setFieldValue("icon", kb?.icon ?? "
|
|
102
|
+
form.setFieldValue("icon", kb?.icon ?? "");
|
|
106
103
|
form.setFieldValue("name", kb?.name ?? "");
|
|
107
104
|
form.setFieldValue("description", kb?.description ?? "");
|
|
108
105
|
setError("");
|
|
@@ -112,27 +109,22 @@ export function KnowledgeFormDialog({ open, onOpenChange, kb, onSuccess }: Knowl
|
|
|
112
109
|
|
|
113
110
|
return (
|
|
114
111
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
115
|
-
<DialogContent className="sm:max-w-
|
|
116
|
-
<DialogHeader
|
|
117
|
-
<DialogTitle
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
</DialogDescription>
|
|
112
|
+
<DialogContent className="sm:max-w-260 -mt-50">
|
|
113
|
+
<DialogHeader>
|
|
114
|
+
<DialogTitle className="text-xl">
|
|
115
|
+
{isEdit ? t('kbForm.title.edit') : t('kbForm.title.create')}
|
|
116
|
+
</DialogTitle>
|
|
121
117
|
</DialogHeader>
|
|
122
118
|
|
|
123
|
-
<div className="space-y-4
|
|
124
|
-
<div className="flex
|
|
119
|
+
<div className="space-y-3 overflow-y-auto px-4 py-2 max-h-[60vh]">
|
|
120
|
+
<div className="flex gap-2">
|
|
125
121
|
<form.Field name="icon" children={(field) => (
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
value={field.value ?? "notebook"}
|
|
122
|
+
<LucideIconPicker
|
|
123
|
+
value={field.value}
|
|
129
124
|
onChange={(v) => field.handleChange(v)}
|
|
125
|
+
modal
|
|
130
126
|
/>
|
|
131
127
|
)} />
|
|
132
|
-
</div>
|
|
133
|
-
|
|
134
|
-
<div className="flex flex-col gap-3">
|
|
135
|
-
<label className="text-sm font-medium ml-2">{t('kbForm.nameLabel')}</label>
|
|
136
128
|
<form.Field name="name" children={(field) => (
|
|
137
129
|
<Input
|
|
138
130
|
value={field.value ?? ""}
|
|
@@ -140,7 +132,7 @@ export function KnowledgeFormDialog({ open, onOpenChange, kb, onSuccess }: Knowl
|
|
|
140
132
|
onBlur={field.handleBlur}
|
|
141
133
|
placeholder={t('kbForm.namePlaceholder')}
|
|
142
134
|
maxLength={50}
|
|
143
|
-
|
|
135
|
+
className="h-10 min-w-0 flex-1"
|
|
144
136
|
onKeyDown={(e) => {
|
|
145
137
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
146
138
|
e.preventDefault();
|
|
@@ -151,26 +143,25 @@ export function KnowledgeFormDialog({ open, onOpenChange, kb, onSuccess }: Knowl
|
|
|
151
143
|
)} />
|
|
152
144
|
</div>
|
|
153
145
|
|
|
154
|
-
<
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
<Textarea
|
|
146
|
+
<form.Field name="description" children={(field) => (
|
|
147
|
+
<div className="notion-full-width min-h-50 max-h-150 flex flex-col overflow-y-auto border rounded-md border-border">
|
|
148
|
+
<textarea
|
|
158
149
|
value={field.value ?? ""}
|
|
159
150
|
onChange={(e) => field.handleChange(e.target.value)}
|
|
160
151
|
onBlur={field.handleBlur}
|
|
161
152
|
placeholder={t('kbForm.descriptionPlaceholder')}
|
|
162
153
|
maxLength={200}
|
|
163
|
-
|
|
154
|
+
className="flex-1 min-h-0 resize-none bg-transparent px-3 py-2 text-sm outline-none placeholder:text-muted-foreground"
|
|
164
155
|
/>
|
|
165
|
-
|
|
166
|
-
|
|
156
|
+
</div>
|
|
157
|
+
)} />
|
|
167
158
|
|
|
168
159
|
{error && (
|
|
169
160
|
<p className="text-sm text-destructive break-words">{error}</p>
|
|
170
161
|
)}
|
|
171
162
|
</div>
|
|
172
163
|
|
|
173
|
-
<DialogFooter>
|
|
164
|
+
<DialogFooter className="items-center gap-3">
|
|
174
165
|
<Button variant="outline" onClick={() => handleOpenChange(false)}>
|
|
175
166
|
{t('kbForm.cancel')}
|
|
176
167
|
</Button>
|
|
@@ -63,8 +63,8 @@ vi.mock("@/lib/sidebar-utils", () => ({
|
|
|
63
63
|
isParentActive: (pathname: string, target: string) => pathname.startsWith(target),
|
|
64
64
|
}));
|
|
65
65
|
|
|
66
|
-
vi.mock("@/components/
|
|
67
|
-
|
|
66
|
+
vi.mock("@/components/shared/lucide-icon-picker/lucide-icon-by-name", () => ({
|
|
67
|
+
LucideIconByName: ({ name }: any) => <span data-testid="project-icon">{name}</span>,
|
|
68
68
|
}));
|
|
69
69
|
|
|
70
70
|
vi.mock("@/components/workspace/create-workspace-dialog", () => ({
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
} from "@/components/ui/sidebar";
|
|
29
29
|
import { cn } from "@/lib/utils";
|
|
30
30
|
import { isParentActive } from "@/lib/sidebar-utils";
|
|
31
|
-
import {
|
|
31
|
+
import { LucideIconByName } from "@/components/shared/lucide-icon-picker/lucide-icon-by-name";
|
|
32
32
|
import { CreateWorkspaceDialog } from "@/components/workspace/create-workspace-dialog";
|
|
33
33
|
import { useLiveQuery } from "@tanstack/react-db";
|
|
34
34
|
import { workspacesCollection } from "@/lib/collections/reference/workspaces-collection";
|
|
@@ -74,7 +74,7 @@ function WorkspaceLinks({ pathname, t }: { pathname: string; t: any }) {
|
|
|
74
74
|
params={{ workspaceId: ws.id }}
|
|
75
75
|
className="flex w-full items-center"
|
|
76
76
|
>
|
|
77
|
-
<
|
|
77
|
+
<LucideIconByName
|
|
78
78
|
name={ws.icon}
|
|
79
79
|
size={16}
|
|
80
80
|
className="text-sidebar-foreground"
|