From 4df83dcdbb584a1962f5c2b684fed247ccaba309 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Sat, 25 Apr 2026 22:53:26 +0200 Subject: [PATCH] feat(ST-108/ST-208): replace inline forms with PBI and story dialogs - PbiDialog: create/edit with priority select and optional description - StoryDialog: create/edit with priority, description, acceptance criteria, activity log, and delete - PrioritySelect: reusable controlled select component - Edit icons always visible on PBI rows and story blocks - Dialog backdrop uses 40% opacity blur Co-Authored-By: Claude Sonnet 4.6 --- app/(app)/products/[id]/page.tsx | 2 +- components/backlog/pbi-dialog.tsx | 149 ++++++++++++++ components/backlog/pbi-list.tsx | 121 ++++------- components/backlog/story-dialog.tsx | 282 ++++++++++++++++++++++++++ components/backlog/story-panel.tsx | 282 +++----------------------- components/shared/priority-select.tsx | 43 ++++ components/ui/dialog.tsx | 2 +- 7 files changed, 538 insertions(+), 343 deletions(-) create mode 100644 components/backlog/pbi-dialog.tsx create mode 100644 components/backlog/story-dialog.tsx create mode 100644 components/shared/priority-select.tsx diff --git a/app/(app)/products/[id]/page.tsx b/app/(app)/products/[id]/page.tsx index cd31617..ecb1507 100644 --- a/app/(app)/products/[id]/page.tsx +++ b/app/(app)/products/[id]/page.tsx @@ -89,7 +89,7 @@ export default async function ProductBacklogPage({ params }: Props) { left={ ({ id: p.id, title: p.title, priority: p.priority }))} + pbis={pbis.map((p: (typeof pbis)[number]) => ({ id: p.id, title: p.title, priority: p.priority, description: p.description }))} isDemo={isDemo} /> } diff --git a/components/backlog/pbi-dialog.tsx b/components/backlog/pbi-dialog.tsx new file mode 100644 index 0000000..166bb12 --- /dev/null +++ b/components/backlog/pbi-dialog.tsx @@ -0,0 +1,149 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' +import { useActionState } from 'react' +import { useFormStatus } from 'react-dom' +import { toast } from 'sonner' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogClose, +} from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Textarea } from '@/components/ui/textarea' +import { PrioritySelect } from '@/components/shared/priority-select' +import { createPbiAction, updatePbiAction } from '@/actions/pbis' + +export interface PbiDialogPbi { + id: string + title: string + priority: number + description?: string | null +} + +type CreateState = { mode: 'create'; productId: string; defaultPriority?: number } +type EditState = { mode: 'edit'; pbi: PbiDialogPbi; productId: string } +export type PbiDialogState = CreateState | EditState + +interface PbiDialogProps { + state: PbiDialogState | null + onClose: () => void +} + +function SubmitButton({ label }: { label: string }) { + const { pending } = useFormStatus() + return ( + + ) +} + +export function PbiDialog({ state, onClose }: PbiDialogProps) { + const isEdit = state?.mode === 'edit' + const pbi = isEdit ? state.pbi : null + + const initialPriority = isEdit ? pbi!.priority : (state?.defaultPriority ?? 2) + const [priority, setPriority] = useState(initialPriority) + + // Sync priority when dialog opens for a different PBI or switches create/edit mode + useEffect(() => { + if (state) { + // eslint-disable-next-line react-hooks/set-state-in-effect + setPriority(isEdit ? (state as EditState).pbi.priority : ((state as CreateState).defaultPriority ?? 2)) + } + }, [state, isEdit]) + + const [createState, createAction] = useActionState( + async (_prev: unknown, fd: FormData) => { + const result = await createPbiAction(_prev, fd) + if (result?.success) { toast.success('PBI aangemaakt'); onClose() } + else if (typeof result?.error === 'string') toast.error(result.error) + return result + }, + undefined + ) + + const [updateState, updateAction] = useActionState( + async (_prev: unknown, fd: FormData) => { + const result = await updatePbiAction(_prev, fd) + if (result?.success) { toast.success('PBI opgeslagen'); onClose() } + else if (typeof result?.error === 'string') toast.error(result.error) + return result + }, + undefined + ) + + const error = isEdit + ? (typeof updateState?.error === 'string' ? updateState.error : null) + : (typeof createState?.error === 'string' ? createState.error : null) + + const titleRef = useRef(null) + useEffect(() => { + if (state) { + setTimeout(() => titleRef.current?.focus(), 50) + } + }, [state]) + + return ( + { if (!open) onClose() }}> + + + {isEdit ? 'PBI bewerken' : 'Nieuw PBI'} + + +
+ {isEdit && } + {!isEdit && } + + +
+ + +
+ +
+ + +
+ +
+ +