From 54e542b5b81f3b6f5a3239fa4108125aaa5b97da Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 15 Dec 2023 10:50:19 +0000 Subject: [PATCH] feat: Delete cards --- src/app/boards/[...id]/page.tsx | 2 +- .../components/DeleteCardConfirmation.tsx | 56 ++ src/app/cards/[...id]/components/Dropdown.tsx | 43 ++ src/app/cards/[...id]/page.tsx | 23 +- src/server/api/routers/board.ts | 3 +- src/server/api/routers/card.ts | 56 +- .../db/migrations/0002_colossal_randall.sql | 2 + .../db/migrations/meta/0002_snapshot.json | 508 ++++++++++++++++++ src/server/db/migrations/meta/_journal.json | 7 + src/server/db/schema.ts | 8 +- 10 files changed, 693 insertions(+), 15 deletions(-) create mode 100644 src/app/cards/[...id]/components/DeleteCardConfirmation.tsx create mode 100644 src/app/cards/[...id]/components/Dropdown.tsx create mode 100644 src/server/db/migrations/0002_colossal_randall.sql create mode 100644 src/server/db/migrations/meta/0002_snapshot.json diff --git a/src/app/boards/[...id]/page.tsx b/src/app/boards/[...id]/page.tsx index 2b2488a9..14a9ba1e 100644 --- a/src/app/boards/[...id]/page.tsx +++ b/src/app/boards/[...id]/page.tsx @@ -214,7 +214,7 @@ export default function BoardPage() {
{list.cards?.map((card, index) => ( { + closeModal(); + router.push(`/boards/${boardPublicId}`); + }, + }); + + return ( + <> +
+

+ Are you sure you want to delete this card? +

+

+ {"This action can't be undone."} +

+
+
+ + +
+ + ); +} diff --git a/src/app/cards/[...id]/components/Dropdown.tsx b/src/app/cards/[...id]/components/Dropdown.tsx new file mode 100644 index 00000000..b3ac0e84 --- /dev/null +++ b/src/app/cards/[...id]/components/Dropdown.tsx @@ -0,0 +1,43 @@ +import { Fragment } from "react"; +import { Menu, Transition } from "@headlessui/react"; +import { HiEllipsisHorizontal } from "react-icons/hi2"; +import { useModal } from "~/app/providers/modal"; + +export default function Dropdown() { + const { openModal } = useModal(); + + return ( + +
+ + + +
+ + + +
+ + {() => ( + + )} + +
+
+
+
+ ); +} diff --git a/src/app/cards/[...id]/page.tsx b/src/app/cards/[...id]/page.tsx index b32d59e4..923af118 100644 --- a/src/app/cards/[...id]/page.tsx +++ b/src/app/cards/[...id]/page.tsx @@ -4,8 +4,13 @@ import { useParams } from "next/navigation"; import { useFormik } from "formik"; import ContentEditable from "react-contenteditable"; -import { api } from "~/trpc/react"; +import Dropdown from "./components/Dropdown"; +import { DeleteCardConfirmation } from "./components/DeleteCardConfirmation"; +import Modal from "~/app/_components/modal"; +import { useModal } from "~/app/providers/modal"; + +import { api } from "~/trpc/react"; interface FormValues { cardId: string; title: string; @@ -14,11 +19,14 @@ interface FormValues { export default function CardPage() { const params = useParams(); + const { modalContentType } = useModal(); const cardId = params?.id?.length ? params.id[0] : null; const { data } = api.card.byId.useQuery({ id: cardId ?? "" }); + const boardId = data?.list?.board?.publicId; + const updateCard = api.card.update.useMutation(); const formik = useFormik({ @@ -43,7 +51,7 @@ export default function CardPage() {
-
+
+
+ +
@@ -71,6 +82,14 @@ export default function CardPage() {
+ + {modalContentType === "DELETE_CARD" && ( + + )} +
); } diff --git a/src/server/api/routers/board.ts b/src/server/api/routers/board.ts index 790f9c7c..83180e06 100644 --- a/src/server/api/routers/board.ts +++ b/src/server/api/routers/board.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { eq, asc } from "drizzle-orm"; +import { eq, asc, isNull } from "drizzle-orm"; import { boards, cards, lists } from "~/server/db/schema"; import { generateUID } from "~/utils/generateUID"; @@ -43,6 +43,7 @@ export const boardRouter = createTRPCRouter({ }, with: { cards: { + where: isNull(cards.deletedAt), orderBy: [asc(cards.index)], columns: { publicId: true, diff --git a/src/server/api/routers/card.ts b/src/server/api/routers/card.ts index 14bb1550..d0139e10 100644 --- a/src/server/api/routers/card.ts +++ b/src/server/api/routers/card.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { desc, eq, sql } from "drizzle-orm"; +import { and, desc, eq, isNull, sql } from "drizzle-orm"; import { cards, lists } from "~/server/db/schema"; import { generateUID } from "~/utils/generateUID"; @@ -33,7 +33,7 @@ export const cardRouter = createTRPCRouter({ if (!list) return; const latestCard = await tx.query.cards.findFirst({ - where: eq(cards.listId, list.id), + where: and(eq(cards.listId, list.id), isNull(cards.deletedAt)), columns: { index: true }, @@ -53,7 +53,21 @@ export const cardRouter = createTRPCRouter({ .input(z.object({ id: z.string().min(12) })) .query(({ ctx, input }) => ctx.db.query.cards.findFirst({ - where: eq(cards.publicId, input.id), + with: { + list: { + columns: { + publicId: true, + }, + with: { + board: { + columns: { + publicId: true, + }, + } + } + }, + }, + where: and(eq(cards.publicId, input.id), isNull(cards.deletedAt)), }) ), update: publicProcedure @@ -68,7 +82,29 @@ export const cardRouter = createTRPCRouter({ if (!userId) return; - return ctx.db.update(cards).set({ title: input.title, description: input.description }).where(eq(cards.publicId, input.cardId)); + return ctx.db.update(cards).set({ title: input.title, description: input.description }).where(and(eq(cards.publicId, input.cardId), isNull(cards.deletedAt))); + }), + delete: publicProcedure + .input( + z.object({ + cardPublicId: z.string().min(12), + })) + .mutation(({ ctx, input }) => { + const userId = ctx.session?.user.id; + + if (!userId) return; + + return ctx.db.transaction(async (tx) => { + const card = await tx.query.cards.findFirst({ + where: eq(cards.publicId, input.cardPublicId), + }) + + if (!card) return; + + await tx.update(cards).set({ deletedAt: new Date(), deletedBy: userId}).where(eq(cards.publicId, input.cardPublicId)); + + await tx.execute(sql`UPDATE ${cards} SET ${cards.index} = ${cards.index} - 1 WHERE ${cards.listId} = ${card.listId} AND ${cards.index} > ${card.index} AND ${cards.deletedAt} IS NULL;`); + }) }), reorder: publicProcedure .input( @@ -85,8 +121,8 @@ export const cardRouter = createTRPCRouter({ if (!userId) return; return ctx.db.transaction(async (tx) => { - const [currentList] = await tx.select({ id: lists.id }).from(lists).where(eq(lists.publicId, input.currentListId)) - const [newList] = await tx.select({ id: lists.id }).from(lists).where(eq(lists.publicId, input.newListId)) + const [currentList] = await tx.select({ id: lists.id }).from(lists).where(and(eq(lists.publicId, input.currentListId), isNull(cards.deletedAt))) + const [newList] = await tx.select({ id: lists.id }).from(lists).where(and(eq(lists.publicId, input.newListId), isNull(cards.deletedAt))) if (!currentList?.id || !newList?.id) return; @@ -100,17 +136,17 @@ export const cardRouter = createTRPCRouter({ WHEN ${input.currentIndex} > ${input.newIndex} AND ${cards.index} >= ${input.newIndex} AND ${cards.index} < ${input.currentIndex} THEN ${cards.index} + 1 ELSE ${cards.index} END - WHERE ${cards.listId} = ${currentList.id}; + WHERE ${cards.listId} = ${currentList.id} AND ${cards.deletedAt} IS NULL; `); } else { - await tx.execute(sql`UPDATE ${cards} SET ${cards.index} = ${cards.index} + 1 WHERE ${cards.listId} = ${newList.id} AND ${cards.index} >= ${input.newIndex};`) + await tx.execute(sql`UPDATE ${cards} SET ${cards.index} = ${cards.index} + 1 WHERE ${cards.listId} = ${newList.id} AND ${cards.index} >= ${input.newIndex} AND ${cards.deletedAt} IS NULL;`) - await tx.execute(sql`UPDATE ${cards} SET ${cards.index} = ${cards.index} - 1 WHERE ${cards.listId} = ${currentList.id} AND ${cards.index} >= ${input.currentIndex};`) + await tx.execute(sql`UPDATE ${cards} SET ${cards.index} = ${cards.index} - 1 WHERE ${cards.listId} = ${currentList.id} AND ${cards.index} >= ${input.currentIndex} AND ${cards.deletedAt} IS NULL;`) await tx .update(cards) .set({ listId: newList.id, index: input.newIndex }) - .where(eq(cards.publicId, input.cardId)); + .where(and(eq(cards.publicId, input.cardId), isNull(cards.deletedAt))); } }) }) diff --git a/src/server/db/migrations/0002_colossal_randall.sql b/src/server/db/migrations/0002_colossal_randall.sql new file mode 100644 index 00000000..854f0e95 --- /dev/null +++ b/src/server/db/migrations/0002_colossal_randall.sql @@ -0,0 +1,2 @@ +ALTER TABLE `card` ADD `deletedAt` timestamp;--> statement-breakpoint +ALTER TABLE `card` ADD `deletedBy` varchar(256); \ No newline at end of file diff --git a/src/server/db/migrations/meta/0002_snapshot.json b/src/server/db/migrations/meta/0002_snapshot.json new file mode 100644 index 00000000..57804329 --- /dev/null +++ b/src/server/db/migrations/meta/0002_snapshot.json @@ -0,0 +1,508 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "a623c303-6c0b-416d-aa46-2c0a9e2328cd", + "prevId": "ab7d0b22-efdc-45b3-92cb-86bb630debe8", + "tables": { + "account": { + "name": "account", + "columns": { + "userId": { + "name": "userId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "userId_idx": { + "name": "userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "account_provider_providerAccountId": { + "name": "account_provider_providerAccountId", + "columns": [ + "provider", + "providerAccountId" + ] + } + }, + "uniqueConstraints": {} + }, + "board": { + "name": "board", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdBy": { + "name": "createdBy", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "onUpdate": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "board_id": { + "name": "board_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "board_publicId_unique": { + "name": "board_publicId_unique", + "columns": [ + "publicId" + ] + } + } + }, + "card": { + "name": "card", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdBy": { + "name": "createdBy", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "onUpdate": true + }, + "listId": { + "name": "listId", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "index": { + "name": "index", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "card_id": { + "name": "card_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "card_publicId_unique": { + "name": "card_publicId_unique", + "columns": [ + "publicId" + ] + } + } + }, + "list": { + "name": "list", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdBy": { + "name": "createdBy", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "onUpdate": true + }, + "boardId": { + "name": "boardId", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "index": { + "name": "index", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "list_id": { + "name": "list_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "list_publicId_unique": { + "name": "list_publicId_unique", + "columns": [ + "publicId" + ] + } + } + }, + "session": { + "name": "session", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "userId_idx": { + "name": "userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "session_sessionToken": { + "name": "session_sessionToken", + "columns": [ + "sessionToken" + ] + } + }, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3)" + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_id": { + "name": "user_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ] + } + } + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token": { + "name": "verificationToken_identifier_token", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {} + } + }, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/src/server/db/migrations/meta/_journal.json b/src/server/db/migrations/meta/_journal.json index 10c04f27..a2c48fe3 100644 --- a/src/server/db/migrations/meta/_journal.json +++ b/src/server/db/migrations/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1701122139821, "tag": "0001_warm_jamie_braddock", "breakpoints": true + }, + { + "idx": 2, + "version": "5", + "when": 1702630369122, + "tag": "0002_colossal_randall", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index 7396370a..2708c52c 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -82,7 +82,9 @@ export const cards = mySqlTable( .notNull(), updatedAt: timestamp("updatedAt").onUpdateNow(), listId: bigint("listId", { mode: "number" }).notNull(), - index: int("index").notNull() + index: int("index").notNull(), + deletedAt: timestamp("deletedAt"), + deletedBy: varchar("deletedBy", { length: 256 }) } ); @@ -95,6 +97,10 @@ export const cardsRelations = relations(cards, ({ one }) => ({ fields: [cards.listId], references: [lists.id], }), + deletedBy: one(users, { + fields: [cards.deletedBy], + references: [users.id], + }), })); export const users = mySqlTable("user", {