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 = (