From 32c817d0aa4a4486592e07d562dc62f05bb271ab Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Jan 2026 14:19:22 +0000 Subject: [PATCH] fix(reorder): allow fractional and negative positions for task reordering The Zod validation schema required newPosition to be a non-negative integer, but the frontend uses fractional positioning (averaging neighbor positions) when reordering tasks between items. This caused validation failures when reordering task lists with more than 2 tasks. - Remove .int() and .gte(0) constraints from reorderSchema - Add .finite() to prevent Infinity/NaN values - Update tests to verify fractional (2.5) and negative (-1) positions work --- .../api/pages/reorder/__tests__/route.test.ts | 30 +++++++++++++++++-- apps/web/src/app/api/pages/reorder/route.ts | 2 +- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/api/pages/reorder/__tests__/route.test.ts b/apps/web/src/app/api/pages/reorder/__tests__/route.test.ts index c59f51911..949571d8d 100644 --- a/apps/web/src/app/api/pages/reorder/__tests__/route.test.ts +++ b/apps/web/src/app/api/pages/reorder/__tests__/route.test.ts @@ -161,16 +161,40 @@ describe('PATCH /api/pages/reorder', () => { expect(pageReorderService.reorderPage).not.toHaveBeenCalled(); }); - it('returns 400 for negative position values', async () => { + it('accepts negative position values (for fractional positioning)', async () => { const response = await PATCH(createRequest({ pageId: mockPageId, newParentId: null, newPosition: -1, })); - const body = await response.json(); + + expect(response.status).toBe(200); + expect(pageReorderService.reorderPage).toHaveBeenCalledWith( + expect.objectContaining({ newPosition: -1 }) + ); + }); + + it('accepts fractional position values (for between-item positioning)', async () => { + const response = await PATCH(createRequest({ + pageId: mockPageId, + newParentId: null, + newPosition: 2.5, + })); + + expect(response.status).toBe(200); + expect(pageReorderService.reorderPage).toHaveBeenCalledWith( + expect.objectContaining({ newPosition: 2.5 }) + ); + }); + + it('returns 400 for non-finite position values', async () => { + const response = await PATCH(createRequest({ + pageId: mockPageId, + newParentId: null, + newPosition: Infinity, + })); expect(response.status).toBe(400); - expect(body.error).toMatch(/position|non-negative/i); expect(pageReorderService.reorderPage).not.toHaveBeenCalled(); }); diff --git a/apps/web/src/app/api/pages/reorder/route.ts b/apps/web/src/app/api/pages/reorder/route.ts index bf2805963..ba7b229f4 100644 --- a/apps/web/src/app/api/pages/reorder/route.ts +++ b/apps/web/src/app/api/pages/reorder/route.ts @@ -10,7 +10,7 @@ const AUTH_OPTIONS = { allow: ['jwt', 'mcp'] as const, requireCSRF: true }; const reorderSchema = z.object({ pageId: z.string(), newParentId: z.string().nullable(), - newPosition: z.number().int().gte(0, 'Position must be a non-negative integer'), + newPosition: z.number().finite(), }); export async function PATCH(request: Request) {