feat(PBI-79): pendingSprintDraft session-only + concept-entry + leave-guard

Scope-aanpassing uit plan-revisie: drafts persisten niet meer server-side.

Wijzigingen:
- stores/user-settings/store.ts:
  - hydrate() strip nu workflow.pendingSprintDraft uit serverstate
    (legacy DB-entries blijven harmless aanwezig maar worden niet
    gehydreerd → effectief unreachable voor de UI).
  - setPendingSprintDraft / clearPendingSprintDraft worden lokale-only;
    geen import van sprint-draft-actions, geen server-roundtrip.
  - upsertPbiIntent / upsertStoryOverride blijven via setPendingSprintDraft
    routeren → ook session-only.
- components/shared/sprint-switcher.tsx: leest draft-goal uit user-settings
  store en toont '⚙ Concept — [goal]' als niet-selecteerbare entry
  bovenaan de dropdown zolang er een draft loopt.
- components/backlog/sprint-draft-leave-guard.tsx (nieuw): registreert
  een beforeunload-listener zolang er een draft is. Browser-refresh,
  tab-close en back-navigatie tonen daarmee de standaard confirm. In-app
  route-changes blijven via de banner-Annuleren-knop lopen.
- app/(app)/products/[id]/page.tsx: SprintDraftLeaveGuard gemount naast
  de banner.
- Tests: user-settings store-tests aangepast (geen server-call assert
  meer, hydrate strip-assert toegevoegd; upsert-tests seed nu via
  setPendingSprintDraft i.p.v. legacy hydrate).

setPendingSprintDraftAction + clearPendingSprintDraftAction blijven bestaan
voor eventuele toekomstige opruim-flows, maar worden niet meer aangeroepen
vanuit de UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-11 18:31:04 +02:00
parent e4252cad3e
commit 2a4ee6aded
5 changed files with 114 additions and 97 deletions

View file

@ -0,0 +1,37 @@
'use client'
import { useEffect } from 'react'
import { useUserSettingsStore } from '@/stores/user-settings/store'
interface SprintDraftLeaveGuardProps {
productId: string
}
/**
* PBI-79: window.beforeunload-waarschuwing zolang er een pendingSprintDraft
* loopt voor dit product. De draft is session-only en gaat verloren bij
* refresh/close deze guard zorgt dat de gebruiker dat eerst bevestigt.
* Voor in-app route-changes (klikken op een andere product) doet Next.js
* geen onbeforeunload; daar vangen we het op via de banner-Annuleren-flow.
*/
export function SprintDraftLeaveGuard({
productId,
}: SprintDraftLeaveGuardProps) {
const hasDraft = useUserSettingsStore(
(s) => !!s.entities.settings.workflow?.pendingSprintDraft?.[productId],
)
useEffect(() => {
if (!hasDraft) return
function handler(e: BeforeUnloadEvent) {
e.preventDefault()
// Moderne browsers tonen een eigen vertaalde tekst; returnValue is
// alleen nodig voor legacy compat.
e.returnValue = ''
}
window.addEventListener('beforeunload', handler)
return () => window.removeEventListener('beforeunload', handler)
}, [hasDraft])
return null
}