Load/render workspace alignment (#182)
* docs: plan load render workspace alignment * fix: normalize workspace status hydration * fix: avoid duplicate backlog hydration load * refactor: use sprint store active story * refactor: migrate solo to workspace store * chore: stabilize verification ignores
This commit is contained in:
parent
98ee05d458
commit
3b5cee823c
28 changed files with 1845 additions and 577 deletions
|
|
@ -151,6 +151,7 @@ export default async function ProductBacklogPage({ params, searchParams }: Props
|
|||
<div className="flex-1 overflow-hidden">
|
||||
<BacklogHydrationWrapper
|
||||
productId={id}
|
||||
productName={product.name}
|
||||
initialData={{
|
||||
pbis: pbis.map((p) => ({ id: p.id, code: p.code, title: p.title, priority: p.priority, sort_order: p.sort_order, description: p.description, created_at: p.created_at, status: pbiStatusToApi(p.status) })),
|
||||
storiesByPbi,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
import { notFound, redirect } from 'next/navigation'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getAccessibleProduct } from '@/lib/product-access'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { resolveActiveSprint } from '@/lib/active-sprint'
|
||||
import { getSprintSwitcherData } from '@/lib/sprint-switcher-data'
|
||||
import { getSoloWorkspaceSnapshot } from '@/lib/solo-workspace-server'
|
||||
import { SoloBoard } from '@/components/solo/solo-board'
|
||||
import { SoloHydrationWrapper } from '@/components/solo/solo-hydration-wrapper'
|
||||
import { NoActiveSprint } from '@/components/solo/no-active-sprint'
|
||||
import { SprintSwitcher } from '@/components/shared/sprint-switcher'
|
||||
import type { SoloTask } from '@/components/solo/solo-board'
|
||||
import type { UnassignedStory } from '@/components/solo/unassigned-stories-sheet'
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ id: string }>
|
||||
|
|
@ -22,12 +20,10 @@ export default async function SoloProductPage({ params }: Props) {
|
|||
const product = await getAccessibleProduct(id, session.userId)
|
||||
if (!product) notFound()
|
||||
|
||||
const active = await resolveActiveSprint(id)
|
||||
const sprint = active
|
||||
? await prisma.sprint.findFirst({ where: { id: active.id, product_id: id } })
|
||||
: null
|
||||
|
||||
const switcherData = await getSprintSwitcherData(id, { activeSprintId: sprint?.id ?? null })
|
||||
const initialData = await getSoloWorkspaceSnapshot(id, session.userId)
|
||||
const switcherData = await getSprintSwitcherData(id, {
|
||||
activeSprintId: initialData?.sprint.id ?? null,
|
||||
})
|
||||
|
||||
const switcherBar = (
|
||||
<div className="px-4 py-3 border-b border-border bg-surface-container-low shrink-0 flex items-center justify-center">
|
||||
|
|
@ -40,7 +36,7 @@ export default async function SoloProductPage({ params }: Props) {
|
|||
</div>
|
||||
)
|
||||
|
||||
if (!sprint) {
|
||||
if (!initialData) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{switcherBar}
|
||||
|
|
@ -49,94 +45,19 @@ export default async function SoloProductPage({ params }: Props) {
|
|||
)
|
||||
}
|
||||
|
||||
const [rawTasks, rawUnassigned] = await Promise.all([
|
||||
prisma.task.findMany({
|
||||
where: {
|
||||
story: {
|
||||
sprint_id: sprint.id,
|
||||
assignee_id: session.userId,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
story: {
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
title: true,
|
||||
tasks: { select: { id: true }, orderBy: { sort_order: 'asc' } },
|
||||
pbi: { select: { code: true, title: true, description: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [
|
||||
{ story: { pbi: { priority: 'asc' } } },
|
||||
{ story: { pbi: { sort_order: 'asc' } } },
|
||||
{ story: { sort_order: 'asc' } },
|
||||
{ priority: 'asc' },
|
||||
{ sort_order: 'asc' },
|
||||
],
|
||||
}),
|
||||
prisma.story.findMany({
|
||||
where: { sprint_id: sprint.id, assignee_id: null },
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
title: true,
|
||||
tasks: {
|
||||
select: { id: true, title: true, description: true, priority: true, status: true },
|
||||
orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }],
|
||||
},
|
||||
},
|
||||
orderBy: { sort_order: 'asc' },
|
||||
}),
|
||||
])
|
||||
|
||||
const tasks: SoloTask[] = rawTasks.map(t => ({
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
description: t.description,
|
||||
implementation_plan: t.implementation_plan,
|
||||
priority: t.priority,
|
||||
sort_order: t.sort_order,
|
||||
status: t.status as SoloTask['status'],
|
||||
verify_only: t.verify_only,
|
||||
verify_required: t.verify_required as SoloTask['verify_required'],
|
||||
story_id: t.story.id,
|
||||
story_code: t.story.code,
|
||||
story_title: t.story.title,
|
||||
task_code: t.code,
|
||||
pbi_code: t.story.pbi?.code ?? null,
|
||||
pbi_title: t.story.pbi?.title ?? null,
|
||||
pbi_description: t.story.pbi?.description ?? null,
|
||||
}))
|
||||
|
||||
const unassignedStories: UnassignedStory[] = rawUnassigned.map(s => ({
|
||||
id: s.id,
|
||||
code: s.code,
|
||||
title: s.title,
|
||||
tasks: s.tasks.map(t => ({
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
description: t.description,
|
||||
priority: t.priority,
|
||||
status: t.status,
|
||||
})),
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{switcherBar}
|
||||
<div className="flex-1 min-h-0">
|
||||
<SoloBoard
|
||||
key={sprint.id}
|
||||
productId={id}
|
||||
sprintGoal={sprint.sprint_goal}
|
||||
tasks={tasks}
|
||||
unassignedStories={unassignedStories}
|
||||
isDemo={session.isDemo ?? false}
|
||||
currentUserId={session.userId}
|
||||
repoUrl={product.repo_url}
|
||||
/>
|
||||
<SoloHydrationWrapper initialData={initialData}>
|
||||
<SoloBoard
|
||||
key={initialData.sprint.id}
|
||||
productId={id}
|
||||
sprintGoal={initialData.sprint.sprint_goal}
|
||||
isDemo={session.isDemo ?? false}
|
||||
repoUrl={product.repo_url}
|
||||
/>
|
||||
</SoloHydrationWrapper>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ export default async function MobileProductBacklogPage({ params, searchParams }:
|
|||
<div className="flex flex-col h-full">
|
||||
<BacklogHydrationWrapper
|
||||
productId={id}
|
||||
productName={product.name}
|
||||
initialData={{
|
||||
pbis: pbis.map((p) => ({ id: p.id, code: p.code, title: p.title, priority: p.priority, sort_order: p.sort_order, description: p.description, created_at: p.created_at, status: pbiStatusToApi(p.status) })),
|
||||
storiesByPbi,
|
||||
|
|
|
|||
|
|
@ -6,13 +6,11 @@
|
|||
|
||||
import { notFound } from 'next/navigation'
|
||||
import { getAccessibleProduct } from '@/lib/product-access'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { requireSession } from '@/lib/auth-guard'
|
||||
import { resolveActiveSprint } from '@/lib/active-sprint'
|
||||
import { getSoloWorkspaceSnapshot } from '@/lib/solo-workspace-server'
|
||||
import { SoloBoard } from '@/components/solo/solo-board'
|
||||
import { SoloHydrationWrapper } from '@/components/solo/solo-hydration-wrapper'
|
||||
import { NoActiveSprint } from '@/components/solo/no-active-sprint'
|
||||
import type { SoloTask } from '@/components/solo/solo-board'
|
||||
import type { UnassignedStory } from '@/components/solo/unassigned-stories-sheet'
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ id: string }>
|
||||
|
|
@ -25,12 +23,9 @@ export default async function MobileSoloProductPage({ params }: Props) {
|
|||
const product = await getAccessibleProduct(id, session.userId)
|
||||
if (!product) notFound()
|
||||
|
||||
const active = await resolveActiveSprint(id)
|
||||
const sprint = active
|
||||
? await prisma.sprint.findFirst({ where: { id: active.id, product_id: id } })
|
||||
: null
|
||||
const initialData = await getSoloWorkspaceSnapshot(id, session.userId)
|
||||
|
||||
if (!sprint) {
|
||||
if (!initialData) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<NoActiveSprint productId={id} productName={product.name} />
|
||||
|
|
@ -38,90 +33,15 @@ export default async function MobileSoloProductPage({ params }: Props) {
|
|||
)
|
||||
}
|
||||
|
||||
const [rawTasks, rawUnassigned] = await Promise.all([
|
||||
prisma.task.findMany({
|
||||
where: {
|
||||
story: {
|
||||
sprint_id: sprint.id,
|
||||
assignee_id: session.userId,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
story: {
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
title: true,
|
||||
tasks: { select: { id: true }, orderBy: { sort_order: 'asc' } },
|
||||
pbi: { select: { code: true, title: true, description: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [
|
||||
{ story: { pbi: { priority: 'asc' } } },
|
||||
{ story: { pbi: { sort_order: 'asc' } } },
|
||||
{ story: { sort_order: 'asc' } },
|
||||
{ priority: 'asc' },
|
||||
{ sort_order: 'asc' },
|
||||
],
|
||||
}),
|
||||
prisma.story.findMany({
|
||||
where: { sprint_id: sprint.id, assignee_id: null },
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
title: true,
|
||||
tasks: {
|
||||
select: { id: true, title: true, description: true, priority: true, status: true },
|
||||
orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }],
|
||||
},
|
||||
},
|
||||
orderBy: { sort_order: 'asc' },
|
||||
}),
|
||||
])
|
||||
|
||||
const tasks: SoloTask[] = rawTasks.map(t => ({
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
description: t.description,
|
||||
implementation_plan: t.implementation_plan,
|
||||
priority: t.priority,
|
||||
sort_order: t.sort_order,
|
||||
status: t.status as SoloTask['status'],
|
||||
verify_only: t.verify_only,
|
||||
verify_required: t.verify_required as SoloTask['verify_required'],
|
||||
story_id: t.story.id,
|
||||
story_code: t.story.code,
|
||||
story_title: t.story.title,
|
||||
task_code: t.code,
|
||||
pbi_code: t.story.pbi?.code ?? null,
|
||||
pbi_title: t.story.pbi?.title ?? null,
|
||||
pbi_description: t.story.pbi?.description ?? null,
|
||||
}))
|
||||
|
||||
const unassignedStories: UnassignedStory[] = rawUnassigned.map(s => ({
|
||||
id: s.id,
|
||||
code: s.code,
|
||||
title: s.title,
|
||||
tasks: s.tasks.map(t => ({
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
description: t.description,
|
||||
priority: t.priority,
|
||||
status: t.status,
|
||||
})),
|
||||
}))
|
||||
|
||||
return (
|
||||
<SoloBoard
|
||||
key={sprint.id}
|
||||
productId={id}
|
||||
sprintGoal={sprint.sprint_goal}
|
||||
tasks={tasks}
|
||||
unassignedStories={unassignedStories}
|
||||
isDemo={session.isDemo ?? false}
|
||||
currentUserId={session.userId}
|
||||
repoUrl={product.repo_url}
|
||||
/>
|
||||
<SoloHydrationWrapper initialData={initialData}>
|
||||
<SoloBoard
|
||||
key={initialData.sprint.id}
|
||||
productId={id}
|
||||
sprintGoal={initialData.sprint.sprint_goal}
|
||||
isDemo={session.isDemo ?? false}
|
||||
repoUrl={product.repo_url}
|
||||
/>
|
||||
</SoloHydrationWrapper>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
25
app/api/products/[id]/solo-workspace/route.ts
Normal file
25
app/api/products/[id]/solo-workspace/route.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { authenticateApiRequest } from '@/lib/api-auth'
|
||||
import { getSoloWorkspaceSnapshot } from '@/lib/solo-workspace-server'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const auth = await authenticateApiRequest(request)
|
||||
if ('error' in auth) {
|
||||
return Response.json({ error: auth.error }, { status: auth.status })
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
const url = new URL(request.url)
|
||||
const sprintId = url.searchParams.get('sprint_id')
|
||||
const snapshot = await getSoloWorkspaceSnapshot(id, auth.userId, sprintId)
|
||||
|
||||
if (!snapshot) {
|
||||
return Response.json({ error: 'Solo workspace niet gevonden' }, { status: 404 })
|
||||
}
|
||||
|
||||
return Response.json(snapshot)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue