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() {
+
+ {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", {