refactor: move reorder card to update handler
This commit is contained in:
@@ -138,7 +138,7 @@ export default function BoardPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateCardMutation = api.card.reorder.useMutation({
|
const updateCardMutation = api.card.update.useMutation({
|
||||||
onMutate: async (args) => {
|
onMutate: async (args) => {
|
||||||
await utils.board.byId.cancel();
|
await utils.board.byId.cancel();
|
||||||
|
|
||||||
@@ -148,21 +148,29 @@ export default function BoardPage() {
|
|||||||
if (!oldBoard) return oldBoard;
|
if (!oldBoard) return oldBoard;
|
||||||
|
|
||||||
const updatedLists = Array.from(oldBoard.lists);
|
const updatedLists = Array.from(oldBoard.lists);
|
||||||
const sourceList = updatedLists.find(
|
|
||||||
(list) => list.publicId === args.currentListPublicId,
|
const sourceList = updatedLists.find((list) =>
|
||||||
|
list.cards.some((card) => card.publicId === args.cardPublicId),
|
||||||
);
|
);
|
||||||
const destinationList = updatedLists.find(
|
const destinationList = updatedLists.find(
|
||||||
(list) => list.publicId === args.newListPublicId,
|
(list) => list.publicId === args.listPublicId,
|
||||||
);
|
);
|
||||||
const removedCard = sourceList?.cards.splice(args.currentIndex, 1)[0];
|
|
||||||
|
const cardToMove = sourceList?.cards.find(
|
||||||
|
(card) => card.publicId === args.cardPublicId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!cardToMove) return oldBoard;
|
||||||
|
|
||||||
|
const removedCard = sourceList?.cards.splice(cardToMove.index, 1)[0];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
sourceList &&
|
sourceList &&
|
||||||
destinationList &&
|
destinationList &&
|
||||||
removedCard &&
|
removedCard &&
|
||||||
args.newIndex !== undefined
|
args.index !== undefined
|
||||||
) {
|
) {
|
||||||
destinationList.cards.splice(args.newIndex, 0, removedCard);
|
destinationList.cards.splice(args.index, 0, removedCard);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...oldBoard,
|
...oldBoard,
|
||||||
@@ -217,10 +225,9 @@ export default function BoardPage() {
|
|||||||
if (type === "CARD") {
|
if (type === "CARD") {
|
||||||
updateCardMutation.mutate({
|
updateCardMutation.mutate({
|
||||||
cardPublicId: draggableId,
|
cardPublicId: draggableId,
|
||||||
currentListPublicId: source.droppableId,
|
|
||||||
newListPublicId: destination.droppableId,
|
listPublicId: destination.droppableId,
|
||||||
currentIndex: source.index,
|
index: destination.index,
|
||||||
newIndex: destination.index,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -575,8 +575,10 @@ export const cardRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
cardPublicId: z.string().min(12),
|
cardPublicId: z.string().min(12),
|
||||||
title: z.string().min(1),
|
title: z.string().min(1).optional(),
|
||||||
description: z.string(),
|
description: z.string().optional(),
|
||||||
|
index: z.number().optional(),
|
||||||
|
listPublicId: z.string().min(12).optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(z.custom<Awaited<ReturnType<typeof cardRepo.update>>>())
|
.output(z.custom<Awaited<ReturnType<typeof cardRepo.update>>>())
|
||||||
@@ -594,6 +596,23 @@ export const cardRouter = createTRPCRouter({
|
|||||||
input.cardPublicId,
|
input.cardPublicId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let newListId: number | undefined;
|
||||||
|
|
||||||
|
if (input.listPublicId) {
|
||||||
|
const newList = await listRepo.getByPublicId(
|
||||||
|
ctx.db,
|
||||||
|
input.listPublicId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!newList)
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `List with public ID ${input.listPublicId} not found`,
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
});
|
||||||
|
|
||||||
|
newListId = newList.id;
|
||||||
|
}
|
||||||
|
|
||||||
if (!existingCard) {
|
if (!existingCard) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
message: `Card with public ID ${input.cardPublicId} not found`,
|
message: `Card with public ID ${input.cardPublicId} not found`,
|
||||||
@@ -601,11 +620,33 @@ export const cardRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await cardRepo.update(
|
let result:
|
||||||
ctx.db,
|
| {
|
||||||
{ title: input.title, description: input.description },
|
id: number;
|
||||||
{ cardPublicId: input.cardPublicId },
|
title: string;
|
||||||
);
|
description: string | null;
|
||||||
|
publicId: string;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
if (input.title || input.description) {
|
||||||
|
result = await cardRepo.update(
|
||||||
|
ctx.db,
|
||||||
|
{
|
||||||
|
...(input.title && { title: input.title }),
|
||||||
|
...(input.description && { description: input.description }),
|
||||||
|
},
|
||||||
|
{ cardPublicId: input.cardPublicId },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.index !== undefined) {
|
||||||
|
result = await cardRepo.reorder(ctx.db, {
|
||||||
|
cardId: existingCard.id,
|
||||||
|
newIndex: input.index,
|
||||||
|
newListId: newListId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!result)
|
if (!result)
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -705,111 +746,4 @@ export const cardRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
reorder: protectedProcedure
|
|
||||||
.meta({
|
|
||||||
openapi: {
|
|
||||||
summary: "Reorder a card",
|
|
||||||
method: "PUT",
|
|
||||||
path: "/cards/{cardPublicId}/reorder",
|
|
||||||
description: "Reorders the position of a card in a given list",
|
|
||||||
tags: ["Cards"],
|
|
||||||
protect: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
cardPublicId: z.string().min(12),
|
|
||||||
currentListPublicId: z.string().min(12).optional(),
|
|
||||||
newListPublicId: z.string().min(12),
|
|
||||||
currentIndex: z.number().optional(),
|
|
||||||
newIndex: z.number().optional(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.output(z.object({ success: z.boolean() }))
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const userId = ctx.user?.id;
|
|
||||||
|
|
||||||
if (!userId)
|
|
||||||
throw new TRPCError({
|
|
||||||
message: `User not authenticated`,
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
});
|
|
||||||
|
|
||||||
const card = await cardRepo.getCardWithListByPublicId(
|
|
||||||
ctx.db,
|
|
||||||
input.cardPublicId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!card?.list)
|
|
||||||
throw new TRPCError({
|
|
||||||
message: `Card with public ID ${input.cardPublicId} not found`,
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentList = card.list;
|
|
||||||
const currentIndex = card.index;
|
|
||||||
|
|
||||||
let newIndex = input.newIndex;
|
|
||||||
|
|
||||||
const newList = await listRepo.getWithCardsByPublicId(
|
|
||||||
ctx.db,
|
|
||||||
input.newListPublicId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!newList)
|
|
||||||
throw new TRPCError({
|
|
||||||
message: `List with public ID ${input.newListPublicId} not found`,
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (newIndex === undefined) {
|
|
||||||
const lastCardIndex = newList.cards.length
|
|
||||||
? newList.cards[0]?.index
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
newIndex = lastCardIndex !== undefined ? lastCardIndex + 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { success } = await cardRepo.reorder(ctx.supabaseClient, {
|
|
||||||
currentListId: currentList.id,
|
|
||||||
newListId: newList.id,
|
|
||||||
currentIndex,
|
|
||||||
newIndex,
|
|
||||||
cardId: card.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!success)
|
|
||||||
throw new TRPCError({
|
|
||||||
message: `Failed to reorder card`,
|
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
|
||||||
});
|
|
||||||
|
|
||||||
const activities = [];
|
|
||||||
|
|
||||||
if (currentIndex !== newIndex) {
|
|
||||||
activities.push({
|
|
||||||
type: "card.updated.index" as const,
|
|
||||||
cardId: card.id,
|
|
||||||
createdBy: userId,
|
|
||||||
fromIndex: currentIndex,
|
|
||||||
toIndex: newIndex,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentList.id !== newList.id) {
|
|
||||||
activities.push({
|
|
||||||
type: "card.updated.list" as const,
|
|
||||||
cardId: card.id,
|
|
||||||
createdBy: userId,
|
|
||||||
fromListId: currentList.id,
|
|
||||||
toListId: newList.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activities.length > 0) {
|
|
||||||
await cardActivityRepo.bulkCreate(ctx.db, activities);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success };
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -167,8 +167,6 @@ export const listRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
let result: { name: string; publicId: string } | undefined;
|
let result: { name: string; publicId: string } | undefined;
|
||||||
|
|
||||||
console.log({ input });
|
|
||||||
|
|
||||||
if (input.name) {
|
if (input.name) {
|
||||||
result = await listRepo.update(
|
result = await listRepo.update(
|
||||||
ctx.db,
|
ctx.db,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { RouterInputs, RouterOutputs } from "../index";
|
|||||||
|
|
||||||
export type GetBoardByIdOutput = RouterOutputs["board"]["byId"];
|
export type GetBoardByIdOutput = RouterOutputs["board"]["byId"];
|
||||||
export type GetCardByIdOutput = RouterOutputs["card"]["byId"];
|
export type GetCardByIdOutput = RouterOutputs["card"]["byId"];
|
||||||
export type ReorderCardInput = RouterInputs["card"]["reorder"];
|
|
||||||
export type UpdateBoardInput = RouterInputs["board"]["update"];
|
export type UpdateBoardInput = RouterInputs["board"]["update"];
|
||||||
export type NewLabelInput = RouterInputs["label"]["create"];
|
export type NewLabelInput = RouterInputs["label"]["create"];
|
||||||
export type NewListInput = RouterInputs["list"]["create"];
|
export type NewListInput = RouterInputs["list"]["create"];
|
||||||
|
|||||||
@@ -1,54 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
LANGUAGE PLPGSQL
|
|
||||||
AS $$
|
|
||||||
DECLARE
|
|
||||||
card_index INT;
|
|
||||||
BEGIN
|
|
||||||
SELECT index INTO card_index FROM card WHERE "listId" = current_list_id AND id = card_id AND "deletedAt" IS NULL;
|
|
||||||
|
|
||||||
IF current_list_id = new_list_id THEN
|
|
||||||
UPDATE card
|
|
||||||
SET index =
|
|
||||||
CASE
|
|
||||||
WHEN index = current_index 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 "listId" = current_list_id AND "deletedAt" IS NULL;
|
|
||||||
ELSE
|
|
||||||
UPDATE card
|
|
||||||
SET index = index + 1
|
|
||||||
WHERE "listId" = new_list_id AND index >= new_index AND "deletedAt" IS NULL;
|
|
||||||
|
|
||||||
UPDATE card
|
|
||||||
SET index = index - 1
|
|
||||||
WHERE "listId" = current_list_id AND index >= current_index AND "deletedAt" IS NULL;
|
|
||||||
|
|
||||||
UPDATE card
|
|
||||||
SET "listId" = new_list_id, index = new_index
|
|
||||||
WHERE id = card_id AND "deletedAt" IS NULL;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- Check for duplicate indices in both affected lists
|
|
||||||
IF EXISTS (
|
|
||||||
SELECT index, COUNT(*)
|
|
||||||
FROM card
|
|
||||||
WHERE "listId" IN (current_list_id, new_list_id)
|
|
||||||
AND "deletedAt" IS NULL
|
|
||||||
GROUP BY "listId", index
|
|
||||||
HAVING COUNT(*) > 1
|
|
||||||
) THEN
|
|
||||||
RAISE EXCEPTION 'Duplicate indices found after reordering in list % or %', current_list_id, new_list_id;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
RETURN TRUE;
|
|
||||||
END;
|
|
||||||
$$;
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION shift_list_index(board_id BIGINT, list_index INT)
|
CREATE OR REPLACE FUNCTION shift_list_index(board_id BIGINT, list_index INT)
|
||||||
RETURNS VOID
|
RETURNS VOID
|
||||||
LANGUAGE SQL
|
LANGUAGE SQL
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||||
import { and, asc, eq, inArray, isNull, sql } from "drizzle-orm";
|
import { and, asc, desc, eq, gt, inArray, isNull, sql } from "drizzle-orm";
|
||||||
|
|
||||||
import type { dbClient } from "@kan/db/client";
|
import type { dbClient } from "@kan/db/client";
|
||||||
import type { Database } from "@kan/db/types/database.types";
|
import type { Database } from "@kan/db/types/database.types";
|
||||||
@@ -108,8 +108,8 @@ export const bulkCreateCardWorkspaceMemberRelationships = async (
|
|||||||
export const update = async (
|
export const update = async (
|
||||||
db: dbClient,
|
db: dbClient,
|
||||||
cardInput: {
|
cardInput: {
|
||||||
title: string;
|
title?: string;
|
||||||
description: string;
|
description?: string;
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
cardPublicId: string;
|
cardPublicId: string;
|
||||||
@@ -428,26 +428,143 @@ export const getWithListAndMembersByPublicId = async (
|
|||||||
return formattedResult;
|
return formattedResult;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Move to update - should take two arguments cardId and index
|
|
||||||
export const reorder = async (
|
export const reorder = async (
|
||||||
db: SupabaseClient<Database>,
|
db: dbClient,
|
||||||
args: {
|
args: {
|
||||||
currentListId: number;
|
newListId: number | undefined;
|
||||||
newListId: number;
|
newIndex: number | undefined;
|
||||||
currentIndex: number;
|
|
||||||
newIndex: number;
|
|
||||||
cardId: number;
|
cardId: number;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const { error } = await db.rpc("reorder_cards", {
|
return db.transaction(async (tx) => {
|
||||||
current_list_id: args.currentListId,
|
const card = await tx.query.cards.findFirst({
|
||||||
new_list_id: args.newListId,
|
columns: {
|
||||||
current_index: args.currentIndex,
|
id: true,
|
||||||
new_index: args.newIndex,
|
index: true,
|
||||||
card_id: args.cardId,
|
},
|
||||||
});
|
where: and(eq(cards.id, args.cardId), isNull(cards.deletedAt)),
|
||||||
|
with: {
|
||||||
|
list: {
|
||||||
|
columns: {
|
||||||
|
id: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return { success: !error };
|
if (!card?.list)
|
||||||
|
throw new Error(`Card not found for public ID ${args.cardId}`);
|
||||||
|
|
||||||
|
const currentList = card.list;
|
||||||
|
const currentIndex = card.index;
|
||||||
|
let newList:
|
||||||
|
| { id: number; index: number; cards: { id: number; index: number }[] }
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
if (args.newListId) {
|
||||||
|
newList = await tx.query.lists.findFirst({
|
||||||
|
columns: {
|
||||||
|
id: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
with: {
|
||||||
|
cards: {
|
||||||
|
columns: {
|
||||||
|
id: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
orderBy: desc(cards.index),
|
||||||
|
limit: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: and(eq(lists.id, args.newListId), isNull(lists.deletedAt)),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!newList)
|
||||||
|
throw new Error(`List not found for public ID ${args.newListId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let newIndex = args.newIndex;
|
||||||
|
|
||||||
|
if (newIndex === undefined) {
|
||||||
|
const lastCardIndex = newList?.cards.length
|
||||||
|
? newList.cards[0]?.index
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
newIndex = lastCardIndex !== undefined ? lastCardIndex + 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentList.id === newList?.id) {
|
||||||
|
await tx.execute(sql`
|
||||||
|
UPDATE card
|
||||||
|
SET index =
|
||||||
|
CASE
|
||||||
|
WHEN index = ${currentIndex} THEN ${newIndex}
|
||||||
|
WHEN ${currentIndex} < ${newIndex} AND index > ${currentIndex} AND index <= ${newIndex} THEN index - 1
|
||||||
|
WHEN ${currentIndex} > ${newIndex} AND index >= ${newIndex} AND index < ${currentIndex} THEN index + 1
|
||||||
|
ELSE index
|
||||||
|
END
|
||||||
|
WHERE "listId" = ${currentList.id} AND "deletedAt" IS NULL;
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
await tx.execute(sql`
|
||||||
|
UPDATE card
|
||||||
|
SET index = index + 1
|
||||||
|
WHERE "listId" = ${newList?.id} AND index >= ${newIndex} AND "deletedAt" IS NULL;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await tx.execute(sql`
|
||||||
|
UPDATE card
|
||||||
|
SET index = index - 1
|
||||||
|
WHERE "listId" = ${currentList.id} AND index >= ${currentIndex} AND "deletedAt" IS NULL;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await tx.execute(sql`
|
||||||
|
UPDATE card
|
||||||
|
SET "listId" = ${newList?.id}, index = ${newIndex}
|
||||||
|
WHERE id = ${card.id} AND "deletedAt" IS NULL;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const countExpr = sql<number>`COUNT(*)`.mapWith(Number);
|
||||||
|
|
||||||
|
const duplicateIndices = await db
|
||||||
|
.select({
|
||||||
|
index: cards.index,
|
||||||
|
count: countExpr,
|
||||||
|
})
|
||||||
|
.from(cards)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(
|
||||||
|
cards.listId,
|
||||||
|
[currentList.id, newList?.id].filter((id) => id !== undefined),
|
||||||
|
),
|
||||||
|
isNull(cards.deletedAt),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.groupBy(cards.listId, cards.index)
|
||||||
|
.having(gt(countExpr, 1));
|
||||||
|
|
||||||
|
if (duplicateIndices.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Duplicate indices found after reordering card ${card.id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedCard = await tx.query.cards.findFirst({
|
||||||
|
columns: {
|
||||||
|
id: true,
|
||||||
|
publicId: true,
|
||||||
|
title: true,
|
||||||
|
description: true,
|
||||||
|
},
|
||||||
|
where: eq(cards.id, card.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedCard;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Again should be handled in update transaction
|
// Again should be handled in update transaction
|
||||||
|
|||||||
Reference in New Issue
Block a user