@allbluecn/web-app 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +239 -30
- package/src/components/iteration/auto-schedule-dialog.tsx +150 -0
- package/src/components/iteration/burndown-chart.tsx +63 -0
- package/src/components/iteration/constants.ts +28 -0
- package/src/components/iteration/create-iteration-dialog.tsx +140 -0
- package/src/components/iteration/iteration-card.tsx +60 -0
- package/src/components/iteration/iteration-editor.tsx +116 -0
- package/src/components/iteration/iteration-progress-bar.tsx +44 -0
- package/src/components/iteration/iteration-status-badge.tsx +33 -0
- package/src/components/knowledge/dialog/knowledge-form-dialog.tsx +21 -30
- package/src/components/knowledge/knowledge-toolbar.tsx +124 -1
- package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +2 -2
- package/src/components/layout/sidebar-03/nav-workspace.tsx +2 -2
- package/src/components/project/create-project-dialog.tsx +44 -57
- package/src/components/shared/__tests__/lucide-curated-icons.test.ts +83 -0
- package/src/components/shared/__tests__/lucide-icon-data.test.ts +131 -0
- package/src/components/shared/__tests__/lucide-icon-picker.test.tsx +197 -0
- package/src/components/shared/lucide-icon-picker/curated-icons.ts +478 -0
- package/src/components/shared/lucide-icon-picker/icon-data.ts +109 -0
- package/src/components/shared/lucide-icon-picker/index.tsx +82 -0
- package/src/components/shared/lucide-icon-picker/lucide-icon-by-name.tsx +78 -0
- package/src/components/shared/lucide-icon-picker/panel.tsx +163 -0
- package/src/components/story/__tests__/constants.test.ts +255 -0
- package/src/components/story/ai/story-ai-panel.tsx +23 -15
- package/src/components/story/ai/story-ai-preview-list.tsx +19 -0
- package/src/components/story/constants.ts +43 -1
- package/src/components/story/create-story-dialog.tsx +42 -16
- package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +114 -0
- package/src/components/story/detail/__tests__/categories.test.tsx +62 -0
- package/src/components/story/detail/__tests__/dependencies-button.test.tsx +49 -0
- package/src/components/story/detail/activity-timeline.tsx +26 -4
- package/src/components/story/detail/story-detail-body.tsx +90 -38
- package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +857 -0
- package/src/components/story/detail/story-detail-toolbar/categories.tsx +23 -18
- package/src/components/story/detail/story-detail-toolbar/children.tsx +9 -4
- package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +95 -27
- package/src/components/story/detail/story-detail-toolbar.tsx +159 -8
- package/src/components/story/detail/story-properties.tsx +21 -4
- package/src/components/story/display/__tests__/group-fields.test.ts +107 -0
- package/src/components/story/display/group-resolver.tsx +2 -2
- package/src/components/story/inline-editors/__tests__/dependency-type-select.test.tsx +155 -0
- package/src/components/story/inline-editors/__tests__/labels-editor.test.tsx +155 -0
- package/src/components/story/inline-editors/__tests__/priority-editor.test.tsx +173 -0
- package/src/components/story/inline-editors/__tests__/state-editor.test.tsx +174 -0
- package/src/components/story/inline-editors/dependency-type-select.tsx +180 -0
- package/src/components/story/inline-editors/index.ts +2 -1
- package/src/components/story/inline-editors/labels-editor.tsx +159 -3
- package/src/components/story/inline-editors/story-select-dialog.tsx +41 -63
- package/src/components/story/peek-panel.tsx +121 -21
- package/src/components/story/relations/__tests__/attachment-manage-dialog.test.tsx +171 -0
- package/src/components/story/relations/__tests__/knowledge-select-dialog.test.tsx +174 -0
- package/src/components/story/relations/__tests__/link-manage-dialog.test.tsx +93 -0
- package/src/components/story/relations/attachment-manage-dialog.tsx +490 -0
- package/src/components/story/relations/file-preview.tsx +173 -0
- package/src/components/story/relations/format.ts +120 -0
- package/src/components/story/relations/general-groups.ts +32 -0
- package/src/components/story/relations/knowledge-select-dialog.tsx +403 -0
- package/src/components/story/relations/link-manage-dialog.tsx +192 -0
- package/src/components/story/relations/story-ref-row.tsx +17 -27
- package/src/components/story/story-panel-header.tsx +25 -19
- package/src/components/story/story-toolbar.tsx +3 -3
- package/src/components/story/transfer-story-popover.tsx +2 -2
- package/src/components/story/types.ts +14 -0
- package/src/components/story/views/create-view-dialog.tsx +3 -3
- package/src/components/story/views/story-list/__tests__/group-header.test.tsx +219 -0
- package/src/components/story/views/story-list/group.tsx +1 -1
- package/src/components/story/views/story-list/inline-story-creator.tsx +2 -2
- package/src/components/story/views/story-list/row.tsx +1 -1
- package/src/components/workspace/create-workspace-dialog.tsx +41 -49
- package/src/lib/__tests__/project-display-id.test.ts +8 -5
- package/src/lib/changelog/sdk-versions.ts +20 -20
- package/src/lib/iteration/__tests__/status.test.ts +41 -0
- package/src/lib/iteration/status.ts +28 -0
- package/src/lib/project-display-id.ts +14 -5
- package/src/lib/project-module-meta.ts +77 -0
- package/src/lib/story/__tests__/video-embed.test.ts +77 -0
- package/src/lib/story/video-embed.ts +59 -0
- package/src/lib/utils.d.ts +1 -1
- package/src/plugins/.gen-manifest.json +13 -0
- package/src/plugins/modules.gen.ts +20 -0
- package/src/plugins/plugins.gen.ts +20 -0
- package/src/plugins/ui.gen.ts +8 -0
- package/src/providers/index.tsx +6 -0
- package/src/routeTree.gen.ts +96 -0
- package/src/routes/__root.tsx +0 -8
- package/src/routes/_nav/inspiration/route.tsx +8 -0
- package/src/routes/_nav/knowledge/index.lazy.tsx +2 -2
- package/src/routes/_nav/prd/$id/route.tsx +9 -0
- package/src/routes/_nav/prd/index.tsx +9 -0
- package/src/routes/_nav/prd/route.tsx +8 -0
- package/src/routes/_nav/projects/$projectId/$storyId/route.lazy.tsx +52 -25
- package/src/routes/_nav/projects/$projectId/$storyId/route.tsx +6 -3
- package/src/routes/_nav/projects/$projectId/index.lazy.tsx +32 -15
- package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.lazy.tsx +289 -0
- package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.tsx +27 -0
- package/src/routes/_nav/projects/$projectId/iterations/index.lazy.tsx +111 -0
- package/src/routes/_nav/projects/$projectId/iterations/index.tsx +24 -0
- package/src/routes/_nav/projects/$projectId/route.tsx +4 -2
- package/src/routes/_nav/projects/$projectId/settings/route.lazy.tsx +78 -7
- package/src/routes/_nav/projects/$projectId/settings/route.tsx +6 -2
- package/src/routes/_nav/stories/$storyId/route.lazy.tsx +5 -1
- package/src/routes/_nav/stories/$storyId/route.tsx +4 -0
- package/src/routes/_nav/tags/$id/route.tsx +8 -0
- package/src/routes/_nav/tags/index.tsx +8 -0
- package/src/routes/_nav/tags/route.tsx +8 -0
- package/src/routes/_nav/tags/settings/route.tsx +9 -0
- package/src/routes/_nav/workspaces/$workspaceId/route.lazy.tsx +3 -3
- package/src/routes/_nav/workspaces/$workspaceId/settings/route.lazy.tsx +4 -4
- package/src/routes/_nav/workspaces/index.lazy.tsx +3 -3
- package/src/routes/api/attachments/$id/content.ts +60 -0
- package/src/routes/api/attachments/upload.ts +148 -0
- package/src/routes/share/prd/$prdId/route.tsx +10 -0
- package/src/server/serverFns/__tests__/project-kb.test.ts +147 -0
- package/src/server/serverFns/iteration/__tests__/auto-schedule-core.test.ts +62 -0
- package/src/server/serverFns/iteration/__tests__/burndown-core.test.ts +90 -0
- package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +211 -0
- package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +155 -0
- package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +136 -0
- package/src/server/serverFns/iteration/auto-schedule-core.ts +54 -0
- package/src/server/serverFns/iteration/auto-schedule-internal.ts +88 -0
- package/src/server/serverFns/iteration/burndown-core.ts +60 -0
- package/src/server/serverFns/iteration/index.ts +22 -0
- package/src/server/serverFns/iteration/iteration-auto-schedule.ts +66 -0
- package/src/server/serverFns/iteration/iteration-burndown.ts +70 -0
- package/src/server/serverFns/iteration/iteration-crud.ts +208 -0
- package/src/server/serverFns/iteration/iteration-stories.ts +111 -0
- package/src/server/serverFns/iteration/iteration-transfer.ts +42 -0
- package/src/server/serverFns/iteration/schemas.ts +58 -0
- package/src/server/serverFns/iteration/transfer-internal.ts +62 -0
- package/src/server/serverFns/knowledge.ts +50 -4
- package/src/server/serverFns/project/module-overview.ts +109 -0
- package/src/server/serverFns/project.ts +45 -2
- package/src/server/serverFns/story/__tests__/kb-scope.test.ts +56 -0
- package/src/server/serverFns/story/__tests__/label-suggest-fn.test.ts +100 -0
- package/src/server/serverFns/story/__tests__/labels.test.ts +67 -0
- package/src/server/serverFns/story/__tests__/story-assoc.test.ts +222 -0
- package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +132 -2
- package/src/server/serverFns/story/index.ts +9 -4
- package/src/server/serverFns/story/kb-general.ts +39 -0
- package/src/server/serverFns/story/kb-scope.ts +52 -0
- package/src/server/serverFns/story/link-preview.ts +115 -0
- package/src/server/serverFns/story/story-assoc.ts +372 -51
- package/src/server/serverFns/story/story-attachments.ts +32 -45
- package/src/{components/project/project-icon.tsx → server/serverFns/story/story-crud/label-palette.ts} +16 -19
- package/src/server/serverFns/story/story-crud/label-suggest-core.ts +88 -0
- package/src/server/serverFns/story/story-crud/label-suggest.ts +38 -0
- package/src/{components/knowledge/knowledge-icon.tsx → server/serverFns/story/story-crud/label-upsert.ts} +13 -19
- package/src/server/serverFns/story/story-crud/labels.ts +3 -5
- package/src/server/serverFns/story/story-crud.ts +53 -6
- package/src/server/serverFns/story/story-dependencies.ts +158 -5
- package/src/server/serverFns/story/story-links.ts +43 -7
- package/src/server/serverFns/story/story-transfer.ts +5 -4
- package/src/server/serverFns/story/story-tree.ts +28 -1
- package/src/server/serverFns/story/types.ts +38 -0
- package/src/styles/globals.css +25 -0
- package/src/types/project.ts +5 -5
- package/vite.shared.ts +2 -0
- package/src/components/project/project-animated-icon.tsx +0 -73
- package/src/components/shared/icon-packs/index.ts +0 -1
- package/src/components/shared/icon-packs/knowledge.ts +0 -1
- package/src/components/shared/icon-packs/prd.ts +0 -1
- package/src/components/shared/icon-packs/project.ts +0 -1
- package/src/components/shared/icon-picker.tsx +0 -1
- package/src/components/story/detail/story-detail-toolbar/attachments.tsx +0 -73
- package/src/components/story/detail/story-detail-toolbar/knowledge.tsx +0 -73
- package/src/components/story/detail/story-detail-toolbar/links.tsx +0 -73
- package/src/components/story/relations/attachments-panel.tsx +0 -179
- package/src/components/story/relations/knowledge-panel.tsx +0 -284
- package/src/components/story/relations/links-panel.tsx +0 -240
- package/src/lib/project-mock.ts +0 -156
package/src/routes/__root.tsx
CHANGED
|
@@ -26,8 +26,6 @@ import globalsCss from '@/styles/globals.css?url'
|
|
|
26
26
|
import type { RouterContext } from '@/lib/router-context'
|
|
27
27
|
import { Providers } from '@/providers'
|
|
28
28
|
import { Toaster } from '@/components/ui/sonner'
|
|
29
|
-
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
|
30
|
-
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
|
|
31
29
|
import { DevtoolsPanel } from '@allblue/devtools'
|
|
32
30
|
import { getSessionFn } from '@/server/serverFns/auth/session'
|
|
33
31
|
import { getLocaleFn } from '@/server/serverFns/locale'
|
|
@@ -82,12 +80,6 @@ function RootComponent() {
|
|
|
82
80
|
<Outlet />
|
|
83
81
|
<ShortcutsPanel />
|
|
84
82
|
<Toaster position="top-center" />
|
|
85
|
-
{import.meta.env.DEV && (
|
|
86
|
-
<ReactQueryDevtools initialIsOpen={false} />
|
|
87
|
-
)}
|
|
88
|
-
{import.meta.env.DEV && (
|
|
89
|
-
<TanStackRouterDevtools initialIsOpen={false} />
|
|
90
|
-
)}
|
|
91
83
|
{import.meta.env.DEV && <DevtoolsPanel />}
|
|
92
84
|
</HotkeysProvider>
|
|
93
85
|
</Providers>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
3
|
+
import type { PluginRouteContext } from '@allbluecn/kernel'
|
|
4
|
+
import { inspiration_route_0 } from '@allbluecn/plugins-core/inspiration/routes'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/_nav/inspiration')({
|
|
7
|
+
component: inspiration_route_0.Component as any,
|
|
8
|
+
})
|
|
@@ -23,7 +23,7 @@ import { Button } from '@/components/ui/button'
|
|
|
23
23
|
import { Card } from '@/components/ui/card'
|
|
24
24
|
import { AnimatedGridPattern } from '@/components/magicui/animated-grid-pattern'
|
|
25
25
|
import { BookOpenTextIcon } from '@/components/animated-icons/book-open-text'
|
|
26
|
-
import {
|
|
26
|
+
import { LucideIconByName } from '@/components/shared/lucide-icon-picker/lucide-icon-by-name'
|
|
27
27
|
import { KnowledgeFormDialog } from '@/components/knowledge/dialog/knowledge-form-dialog'
|
|
28
28
|
|
|
29
29
|
type KnowledgeList = ReturnType<typeof import('@/server/serverFns/knowledge')['listKnowledgeFn']> extends Promise<infer T> ? T : never
|
|
@@ -81,7 +81,7 @@ function KnowledgeListPage() {
|
|
|
81
81
|
<div className="flex-1">
|
|
82
82
|
<div className="flex items-start justify-between">
|
|
83
83
|
<div className="flex size-10 items-center justify-center rounded-lg bg-primary/10">
|
|
84
|
-
<
|
|
84
|
+
<LucideIconByName name={item.icon} size={20} />
|
|
85
85
|
</div>
|
|
86
86
|
</div>
|
|
87
87
|
<h3 className="mt-4 font-medium text-lg">{item.name}</h3>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
3
|
+
import type { PluginRouteContext } from '@allbluecn/kernel'
|
|
4
|
+
import { prd_route_2 } from '@allbluecn/plugins-core/prd/routes'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/_nav/prd/$id')({
|
|
7
|
+
beforeLoad: ((ctx: PluginRouteContext) => prd_route_2.beforeLoad(ctx)) as any,
|
|
8
|
+
component: prd_route_2.Component as any,
|
|
9
|
+
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
3
|
+
import type { PluginRouteContext } from '@allbluecn/kernel'
|
|
4
|
+
import { prd_route_1 } from '@allbluecn/plugins-core/prd/routes'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/_nav/prd/')({
|
|
7
|
+
loader: ((ctx: PluginRouteContext) => prd_route_1.loader(ctx)) as any,
|
|
8
|
+
component: prd_route_1.Component as any,
|
|
9
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
3
|
+
import type { PluginRouteContext } from '@allbluecn/kernel'
|
|
4
|
+
import { prd_route_0 } from '@allbluecn/plugins-core/prd/routes'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/_nav/prd')({
|
|
7
|
+
component: prd_route_0.Component as any,
|
|
8
|
+
})
|
|
@@ -20,25 +20,44 @@ import { useTranslation } from "react-i18next";
|
|
|
20
20
|
import { ArrowLeft, ChevronRight } from "lucide-react";
|
|
21
21
|
import { Button } from "@/components/ui/button";
|
|
22
22
|
import { Badge } from "@/components/ui/badge";
|
|
23
|
-
import {
|
|
24
|
-
import { PROJECT_MODULE_META } from "@/lib/project-
|
|
23
|
+
import { LucideIconByName } from "@/components/shared/lucide-icon-picker/lucide-icon-by-name";
|
|
24
|
+
import { PROJECT_MODULE_META } from "@/lib/project-module-meta";
|
|
25
25
|
import type { ProjectModuleItem } from "@/types/project";
|
|
26
26
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
const STATE_VARIANT: Record<string, "default" | "secondary" | "outline" | "destructive"> = {
|
|
28
|
+
done: "secondary",
|
|
29
|
+
cancelled: "outline",
|
|
30
|
+
in_progress: "default",
|
|
31
|
+
todo: "outline",
|
|
32
|
+
backlog: "outline",
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
const PRIORITY_VARIANT: Record<string, "default" | "secondary" | "outline" | "destructive"> = {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
urgent: "destructive",
|
|
37
|
+
high: "destructive",
|
|
38
|
+
medium: "secondary",
|
|
39
|
+
low: "outline",
|
|
40
|
+
none: "outline",
|
|
39
41
|
};
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
const STATE_LABEL_KEYS: Record<string, string> = {
|
|
44
|
+
backlog: "state.backlog",
|
|
45
|
+
todo: "state.todo",
|
|
46
|
+
in_progress: "state.in_progress",
|
|
47
|
+
done: "state.done",
|
|
48
|
+
cancelled: "state.cancelled",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const PRIORITY_LABEL_KEYS: Record<string, string> = {
|
|
52
|
+
urgent: "priority.urgent",
|
|
53
|
+
high: "priority.high",
|
|
54
|
+
medium: "priority.medium",
|
|
55
|
+
low: "priority.low",
|
|
56
|
+
none: "priority.none",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function ModuleRow({ item, t }: { item: ProjectModuleItem; t: (key: string, opts?: Record<string, unknown>) => string }) {
|
|
60
|
+
const stateLabel = STATE_LABEL_KEYS[item.state] ?? item.state;
|
|
42
61
|
return (
|
|
43
62
|
<div className="flex items-center gap-4 px-4 py-3 transition-colors hover:bg-accent/40">
|
|
44
63
|
<div className="min-w-0 flex-1">
|
|
@@ -47,9 +66,9 @@ function ModuleRow({ item, t }: { item: ProjectModuleItem; t: (key: string) => s
|
|
|
47
66
|
{t('module.assigneeLabel')}{item.assignee ?? t('module.unassigned')}
|
|
48
67
|
</p>
|
|
49
68
|
</div>
|
|
50
|
-
<Badge variant={
|
|
69
|
+
<Badge variant={STATE_VARIANT[item.state] ?? "outline"}>{t(`story:${stateLabel}`)}</Badge>
|
|
51
70
|
<Badge variant={PRIORITY_VARIANT[item.priority] ?? "outline"} className="hidden sm:inline-flex">
|
|
52
|
-
{item.priority}
|
|
71
|
+
{t(`story:${PRIORITY_LABEL_KEYS[item.priority] ?? item.priority}`)}
|
|
53
72
|
</Badge>
|
|
54
73
|
<span className="hidden text-xs text-muted-foreground md:block">
|
|
55
74
|
{new Intl.DateTimeFormat("zh-CN", { month: "short", day: "numeric" }).format(new Date(item.updatedAt))}
|
|
@@ -60,7 +79,7 @@ function ModuleRow({ item, t }: { item: ProjectModuleItem; t: (key: string) => s
|
|
|
60
79
|
}
|
|
61
80
|
|
|
62
81
|
function ProjectModulePage() {
|
|
63
|
-
const { t } = useTranslation(
|
|
82
|
+
const { t } = useTranslation(["projects", "story"]);
|
|
64
83
|
const { project, items, moduleKey } = Route.useLoaderData();
|
|
65
84
|
const meta = PROJECT_MODULE_META[moduleKey];
|
|
66
85
|
const Icon = meta.icon;
|
|
@@ -86,25 +105,33 @@ function ProjectModulePage() {
|
|
|
86
105
|
<p className="text-sm text-muted-foreground">{meta.description}</p>
|
|
87
106
|
</div>
|
|
88
107
|
<div className="ml-auto flex items-center gap-2">
|
|
89
|
-
<
|
|
108
|
+
<LucideIconByName name={project.icon} size={20} />
|
|
90
109
|
<span className="text-sm text-muted-foreground">{project.name}</span>
|
|
91
110
|
</div>
|
|
92
111
|
</div>
|
|
93
112
|
|
|
94
113
|
<div className="mb-3 flex items-center justify-between">
|
|
95
114
|
<p className="text-sm text-muted-foreground">
|
|
96
|
-
{t('module.totalItems', { count: items.length })}
|
|
115
|
+
{t('projects:module.totalItems', { count: items.length })}
|
|
97
116
|
</p>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
117
|
+
{moduleKey === "requirement" && (
|
|
118
|
+
<Badge variant="outline" className="text-muted-foreground">
|
|
119
|
+
{t('projects:module.liveBadge')}
|
|
120
|
+
</Badge>
|
|
121
|
+
)}
|
|
101
122
|
</div>
|
|
102
123
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
124
|
+
{items.length === 0 ? (
|
|
125
|
+
<div className="rounded-lg border bg-card px-4 py-10 text-center text-sm text-muted-foreground">
|
|
126
|
+
{t('projects:module.empty')}
|
|
127
|
+
</div>
|
|
128
|
+
) : (
|
|
129
|
+
<div className="divide-y rounded-lg border bg-card">
|
|
130
|
+
{items.map((item) => (
|
|
131
|
+
<ModuleRow key={item.id} item={item} t={t} />
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
108
135
|
</div>
|
|
109
136
|
</div>
|
|
110
137
|
);
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
|
|
18
18
|
import { createFileRoute, notFound } from "@tanstack/react-router";
|
|
19
19
|
import { getProjectDetailFn } from "@/server/serverFns/project";
|
|
20
|
+
import { listModuleStoriesFn } from "@/server/serverFns/project/module-overview";
|
|
20
21
|
import {
|
|
21
|
-
listModuleItems,
|
|
22
22
|
PROJECT_MODULE_KEYS,
|
|
23
|
-
} from "@/lib/project-
|
|
23
|
+
} from "@/lib/project-module-meta";
|
|
24
24
|
import type { ProjectModuleKey } from "@/types/project";
|
|
25
25
|
|
|
26
26
|
export const Route = createFileRoute("/_nav/projects/$projectId/$storyId")({
|
|
@@ -32,7 +32,10 @@ export const Route = createFileRoute("/_nav/projects/$projectId/$storyId")({
|
|
|
32
32
|
const project = await getProjectDetailFn({
|
|
33
33
|
data: { projectId: params.projectId },
|
|
34
34
|
});
|
|
35
|
-
const items =
|
|
35
|
+
const items =
|
|
36
|
+
moduleKey === "requirement"
|
|
37
|
+
? (await listModuleStoriesFn({ data: { projectId: params.projectId } })).items
|
|
38
|
+
: [];
|
|
36
39
|
return { project, items, moduleKey };
|
|
37
40
|
},
|
|
38
41
|
});
|
|
@@ -17,17 +17,17 @@
|
|
|
17
17
|
|
|
18
18
|
import { createLazyFileRoute, Link, useLoaderData } from "@tanstack/react-router";
|
|
19
19
|
import { useTranslation } from "react-i18next";
|
|
20
|
-
import { Settings, LayoutGrid, Activity as ActivityIcon } from "lucide-react";
|
|
20
|
+
import { Settings, LayoutGrid, Activity as ActivityIcon, CalendarRange } from "lucide-react";
|
|
21
21
|
import { useTheme } from "next-themes";
|
|
22
22
|
import { UserAvatar } from "@allbluecn/ui";
|
|
23
23
|
import type { AvatarConfig, ShapeType } from "@allbluecn/shared";
|
|
24
24
|
import { Button } from "@/components/ui/button";
|
|
25
25
|
import { AvatarGroup } from "@/components/magicui/components/animate/avatar-group";
|
|
26
26
|
import { GlyphMatrix } from "@/components/magicui/glyph-matrix";
|
|
27
|
-
import {
|
|
27
|
+
import { LucideIconByName } from "@/components/shared/lucide-icon-picker/lucide-icon-by-name";
|
|
28
28
|
import { ProjectModuleCard } from "@/components/project/project-module-card";
|
|
29
29
|
import { ProjectActivityList } from "@/components/project/project-activity-list";
|
|
30
|
-
import {
|
|
30
|
+
import { PROJECT_MODULE_META, PROJECT_MODULE_KEYS } from "@/lib/project-module-meta";
|
|
31
31
|
import { useMemo } from "react";
|
|
32
32
|
import { formatInTimeZone } from "date-fns-tz";
|
|
33
33
|
import { subDays } from "date-fns";
|
|
@@ -63,14 +63,20 @@ function buildContributionData(
|
|
|
63
63
|
|
|
64
64
|
function ProjectDetailPage() {
|
|
65
65
|
const { t } = useTranslation('projects');
|
|
66
|
-
const { project, members, activities } = useLoaderData({
|
|
66
|
+
const { project, members, activities, moduleOverview } = useLoaderData({
|
|
67
67
|
from: "/_nav/projects/$projectId",
|
|
68
68
|
});
|
|
69
69
|
const { resolvedTheme } = useTheme();
|
|
70
70
|
const glyphColor = resolvedTheme === "dark" ? "#6b7280" : "#9ca3af";
|
|
71
71
|
const timezone = useTimezone();
|
|
72
72
|
|
|
73
|
-
const modules =
|
|
73
|
+
const modules = PROJECT_MODULE_KEYS.map((key) => ({
|
|
74
|
+
...PROJECT_MODULE_META[key],
|
|
75
|
+
count: moduleOverview[key]?.count ?? 0,
|
|
76
|
+
lastUpdatedAt: moduleOverview[key]?.lastUpdatedAt
|
|
77
|
+
? new Date(moduleOverview[key]!.lastUpdatedAt!)
|
|
78
|
+
: null,
|
|
79
|
+
}));
|
|
74
80
|
const contributionData = useMemo(
|
|
75
81
|
() => buildContributionData(activities, timezone),
|
|
76
82
|
[activities, timezone],
|
|
@@ -85,16 +91,27 @@ function ProjectDetailPage() {
|
|
|
85
91
|
</div>
|
|
86
92
|
<div className="relative z-10 mx-auto max-w-6xl px-6 py-6">
|
|
87
93
|
<div className="relative mb-10 mt-2 flex flex-col items-center py-10 text-center">
|
|
88
|
-
<
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
<div className="absolute right-0 top-0 flex gap-2">
|
|
95
|
+
<Button variant="outline" size="sm" asChild>
|
|
96
|
+
<Link
|
|
97
|
+
to="/projects/$projectId/iterations"
|
|
98
|
+
params={{ projectId: project.id }}
|
|
99
|
+
>
|
|
100
|
+
<CalendarRange className="size-4" />
|
|
101
|
+
{t('detail.iterations')}
|
|
102
|
+
</Link>
|
|
103
|
+
</Button>
|
|
104
|
+
<Button variant="outline" size="sm" asChild>
|
|
105
|
+
<Link
|
|
106
|
+
to="/projects/$projectId/settings"
|
|
107
|
+
params={{ projectId: project.id }}
|
|
108
|
+
>
|
|
109
|
+
<Settings className="size-4" />
|
|
110
|
+
{t('detail.settings')}
|
|
111
|
+
</Link>
|
|
112
|
+
</Button>
|
|
113
|
+
</div>
|
|
114
|
+
<LucideIconByName name={project.icon} size={56} className="mb-4 text-primary" />
|
|
98
115
|
<h1 className="text-3xl font-semibold">{project.name}</h1>
|
|
99
116
|
<p className="mt-2 max-w-2xl text-sm text-muted-foreground">
|
|
100
117
|
{project.description || t('detail.noDescription')}
|
|
@@ -0,0 +1,289 @@
|
|
|
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 { createLazyFileRoute, Link, useLoaderData, useRouter } from "@tanstack/react-router";
|
|
20
|
+
import { useTranslation } from "react-i18next";
|
|
21
|
+
import { CalendarRange, MoreVertical, Pencil, Trash2, Archive, ArchiveX, Plus } from "lucide-react";
|
|
22
|
+
import { Button } from "@/components/ui/button";
|
|
23
|
+
import { IterationStatusBadge } from "@/components/iteration/iteration-status-badge";
|
|
24
|
+
import { IterationProgressBar } from "@/components/iteration/iteration-progress-bar";
|
|
25
|
+
import { BurndownChart } from "@/components/iteration/burndown-chart";
|
|
26
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
|
27
|
+
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
|
|
28
|
+
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
29
|
+
import { StorySelectDialog } from "@/components/story/inline-editors/story-select-dialog";
|
|
30
|
+
import { listIterationStoriesFn, archiveIterationsFn, deleteIterationsFn, transferUnfinishedFn, getIterationBurndownFn, listIterationsFn, addStoriesToIterationFn } from "@/server/serverFns/iteration";
|
|
31
|
+
import { listProjectsBriefFn } from "@/server/serverFns/project";
|
|
32
|
+
import { toast } from "@/components/ui/sonner";
|
|
33
|
+
import { useQuery } from "@tanstack/react-query";
|
|
34
|
+
import type { IterationSummary } from "@/server/serverFns/iteration/schemas";
|
|
35
|
+
import type { StoryState } from "@/components/story/constants";
|
|
36
|
+
|
|
37
|
+
interface IterationStoryRow {
|
|
38
|
+
id: string;
|
|
39
|
+
identifier: string;
|
|
40
|
+
name: string;
|
|
41
|
+
state: StoryState;
|
|
42
|
+
priority: string;
|
|
43
|
+
assignee?: { id: string; username: string; email: string; avatarConfig?: any; bgShape?: string; bgColor?: string } | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const Route = createLazyFileRoute("/_nav/projects/$projectId/iterations/$iterationId")({
|
|
47
|
+
component: IterationDetailPage,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function IterationDetailPage() {
|
|
51
|
+
const { t } = useTranslation("iterations");
|
|
52
|
+
const { iteration, stories } = useLoaderData({ from: "/_nav/projects/$projectId/iterations/$iterationId" }) as {
|
|
53
|
+
iteration: IterationSummary;
|
|
54
|
+
stories: { items: IterationStoryRow[] };
|
|
55
|
+
};
|
|
56
|
+
const projectId = Route.useParams().projectId;
|
|
57
|
+
const iterationId = Route.useParams().iterationId;
|
|
58
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
59
|
+
const remainingDays = Math.round((Date.parse(iteration.endDate) - Date.parse(today)) / 86400000);
|
|
60
|
+
const isCompleted = iteration.status === "completed";
|
|
61
|
+
const isOverdue = remainingDays < 0;
|
|
62
|
+
const [transferOpen, setTransferOpen] = useState(false);
|
|
63
|
+
const [addStoryOpen, setAddStoryOpen] = useState(false);
|
|
64
|
+
const router = useRouter();
|
|
65
|
+
|
|
66
|
+
const { data: burndown, isLoading: burndownLoading } = useQuery({
|
|
67
|
+
queryKey: ["iteration-burndown", iterationId],
|
|
68
|
+
queryFn: () => getIterationBurndownFn({ data: { iterationId } }),
|
|
69
|
+
enabled: iteration.storyCount > 0,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
async function handleRemoveStory(storyId: string) {
|
|
73
|
+
await import("@/server/serverFns/iteration/iteration-stories").then((m) => m.removeStoryFromIterationFn({ data: { storyId } }));
|
|
74
|
+
toast.success(t("actions.removeStory"));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="h-full overflow-y-auto">
|
|
79
|
+
<div className="mx-auto max-w-4xl px-6 py-6">
|
|
80
|
+
{/* Header */}
|
|
81
|
+
<div className="mb-6 flex items-start justify-between">
|
|
82
|
+
<div>
|
|
83
|
+
<Link to="/projects/$projectId/iterations" params={{ projectId }} className="text-sm text-muted-foreground hover:text-foreground">
|
|
84
|
+
← {t("title")}
|
|
85
|
+
</Link>
|
|
86
|
+
<div className="mt-2 flex items-center gap-3">
|
|
87
|
+
<h1 className="text-2xl font-semibold">{iteration.name}</h1>
|
|
88
|
+
<IterationStatusBadge status={iteration.status} />
|
|
89
|
+
</div>
|
|
90
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
91
|
+
{iteration.startDate} ~ {iteration.endDate}
|
|
92
|
+
{iteration.status === "active" && !isOverdue && (
|
|
93
|
+
<span className="ml-2">({t("progress.remainingDays", { days: remainingDays })})</span>
|
|
94
|
+
)}
|
|
95
|
+
{isOverdue && (
|
|
96
|
+
<span className="ml-2 text-red-500">{t("progress.overdueDays", { days: Math.abs(remainingDays) })}</span>
|
|
97
|
+
)}
|
|
98
|
+
</p>
|
|
99
|
+
</div>
|
|
100
|
+
<div className="flex items-center gap-2">
|
|
101
|
+
<Button variant="outline" size="sm" disabled={isCompleted}>
|
|
102
|
+
<Pencil className="mr-1 size-4" />
|
|
103
|
+
{t("actions.edit")}
|
|
104
|
+
</Button>
|
|
105
|
+
<DropdownMenu>
|
|
106
|
+
<DropdownMenuTrigger asChild>
|
|
107
|
+
<Button variant="outline" size="sm">
|
|
108
|
+
<MoreVertical className="size-4" />
|
|
109
|
+
</Button>
|
|
110
|
+
</DropdownMenuTrigger>
|
|
111
|
+
<DropdownMenuContent align="end">
|
|
112
|
+
<DropdownMenuItem onClick={() => void (async () => {
|
|
113
|
+
await archiveIterationsFn({ data: { id: iterationId, archived: !iteration.archived } });
|
|
114
|
+
toast.success(iteration.archived ? t("actions.unarchive") : t("actions.archive"));
|
|
115
|
+
})()}>
|
|
116
|
+
{iteration.archived ? <ArchiveX className="mr-2 size-4" /> : <Archive className="mr-2 size-4" />}
|
|
117
|
+
{iteration.archived ? t("actions.unarchive") : t("actions.archive")}
|
|
118
|
+
</DropdownMenuItem>
|
|
119
|
+
<DropdownMenuItem className="text-destructive" onClick={() => document.getElementById(`delete-iter-${iterationId}`)?.click()}>
|
|
120
|
+
<Trash2 className="mr-2 size-4" />
|
|
121
|
+
{t("actions.delete")}
|
|
122
|
+
</DropdownMenuItem>
|
|
123
|
+
</DropdownMenuContent>
|
|
124
|
+
</DropdownMenu>
|
|
125
|
+
<AlertDialog>
|
|
126
|
+
<AlertDialogTrigger asChild>
|
|
127
|
+
<Button variant="destructive" size="sm" id={`delete-iter-${iterationId}`} className="hidden">Delete</Button>
|
|
128
|
+
</AlertDialogTrigger>
|
|
129
|
+
<AlertDialogContent>
|
|
130
|
+
<AlertDialogHeader>
|
|
131
|
+
<AlertDialogTitle>{t("actions.delete")}</AlertDialogTitle>
|
|
132
|
+
<AlertDialogDescription>{t("errors.deleteConfirm")}</AlertDialogDescription>
|
|
133
|
+
</AlertDialogHeader>
|
|
134
|
+
<AlertDialogFooter>
|
|
135
|
+
<AlertDialogCancel>{t("actions.cancel")}</AlertDialogCancel>
|
|
136
|
+
<AlertDialogAction
|
|
137
|
+
onClick={async () => {
|
|
138
|
+
await deleteIterationsFn({ data: { id: iterationId } });
|
|
139
|
+
toast.success(t("actions.delete"));
|
|
140
|
+
void router.navigate({ to: "/projects/$projectId/iterations", params: { projectId } });
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
{t("actions.delete")}
|
|
144
|
+
</AlertDialogAction>
|
|
145
|
+
</AlertDialogFooter>
|
|
146
|
+
</AlertDialogContent>
|
|
147
|
+
</AlertDialog>
|
|
148
|
+
{isCompleted && (
|
|
149
|
+
<Button variant="outline" size="sm" onClick={() => setTransferOpen(true)}>
|
|
150
|
+
{t("actions.transfer")}
|
|
151
|
+
</Button>
|
|
152
|
+
)}
|
|
153
|
+
{!isCompleted && (
|
|
154
|
+
<Button variant="outline" size="sm" onClick={() => setAddStoryOpen(true)}>
|
|
155
|
+
<Plus className="mr-1 size-4" />
|
|
156
|
+
{t("actions.addStories")}
|
|
157
|
+
</Button>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
{/* Stats + Burndown */}
|
|
163
|
+
{iteration.storyCount > 0 && (
|
|
164
|
+
<div className="mb-6 space-y-4">
|
|
165
|
+
<div className="rounded-lg border bg-card p-4">
|
|
166
|
+
<IterationProgressBar stateCounts={iteration.stateCounts} />
|
|
167
|
+
</div>
|
|
168
|
+
<div className="rounded-lg border bg-card p-4">
|
|
169
|
+
{burndownLoading ? (
|
|
170
|
+
<div className="flex h-48 items-center justify-center text-sm text-muted-foreground">加载中...</div>
|
|
171
|
+
) : burndown ? (
|
|
172
|
+
<BurndownChart data={burndown} />
|
|
173
|
+
) : null}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
{/* Stories list */}
|
|
179
|
+
<div className="space-y-2">
|
|
180
|
+
{stories.items.length === 0 ? (
|
|
181
|
+
<p className="py-8 text-center text-sm text-muted-foreground">{t("empty.description")}</p>
|
|
182
|
+
) : (
|
|
183
|
+
stories.items.map((story) => (
|
|
184
|
+
<div key={story.id} className="flex items-center gap-3 rounded-lg border bg-card px-4 py-3">
|
|
185
|
+
<span className="font-mono text-xs text-muted-foreground">{story.identifier}</span>
|
|
186
|
+
<Link to="/stories/$storyId" params={{ storyId: story.id }} className="flex-1 text-sm font-medium hover:underline">
|
|
187
|
+
{story.name}
|
|
188
|
+
</Link>
|
|
189
|
+
<span className="text-xs text-muted-foreground">{story.state}</span>
|
|
190
|
+
<Button variant="ghost" size="sm" className="ml-auto text-muted-foreground" onClick={() => void handleRemoveStory(story.id)}>
|
|
191
|
+
{t("actions.removeStory")}
|
|
192
|
+
</Button>
|
|
193
|
+
</div>
|
|
194
|
+
))
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
{/* Transfer dialog */}
|
|
200
|
+
<TransferDialog open={transferOpen} onOpenChange={setTransferOpen} iterationId={iterationId} projectId={projectId} onSaved={() => void router.invalidate()} />
|
|
201
|
+
<AddStoriesDialog open={addStoryOpen} onOpenChange={setAddStoryOpen} iterationId={iterationId} projectId={projectId} onAdded={() => void router.invalidate()} />
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function TransferDialog({ open, onOpenChange, iterationId, projectId, onSaved }: { open: boolean; onOpenChange: (v: boolean) => void; iterationId: string; projectId: string; onSaved?: () => void }) {
|
|
207
|
+
const { t } = useTranslation("iterations");
|
|
208
|
+
const [targetId, setTargetId] = useState("");
|
|
209
|
+
const [loading, setLoading] = useState(false);
|
|
210
|
+
const { data } = useQuery({
|
|
211
|
+
queryKey: ["iterations-list", projectId],
|
|
212
|
+
queryFn: () => listIterationsFn({ data: { projectId, includeArchived: false } }),
|
|
213
|
+
enabled: open,
|
|
214
|
+
});
|
|
215
|
+
const targets = (data?.items ?? []).filter((i: IterationSummary) => i.status !== "completed");
|
|
216
|
+
|
|
217
|
+
async function handleTransfer() {
|
|
218
|
+
if (!targetId) return;
|
|
219
|
+
setLoading(true);
|
|
220
|
+
try {
|
|
221
|
+
const r = await transferUnfinishedFn({ data: { iterationId, targetIterationId: targetId } });
|
|
222
|
+
toast.success(t("transfer.success", { count: r.count }));
|
|
223
|
+
onSaved?.();
|
|
224
|
+
onOpenChange(false);
|
|
225
|
+
} catch (e) {
|
|
226
|
+
toast.error(e instanceof Error ? e.message : "转移失败");
|
|
227
|
+
} finally {
|
|
228
|
+
setLoading(false);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
234
|
+
<DialogContent className="sm:max-w-md">
|
|
235
|
+
<DialogHeader>
|
|
236
|
+
<DialogTitle>{t("transfer.title")}</DialogTitle>
|
|
237
|
+
</DialogHeader>
|
|
238
|
+
<p className="text-sm text-muted-foreground">{t("transfer.description")}</p>
|
|
239
|
+
<div className="space-y-2 py-4">
|
|
240
|
+
<label className="text-sm font-medium">{t("transfer.target")}</label>
|
|
241
|
+
<select
|
|
242
|
+
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors"
|
|
243
|
+
value={targetId}
|
|
244
|
+
onChange={(e) => setTargetId(e.target.value)}
|
|
245
|
+
>
|
|
246
|
+
<option value="">{t("transfer.noTarget") ?? "请选择目标迭代"}</option>
|
|
247
|
+
{targets.map((i: IterationSummary) => (
|
|
248
|
+
<option key={i.id} value={i.id}>{i.name} ({i.startDate} ~ {i.endDate})</option>
|
|
249
|
+
))}
|
|
250
|
+
</select>
|
|
251
|
+
</div>
|
|
252
|
+
<DialogFooter>
|
|
253
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>{t("actions.cancel")}</Button>
|
|
254
|
+
<Button onClick={() => void handleTransfer()} disabled={!targetId || loading}>{loading ? "转移中..." : t("actions.transfer")}</Button>
|
|
255
|
+
</DialogFooter>
|
|
256
|
+
</DialogContent>
|
|
257
|
+
</Dialog>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function AddStoriesDialog({ open, onOpenChange, iterationId, projectId, onAdded }: { open: boolean; onOpenChange: (v: boolean) => void; iterationId: string; projectId: string; onAdded?: () => void }) {
|
|
262
|
+
const { t } = useTranslation("iterations");
|
|
263
|
+
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
|
264
|
+
const { data: projects } = useQuery({
|
|
265
|
+
queryKey: ["projects-brief"],
|
|
266
|
+
queryFn: () => listProjectsBriefFn(),
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
async function handleSelected(ids: string | string[]) {
|
|
270
|
+
const storyIds = Array.isArray(ids) ? ids : [ids];
|
|
271
|
+
if (!storyIds.length) return;
|
|
272
|
+
await addStoriesToIterationFn({ data: { iterationId, storyIds } });
|
|
273
|
+
toast.success(t("actions.addStories"));
|
|
274
|
+
onAdded?.();
|
|
275
|
+
onOpenChange(false);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<StorySelectDialog
|
|
280
|
+
open={open}
|
|
281
|
+
onOpenChange={onOpenChange}
|
|
282
|
+
storyId=""
|
|
283
|
+
projects={projects ?? []}
|
|
284
|
+
mode="iteration"
|
|
285
|
+
title={t("actions.addStories")}
|
|
286
|
+
onSelected={handleSelected}
|
|
287
|
+
/>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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 { createFileRoute, notFound } from "@tanstack/react-router";
|
|
19
|
+
import { getIterationFn, listIterationStoriesFn } from "@/server/serverFns/iteration";
|
|
20
|
+
|
|
21
|
+
export const Route = createFileRoute("/_nav/projects/$projectId/iterations/$iterationId")({
|
|
22
|
+
loader: async ({ params }) => {
|
|
23
|
+
const iteration = await getIterationFn({ data: { id: params.iterationId } });
|
|
24
|
+
const stories = await listIterationStoriesFn({ data: { iterationId: params.iterationId } });
|
|
25
|
+
return { iteration, stories };
|
|
26
|
+
},
|
|
27
|
+
});
|