From f68d985c2c205f12edafa0e40a2f77caa540a4a2 Mon Sep 17 00:00:00 2001
From: Scrum4Me Agent <30029041+madhura68@users.noreply.github.com>
Date: Thu, 14 May 2026 16:29:56 +0200
Subject: [PATCH] refactor(dnd): remove drag-and-drop reorder for stories and
tasks
- Remove reorderStoriesAction, reorderTasksAction, reorderSprintStoriesAction
- Delete REST route app/api/stories/[id]/tasks/reorder/route.ts
- Remove DnD from backlog story-panel and task-panel (flat list)
- Remove reorder-within-sprint branch from sprint-board-client handleDragEnd
- Switch SortableSprintRow to plain SprintRow using useDraggable (membership drag kept)
- Remove all DnD from task-list (status toggle + edit kept)
- Remove story-order/task-order/sprint-story-order/sprint-task-order mutation types and store handlers
- Remove related tests for deleted reorder route; fix sprint store tests
---
__tests__/api/reorder.test.ts | 111 -----------
__tests__/api/security.test.ts | 51 ------
.../stores/sprint-workspace/store.test.ts | 50 -----
actions/sprints.ts | 26 ---
actions/stories.ts | 40 ----
actions/tasks.ts | 19 --
app/api/stories/[id]/tasks/reorder/route.ts | 56 ------
components/backlog/story-panel.tsx | 173 ++----------------
components/backlog/task-panel.tsx | 129 ++-----------
components/sprint/sprint-backlog.tsx | 14 +-
components/sprint/sprint-board-client.tsx | 37 +---
components/sprint/task-list.tsx | 118 ++----------
stores/product-workspace/store.ts | 10 -
stores/product-workspace/types.ts | 14 --
stores/sprint-workspace/store.ts | 6 -
stores/sprint-workspace/types.ts | 14 --
16 files changed, 52 insertions(+), 816 deletions(-)
delete mode 100644 __tests__/api/reorder.test.ts
delete mode 100644 app/api/stories/[id]/tasks/reorder/route.ts
diff --git a/__tests__/api/reorder.test.ts b/__tests__/api/reorder.test.ts
deleted file mode 100644
index cff62ae..0000000
--- a/__tests__/api/reorder.test.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest'
-
-vi.mock('@/lib/prisma', () => ({
- prisma: {
- story: {
- findFirst: vi.fn(),
- },
- task: {
- update: vi.fn(),
- },
- $transaction: vi.fn(),
- },
-}))
-
-vi.mock('@/lib/api-auth', () => ({
- authenticateApiRequest: vi.fn(),
-}))
-
-import { prisma } from '@/lib/prisma'
-import { authenticateApiRequest } from '@/lib/api-auth'
-import { PATCH as patchReorder } from '@/app/api/stories/[id]/tasks/reorder/route'
-
-const mockPrisma = prisma as unknown as {
- story: { findFirst: ReturnType }
- task: { update: ReturnType }
- $transaction: ReturnType
-}
-const mockAuth = authenticateApiRequest as ReturnType
-
-function makeStory(taskIds: string[]) {
- return {
- id: 'story-1',
- product_id: 'prod-1',
- tasks: taskIds.map(id => ({ id })),
- }
-}
-
-function makeRequest(body: unknown, storyId = 'story-1'): [Request, { params: Promise<{ id: string }> }] {
- return [
- new Request(`http://localhost/api/stories/${storyId}/tasks/reorder`, {
- method: 'PATCH',
- headers: { Authorization: 'Bearer test-token', 'Content-Type': 'application/json' },
- body: JSON.stringify(body),
- }),
- { params: Promise.resolve({ id: storyId }) },
- ]
-}
-
-describe('PATCH /api/stories/:id/tasks/reorder', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- mockAuth.mockResolvedValue({ userId: 'user-1', isDemo: false })
- mockPrisma.$transaction.mockResolvedValue([])
- mockPrisma.task.update.mockResolvedValue({ id: 'task-1', sort_order: 1 })
- })
-
- // TC-RO-06 — body validation fires before story lookup
- it('returns 422 when task_ids is an empty array', async () => {
- const res = await patchReorder(...makeRequest({ task_ids: [] }))
- expect(res.status).toBe(422)
- expect(mockPrisma.story.findFirst).not.toHaveBeenCalled()
- })
-
- // TC-RO-07
- it('returns 422 when task_ids is not an array', async () => {
- const res = await patchReorder(...makeRequest({ task_ids: 'task-1' }))
- expect(res.status).toBe(422)
- expect(mockPrisma.story.findFirst).not.toHaveBeenCalled()
- })
-
- it('returns 422 when task_ids is missing entirely', async () => {
- const res = await patchReorder(...makeRequest({}))
- expect(res.status).toBe(422)
- })
-
- // TC-RO-08
- it('returns 422 when task_ids contains an ID not belonging to the story', async () => {
- mockPrisma.story.findFirst.mockResolvedValue(makeStory(['task-1', 'task-2']))
-
- const res = await patchReorder(...makeRequest({ task_ids: ['task-1', 'task-from-other-story'] }))
- const data = await res.json()
-
- expect(res.status).toBe(422)
- expect(data.error).toContain('task-from-other-story')
- })
-
- // TC-RO-09
- it('reorders tasks and returns 200 with success: true', async () => {
- mockPrisma.story.findFirst.mockResolvedValue(makeStory(['task-1', 'task-2', 'task-3']))
-
- const res = await patchReorder(...makeRequest({ task_ids: ['task-3', 'task-1', 'task-2'] }))
- const data = await res.json()
-
- expect(res.status).toBe(200)
- expect(data).toEqual({ success: true })
- expect(mockPrisma.$transaction).toHaveBeenCalled()
- })
-
- it('updates each task with its new sort_order index', async () => {
- mockPrisma.story.findFirst.mockResolvedValue(makeStory(['task-1', 'task-2']))
-
- await patchReorder(...makeRequest({ task_ids: ['task-2', 'task-1'] }))
-
- expect(mockPrisma.task.update).toHaveBeenCalledWith(
- expect.objectContaining({ where: { id: 'task-2' }, data: { sort_order: 1 } })
- )
- expect(mockPrisma.task.update).toHaveBeenCalledWith(
- expect.objectContaining({ where: { id: 'task-1' }, data: { sort_order: 2 } })
- )
- })
-})
diff --git a/__tests__/api/security.test.ts b/__tests__/api/security.test.ts
index 467e248..9a1d508 100644
--- a/__tests__/api/security.test.ts
+++ b/__tests__/api/security.test.ts
@@ -54,7 +54,6 @@ import { authenticateApiRequest } from '@/lib/api-auth'
import { GET as getProducts } from '@/app/api/products/route'
import { GET as getNextStory } from '@/app/api/products/[id]/next-story/route'
import { GET as getSprintTasks } from '@/app/api/sprints/[id]/tasks/route'
-import { PATCH as patchReorder } from '@/app/api/stories/[id]/tasks/reorder/route'
import { POST as postStoryLog } from '@/app/api/stories/[id]/log/route'
import { PATCH as patchTask } from '@/app/api/tasks/[id]/route'
@@ -276,56 +275,6 @@ describe('GET /api/sprints/:id/tasks', () => {
})
})
-// ─── PATCH /api/stories/:id/tasks/reorder ────────────────────────────────────
-
-describe('PATCH /api/stories/:id/tasks/reorder', () => {
- const VALID_BODY = { task_ids: ['task-x'] }
-
- // TC-RO-01
- it('returns 401 when no valid token provided', async () => {
- mockAuth.mockResolvedValue(UNAUTHORIZED)
- const res = await patchReorder(
- makePatch('http://localhost/api/stories/story-1/tasks/reorder', VALID_BODY),
- routeCtx('story-1')
- )
- expect(res.status).toBe(401)
- })
-
- // TC-RO-03
- it('returns 403 for demo users', async () => {
- mockAuth.mockResolvedValue(DEMO_AUTH)
- const res = await patchReorder(
- makePatch('http://localhost/api/stories/story-1/tasks/reorder', VALID_BODY),
- routeCtx('story-1')
- )
- expect(res.status).toBe(403)
- const data = await res.json()
- expect(data.error).toBe('Niet beschikbaar in demo-modus')
- })
-
- // TC-RO-04 / TC-RO-05
- it('returns 404 when story is not accessible to the authenticated user', async () => {
- mockAuth.mockResolvedValue(USER_2_AUTH)
- mockPrisma.story.findFirst.mockResolvedValue(null)
-
- const res = await patchReorder(
- makePatch('http://localhost/api/stories/story-1/tasks/reorder', VALID_BODY),
- routeCtx('story-1')
- )
- expect(res.status).toBe(404)
- expect(mockPrisma.story.findFirst).toHaveBeenCalledWith(
- expect.objectContaining({
- where: expect.objectContaining({
- id: 'story-1',
- product: expect.objectContaining({
- OR: expect.arrayContaining([{ user_id: 'user-2' }]),
- }),
- }),
- })
- )
- })
-})
-
// ─── POST /api/stories/:id/log ────────────────────────────────────────────────
describe('POST /api/stories/:id/log', () => {
diff --git a/__tests__/stores/sprint-workspace/store.test.ts b/__tests__/stores/sprint-workspace/store.test.ts
index 6e7757a..5fa0502 100644
--- a/__tests__/stores/sprint-workspace/store.test.ts
+++ b/__tests__/stores/sprint-workspace/store.test.ts
@@ -852,56 +852,6 @@ describe('restore-hint flow — chain triggert na ensure*Loaded', () => {
// ─────────────────────────────────────────────────────────────────────────
describe('optimistic mutations', () => {
- it('rollback herstelt vorige sprint-story-order', () => {
- useSprintWorkspaceStore.getState().hydrateSnapshot(
- snapshotWith(
- makeSprint({ id: 'sp-1', product_id: 'prod-1' }),
- [
- makeStory({ id: 'a', pbi_id: 'p', sprint_id: 'sp-1', sort_order: 1 }),
- makeStory({ id: 'b', pbi_id: 'p', sprint_id: 'sp-1', sort_order: 2 }),
- ],
- ),
- )
- const prevOrder = [
- ...useSprintWorkspaceStore.getState().relations.storyIdsBySprint['sp-1'],
- ]
-
- const id = useSprintWorkspaceStore.getState().applyOptimisticMutation({
- kind: 'sprint-story-order',
- sprintId: 'sp-1',
- prevStoryIds: prevOrder,
- })
- useSprintWorkspaceStore.setState((s) => {
- s.relations.storyIdsBySprint['sp-1'] = ['b', 'a']
- })
-
- useSprintWorkspaceStore.getState().rollbackMutation(id)
- expect(useSprintWorkspaceStore.getState().relations.storyIdsBySprint['sp-1']).toEqual(
- prevOrder,
- )
- expect(useSprintWorkspaceStore.getState().pendingMutations[id]).toBeUndefined()
- })
-
- it('settle ruimt pending op zonder state te wijzigen', () => {
- useSprintWorkspaceStore.getState().hydrateSnapshot(
- snapshotWith(makeSprint({ id: 'sp-1', product_id: 'prod-1' }), [
- makeStory({ id: 'a', pbi_id: 'p', sprint_id: 'sp-1' }),
- ]),
- )
- const id = useSprintWorkspaceStore.getState().applyOptimisticMutation({
- kind: 'sprint-story-order',
- sprintId: 'sp-1',
- prevStoryIds: ['a'],
- })
- expect(useSprintWorkspaceStore.getState().pendingMutations[id]).toBeDefined()
-
- useSprintWorkspaceStore.getState().settleMutation(id)
- expect(useSprintWorkspaceStore.getState().pendingMutations[id]).toBeUndefined()
- expect(useSprintWorkspaceStore.getState().relations.storyIdsBySprint['sp-1']).toEqual([
- 'a',
- ])
- })
-
it('SSE-echo van een al-bestaande sprint is idempotent', () => {
useSprintWorkspaceStore.setState((s) => {
s.context.activeProduct = { id: 'prod-1', name: 'P' }
diff --git a/actions/sprints.ts b/actions/sprints.ts
index eb627a5..8ccc80e 100644
--- a/actions/sprints.ts
+++ b/actions/sprints.ts
@@ -562,32 +562,6 @@ export async function removeStoryFromSprintAction(storyId: string) {
return { success: true }
}
-export async function reorderSprintStoriesAction(sprintId: string, orderedIds: string[]) {
- const session = await getSession()
- if (!session.userId) return { error: 'Niet ingelogd' }
- if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' }
- if (hasDuplicateIds(orderedIds)) return { error: 'Ongeldige Sprint Backlog-volgorde' }
-
- const sprint = await prisma.sprint.findFirst({
- where: { id: sprintId, product: productAccessFilter(session.userId) },
- })
- if (!sprint) return { error: 'Sprint niet gevonden' }
-
- const stories = await prisma.story.findMany({
- where: { id: { in: orderedIds }, sprint_id: sprintId, product_id: sprint.product_id },
- select: { id: true },
- })
- if (stories.length !== orderedIds.length) return { error: 'Ongeldige Sprint Backlog-volgorde' }
-
- await prisma.$transaction(
- orderedIds.map((id, i) =>
- prisma.story.update({ where: { id }, data: { sort_order: i + 1.0 } })
- )
- )
-
- revalidatePath(`/products/${sprint.product_id}/sprint`)
- return { success: true }
-}
export async function completeSprintAction(
sprintId: string,
diff --git a/actions/stories.ts b/actions/stories.ts
index 2bc30c5..dbac04a 100644
--- a/actions/stories.ts
+++ b/actions/stories.ts
@@ -357,43 +357,3 @@ export async function claimAllUnassignedInActiveSprintAction(productId: string)
return { success: true, count: result.count }
}
-export async function reorderStoriesAction(
- pbiId: string,
- productId: string,
- orderedIds: string[],
- newPriority?: number
-) {
- const session = await getSession()
- if (!session.userId) return { error: 'Niet ingelogd' }
- if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' }
- if (hasDuplicateIds(orderedIds)) return { error: 'Ongeldige story-volgorde' }
- if (newPriority !== undefined && (!Number.isInteger(newPriority) || newPriority < 1 || newPriority > 4)) {
- return { error: 'Ongeldige prioriteit' }
- }
-
- const pbi = await prisma.pbi.findFirst({
- where: { id: pbiId, product: productAccessFilter(session.userId) },
- })
- if (!pbi) return { error: 'PBI niet gevonden' }
-
- const stories = await prisma.story.findMany({
- where: { id: { in: orderedIds }, pbi_id: pbiId, product_id: pbi.product_id },
- select: { id: true },
- })
- if (stories.length !== orderedIds.length) return { error: 'Ongeldige story-volgorde' }
-
- await prisma.$transaction(
- orderedIds.map((id, i) =>
- prisma.story.update({
- where: { id },
- data: {
- sort_order: i + 1.0,
- ...(newPriority !== undefined ? { priority: newPriority } : {}),
- },
- })
- )
- )
-
- revalidatePath(`/products/${pbi.product_id}`)
- return { success: true }
-}
diff --git a/actions/tasks.ts b/actions/tasks.ts
index 56cca67..1a4ef45 100644
--- a/actions/tasks.ts
+++ b/actions/tasks.ts
@@ -322,22 +322,3 @@ export async function updateTaskPlanAction(taskId: string, productId: string, im
return { success: true }
}
-export async function reorderTasksAction(storyId: string, orderedIds: string[]) {
- const session = await getSession()
- if (!session.userId) return { error: 'Niet ingelogd' }
- if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' }
-
- const story = await prisma.story.findFirst({
- where: { id: storyId, product: productAccessFilter(session.userId) },
- })
- if (!story) return { error: 'Story niet gevonden' }
-
- await prisma.$transaction(
- orderedIds.map((id, i) =>
- prisma.task.update({ where: { id }, data: { sort_order: i + 1.0 } })
- )
- )
-
- revalidatePath(`/products/${story.product_id}/sprint/planning`)
- return { success: true }
-}
diff --git a/app/api/stories/[id]/tasks/reorder/route.ts b/app/api/stories/[id]/tasks/reorder/route.ts
deleted file mode 100644
index 53aeab5..0000000
--- a/app/api/stories/[id]/tasks/reorder/route.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { authenticateApiRequest } from '@/lib/api-auth'
-import { prisma } from '@/lib/prisma'
-import { productAccessFilter } from '@/lib/product-access'
-import { z } from 'zod'
-
-const bodySchema = z.object({
- task_ids: z.array(z.string()).min(1),
-})
-
-export async function PATCH(
- request: Request,
- { params }: { params: Promise<{ id: string }> }
-) {
- const auth = await authenticateApiRequest(request)
- if ('error' in auth) {
- return Response.json({ error: auth.error }, { status: auth.status })
- }
- if (auth.isDemo) {
- return Response.json({ error: 'Niet beschikbaar in demo-modus' }, { status: 403 })
- }
-
- const { id: storyId } = await params
-
- let body: unknown
- try {
- body = await request.json()
- } catch {
- return Response.json({ error: 'Malformed JSON' }, { status: 400 })
- }
- const parsed = bodySchema.safeParse(body)
- if (!parsed.success) {
- return Response.json({ error: parsed.error.flatten() }, { status: 422 })
- }
-
- const story = await prisma.story.findFirst({
- where: { id: storyId, product: productAccessFilter(auth.userId) },
- include: { tasks: { select: { id: true } } },
- })
- if (!story) {
- return Response.json({ error: 'Story niet gevonden' }, { status: 404 })
- }
-
- const storyTaskIds = new Set(story.tasks.map(t => t.id))
- const invalidId = parsed.data.task_ids.find(id => !storyTaskIds.has(id))
- if (invalidId) {
- return Response.json({ error: `Ongeldig task_id: ${invalidId}` }, { status: 422 })
- }
-
- await prisma.$transaction(
- parsed.data.task_ids.map((id, i) =>
- prisma.task.update({ where: { id }, data: { sort_order: i + 1.0 } })
- )
- )
-
- return Response.json({ success: true })
-}
diff --git a/components/backlog/story-panel.tsx b/components/backlog/story-panel.tsx
index fdfd186..54e56db 100644
--- a/components/backlog/story-panel.tsx
+++ b/components/backlog/story-panel.tsx
@@ -1,26 +1,6 @@
'use client'
-import { useState, useTransition } from 'react'
-import {
- DndContext,
- DragEndEvent,
- DragOverlay,
- DragStartEvent,
- KeyboardSensor,
- PointerSensor,
- useSensor,
- useSensors,
- closestCenter,
-} from '@dnd-kit/core'
-import {
- SortableContext,
- useSortable,
- rectSortingStrategy,
- arrayMove,
- sortableKeyboardCoordinates,
-} from '@dnd-kit/sortable'
-import { CSS } from '@dnd-kit/utilities'
-import { toast } from 'sonner'
+import { useState } from 'react'
import { CheckSquare, Square } from 'lucide-react'
import {
Tooltip,
@@ -40,7 +20,6 @@ import {
selectStoryIsBlocked,
} from '@/stores/product-workspace/selectors'
import type { BacklogStory as WorkspaceStory } from '@/stores/product-workspace/types'
-import { reorderStoriesAction } from '@/actions/stories'
import { StoryDialog, type StoryDialogState } from './story-dialog'
import { debugProps } from '@/lib/debug'
import { BacklogCard } from './backlog-card'
@@ -80,8 +59,7 @@ interface StoryPanelProps {
activeSprintId?: string | null
}
-// --- Sortable story block ---
-function SortableStoryBlock({
+function StoryBlock({
story,
isSelected,
cherrypick,
@@ -98,26 +76,11 @@ function SortableStoryBlock({
onSelect: () => void
onEdit: () => void
}) {
- const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
- id: story.id,
- })
-
- const style = {
- transform: CSS.Transform.toString(transform),
- transition,
- opacity: isDragging ? 0.4 : 1,
- }
-
return (
s.context.activePbiId)
const selectedStoryId = useProductWorkspaceStore((s) => s.context.activeStoryId)
@@ -210,14 +171,8 @@ export function StoryPanel({ productId, isDemo, activeSprintId = null }: StoryPa
const setPref = useUserSettingsStore((s) => s.setPref)
const setSortMode = (v: SortMode) => void setPref(['views', 'storyPanel', 'sort'], v)
const [storyDialogState, setStoryDialogState] = useState(null)
- const [activeDragId, setActiveDragId] = useState(null)
- const [, startTransition] = useTransition()
- // rawStories komt al gesorteerd binnen via selectStoriesForActivePbi.
- const storyMap = Object.fromEntries(rawStories.map(s => [s.id, s]))
- const orderedStories = rawStories
-
- const base = orderedStories
+ const base = rawStories
.filter(s => !filterStatus || s.status === filterStatus)
.filter(s => !filterPriority || s.priority === filterPriority)
@@ -231,74 +186,6 @@ export function StoryPanel({ productId, isDemo, activeSprintId = null }: StoryPa
return a.priority !== b.priority ? a.priority - b.priority : 0
})
- const sensors = useSensors(
- useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
- useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
- )
-
- function handleDragStart(event: DragStartEvent) {
- setActiveDragId(event.active.id as string)
- }
-
- function handleDragEnd(event: DragEndEvent) {
- setActiveDragId(null)
- const { active, over } = event
- if (!over || active.id === over.id || !selectedPbiId) return
-
- const activeStory = storyMap[active.id as string]
- const overStory = storyMap[over.id as string]
- if (!activeStory || !overStory) return
-
- const store = useProductWorkspaceStore.getState()
- const prevOrder = [...(store.relations.storyIdsByPbi[selectedPbiId] ?? [])]
- const oldIndex = prevOrder.indexOf(active.id as string)
- const newIndex = prevOrder.indexOf(over.id as string)
- if (oldIndex === -1 || newIndex === -1) return
- const newOrder = arrayMove([...prevOrder], oldIndex, newIndex)
-
- const orderMutationId = store.applyOptimisticMutation({
- kind: 'story-order',
- pbiId: selectedPbiId,
- prevStoryIds: prevOrder,
- })
- useProductWorkspaceStore.setState((s) => {
- s.relations.storyIdsByPbi[selectedPbiId] = newOrder
- })
-
- const priorityChanged = activeStory.priority !== overStory.priority
- let priorityMutationId: string | null = null
- if (priorityChanged) {
- priorityMutationId = store.applyOptimisticMutation({
- kind: 'entity-patch',
- entity: 'story',
- id: active.id as string,
- prev: store.entities.storiesById[active.id as string],
- })
- useProductWorkspaceStore.setState((s) => {
- const story = s.entities.storiesById[active.id as string]
- if (story) story.priority = overStory.priority
- })
- }
-
- startTransition(async () => {
- const result = await reorderStoriesAction(
- selectedPbiId,
- productId,
- newOrder,
- priorityChanged ? overStory.priority : undefined
- )
- const st = useProductWorkspaceStore.getState()
- if (result.success) {
- if (priorityMutationId) st.settleMutation(priorityMutationId)
- st.settleMutation(orderMutationId)
- } else {
- if (priorityMutationId) st.rollbackMutation(priorityMutationId)
- st.rollbackMutation(orderMutationId)
- toast.error('Volgorde opslaan mislukt')
- }
- })
- }
-
const hasActiveFilters = filterStatus !== null || filterPriority !== null
return (
@@ -361,39 +248,19 @@ export function StoryPanel({ productId, isDemo, activeSprintId = null }: StoryPa
action={{ label: 'Maak je eerste story aan', onClick: () => setStoryDialogState({ mode: 'create', pbiId: selectedPbiId, productId, defaultPriority: 2 }), disabled: isDemo }}
/>
) : (
-
- s.id)} strategy={rectSortingStrategy}>
-
- {filtered.map(story => (
- useProductWorkspaceStore.getState().setActiveStory(story.id)}
- onEdit={() => setStoryDialogState({ mode: 'edit', story, productId })}
- />
- ))}
-
-
-
-
- {activeDragId && storyMap[activeDragId] && (
-
- )}
-
-
+
+ {filtered.map(story => (
+ useProductWorkspaceStore.getState().setActiveStory(story.id)}
+ onEdit={() => setStoryDialogState({ mode: 'edit', story, productId })}
+ />
+ ))}
+
)}
@@ -406,9 +273,7 @@ export function StoryPanel({ productId, isDemo, activeSprintId = null }: StoryPa
)
}
-// PBI-79 / ST-1337: wrapper rond SortableStoryBlock met cherrypick-handling.
-// Subscribed per story zodat enkel de relevante rij re-rendert bij draft- of
-// crossSprintBlocks-mutaties.
+// PBI-79 / ST-1337: wrapper rond StoryBlock met cherrypick-handling.
function StoryBlockWithCherrypick({
story,
productId,
@@ -443,7 +308,6 @@ function StoryBlockWithCherrypick({
} | null = null
if (draft) {
- // State A′: muteer draft via per-PBI overrides.
const intent = draft.pbiIntent[story.pbi_id] ?? 'none'
const override = draft.storyOverrides[story.pbi_id] ?? {
add: [],
@@ -474,7 +338,6 @@ function StoryBlockWithCherrypick({
},
}
} else if (activeSprintId) {
- // State B: muteer pending buffer via toggleStorySprintMembership.
const inSprintDb = story.sprint_id === activeSprintId
const inAdds = pending.adds.includes(story.id)
const inRemoves = pending.removes.includes(story.id)
@@ -489,7 +352,7 @@ function StoryBlockWithCherrypick({
}
return (
- = {
DONE: 'Klaar',
}
-function SortableTaskCard({
+function TaskCard({
task,
- isDemo,
onClick,
}: {
task: BacklogTask | TaskDetail
- isDemo: boolean
onClick: () => void
}) {
- const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
- useSortable({ id: task.id })
-
- const style = {
- transform: CSS.Transform.toString(transform),
- transition,
- }
-
return (
s.context.activeStoryId)
const rawTasks = useProductWorkspaceStore(useShallow(selectTasksForActiveStory)) as
| (BacklogTask | TaskDetail)[]
- const [activeDragId, setActiveDragId] = useState(null)
const tasks: (BacklogTask | TaskDetail)[] | null = selectedStoryId
? rawTasks
: null
- const sensors = useSensors(
- useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
- useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
- )
-
- function handleDragStart(event: DragStartEvent) {
- setActiveDragId(event.active.id as string)
- }
-
- function handleDragEnd(event: DragEndEvent) {
- setActiveDragId(null)
- if (!selectedStoryId || !tasks) return
- const { active, over } = event
- if (!over || active.id === over.id) return
-
- const store = useProductWorkspaceStore.getState()
- const prevOrder = [...(store.relations.taskIdsByStory[selectedStoryId] ?? [])]
- const oldIndex = prevOrder.indexOf(active.id as string)
- const newIndex = prevOrder.indexOf(over.id as string)
- if (oldIndex === -1 || newIndex === -1) return
- const newOrder = arrayMove([...prevOrder], oldIndex, newIndex)
-
- const orderMutationId = store.applyOptimisticMutation({
- kind: 'task-order',
- storyId: selectedStoryId,
- prevTaskIds: prevOrder,
- })
- useProductWorkspaceStore.setState((s) => {
- s.relations.taskIdsByStory[selectedStoryId] = newOrder
- })
-
- startTransition(async () => {
- const result = await reorderTasksAction(selectedStoryId, newOrder)
- const st = useProductWorkspaceStore.getState()
- if (result?.error) {
- st.rollbackMutation(orderMutationId)
- toast.error(result.error)
- } else {
- st.settleMutation(orderMutationId)
- }
- })
- }
-
const navActions = (
) : (
- s.id)} strategy={verticalListSortingStrategy}>
+ <>
{orderedStories.map(story => (
-
))}
-
+ >
)}
id !== activeId), activeId]
-
- const mutationId = store.applyOptimisticMutation({
- kind: 'sprint-story-order',
- sprintId,
- prevStoryIds: prevOrder,
- })
- useSprintWorkspaceStore.setState((s) => {
- s.relations.storyIdsBySprint[sprintId] = newOrder
- })
-
- startTransition(async () => {
- const result = await reorderSprintStoriesAction(sprintId, newOrder)
- const st = useSprintWorkspaceStore.getState()
- if (result.success) {
- st.settleMutation(mutationId)
- } else {
- st.rollbackMutation(mutationId)
- toast.error('Volgorde opslaan mislukt')
- }
- })
- }
-
function handleAssigneeChange(storyId: string, assigneeId: string | null, assigneeUsername: string | null) {
useSprintWorkspaceStore.setState((s) => {
const story = s.entities.storiesById[storyId]
diff --git a/components/sprint/task-list.tsx b/components/sprint/task-list.tsx
index c4ea4a3..5e88d19 100644
--- a/components/sprint/task-list.tsx
+++ b/components/sprint/task-list.tsx
@@ -1,18 +1,8 @@
'use client'
-import { useState, useTransition } from 'react'
+import { useTransition } from 'react'
import { useRouter, usePathname } from 'next/navigation'
-import {
- DndContext, DragEndEvent, DragOverlay,
- KeyboardSensor, PointerSensor, useSensor, useSensors, closestCenter,
-} from '@dnd-kit/core'
-import {
- SortableContext, useSortable, verticalListSortingStrategy, arrayMove,
- sortableKeyboardCoordinates,
-} from '@dnd-kit/sortable'
-import { CSS } from '@dnd-kit/utilities'
import { Pencil } from 'lucide-react'
-import { toast } from 'sonner'
import { useShallow } from 'zustand/react/shallow'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
@@ -25,7 +15,7 @@ import type {
SprintWorkspaceTask,
SprintWorkspaceTaskDetail,
} from '@/stores/sprint-workspace/types'
-import { updateTaskStatusAction, reorderTasksAction } from '@/actions/tasks'
+import { updateTaskStatusAction } from '@/actions/tasks'
import { DemoTooltip } from '@/components/shared/demo-tooltip'
import { debugProps } from '@/lib/debug'
import { cn } from '@/lib/utils'
@@ -53,7 +43,6 @@ const STATUS_LABELS: Record = {
EXCLUDED: 'Uitgesloten',
}
-
// Behouden voor type-compat met SprintBoardClient props (verdwijnt zodra
// SprintBoardClient ook geen tasks-prop meer doorgeeft — T-883).
export interface Task {
@@ -75,7 +64,7 @@ interface TaskListProps {
isDemo: boolean
}
-function SortableTaskRow({
+function TaskRow({
task, code, isDemo, onStatusToggle, onEdit,
}: {
task: WorkspaceTask
@@ -84,11 +73,8 @@ function SortableTaskRow({
onStatusToggle: () => void
onEdit: () => void
}) {
- const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: task.id })
- const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.4 : 1 }
-
return (
-
+
- {!isDemo && (
-
e.stopPropagation()}
- className="text-muted-foreground cursor-grab active:cursor-grabbing shrink-0 text-sm select-none mt-0.5"
- aria-hidden="true"
- >
- ⠿
-
- )}
(null)
const [, startTransition] = useTransition()
const router = useRouter()
const pathname = usePathname()
- const taskMap: Record = {}
- for (const t of orderedTasks) taskMap[t.id] = t
-
const doneCount = orderedTasks.filter(t => t.status === 'DONE').length
- const sensors = useSensors(
- useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
- useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
- )
-
- function handleDragEnd(event: DragEndEvent) {
- const { active, over } = event
- if (!storyId) return
- if (!over || active.id === over.id) return
- const store = useSprintWorkspaceStore.getState()
- const prevOrder = [...(store.relations.taskIdsByStory[storyId] ?? [])]
- const newOrder = arrayMove(
- [...prevOrder],
- prevOrder.indexOf(active.id as string),
- prevOrder.indexOf(over.id as string),
- )
-
- const mutationId = store.applyOptimisticMutation({
- kind: 'sprint-task-order',
- storyId,
- prevTaskIds: prevOrder,
- })
- useSprintWorkspaceStore.setState((s) => {
- s.relations.taskIdsByStory[storyId] = newOrder
- })
- setActiveDragId(null)
-
- startTransition(async () => {
- const result = await reorderTasksAction(storyId, newOrder)
- const st = useSprintWorkspaceStore.getState()
- if (result.success) {
- st.settleMutation(mutationId)
- } else {
- st.rollbackMutation(mutationId)
- toast.error('Volgorde opslaan mislukt')
- }
- })
- }
-
function handleStatusToggle(task: WorkspaceTask) {
startTransition(async () => {
await updateTaskStatusAction(task.id, STATUS_CYCLE[task.status] ?? 'TO_DO')
@@ -263,36 +195,18 @@ export function TaskList({ sprintId: _sprintId, productId: _productId, isDemo }:
) : (
-
setActiveDragId(e.active.id as string)}
- onDragEnd={handleDragEnd}
- >
- t.id)} strategy={verticalListSortingStrategy}>
- {orderedTasks.map((task) => (
- handleStatusToggle(task)}
- onEdit={() => openEditDialog(task.id)}
- />
- ))}
-
-
- {activeDragId && taskMap[activeDragId] && (
-
- {taskMap[activeDragId].title}
-
- )}
-
-
+ <>
+ {orderedTasks.map((task) => (
+
handleStatusToggle(task)}
+ onEdit={() => openEditDialog(task.id)}
+ />
+ ))}
+ >
)}
diff --git a/stores/product-workspace/store.ts b/stores/product-workspace/store.ts
index ecec31f..040a808 100644
--- a/stores/product-workspace/store.ts
+++ b/stores/product-workspace/store.ts
@@ -557,10 +557,6 @@ export const useProductWorkspaceStore = create
()(
case 'pbi-order':
// store-call passes new order via separate set, snapshot is prevPbiIds
break
- case 'story-order':
- break
- case 'task-order':
- break
case 'entity-patch':
break
}
@@ -577,12 +573,6 @@ export const useProductWorkspaceStore = create()(
case 'pbi-order':
s.relations.pbiIds = [...mutation.prevPbiIds]
break
- case 'story-order':
- s.relations.storyIdsByPbi[mutation.pbiId] = [...mutation.prevStoryIds]
- break
- case 'task-order':
- s.relations.taskIdsByStory[mutation.storyId] = [...mutation.prevTaskIds]
- break
case 'entity-patch': {
const { entity, id, prev } = mutation
if (prev) {
diff --git a/stores/product-workspace/types.ts b/stores/product-workspace/types.ts
index 49727ab..18bd6d2 100644
--- a/stores/product-workspace/types.ts
+++ b/stores/product-workspace/types.ts
@@ -109,18 +109,6 @@ export interface OptimisticPbiOrderMutation {
prevPbiIds: string[]
}
-export interface OptimisticStoryOrderMutation {
- kind: 'story-order'
- pbiId: string
- prevStoryIds: string[]
-}
-
-export interface OptimisticTaskOrderMutation {
- kind: 'task-order'
- storyId: string
- prevTaskIds: string[]
-}
-
export interface OptimisticEntityPatchMutation {
kind: 'entity-patch'
entity: 'pbi' | 'story' | 'task'
@@ -130,8 +118,6 @@ export interface OptimisticEntityPatchMutation {
export type OptimisticMutation =
| OptimisticPbiOrderMutation
- | OptimisticStoryOrderMutation
- | OptimisticTaskOrderMutation
| OptimisticEntityPatchMutation
export interface PendingOptimisticMutation {
diff --git a/stores/sprint-workspace/store.ts b/stores/sprint-workspace/store.ts
index c33ff2b..34a1941 100644
--- a/stores/sprint-workspace/store.ts
+++ b/stores/sprint-workspace/store.ts
@@ -519,12 +519,6 @@ export const useSprintWorkspaceStore = create()(
const { mutation } = pending
set((s) => {
switch (mutation.kind) {
- case 'sprint-story-order':
- s.relations.storyIdsBySprint[mutation.sprintId] = [...mutation.prevStoryIds]
- break
- case 'sprint-task-order':
- s.relations.taskIdsByStory[mutation.storyId] = [...mutation.prevTaskIds]
- break
case 'entity-patch': {
const { entity, id, prev } = mutation
if (prev) {
diff --git a/stores/sprint-workspace/types.ts b/stores/sprint-workspace/types.ts
index 1ff6802..00858d8 100644
--- a/stores/sprint-workspace/types.ts
+++ b/stores/sprint-workspace/types.ts
@@ -122,18 +122,6 @@ export type ResyncReason =
export type RealtimeStatus = 'connecting' | 'open' | 'disconnected'
-export interface OptimisticSprintStoryOrderMutation {
- kind: 'sprint-story-order'
- sprintId: string
- prevStoryIds: string[]
-}
-
-export interface OptimisticSprintTaskOrderMutation {
- kind: 'sprint-task-order'
- storyId: string
- prevTaskIds: string[]
-}
-
export interface OptimisticEntityPatchMutation {
kind: 'entity-patch'
entity: 'sprint' | 'story' | 'task'
@@ -147,8 +135,6 @@ export interface OptimisticEntityPatchMutation {
}
export type OptimisticMutation =
- | OptimisticSprintStoryOrderMutation
- | OptimisticSprintTaskOrderMutation
| OptimisticEntityPatchMutation
export interface PendingOptimisticMutation {