diff --git a/app/(app)/products/[id]/sprint/page.tsx b/app/(app)/products/[id]/sprint/page.tsx index 6ea9ee1..620f0d3 100644 --- a/app/(app)/products/[id]/sprint/page.tsx +++ b/app/(app)/products/[id]/sprint/page.tsx @@ -3,16 +3,17 @@ import { cookies } from 'next/headers' import { getIronSession } from 'iron-session' import { SessionData, sessionOptions } from '@/lib/session' import { prisma } from '@/lib/prisma' -import { SprintBacklogClient } from '@/components/sprint/sprint-backlog-client' +import { SprintBoardClient } from '@/components/sprint/sprint-board-client' import { SprintHeader } from '@/components/sprint/sprint-header' import type { SprintStory, PbiWithStories } from '@/components/sprint/sprint-backlog' +import type { Task } from '@/components/sprint/task-list' import Link from 'next/link' interface Props { params: Promise<{ id: string }> } -export default async function SprintBacklogPage({ params }: Props) { +export default async function SprintBoardPage({ params }: Props) { const { id } = await params const session = await getIronSession(await cookies(), sessionOptions) @@ -26,23 +27,40 @@ export default async function SprintBacklogPage({ params }: Props) { }) if (!sprint) redirect(`/products/${id}`) - // Stories in this sprint + // Sprint stories with full task data const sprintStories = await prisma.story.findMany({ where: { sprint_id: sprint.id }, orderBy: { sort_order: 'asc' }, - include: { tasks: { select: { id: true, status: true } } }, + include: { + tasks: { + orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }], + }, + }, }) - const sprintStoryItems: SprintStory[] = sprintStories.map((s: (typeof sprintStories)[number]) => ({ + const sprintStoryItems: SprintStory[] = sprintStories.map(s => ({ id: s.id, title: s.title, priority: s.priority, status: s.status, taskCount: s.tasks.length, - doneCount: s.tasks.filter((t: (typeof s.tasks)[number]) => t.status === 'DONE').length, + doneCount: s.tasks.filter(t => t.status === 'DONE').length, })) - // All PBIs with their non-sprint stories for the right panel + const tasksByStory: Record = {} + for (const story of sprintStories) { + tasksByStory[story.id] = story.tasks.map(t => ({ + id: t.id, + title: t.title, + description: t.description, + priority: t.priority, + status: t.status, + story_id: t.story_id, + sprint_id: t.sprint_id, + })) + } + + // All PBIs with their stories for the left (product backlog) panel const pbis = await prisma.pbi.findMany({ where: { product_id: id }, orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }], @@ -54,11 +72,11 @@ export default async function SprintBacklogPage({ params }: Props) { }) const pbisWithStories: PbiWithStories[] = pbis - .filter((pbi: (typeof pbis)[number]) => pbi.stories.length > 0) - .map((pbi: (typeof pbis)[number]) => ({ + .filter(pbi => pbi.stories.length > 0) + .map(pbi => ({ id: pbi.id, title: pbi.title, - stories: pbi.stories.map((s: (typeof pbi.stories)[number]) => ({ + stories: pbi.stories.map(s => ({ id: s.id, title: s.title, priority: s.priority, @@ -68,7 +86,7 @@ export default async function SprintBacklogPage({ params }: Props) { })), })) - const sprintStoryIdList = sprintStories.map((s: (typeof sprintStories)[number]) => s.id) + const sprintStoryIdList = sprintStories.map(s => s.id) const isDemo = session.isDemo ?? false return ( @@ -82,20 +100,18 @@ export default async function SprintBacklogPage({ params }: Props) { />
-
-
- - Sprint Planning → - +
← Product Backlog diff --git a/app/(app)/products/[id]/sprint/planning/page.tsx b/app/(app)/products/[id]/sprint/planning/page.tsx index cb82fb4..8256d6f 100644 --- a/app/(app)/products/[id]/sprint/planning/page.tsx +++ b/app/(app)/products/[id]/sprint/planning/page.tsx @@ -1,134 +1,10 @@ -import { notFound, redirect } from 'next/navigation' -import { cookies } from 'next/headers' -import { getIronSession } from 'iron-session' -import { SessionData, sessionOptions } from '@/lib/session' -import { prisma } from '@/lib/prisma' -import { SplitPane } from '@/components/split-pane/split-pane' -import { PlanningLeft } from '@/components/sprint/planning-left' -import type { Task } from '@/components/sprint/task-list' -import { SprintHeader } from '@/components/sprint/sprint-header' -import type { SprintStory } from '@/components/sprint/sprint-backlog' -import Link from 'next/link' +import { redirect } from 'next/navigation' interface Props { params: Promise<{ id: string }> } -export default async function SprintPlanningPage({ params }: Props) { +export default async function SprintPlanningRedirect({ params }: Props) { const { id } = await params - const session = await getIronSession(await cookies(), sessionOptions) - - const product = await prisma.product.findFirst({ - where: { id, user_id: session.userId }, - }) - if (!product) notFound() - - const sprint = await prisma.sprint.findFirst({ - where: { product_id: id, status: 'ACTIVE' }, - }) - if (!sprint) redirect(`/products/${id}`) - - const sprintStories = await prisma.story.findMany({ - where: { sprint_id: sprint.id }, - orderBy: { sort_order: 'asc' }, - include: { - tasks: { - orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }], - }, - }, - }) - - const sprintStoryItems: SprintStory[] = sprintStories.map((s: (typeof sprintStories)[number]) => ({ - id: s.id, - title: s.title, - priority: s.priority, - status: s.status, - taskCount: s.tasks.length, - doneCount: s.tasks.filter((t: (typeof s.tasks)[number]) => t.status === 'DONE').length, - })) - - // Tasks by story - const tasksByStory: Record = {} - for (const story of sprintStories) { - tasksByStory[story.id] = story.tasks.map((t: (typeof story.tasks)[number]) => ({ - id: t.id, - title: t.title, - description: t.description, - priority: t.priority, - status: t.status, - story_id: t.story_id, - sprint_id: t.sprint_id, - })) - } - - const isDemo = session.isDemo ?? false - - return ( -
- - -
- - } - right={ - - } - /> -
- -
- - ← Sprint Backlog - -
-
- ) + redirect(`/products/${id}/sprint`) } - -// Right panel — shows tasks of selected story -function PlanningRight({ - sprintId, - productId, - stories, - tasksByStory, - isDemo, -}: { - sprintId: string - productId: string - stories: SprintStory[] - tasksByStory: Record - isDemo: boolean -}) { - // This is a Server Component wrapper — PlanningLeft manages selection via URL/store - // We render TaskList for the first story if only one, or show instruction - // The actual selection is client-side via PlanningLeft - return ( - - ) -} - -// We need a client component for the right side that reads selection store -import { PlanningRightClient } from '@/components/sprint/planning-right-client' diff --git a/components/split-pane/triple-pane.tsx b/components/split-pane/triple-pane.tsx new file mode 100644 index 0000000..ee4a728 --- /dev/null +++ b/components/split-pane/triple-pane.tsx @@ -0,0 +1,137 @@ +'use client' + +import { useRef, useState, useEffect, useCallback } from 'react' +import { cn } from '@/lib/utils' + +interface TriplePaneProps { + left: React.ReactNode + middle: React.ReactNode + right: React.ReactNode + storageKey: string + defaultLeft?: number // % width for left pane + defaultMiddle?: number // % width for middle pane, right gets the rest + minSize?: number // minimum px per pane +} + +export function TriplePane({ + left, middle, right, storageKey, + defaultLeft = 28, defaultMiddle = 35, minSize = 180, +}: TriplePaneProps) { + const containerRef = useRef(null) + + const load = (key: string, def: number) => { + if (typeof window === 'undefined') return def + const stored = localStorage.getItem(`triple-pane:${storageKey}:${key}`) + if (stored) { + const val = parseFloat(stored) + if (!isNaN(val) && val > 0 && val < 100) return val + } + return def + } + + const [leftPct, setLeftPct] = useState(() => load('left', defaultLeft)) + const [midPct, setMidPct] = useState(() => load('mid', defaultMiddle)) + const [dragging, setDragging] = useState<'left' | 'right' | null>(null) + const [isMobile, setIsMobile] = useState(false) + const [activeTab, setActiveTab] = useState<'left' | 'middle' | 'right'>('left') + + useEffect(() => { + const check = () => setIsMobile(window.innerWidth < 1024) + check() + window.addEventListener('resize', check) + return () => window.removeEventListener('resize', check) + }, []) + + const onMouseMove = useCallback((e: MouseEvent) => { + if (!dragging || !containerRef.current) return + const rect = containerRef.current.getBoundingClientRect() + const width = rect.width + const minPct = (minSize / width) * 100 + const offsetPct = ((e.clientX - rect.left) / width) * 100 + + if (dragging === 'left') { + const clamped = Math.min(Math.max(offsetPct, minPct), 100 - midPct - minPct) + setLeftPct(clamped) + localStorage.setItem(`triple-pane:${storageKey}:left`, String(clamped)) + } else { + const clamped = Math.min(Math.max(offsetPct - leftPct, minPct), 100 - leftPct - minPct) + setMidPct(clamped) + localStorage.setItem(`triple-pane:${storageKey}:mid`, String(clamped)) + } + }, [dragging, leftPct, midPct, minSize, storageKey]) + + const onMouseUp = useCallback(() => setDragging(null), []) + + useEffect(() => { + if (dragging) { + window.addEventListener('mousemove', onMouseMove) + window.addEventListener('mouseup', onMouseUp) + } + return () => { + window.removeEventListener('mousemove', onMouseMove) + window.removeEventListener('mouseup', onMouseUp) + } + }, [dragging, onMouseMove, onMouseUp]) + + if (isMobile) { + const tabs = ['left', 'middle', 'right'] as const + const labels = ['Backlog', 'Sprint', 'Taken'] + return ( +
+
+ {tabs.map((tab, i) => ( + + ))} +
+
+ {activeTab === 'left' ? left : activeTab === 'middle' ? middle : right} +
+
+ ) + } + + const rightPct = 100 - leftPct - midPct + + return ( +
+
+ {left} +
+ +
setDragging('left')} + className={cn( + 'w-1 shrink-0 bg-border hover:bg-primary transition-colors cursor-col-resize', + dragging === 'left' && 'bg-primary' + )} + /> + +
+ {middle} +
+ +
setDragging('right')} + className={cn( + 'w-1 shrink-0 bg-border hover:bg-primary transition-colors cursor-col-resize', + dragging === 'right' && 'bg-primary' + )} + /> + +
+ {right} +
+
+ ) +} diff --git a/components/sprint/planning-left.tsx b/components/sprint/planning-left.tsx deleted file mode 100644 index 73b3917..0000000 --- a/components/sprint/planning-left.tsx +++ /dev/null @@ -1,57 +0,0 @@ -'use client' - -import { useSelectionStore } from '@/stores/selection-store' -import { PanelNavBar } from '@/components/shared/panel-nav-bar' -import { Badge } from '@/components/ui/badge' -import { cn } from '@/lib/utils' -import type { SprintStory } from './sprint-backlog' - -const PRIORITY_COLORS: Record = { - 1: 'bg-priority-critical/15 text-priority-critical border-priority-critical/30', - 2: 'bg-priority-high/15 text-priority-high border-priority-high/30', - 3: 'bg-priority-medium/15 text-priority-medium border-priority-medium/30', - 4: 'bg-priority-low/15 text-priority-low border-priority-low/30', -} -const PRIORITY_LABELS: Record = { 1: 'Kritiek', 2: 'Hoog', 3: 'Gemiddeld', 4: 'Laag' } - -interface PlanningLeftProps { - stories: SprintStory[] -} - -export function PlanningLeft({ stories }: PlanningLeftProps) { - const { selectedStoryId, selectStory } = useSelectionStore() - - return ( -
- -
- {stories.length === 0 ? ( -

- Geen stories in de Sprint. -

- ) : ( - stories.map(story => ( -
selectStory(story.id)} - className={cn( - 'flex items-center gap-3 px-4 py-2.5 border-b border-border cursor-pointer transition-colors hover:bg-surface-container', - selectedStoryId === story.id && 'bg-primary-container text-primary-container-foreground' - )} - > -
-

{story.title}

-
- - {PRIORITY_LABELS[story.priority]} - - {story.doneCount}/{story.taskCount} klaar -
-
-
- )) - )} -
-
- ) -} diff --git a/components/sprint/planning-right-client.tsx b/components/sprint/planning-right-client.tsx deleted file mode 100644 index 2c5ad2a..0000000 --- a/components/sprint/planning-right-client.tsx +++ /dev/null @@ -1,39 +0,0 @@ -'use client' - -import { useSelectionStore } from '@/stores/selection-store' -import { TaskList } from './task-list' -import type { Task } from './task-list' -import type { SprintStory } from './sprint-backlog' - -interface PlanningRightClientProps { - sprintId: string - productId: string - stories: SprintStory[] - tasksByStory: Record - isDemo: boolean -} - -export function PlanningRightClient({ sprintId, productId, stories, tasksByStory, isDemo }: PlanningRightClientProps) { - const { selectedStoryId } = useSelectionStore() - - const story = stories.find(s => s.id === selectedStoryId) - const tasks = selectedStoryId ? (tasksByStory[selectedStoryId] ?? []) : [] - - if (!selectedStoryId || !story) { - return ( -
-

Selecteer een story om de taken te bekijken.

-
- ) - } - - return ( - - ) -} diff --git a/components/sprint/sprint-backlog.tsx b/components/sprint/sprint-backlog.tsx index 65d92c6..143e90d 100644 --- a/components/sprint/sprint-backlog.tsx +++ b/components/sprint/sprint-backlog.tsx @@ -1,6 +1,7 @@ 'use client' import { useState } from 'react' +import { Trash2 } from 'lucide-react' import { useDroppable, useDraggable } from '@dnd-kit/core' import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' @@ -42,8 +43,14 @@ export interface PbiWithStories { // --- Left panel: Sprint Backlog --- function SortableSprintRow({ - story, isDemo, onRemove, -}: { story: SprintStory; isDemo: boolean; onRemove: () => void }) { + story, isDemo, onRemove, onSelect, isSelected, +}: { + story: SprintStory + isDemo: boolean + onRemove: () => void + onSelect: () => void + isSelected: boolean +}) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: story.id }) const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.4 : 1 } @@ -51,7 +58,13 @@ function SortableSprintRow({
{!isDemo && ( e.stopPropagation()} > ⠿ @@ -75,9 +89,10 @@ function SortableSprintRow({ {!isDemo && ( )}
@@ -89,9 +104,11 @@ interface SprintBacklogLeftProps { stories: SprintStory[] isDemo: boolean onRemove: (storyId: string) => void + onSelect: (storyId: string) => void + selectedStoryId: string | null } -export function SprintBacklogLeft({ sprintId, stories, isDemo, onRemove }: SprintBacklogLeftProps) { +export function SprintBacklogLeft({ sprintId, stories, isDemo, onRemove, onSelect, selectedStoryId }: SprintBacklogLeftProps) { const { sprintStoryOrder } = useSprintStore() const { setNodeRef, isOver } = useDroppable({ id: 'sprint-zone' }) @@ -114,7 +131,7 @@ export function SprintBacklogLeft({ sprintId, stories, isDemo, onRemove }: Sprin 'text-sm text-muted-foreground text-center mt-8 px-4', isOver && 'text-primary' )}> - {isOver ? 'Loslaten om toe te voegen aan Sprint' : 'Geen stories in de Sprint. Sleep stories vanuit het rechterpaneel.'} + {isOver ? 'Loslaten om toe te voegen aan Sprint' : 'Geen stories in de Sprint. Sleep stories vanuit het linkerpaneel.'}

) : ( s.id)} strategy={verticalListSortingStrategy}> @@ -124,6 +141,8 @@ export function SprintBacklogLeft({ sprintId, stories, isDemo, onRemove }: Sprin story={story} isDemo={isDemo} onRemove={() => onRemove(story.id)} + onSelect={() => onSelect(story.id)} + isSelected={selectedStoryId === story.id} /> ))} @@ -135,7 +154,15 @@ export function SprintBacklogLeft({ sprintId, stories, isDemo, onRemove }: Sprin // --- Right panel: Product Backlog grouped by PBI --- -function DraggablePbiStoryRow({ story, isDemo }: { story: SprintStory; isDemo: boolean }) { +function DraggablePbiStoryRow({ + story, + isDemo, + onAdd, +}: { + story: SprintStory + isDemo: boolean + onAdd: () => void +}) { const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id: `pb:${story.id}` }) const style = transform ? { transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`, zIndex: 50, position: 'relative' as const } @@ -145,8 +172,11 @@ function DraggablePbiStoryRow({ story, isDemo }: { story: SprintStory; isDemo: b
@@ -156,6 +186,7 @@ function DraggablePbiStoryRow({ story, isDemo }: { story: SprintStory; isDemo: b {...listeners} aria-label="Sleep naar Sprint Backlog" className="text-muted-foreground cursor-grab active:cursor-grabbing shrink-0 select-none text-sm" + onClick={e => e.stopPropagation()} > ⠿ @@ -166,6 +197,9 @@ function DraggablePbiStoryRow({ story, isDemo }: { story: SprintStory; isDemo: b {STATUS_LABELS[story.status]}
+ {!isDemo && ( + + toevoegen + )}
) } @@ -174,9 +208,10 @@ interface SprintBacklogRightProps { pbisWithStories: PbiWithStories[] sprintStoryIds: Set isDemo: boolean + onAdd: (storyId: string) => void } -export function SprintBacklogRight({ pbisWithStories, sprintStoryIds, isDemo }: SprintBacklogRightProps) { +export function SprintBacklogRight({ pbisWithStories, sprintStoryIds, isDemo, onAdd }: SprintBacklogRightProps) { const [collapsed, setCollapsed] = useState>(new Set()) const { setNodeRef, isOver } = useDroppable({ id: 'backlog-zone' }) @@ -228,7 +263,7 @@ export function SprintBacklogRight({ pbisWithStories, sprintStoryIds, isDemo }:
) } - return + return onAdd(story.id)} /> })}
))} diff --git a/components/sprint/sprint-backlog-client.tsx b/components/sprint/sprint-board-client.tsx similarity index 59% rename from components/sprint/sprint-backlog-client.tsx rename to components/sprint/sprint-board-client.tsx index 3a3fbce..3c064d2 100644 --- a/components/sprint/sprint-backlog-client.tsx +++ b/components/sprint/sprint-board-client.tsx @@ -2,14 +2,16 @@ import { useState, useEffect, useTransition } from 'react' import { - DndContext, DragEndEvent, + DndContext, DragEndEvent, DragStartEvent, DragOverlay, KeyboardSensor, PointerSensor, useSensor, useSensors, closestCenter, } from '@dnd-kit/core' import { sortableKeyboardCoordinates, arrayMove } from '@dnd-kit/sortable' import { toast } from 'sonner' -import { SplitPane } from '@/components/split-pane/split-pane' +import { TriplePane } from '@/components/split-pane/triple-pane' import { SprintBacklogLeft, SprintBacklogRight } from './sprint-backlog' import type { SprintStory, PbiWithStories } from './sprint-backlog' +import { TaskList } from './task-list' +import type { Task } from './task-list' import { useSprintStore } from '@/stores/sprint-store' import { addStoryToSprintAction, @@ -17,25 +19,28 @@ import { reorderSprintStoriesAction, } from '@/actions/sprints' -interface SprintBacklogClientProps { +interface SprintBoardClientProps { productId: string sprintId: string stories: SprintStory[] pbisWithStories: PbiWithStories[] sprintStoryIdList: string[] + tasksByStory: Record isDemo: boolean } -export function SprintBacklogClient({ +export function SprintBoardClient({ productId, sprintId, stories, pbisWithStories, sprintStoryIdList, + tasksByStory, isDemo, -}: SprintBacklogClientProps) { +}: SprintBoardClientProps) { const [sprintStories, setSprintStories] = useState(stories) const [sprintStoryIds, setSprintStoryIds] = useState>(() => new Set(sprintStoryIdList)) + const [selectedStoryId, setSelectedStoryId] = useState(null) const { sprintStoryOrder, initSprint, @@ -44,6 +49,7 @@ export function SprintBacklogClient({ reorderSprintStories, rollbackSprint, } = useSprintStore() + const [activeDragStory, setActiveDragStory] = useState(null) const [, startTransition] = useTransition() useEffect(() => { @@ -56,7 +62,16 @@ export function SprintBacklogClient({ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) ) + function handleDragStart(event: DragStartEvent) { + const id = event.active.id.toString() + if (id.startsWith('pb:')) { + const story = pbisWithStories.flatMap(p => p.stories).find(s => s.id === id.slice(3)) + setActiveDragStory(story ?? null) + } + } + function handleDragEnd(event: DragEndEvent) { + setActiveDragStory(null) const { active, over } = event if (!over) return @@ -64,7 +79,7 @@ export function SprintBacklogClient({ const overId = over.id.toString() const order = sprintStoryOrder[sprintId] ?? sprintStories.map(s => s.id) - // Dragged from right panel (pb: prefix) → add to sprint + // Drag from left (product backlog) → add to sprint (middle) if (activeId.startsWith('pb:')) { const storyId = activeId.slice(3) const droppingOnSprint = @@ -89,12 +104,13 @@ export function SprintBacklogClient({ return } - // Dragged from left panel to right panel → remove from sprint + // Drag from middle (sprint backlog) → left (product backlog) → remove if (overId === 'backlog-zone') { const storyData = sprintStories.find(s => s.id === activeId) setSprintStoryIds(prev => { const n = new Set(prev); n.delete(activeId); return n }) setSprintStories(prev => prev.filter(s => s.id !== activeId)) removeStoryFromSprint(sprintId, activeId) + if (selectedStoryId === activeId) setSelectedStoryId(null) startTransition(async () => { const result = await removeStoryFromSprintAction(activeId) if (!result.success) { @@ -109,10 +125,13 @@ export function SprintBacklogClient({ return } - // Reorder within sprint - if (activeId !== overId && order.includes(overId)) { + // Reorder within sprint (middle panel) + if (activeId !== overId && !activeId.startsWith('pb:')) { const prevOrder = [...order] - const newOrder = arrayMove([...order], order.indexOf(activeId), order.indexOf(overId)) + const newOrder = order.includes(overId) + ? arrayMove([...order], order.indexOf(activeId), order.indexOf(overId)) + : [...order.filter(id => id !== activeId), activeId] + reorderSprintStories(sprintId, newOrder) startTransition(async () => { const result = await reorderSprintStoriesAction(sprintId, newOrder) @@ -124,11 +143,30 @@ export function SprintBacklogClient({ } } + function handleAdd(storyId: string) { + if (sprintStoryIds.has(storyId)) return + const storyData = pbisWithStories.flatMap(p => p.stories).find(s => s.id === storyId) + if (!storyData) return + setSprintStoryIds(prev => new Set([...prev, storyId])) + setSprintStories(prev => [...prev, storyData]) + addStoryToSprint(sprintId, storyId) + startTransition(async () => { + const result = await addStoryToSprintAction(sprintId, storyId) + if (!result.success) { + setSprintStoryIds(prev => { const n = new Set(prev); n.delete(storyId); return n }) + setSprintStories(prev => prev.filter(s => s.id !== storyId)) + removeStoryFromSprint(sprintId, storyId) + toast.error(result.error ?? 'Toevoegen mislukt') + } + }) + } + function handleRemove(storyId: string) { const storyData = sprintStories.find(s => s.id === storyId) setSprintStoryIds(prev => { const n = new Set(prev); n.delete(storyId); return n }) setSprintStories(prev => prev.filter(s => s.id !== storyId)) removeStoryFromSprint(sprintId, storyId) + if (selectedStoryId === storyId) setSelectedStoryId(null) startTransition(async () => { const result = await removeStoryFromSprintAction(storyId) if (!result.success) { @@ -142,26 +180,60 @@ export function SprintBacklogClient({ }) } + const selectedTasks = selectedStoryId ? (tasksByStory[selectedStoryId] ?? []) : [] + return ( - - + + } + middle={ } right={ - + selectedStoryId ? ( + + ) : ( +
+

Selecteer een story om de taken te bekijken.

+
+ ) } /> + + {activeDragStory && ( +
+ + {activeDragStory.title} +
+ )} +
) } diff --git a/components/sprint/task-list.tsx b/components/sprint/task-list.tsx index a88a7c2..b7cefe4 100644 --- a/components/sprint/task-list.tsx +++ b/components/sprint/task-list.tsx @@ -119,22 +119,28 @@ function EditSubmitButton() { } function CreateTaskForm({ storyId, sprintId, onDone }: { storyId: string; sprintId: string; onDone: () => void }) { - const [, formAction] = useActionState( + const [state, formAction] = useActionState( async (_prev: unknown, fd: FormData) => { const result = await createTaskAction(_prev, fd) - if (result?.success) onDone() + if (result?.success) { onDone(); return result } + if (result?.error) toast.error(typeof result.error === 'string' ? result.error : 'Aanmaken mislukt') return result }, undefined ) return ( -
+ - - - +
+ + + +
+ {state && 'error' in state && typeof state.error === 'string' && ( +

{state.error}

+ )} ) } @@ -219,6 +225,7 @@ export function TaskList({ storyId, sprintId, productId: _productId, tasks, isDe
) : ( setActiveDragId(e.active.id as string)}