From 03c90c1fddc7e5d5ef9cb968695451dccfef43ee Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Wed, 29 Apr 2026 23:54:20 +0200 Subject: [PATCH] feat(ST-1112): add Suspense skeleton for edit-mode task loading Extracts task fetch into EditTaskLoader (async server component) so the sprint board renders immediately while the task loads. TaskDialogSkeleton shows 3 grey bars during the fetch. Invalid or out-of-scope task IDs redirect to closePath. Co-Authored-By: Claude Sonnet 4.6 --- app/(app)/products/[id]/sprint/page.tsx | 44 +++++++---------- app/_components/tasks/edit-task-loader.tsx | 47 +++++++++++++++++++ .../tasks/task-dialog-skeleton.tsx | 41 ++++++++++++++++ 3 files changed, 104 insertions(+), 28 deletions(-) create mode 100644 app/_components/tasks/edit-task-loader.tsx create mode 100644 app/_components/tasks/task-dialog-skeleton.tsx diff --git a/app/(app)/products/[id]/sprint/page.tsx b/app/(app)/products/[id]/sprint/page.tsx index d58603c..3b16d5f 100644 --- a/app/(app)/products/[id]/sprint/page.tsx +++ b/app/(app)/products/[id]/sprint/page.tsx @@ -1,14 +1,15 @@ +import { Suspense } from 'react' import { notFound, redirect } from 'next/navigation' import { getSession } from '@/lib/auth' import { getAccessibleProduct } from '@/lib/product-access' -import { productAccessFilter } from '@/lib/product-access' import { prisma } from '@/lib/prisma' import { SprintBoardClient } from '@/components/sprint/sprint-board-client' import { SprintHeader } from '@/components/sprint/sprint-header' import type { SprintStory, PbiWithStories, ProductMember } from '@/components/sprint/sprint-backlog' import type { Task } from '@/components/sprint/task-list' import { TaskDialog } from '@/app/_components/tasks/task-dialog' -import type { TaskDialogTask } from '@/app/_components/tasks/task-dialog' +import { EditTaskLoader } from '@/app/_components/tasks/edit-task-loader' +import { TaskDialogSkeleton } from '@/app/_components/tasks/task-dialog-skeleton' import Link from 'next/link' interface Props { @@ -116,30 +117,6 @@ export default async function SprintBoardPage({ params, searchParams }: Props) { const isDemo = session.isDemo ?? false const closePath = `/products/${id}/sprint` - // Fetch task for edit mode (auth-scoped) - let editTaskData: TaskDialogTask | null = null - if (editTask) { - const t = await prisma.task.findFirst({ - where: { - id: editTask, - story: { product: productAccessFilter(session.userId) }, - }, - select: { - id: true, - title: true, - description: true, - implementation_plan: true, - priority: true, - status: true, - created_at: true, - }, - }) - if (!t) redirect(closePath) - editTaskData = t - } - - const showDialog = !!newTask || !!editTaskData - return (
- {showDialog && ( + {newTask && ( )} + + {editTask && !newTask && ( + }> + + + )} ) } diff --git a/app/_components/tasks/edit-task-loader.tsx b/app/_components/tasks/edit-task-loader.tsx new file mode 100644 index 0000000..f66cce9 --- /dev/null +++ b/app/_components/tasks/edit-task-loader.tsx @@ -0,0 +1,47 @@ +import { redirect } from 'next/navigation' +import { prisma } from '@/lib/prisma' +import { productAccessFilter } from '@/lib/product-access' +import { TaskDialog } from './task-dialog' + +interface EditTaskLoaderProps { + taskId: string + userId: string + productId: string + closePath: string + isDemo: boolean +} + +export async function EditTaskLoader({ + taskId, + userId, + productId, + closePath, + isDemo, +}: EditTaskLoaderProps) { + const task = await prisma.task.findFirst({ + where: { + id: taskId, + story: { product: productAccessFilter(userId) }, + }, + select: { + id: true, + title: true, + description: true, + implementation_plan: true, + priority: true, + status: true, + created_at: true, + }, + }) + + if (!task) redirect(closePath) + + return ( + + ) +} diff --git a/app/_components/tasks/task-dialog-skeleton.tsx b/app/_components/tasks/task-dialog-skeleton.tsx new file mode 100644 index 0000000..0b4512d --- /dev/null +++ b/app/_components/tasks/task-dialog-skeleton.tsx @@ -0,0 +1,41 @@ +import { Skeleton } from '@/components/ui/skeleton' +import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog' +import { cn } from '@/lib/utils' + +export function TaskDialogSkeleton() { + return ( + + + Taak laden… + + {/* Header */} +
+ +
+ + {/* Body — 3 bars mimicking title + description + plan */} +
+ + + +
+ + {/* Footer */} +
+
+ + +
+
+
+
+ ) +}