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 (