From d99e95826f222e3fcdcf94d64687ea5ad7207930 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 29 Apr 2025 08:58:07 +0100 Subject: [PATCH] refactor: move reorder lists to update list --- apps/web/src/views/board/index.tsx | 22 +++++-- packages/api/src/routers/list.ts | 76 ++++++++-------------- packages/api/src/types/router.types.ts | 1 - packages/db/seed.sql | 29 --------- packages/db/src/repository/list.repo.ts | 85 +++++++++++++++++++------ 5 files changed, 106 insertions(+), 107 deletions(-) diff --git a/apps/web/src/views/board/index.tsx b/apps/web/src/views/board/index.tsx index 46f65d37..e7a608e6 100644 --- a/apps/web/src/views/board/index.tsx +++ b/apps/web/src/views/board/index.tsx @@ -92,7 +92,7 @@ export default function BoardPage() { const isLoading = isInitialLoading || isQueryLoading; - const updateListMutation = api.list.reorder.useMutation({ + const updateListMutation = api.list.update.useMutation({ onMutate: async (args) => { await utils.board.byId.cancel(); @@ -102,10 +102,19 @@ export default function BoardPage() { if (!oldBoard) return oldBoard; const updatedLists = Array.from(oldBoard.lists); - const removedList = updatedLists.splice(args.currentIndex, 1)[0]; - if (removedList) { - updatedLists.splice(args.newIndex, 0, removedList); + const sourceList = updatedLists.find( + (list) => list.publicId === args.listPublicId, + ); + + const currentIndex = sourceList?.index; + + if (currentIndex === undefined) return oldBoard; + + const removedList = updatedLists.splice(currentIndex, 1)[0]; + + if (removedList && args.index !== undefined) { + updatedLists.splice(args.index, 0, removedList); return { ...oldBoard, @@ -201,8 +210,7 @@ export default function BoardPage() { if (type === "LIST") { updateListMutation.mutate({ listPublicId: draggableId, - currentIndex: source.index, - newIndex: destination.index, + index: destination.index, }); } @@ -254,7 +262,7 @@ export default function BoardPage() { /> diff --git a/packages/api/src/routers/list.ts b/packages/api/src/routers/list.ts index 956bd6a0..f31bb3ab 100644 --- a/packages/api/src/routers/list.ts +++ b/packages/api/src/routers/list.ts @@ -63,49 +63,6 @@ export const listRouter = createTRPCRouter({ code: "INTERNAL_SERVER_ERROR", }); - return result; - }), - reorder: protectedProcedure - .meta({ - openapi: { - summary: "Reorder a list", - method: "POST", - path: "/lists/{listPublicId}/reorder", - description: "Reorders the position of a list", - tags: ["Lists"], - protect: true, - }, - }) - .input( - z.object({ - listPublicId: z.string().min(12), - currentIndex: z.number(), - newIndex: z.number(), - }), - ) - .output(z.custom>>()) - .mutation(async ({ ctx, input }) => { - const list = await listRepo.getByPublicId(ctx.db, input.listPublicId); - - if (!list) - throw new TRPCError({ - message: `List with public ID ${input.listPublicId} not found`, - code: "NOT_FOUND", - }); - - const result = await listRepo.reorder(ctx.supabaseClient, { - boardPublicId: list.boardId, - listPublicId: list.id, - currentIndex: input.currentIndex, - newIndex: input.newIndex, - }); - - if (!result) - throw new TRPCError({ - message: `Failed to reorder list`, - code: "INTERNAL_SERVER_ERROR", - }); - return result; }), delete: protectedProcedure @@ -197,16 +154,35 @@ export const listRouter = createTRPCRouter({ .input( z.object({ listPublicId: z.string().min(12), - name: z.string().min(1), + name: z.string().min(1).optional(), + index: z.number().optional(), }), ) - .output(z.custom>>()) + .output( + z.custom< + | Awaited> + | Awaited> + >(), + ) .mutation(async ({ ctx, input }) => { - const result = await listRepo.update( - ctx.supabaseClient, - { name: input.name }, - { listPublicId: input.listPublicId }, - ); + let result: { name: string; publicId: string } | undefined; + + console.log({ input }); + + if (input.name) { + result = await listRepo.update( + ctx.db, + { name: input.name }, + { listPublicId: input.listPublicId }, + ); + } + + if (input.index !== undefined) { + result = await listRepo.reorder(ctx.db, { + listPublicId: input.listPublicId, + newIndex: input.index, + }); + } if (!result) throw new TRPCError({ diff --git a/packages/api/src/types/router.types.ts b/packages/api/src/types/router.types.ts index 6c4e97d3..4ee16103 100644 --- a/packages/api/src/types/router.types.ts +++ b/packages/api/src/types/router.types.ts @@ -2,7 +2,6 @@ import type { RouterInputs, RouterOutputs } from "../index"; export type GetBoardByIdOutput = RouterOutputs["board"]["byId"]; export type GetCardByIdOutput = RouterOutputs["card"]["byId"]; -export type ReorderListInput = RouterInputs["list"]["reorder"]; export type ReorderCardInput = RouterInputs["card"]["reorder"]; export type UpdateBoardInput = RouterInputs["board"]["update"]; export type NewLabelInput = RouterInputs["label"]["create"]; diff --git a/packages/db/seed.sql b/packages/db/seed.sql index ce76ef7c..a4076368 100644 --- a/packages/db/seed.sql +++ b/packages/db/seed.sql @@ -1,33 +1,4 @@ -CREATE OR REPLACE FUNCTION reorder_lists(board_id BIGINT, list_id BIGINT, current_index INT, new_index INT) -RETURNS BOOLEAN -LANGUAGE PLPGSQL -AS $$ -BEGIN - UPDATE list - SET index = - CASE - WHEN index = current_index AND id = list_id THEN new_index - WHEN current_index < new_index AND index > current_index AND index <= new_index THEN index - 1 - WHEN current_index > new_index AND index >= new_index AND index < current_index THEN index + 1 - ELSE index - END - WHERE "boardId" = board_id; - -- Check for duplicate indices after the update - IF EXISTS ( - SELECT index, COUNT(*) - FROM list - WHERE "boardId" = board_id - AND "deletedAt" IS NULL - GROUP BY index - HAVING COUNT(*) > 1 - ) THEN - RAISE EXCEPTION 'Duplicate indices found after reordering in board %', board_id; - END IF; - - RETURN TRUE; -END; -$$; CREATE OR REPLACE FUNCTION reorder_cards(card_id BIGINT, current_list_id BIGINT, new_list_id BIGINT, current_index INT, new_index INT) RETURNS BOOLEAN diff --git a/packages/db/src/repository/list.repo.ts b/packages/db/src/repository/list.repo.ts index d4284eb7..53014b41 100644 --- a/packages/db/src/repository/list.repo.ts +++ b/packages/db/src/repository/list.repo.ts @@ -1,5 +1,5 @@ import type { SupabaseClient } from "@supabase/supabase-js"; -import { and, desc, eq, isNull } from "drizzle-orm"; +import { and, desc, eq, gt, isNull, sql } from "drizzle-orm"; import type { dbClient } from "@kan/db/client"; import type { Database } from "@kan/db/types/database.types"; @@ -68,7 +68,7 @@ export const getWithCardsByPublicId = async ( }; export const update = async ( - db: SupabaseClient, + db: dbClient, listInput: { name: string; }, @@ -76,33 +76,78 @@ export const update = async ( listPublicId: string; }, ) => { - const { data } = await db - .from("list") - .update({ name: listInput.name }) - .eq("publicId", args.listPublicId) - .is("deletedAt", null) - .select(`publicId, name`); + const [result] = await db + .update(lists) + .set({ name: listInput.name }) + .where(and(eq(lists.publicId, args.listPublicId), isNull(lists.deletedAt))) + .returning({ + publicId: lists.publicId, + name: lists.name, + }); - return data; + return result; }; export const reorder = async ( - db: SupabaseClient, + db: dbClient, args: { - boardPublicId: number; - listPublicId: number; - currentIndex: number; + listPublicId: string; newIndex: number; }, ) => { - const { data } = await db.rpc("reorder_lists", { - board_id: args.boardPublicId, - list_id: args.listPublicId, - current_index: args.currentIndex, - new_index: args.newIndex, - }); + return db.transaction(async (tx) => { + const list = await tx.query.lists.findFirst({ + columns: { + id: true, + boardId: true, + index: true, + }, + where: eq(lists.publicId, args.listPublicId), + }); - return data; + if (!list) + throw new Error(`List not found for public ID ${args.listPublicId}`); + + await tx.execute(sql` + UPDATE list + SET index = + CASE + WHEN index = ${list.index} AND id = ${list.id} THEN ${args.newIndex} + WHEN ${list.index} < ${args.newIndex} AND index > ${list.index} AND index <= ${args.newIndex} THEN index - 1 + WHEN ${list.index} > ${args.newIndex} AND index >= ${args.newIndex} AND index < ${list.index} THEN index + 1 + ELSE index + END + WHERE "boardId" = ${list.boardId}; + `); + + const countExpr = sql`COUNT(*)`.mapWith(Number); + + const duplicateIndices = await db + .select({ + index: lists.index, + count: countExpr, + }) + .from(lists) + .where(and(eq(lists.boardId, list.boardId), isNull(lists.deletedAt))) + .groupBy(lists.index) + .having(gt(countExpr, 1)); + + if (duplicateIndices.length > 0) { + throw new Error( + `Duplicate indices found after reordering in board ${list.boardId}`, + ); + } + + const updatedList = await tx.query.lists.findFirst({ + columns: { + publicId: true, + name: true, + }, + where: eq(lists.publicId, args.listPublicId), + }); + + return updatedList; + }); }; export const shiftIndex = async (