diff --git a/__tests__/stores/user-settings.test.ts b/__tests__/stores/user-settings.test.ts
index e504ac5..e159bf8 100644
--- a/__tests__/stores/user-settings.test.ts
+++ b/__tests__/stores/user-settings.test.ts
@@ -96,9 +96,8 @@ describe('useUserSettingsStore', () => {
expect(updateAction).not.toHaveBeenCalled()
})
- it('setPendingSprintDraft persists draft optimistically + calls action', async () => {
+ it('setPendingSprintDraft persists draft lokaal (session-only, geen server-call)', async () => {
useUserSettingsStore.getState().hydrate({}, false)
- setDraftAction.mockResolvedValueOnce({ success: true })
const draft: PendingSprintDraft = {
goal: 'Sprint 1',
@@ -113,33 +112,16 @@ describe('useUserSettingsStore', () => {
expect(
s.entities.settings.workflow?.pendingSprintDraft?.['product-1'],
).toMatchObject({ goal: 'Sprint 1' })
- expect(setDraftAction).toHaveBeenCalledWith('product-1', draft)
+ expect(setDraftAction).not.toHaveBeenCalled()
})
- it('setPendingSprintDraft rolls back on server error', async () => {
- useUserSettingsStore.getState().hydrate({}, false)
- setDraftAction.mockResolvedValueOnce({ error: 'boom' })
-
- const draft: PendingSprintDraft = {
- goal: 'Sprint X',
- pbiIntent: {},
- storyOverrides: {},
- }
- await useUserSettingsStore
- .getState()
- .setPendingSprintDraft('product-1', draft)
-
- const s = useUserSettingsStore.getState()
- expect(s.entities.settings.workflow?.pendingSprintDraft).toBeUndefined()
- })
-
- it('clearPendingSprintDraft removes key optimistically', async () => {
+ it('hydrate strips workflow.pendingSprintDraft uit legacy server-state', () => {
useUserSettingsStore.getState().hydrate(
{
workflow: {
pendingSprintDraft: {
'product-1': {
- goal: 'Old',
+ goal: 'Legacy draft',
pbiIntent: {},
storyOverrides: {},
},
@@ -148,7 +130,18 @@ describe('useUserSettingsStore', () => {
},
false,
)
- clearDraftAction.mockResolvedValueOnce({ success: true })
+
+ const s = useUserSettingsStore.getState()
+ expect(s.entities.settings.workflow?.pendingSprintDraft).toBeUndefined()
+ })
+
+ it('clearPendingSprintDraft verwijdert de key lokaal zonder server-call', async () => {
+ useUserSettingsStore.getState().hydrate({}, false)
+ await useUserSettingsStore.getState().setPendingSprintDraft('product-1', {
+ goal: 'Old',
+ pbiIntent: {},
+ storyOverrides: {},
+ })
await useUserSettingsStore
.getState()
@@ -158,28 +151,19 @@ describe('useUserSettingsStore', () => {
expect(
s.entities.settings.workflow?.pendingSprintDraft?.['product-1'],
).toBeUndefined()
- expect(clearDraftAction).toHaveBeenCalledWith('product-1')
+ expect(clearDraftAction).not.toHaveBeenCalled()
})
it('upsertPbiIntent updates intent and wipes storyOverrides for that PBI', async () => {
- useUserSettingsStore.getState().hydrate(
- {
- workflow: {
- pendingSprintDraft: {
- 'product-1': {
- goal: 'g',
- pbiIntent: { pbiA: 'none' },
- storyOverrides: {
- pbiA: { add: ['s-1'], remove: [] },
- pbiB: { add: [], remove: ['s-2'] },
- },
- },
- },
- },
+ useUserSettingsStore.getState().hydrate({}, false)
+ await useUserSettingsStore.getState().setPendingSprintDraft('product-1', {
+ goal: 'g',
+ pbiIntent: { pbiA: 'none' },
+ storyOverrides: {
+ pbiA: { add: ['s-1'], remove: [] },
+ pbiB: { add: [], remove: ['s-2'] },
},
- false,
- )
- setDraftAction.mockResolvedValue({ success: true })
+ })
await useUserSettingsStore
.getState()
@@ -194,23 +178,14 @@ describe('useUserSettingsStore', () => {
})
it('upsertStoryOverride add adds to add[] and removes from remove[]', async () => {
- useUserSettingsStore.getState().hydrate(
- {
- workflow: {
- pendingSprintDraft: {
- 'product-1': {
- goal: 'g',
- pbiIntent: {},
- storyOverrides: {
- pbiA: { add: [], remove: ['story-1'] },
- },
- },
- },
- },
+ useUserSettingsStore.getState().hydrate({}, false)
+ await useUserSettingsStore.getState().setPendingSprintDraft('product-1', {
+ goal: 'g',
+ pbiIntent: {},
+ storyOverrides: {
+ pbiA: { add: [], remove: ['story-1'] },
},
- false,
- )
- setDraftAction.mockResolvedValue({ success: true })
+ })
await useUserSettingsStore
.getState()
@@ -226,23 +201,14 @@ describe('useUserSettingsStore', () => {
})
it('upsertStoryOverride clear removes from both arrays and drops empty entry', async () => {
- useUserSettingsStore.getState().hydrate(
- {
- workflow: {
- pendingSprintDraft: {
- 'product-1': {
- goal: 'g',
- pbiIntent: {},
- storyOverrides: {
- pbiA: { add: ['story-1'], remove: [] },
- },
- },
- },
- },
+ useUserSettingsStore.getState().hydrate({}, false)
+ await useUserSettingsStore.getState().setPendingSprintDraft('product-1', {
+ goal: 'g',
+ pbiIntent: {},
+ storyOverrides: {
+ pbiA: { add: ['story-1'], remove: [] },
},
- false,
- )
- setDraftAction.mockResolvedValue({ success: true })
+ })
await useUserSettingsStore
.getState()
diff --git a/app/(app)/products/[id]/page.tsx b/app/(app)/products/[id]/page.tsx
index 1b52c7a..1b645bf 100644
--- a/app/(app)/products/[id]/page.tsx
+++ b/app/(app)/products/[id]/page.tsx
@@ -17,6 +17,7 @@ import { EditTaskLoader } from '@/app/_components/tasks/edit-task-loader'
import { TaskDialogSkeleton } from '@/app/_components/tasks/task-dialog-skeleton'
import { NewSprintTrigger } from '@/components/backlog/new-sprint-trigger'
import { SprintDraftBanner } from '@/components/backlog/sprint-draft-banner'
+import { SprintDraftLeaveGuard } from '@/components/backlog/sprint-draft-leave-guard'
import { SaveSprintButton } from '@/components/backlog/save-sprint-button'
import { ActiveSelectionHydrator } from '@/components/backlog/active-selection-hydrator'
import { ActivateProductButton } from '@/components/shared/activate-product-button'
@@ -152,8 +153,9 @@ export default async function ProductBacklogPage({ params, searchParams }: Props
- {/* Sprint definition banner (state A′) */}
+ {/* Sprint definition banner (state A′) + beforeunload-guard */}