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
|
|
@ -1,4 +1,3 @@
|
|||
import { Suspense } from 'react'
|
||||
import { notFound, redirect } from 'next/navigation'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getAccessibleProduct } from '@/lib/product-access'
|
||||
|
|
@ -9,6 +8,8 @@ import {
|
|||
SprintHydrationWrapper,
|
||||
type SprintHydrationData,
|
||||
} from '@/components/sprint/sprint-hydration-wrapper'
|
||||
import { SprintTaskDialogMount } from '@/components/sprint/sprint-task-dialog-mount'
|
||||
import { SprintUrlTaskSync } from '@/components/sprint/sprint-url-task-sync'
|
||||
import { SyncActiveSprintCookie } from '@/components/sprint/sync-active-sprint-cookie'
|
||||
import { SprintSwitcher } from '@/components/shared/sprint-switcher'
|
||||
import { getSprintSwitcherData } from '@/lib/sprint-switcher-data'
|
||||
|
|
@ -18,8 +19,6 @@ import { parsePauseContext } from '@/lib/pause-context'
|
|||
import type { SprintStory, PbiWithStories, ProductMember } from '@/components/sprint/sprint-backlog'
|
||||
import type { SprintWorkspaceTask } from '@/stores/sprint-workspace/types'
|
||||
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 Link from 'next/link'
|
||||
|
||||
interface Props {
|
||||
|
|
@ -27,13 +26,12 @@ interface Props {
|
|||
searchParams: Promise<{
|
||||
newTask?: string
|
||||
storyId?: string
|
||||
editTask?: string
|
||||
}>
|
||||
}
|
||||
|
||||
export default async function SprintBoardPage({ params, searchParams }: Props) {
|
||||
const { id, sprintId } = await params
|
||||
const { newTask, storyId: storyIdParam, editTask } = await searchParams
|
||||
const { newTask, storyId: storyIdParam } = await searchParams
|
||||
|
||||
const session = await getSession()
|
||||
if (!session.userId) redirect('/login')
|
||||
|
|
@ -229,6 +227,8 @@ export default async function SprintBoardPage({ params, searchParams }: Props) {
|
|||
currentUserId={session.userId}
|
||||
members={members}
|
||||
/>
|
||||
<SprintTaskDialogMount productId={id} isDemo={isDemo} />
|
||||
<SprintUrlTaskSync />
|
||||
</SprintHydrationWrapper>
|
||||
</div>
|
||||
|
||||
|
|
@ -246,18 +246,6 @@ export default async function SprintBoardPage({ params, searchParams }: Props) {
|
|||
isDemo={isDemo}
|
||||
/>
|
||||
)}
|
||||
|
||||
{editTask && !newTask && (
|
||||
<Suspense fallback={<TaskDialogSkeleton />}>
|
||||
<EditTaskLoader
|
||||
taskId={editTask}
|
||||
userId={session.userId}
|
||||
productId={id}
|
||||
closePath={closePath}
|
||||
isDemo={isDemo}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,9 @@ interface TaskDialogProps {
|
|||
task?: TaskDialogTask
|
||||
storyId?: string
|
||||
productId: string
|
||||
closePath: string
|
||||
closePath?: string
|
||||
onClose?: () => void
|
||||
onSaved?: (taskId: string) => void
|
||||
isDemo?: boolean
|
||||
}
|
||||
|
||||
|
|
@ -81,7 +83,7 @@ const textareaClass = cn(
|
|||
'overflow-y-auto',
|
||||
)
|
||||
|
||||
export function TaskDialog({ task, storyId, productId, closePath, isDemo = false }: TaskDialogProps) {
|
||||
export function TaskDialog({ task, storyId, productId, closePath, onClose, onSaved, isDemo = false }: TaskDialogProps) {
|
||||
const router = useRouter()
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const [confirmDelete, setConfirmDelete] = useState(false)
|
||||
|
|
@ -100,11 +102,12 @@ export function TaskDialog({ task, storyId, productId, closePath, isDemo = false
|
|||
},
|
||||
})
|
||||
|
||||
function handleClose() {
|
||||
router.push(closePath)
|
||||
function close() {
|
||||
if (onClose) { onClose(); return }
|
||||
if (closePath) router.push(closePath)
|
||||
}
|
||||
|
||||
const closeGuard = useDirtyCloseGuard(form.formState.isDirty, handleClose)
|
||||
const closeGuard = useDirtyCloseGuard(form.formState.isDirty, close)
|
||||
const handleKeyDown = useDialogSubmitShortcut(() => form.handleSubmit(onSubmit)())
|
||||
|
||||
function onSubmit(data: TaskInput) {
|
||||
|
|
@ -117,7 +120,8 @@ export function TaskDialog({ task, storyId, productId, closePath, isDemo = false
|
|||
|
||||
if (result.ok) {
|
||||
toast.success(isEdit ? 'Taak opgeslagen' : 'Taak aangemaakt')
|
||||
router.push(closePath)
|
||||
onSaved?.(result.task.id)
|
||||
close()
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -152,7 +156,7 @@ export function TaskDialog({ task, storyId, productId, closePath, isDemo = false
|
|||
const result = await deleteTask(task.id, { productId })
|
||||
if (result.ok) {
|
||||
toast.success('Taak verwijderd')
|
||||
router.push(closePath)
|
||||
close()
|
||||
return
|
||||
}
|
||||
if (result.code === 403) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue