feat(PBI-75): sprint task-edit client-side via workspace-store (#183)
Klik op een taak in het sprint-scherm opent de edit-dialog nu
client-side via setActiveTask op de sprint-workspace-store.
Geen URL-navigatie, geen volledige server re-render — alleen
GET /api/tasks/{id} voor het detail. SSE propageert server-saves
automatisch terug.
- TaskDialog: optionele onClose/onSaved callbacks (closePath
optional gemaakt — backwards compatible)
- SprintTaskDialogMount: nieuwe client-component die
selectActiveTask consumeert en TaskDialog rendert
- SprintUrlTaskSync: deeplink (?editTask=<id>) → store
- Sprint page: mounts toegevoegd, editTask searchParam +
EditTaskLoader-Suspense verwijderd
- TaskList.openEditDialog roept setActiveTask aan ipv router.push
- Vitest integratie-test voor SprintTaskDialogMount
Out-of-scope (follow-up PBIs): newTask-flow, mobile, product-backlog.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3b5cee823c
commit
a9b53dedf0
8 changed files with 335 additions and 25 deletions
119
__tests__/components/sprint/sprint-task-dialog-mount.test.tsx
Normal file
119
__tests__/components/sprint/sprint-task-dialog-mount.test.tsx
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
// @vitest-environment jsdom
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||
|
||||
vi.mock('next/navigation', () => ({ useRouter: () => ({ push: vi.fn() }) }))
|
||||
vi.mock('@/actions/tasks', () => ({
|
||||
saveTask: vi.fn(),
|
||||
deleteTask: vi.fn(),
|
||||
}))
|
||||
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }))
|
||||
|
||||
import { SprintTaskDialogMount } from '@/components/sprint/sprint-task-dialog-mount'
|
||||
import { useSprintWorkspaceStore } from '@/stores/sprint-workspace/store'
|
||||
import type { SprintWorkspaceTaskDetail } from '@/stores/sprint-workspace/types'
|
||||
|
||||
const TASK_DETAIL: SprintWorkspaceTaskDetail = {
|
||||
id: 't1',
|
||||
code: 'T-1',
|
||||
title: 'Mijn taak',
|
||||
description: 'Beschrijving',
|
||||
priority: 2,
|
||||
sort_order: 1,
|
||||
status: 'in_progress',
|
||||
story_id: 'story-1',
|
||||
sprint_id: 'sprint-1',
|
||||
created_at: new Date('2026-01-15'),
|
||||
_detail: true,
|
||||
implementation_plan: 'Stap 1\nStap 2',
|
||||
}
|
||||
|
||||
function resetStore() {
|
||||
useSprintWorkspaceStore.setState((s) => {
|
||||
s.context.activeProduct = null
|
||||
s.context.activeSprintId = null
|
||||
s.context.activeStoryId = null
|
||||
s.context.activeTaskId = null
|
||||
s.entities.sprintsById = {}
|
||||
s.entities.storiesById = {}
|
||||
s.entities.tasksById = {}
|
||||
s.relations.sprintIdsByProduct = {}
|
||||
s.relations.storyIdsBySprint = {}
|
||||
s.relations.taskIdsByStory = {}
|
||||
s.loading.loadedProductSprintsIds = {}
|
||||
s.loading.loadingProductId = null
|
||||
s.loading.loadedSprintIds = {}
|
||||
s.loading.loadingSprintId = null
|
||||
s.loading.loadedStoryIds = {}
|
||||
s.loading.loadedTaskIds = {}
|
||||
s.loading.activeRequestId = null
|
||||
s.pendingMutations = {}
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
resetStore()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('SprintTaskDialogMount', () => {
|
||||
it('rendert niets wanneer er geen active task is', () => {
|
||||
const { container } = render(
|
||||
<SprintTaskDialogMount productId="p1" isDemo={false} />,
|
||||
)
|
||||
expect(container.textContent).toBe('')
|
||||
})
|
||||
|
||||
it('rendert niets wanneer active task geen _detail heeft', () => {
|
||||
useSprintWorkspaceStore.setState((s) => {
|
||||
s.entities.tasksById['t1'] = {
|
||||
id: 't1',
|
||||
code: 'T-1',
|
||||
title: 'Mijn taak',
|
||||
description: null,
|
||||
priority: 2,
|
||||
sort_order: 1,
|
||||
status: 'todo',
|
||||
story_id: 'story-1',
|
||||
sprint_id: 'sprint-1',
|
||||
created_at: new Date(),
|
||||
}
|
||||
s.context.activeTaskId = 't1'
|
||||
})
|
||||
|
||||
const { container } = render(
|
||||
<SprintTaskDialogMount productId="p1" isDemo={false} />,
|
||||
)
|
||||
expect(container.textContent).toBe('')
|
||||
})
|
||||
|
||||
it('rendert TaskDialog met titel "Taak bewerken" wanneer detail aanwezig is', () => {
|
||||
useSprintWorkspaceStore.setState((s) => {
|
||||
s.entities.tasksById['t1'] = TASK_DETAIL
|
||||
s.context.activeTaskId = 't1'
|
||||
})
|
||||
|
||||
render(<SprintTaskDialogMount productId="p1" isDemo={false} />)
|
||||
|
||||
expect(screen.getByText('Taak bewerken')).toBeTruthy()
|
||||
expect((screen.getByLabelText(/Titel/) as HTMLInputElement).value).toBe('Mijn taak')
|
||||
})
|
||||
|
||||
it('clear activeTaskId wanneer Annuleren wordt geklikt', async () => {
|
||||
useSprintWorkspaceStore.setState((s) => {
|
||||
s.entities.tasksById['t1'] = TASK_DETAIL
|
||||
s.context.activeTaskId = 't1'
|
||||
})
|
||||
|
||||
render(<SprintTaskDialogMount productId="p1" isDemo={false} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Annuleren' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(useSprintWorkspaceStore.getState().context.activeTaskId).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue