# PBI-75 — Sprint task-edit client-side via workspace-store ## Context In het Sprint-scherm (`/products//sprint/`) duurt het bewerken van een taak onevenredig lang. Klik op een taakregel of het potlood-icoon roept `router.push(?editTask=)` aan vanuit [`components/sprint/task-list.tsx:226`](../../components/sprint/task-list.tsx). Dat triggert: - **Volledige server re-render** van [`app/(app)/products/[id]/sprint/[sprintId]/page.tsx`](../../app/(app)/products/[id]/sprint/[sprintId]/page.tsx) met zware queries (sprint, alle sprintStories+tasks, productMembers, alle PBIs+stories voor het backlog-paneel) - **Tweede Prisma-query** in [`EditTaskLoader`](../../app/_components/tasks/edit-task-loader.tsx) voor task-detail (incl. `implementation_plan`) - **Na save**: `router.push(closePath)` + `revalidatePath` → opnieuw alle queries De [`sprint-workspace-store`](../../stores/sprint-workspace/store.ts) (sinds PBI-74 Story 9) bevat al alles voor een client-side edit-flow: - [`setActiveTask(taskId)`](../../stores/sprint-workspace/store.ts) (regel 337) — zet `context.activeTaskId` + roept `ensureTaskLoaded()` aan - [`ensureTaskLoaded(taskId)`](../../stores/sprint-workspace/store.ts) (regel 414) — `GET /api/tasks/{id}` → upsert met `_detail: true` - [`selectActiveTask`](../../stores/sprint-workspace/selectors.ts) (regel 91) — bestaat, nog geen consumer - [`applyTaskEvent`](../../stores/sprint-workspace/store.ts) (regel 748) — SSE-events propageren idempotent na server-save - Optimistic-mutation primitives (`applyOptimisticMutation` / `settleMutation` / `rollbackMutation`) Het patroon "URL-param → store" bestaat al voor product-workspace in [`components/backlog/url-task-sync.tsx`](../../components/backlog/url-task-sync.tsx) — we volgen dat als precedent. **Doel**: klik op een taak opent de edit-dialoog client-side via store-state binnen ~100ms. Geen URL-navigatie, geen server re-render, alleen `GET /api/tasks/{id}` voor het detail. ## Aanpak **Architectuur**: store-mounted dialog + URL-sync component voor deeplinks. 1. **Klik-flow**: `TaskList.openEditDialog` roept `setActiveTask(taskId)` aan op de store. Geen `router.push`. 2. **Render-flow**: nieuwe client-component `SprintTaskDialogMount` zit binnen `SprintHydrationWrapper`, subscribet `selectActiveTask`, en rendert `` zodra de active task `_detail === true` is. 3. **Save-flow**: `TaskDialog` krijgt optionele `onClose`/`onSaved` callbacks (backwards compatible met bestaande `closePath`). Mount geeft `onClose = () => setActiveTask(null)`. Server action `saveTask` blijft `revalidatePath` doen voor server-cache; SSE-event update store via `applyTaskEvent`. 4. **Deeplink-flow**: nieuwe `SprintUrlTaskSync` leest `?editTask=` en roept `setActiveTask(id)` aan (analoog aan `url-task-sync.tsx`). ## Bestanden + wijzigingen ### Nieuw — `components/sprint/sprint-task-dialog-mount.tsx` Client component. Subscribet `selectActiveTask` (single-value, geen `useShallow`). Wanneer task aanwezig is en `isDetail(task)` true, mappt naar `TaskDialogTask`-shape: - `status`: via `taskStatusFromApi` uit [`lib/task-status.ts`](../../lib/task-status.ts) (lowercase API → Prisma UPPER_SNAKE) - `implementation_plan: task.implementation_plan ?? null` - `created_at: new Date(task.created_at)` Rendert ` setActiveTask(null)} isDemo={isDemo} />`. Geen render tussen `setActiveTask` en `_detail: true` (detail-fetch <100ms). ### Nieuw — `components/sprint/sprint-url-task-sync.tsx` Kopie van [`components/backlog/url-task-sync.tsx`](../../components/backlog/url-task-sync.tsx) maar tegen `useSprintWorkspaceStore` en `writeTaskHint` uit [`stores/sprint-workspace/restore`](../../stores/sprint-workspace/restore.ts). ### Wijziging — `components/sprint/task-list.tsx` (regels 225-227) Vervang: ```ts function openEditDialog(taskId: string) { router.push(`${pathname}?editTask=${taskId}`) } ``` door: ```ts function openEditDialog(taskId: string) { useSprintWorkspaceStore.getState().setActiveTask(taskId) } ``` `openCreateDialog` (regel 222) blijft URL-gebaseerd — out-of-scope. ### Wijziging — `app/(app)/products/[id]/sprint/[sprintId]/page.tsx` - Verwijder `editTask` uit searchParams-destructuring (regel 36) - Verwijder `editTask &&`-block met `` (regels 250-260) - Verwijder ongebruikte imports (`EditTaskLoader`, `TaskDialogSkeleton`, evt. `Suspense`) - Mount binnen `SprintHydrationWrapper`: ```tsx ``` - `newTask`-block (regels 241-248) blijft ongemoeid — out-of-scope. ### Wijziging — `app/_components/tasks/task-dialog.tsx` Maak `closePath` optioneel + voeg `onClose`/`onSaved` toe (backwards compatible): ```ts interface TaskDialogProps { task?: TaskDialogTask storyId?: string productId: string closePath?: string onClose?: () => void onSaved?: (taskId: string) => void isDemo?: boolean } ``` Refactor de drie `router.push(closePath)`-calls (regels 104, 120, 155) naar één helper: ```ts function close() { if (onClose) { onClose(); return } if (closePath) router.push(closePath) } ``` Bestaande callers (`EditTaskLoader`, mobile, product-page, sprint `newTask`-block) blijven werken via `closePath`. Nieuwe `SprintTaskDialogMount` gebruikt `onClose`. ### Geen wijziging - `stores/sprint-workspace/selectors.ts` — `selectActiveTask` bestaat al - `app/_components/tasks/edit-task-loader.tsx` — nog gebruikt door product-page en mobile ## Edge cases - **Status-enum mapping**: store API-lowercase → Prisma UPPER_SNAKE via `taskStatusFromApi`, fallback `'TO_DO'` - **`_detail: true` race**: mount rendert pas wanneer `isDetail(task)` true is — geen flash met undefined velden - **Demo-mode**: prop blijft via server doorlopen, dialog respecteert al `isDemo` - **Dirty-close-guard**: ingebouwd in dialog (regels 107, 172) — werkt via `onClose` - **SSE na save**: `applyTaskEvent` updatet store automatisch - **Deeplink + task niet bestaat**: `GET /api/tasks/{id}` 404 → store doet niets, dialog opent niet (huidige `redirect()` verdwijnt — acceptabel) ## Verificatie 1. **Browser** (`npm run dev`): klik op task in takenlijst → dialog opent <100ms, geen URL-verandering, alleen `GET /api/tasks/` in Network 2. **Save**: wijzig titel → Opslaan → dialog sluit → store toont nieuwe titel via SSE 3. **Deeplink**: `?editTask=` → dialog opent via `SprintUrlTaskSync` 4. **Bestaande flows ongebroken**: product-page edit, mobile edit, sprint `?newTask=1` 5. **`npm run verify && npm run build`** 6. **Vitest**: `__tests__/components/sprint/sprint-task-dialog-mount.test.tsx` — hydreer store, mock fetch, `setActiveTask(id)`, assert UPPER_SNAKE status + `onClose` clear ## Risico's - Andere mounts (mobile, product-backlog, sprint `newTask`) blijven URL-gebaseerd — `closePath?` optional houdt ze werkend - Geen `redirect()` bij not-found-deeplink (klein UX-verschil) - SSE-latency 100-500ms na save — eventueel later mitigeren via `applyOptimisticMutation` in `onSaved`-callback ## Out-of-scope (follow-up PBIs) - `?newTask=1`-flow naar store - Mobile + product-backlog mounts - `EditTaskLoader` verwijderen wanneer alle callers over zijn