From 947d9702313e60b00c53ae8ff2ab7a34ee347433 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Mon, 11 May 2026 16:48:51 +0200 Subject: [PATCH] =?UTF-8?q?feat(PBI-79/ST-1337):=20state=20A=E2=80=B2=20UI?= =?UTF-8?q?=20=E2=80=94=20metadata=20dialog=20+=20sticky=20banner=20+=20Pb?= =?UTF-8?q?iList=20ombouw?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI-laag voor de sprint-definitie-flow (state A′). Nieuw: - NewSprintMetadataDialog (stap 1): sprint_goal + optionele dates; 'Verder' schrijft via useUserSettingsStore.setPendingSprintDraft. - SprintDefinitionBanner (sticky): toont doel + X PBI's / Y stories teller; 'Annuleren' → AlertDialog confirm → clearPendingSprintDraft; 'Sprint aanmaken' nog niet aangesloten (wacht op ST-1339). - NewSprintTrigger: button in page header die de metadata-dialog opent; verbergt zichzelf zolang er al een draft loopt. - SprintDraftBanner: client-wrapper, rendert banner alleen als draft bestaat. Wijzigingen: - lib/user-settings.ts: pendingSprintDraft startAt/endAt → z.string().date(). - PbiList: oude selectionMode + selectedIds + NewSprintDialog vervangen door hasDraft-afgeleide A′-mode met tri-state vinkjes; togglen muteert upsertPbiIntent('all'|'none') en wist storyOverrides per PBI. - StoryPanel: in A′-mode toont elke story een cherrypick-checkbox die upsertStoryOverride('add'/'remove'/'clear') aanroept; cross-sprint-blocked stories krijgen disabled-icoon met sprint-naam tooltip. - app/(app)/products/[id]/page.tsx: StartSprintButton vervangen door NewSprintTrigger; SprintDraftBanner gepositioneerd boven split-pane. Tests: bestaande tests blijven groen (806 cases) — UI-specifieke component tests volgen later. ST-1339 sluit createSprintWithSelectionAction aan. Co-Authored-By: Claude Opus 4.7 (1M context) --- actions/sprint-draft.ts | 4 +- app/(app)/products/[id]/page.tsx | 11 +- .../backlog/new-sprint-metadata-dialog.tsx | 203 ++++++++++++++++++ components/backlog/new-sprint-trigger.tsx | 46 ++++ components/backlog/pbi-list.tsx | 174 ++++++++------- .../backlog/sprint-definition-banner.tsx | 176 +++++++++++++++ components/backlog/sprint-draft-banner.tsx | 22 ++ components/backlog/story-panel.tsx | 168 +++++++++++++-- lib/user-settings.ts | 4 +- 9 files changed, 708 insertions(+), 100 deletions(-) create mode 100644 components/backlog/new-sprint-metadata-dialog.tsx create mode 100644 components/backlog/new-sprint-trigger.tsx create mode 100644 components/backlog/sprint-definition-banner.tsx create mode 100644 components/backlog/sprint-draft-banner.tsx diff --git a/actions/sprint-draft.ts b/actions/sprint-draft.ts index ddbe395..37beb54 100644 --- a/actions/sprint-draft.ts +++ b/actions/sprint-draft.ts @@ -26,8 +26,8 @@ const StoryOverridesSchema = z.object({ const DraftSchema = z.object({ goal: z.string().min(1), - startAt: z.string().datetime().optional(), - endAt: z.string().datetime().optional(), + startAt: z.string().date().optional(), + endAt: z.string().date().optional(), pbiIntent: z.record(z.string(), z.enum(['all', 'none'])).default({}), storyOverrides: z.record(z.string(), StoryOverridesSchema).default({}), }).strict() diff --git a/app/(app)/products/[id]/page.tsx b/app/(app)/products/[id]/page.tsx index 386fe56..8ea05fe 100644 --- a/app/(app)/products/[id]/page.tsx +++ b/app/(app)/products/[id]/page.tsx @@ -15,7 +15,8 @@ import { UrlTaskSync } from '@/components/backlog/url-task-sync' import { TaskDialog } 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 { StartSprintButton } from '@/components/sprint/start-sprint-button' +import { NewSprintTrigger } from '@/components/backlog/new-sprint-trigger' +import { SprintDraftBanner } from '@/components/backlog/sprint-draft-banner' import { ActivateProductButton } from '@/components/shared/activate-product-button' import { EditProductButton } from '@/components/products/edit-product-button' import { SprintSwitcher } from '@/components/shared/sprint-switcher' @@ -118,13 +119,12 @@ export default async function ProductBacklogPage({ params, searchParams }: Props {!isActiveProduct && ( )} - {hasOpenSprint ? ( + {hasOpenSprint && ( Sprint actief → - ) : ( - !isDemo && )} + {!isDemo && } {!isDemo && product.user_id === session.userId && ( + {/* Sprint definition banner (state A′) */} + + {/* Split pane */}
void +} + +function todayLocalDate(): string { + return new Date().toLocaleDateString('en-CA') +} + +function plusWeeks(weeks: number): string { + const d = new Date() + d.setDate(d.getDate() + weeks * 7) + return d.toLocaleDateString('en-CA') +} + +export function NewSprintMetadataDialog({ + open, + productId, + onOpenChange, +}: NewSprintMetadataDialogProps) { + const [sprintGoal, setSprintGoal] = useState('') + const [startDate, setStartDate] = useState(todayLocalDate()) + const [endDate, setEndDate] = useState(plusWeeks(2)) + const [error, setError] = useState(null) + const [dirty, setDirty] = useState(false) + const [isPending, startTransition] = useTransition() + const formRef = useRef(null) + const setPendingSprintDraft = useUserSettingsStore( + (s) => s.setPendingSprintDraft, + ) + + function reset() { + setSprintGoal('') + setStartDate(todayLocalDate()) + setEndDate(plusWeeks(2)) + setError(null) + setDirty(false) + } + + const closeGuard = useDirtyCloseGuard(dirty, () => { + onOpenChange(false) + reset() + }) + + function handleSubmit(e: React.FormEvent) { + e.preventDefault() + const goal = sprintGoal.trim() + if (!goal) return + setError(null) + startTransition(async () => { + try { + await setPendingSprintDraft(productId, { + goal, + startAt: startDate || undefined, + endAt: endDate || undefined, + pbiIntent: {}, + storyOverrides: {}, + }) + reset() + onOpenChange(false) + } catch (err) { + const message = + err instanceof Error ? err.message : 'Onbekende fout bij opslaan' + setError(message) + toast.error(message) + } + }) + } + + const handleKeyDown = useDialogSubmitShortcut(() => + formRef.current?.requestSubmit(), + ) + + return ( + <> + { + if (!o) closeGuard.attemptClose() + else onOpenChange(o) + }} + > + +
+ + Nieuwe sprint + +

+ Geef het sprint-doel en periode op. Je selecteert daarna PBI's + en stories via vinkjes in de backlog. +

+
+ +
setDirty(true)} + className="flex-1 overflow-y-auto px-6 py-6 space-y-6" + > +
+ +