feat(backlog): server-fetch tasks + hydrate BacklogStore on page load
Page now fetches tasks parallel to stories and groups by story_id. BacklogHydrationWrapper calls setInitialData on mount so the store is ready for downstream SSE consumers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b36f785566
commit
75bbb1ad73
2 changed files with 92 additions and 34 deletions
|
|
@ -7,6 +7,7 @@ import { SplitPane } from '@/components/split-pane/split-pane'
|
|||
import { PbiList } from '@/components/backlog/pbi-list'
|
||||
import { StoryPanel } from '@/components/backlog/story-panel'
|
||||
import type { Story } from '@/components/backlog/story-panel'
|
||||
import { BacklogHydrationWrapper } from '@/components/backlog/backlog-hydration-wrapper'
|
||||
import { StartSprintButton } from '@/components/sprint/start-sprint-button'
|
||||
import { ActivateProductButton } from '@/components/shared/activate-product-button'
|
||||
import Link from 'next/link'
|
||||
|
|
@ -33,21 +34,37 @@ export default async function ProductBacklogPage({ params }: Props) {
|
|||
orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }],
|
||||
})
|
||||
|
||||
const stories = await prisma.story.findMany({
|
||||
where: { product_id: id },
|
||||
orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }],
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
title: true,
|
||||
description: true,
|
||||
acceptance_criteria: true,
|
||||
priority: true,
|
||||
status: true,
|
||||
pbi_id: true,
|
||||
created_at: true,
|
||||
},
|
||||
})
|
||||
const [stories, tasks] = await Promise.all([
|
||||
prisma.story.findMany({
|
||||
where: { product_id: id },
|
||||
orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }],
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
title: true,
|
||||
description: true,
|
||||
acceptance_criteria: true,
|
||||
priority: true,
|
||||
status: true,
|
||||
pbi_id: true,
|
||||
created_at: true,
|
||||
},
|
||||
}),
|
||||
prisma.task.findMany({
|
||||
where: { story: { pbi: { product_id: id } } },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
description: true,
|
||||
priority: true,
|
||||
status: true,
|
||||
sort_order: true,
|
||||
story_id: true,
|
||||
created_at: true,
|
||||
},
|
||||
orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }],
|
||||
}),
|
||||
])
|
||||
|
||||
// Group stories by PBI id
|
||||
const storiesByPbi: Record<string, Story[]> = {}
|
||||
|
|
@ -56,6 +73,13 @@ export default async function ProductBacklogPage({ params }: Props) {
|
|||
storiesByPbi[story.pbi_id].push(story)
|
||||
}
|
||||
|
||||
// Group tasks by story id
|
||||
const tasksByStory: Record<string, typeof tasks> = {}
|
||||
for (const task of tasks) {
|
||||
if (!tasksByStory[task.story_id]) tasksByStory[task.story_id] = []
|
||||
tasksByStory[task.story_id].push(task)
|
||||
}
|
||||
|
||||
const isDemo = session.isDemo ?? false
|
||||
|
||||
return (
|
||||
|
|
@ -90,25 +114,33 @@ export default async function ProductBacklogPage({ params }: Props) {
|
|||
|
||||
{/* Split pane */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<SplitPane
|
||||
cookieKey={`backlog-${id}`}
|
||||
defaultSplit={[20, 80]}
|
||||
tabLabels={['Backlog', 'Stories']}
|
||||
panes={[
|
||||
<PbiList
|
||||
key="pbi"
|
||||
productId={id}
|
||||
pbis={pbis.map((p: (typeof pbis)[number]) => ({ id: p.id, code: p.code, title: p.title, priority: p.priority, description: p.description, created_at: p.created_at, status: pbiStatusToApi(p.status) }))}
|
||||
isDemo={isDemo}
|
||||
/>,
|
||||
<StoryPanel
|
||||
key="story"
|
||||
productId={id}
|
||||
storiesByPbi={storiesByPbi}
|
||||
isDemo={isDemo}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
<BacklogHydrationWrapper
|
||||
initialData={{
|
||||
pbis: pbis.map((p) => ({ id: p.id, code: p.code, title: p.title, priority: p.priority, description: p.description, created_at: p.created_at, status: pbiStatusToApi(p.status) })),
|
||||
storiesByPbi,
|
||||
tasksByStory,
|
||||
}}
|
||||
>
|
||||
<SplitPane
|
||||
cookieKey={`backlog-${id}`}
|
||||
defaultSplit={[20, 80]}
|
||||
tabLabels={['Backlog', 'Stories']}
|
||||
panes={[
|
||||
<PbiList
|
||||
key="pbi"
|
||||
productId={id}
|
||||
pbis={pbis.map((p: (typeof pbis)[number]) => ({ id: p.id, code: p.code, title: p.title, priority: p.priority, description: p.description, created_at: p.created_at, status: pbiStatusToApi(p.status) }))}
|
||||
isDemo={isDemo}
|
||||
/>,
|
||||
<StoryPanel
|
||||
key="story"
|
||||
productId={id}
|
||||
storiesByPbi={storiesByPbi}
|
||||
isDemo={isDemo}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</BacklogHydrationWrapper>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
26
components/backlog/backlog-hydration-wrapper.tsx
Normal file
26
components/backlog/backlog-hydration-wrapper.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { useBacklogStore, type BacklogPbi, type BacklogStory, type BacklogTask } from '@/stores/backlog-store'
|
||||
|
||||
interface InitialData {
|
||||
pbis: BacklogPbi[]
|
||||
storiesByPbi: Record<string, BacklogStory[]>
|
||||
tasksByStory: Record<string, BacklogTask[]>
|
||||
}
|
||||
|
||||
interface BacklogHydrationWrapperProps {
|
||||
initialData: InitialData
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function BacklogHydrationWrapper({ initialData, children }: BacklogHydrationWrapperProps) {
|
||||
const setInitialData = useBacklogStore((s) => s.setInitialData)
|
||||
|
||||
useEffect(() => {
|
||||
setInitialData(initialData)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue