From 8fdacd138819822309b89df66995d942f8c11be1 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 2 Jan 2024 11:24:08 +0000 Subject: [PATCH] feat: delete lists --- src/app/_components/dashboard.tsx | 18 +- .../boards/[...id]/DeleteListConfirmation.tsx | 58 ++ src/app/boards/[...id]/ListDropdown.tsx | 54 ++ src/app/boards/[...id]/page.tsx | 28 +- src/app/layout.tsx | 2 +- src/server/api/routers/board.ts | 1 + src/server/api/routers/list.ts | 28 +- .../db/migrations/0005_hard_colossus.sql | 2 + .../db/migrations/meta/0005_snapshot.json | 661 ++++++++++++++++++ src/server/db/migrations/meta/_journal.json | 7 + src/server/db/schema.ts | 11 +- 11 files changed, 852 insertions(+), 18 deletions(-) create mode 100644 src/app/boards/[...id]/DeleteListConfirmation.tsx create mode 100644 src/app/boards/[...id]/ListDropdown.tsx create mode 100644 src/server/db/migrations/0005_hard_colossus.sql create mode 100644 src/server/db/migrations/meta/0005_snapshot.json diff --git a/src/app/_components/dashboard.tsx b/src/app/_components/dashboard.tsx index 7a204de3..ae2ef2c2 100644 --- a/src/app/_components/dashboard.tsx +++ b/src/app/_components/dashboard.tsx @@ -29,7 +29,7 @@ export default async function Layout(props: { children: React.ReactNode }) {
+
{props.children}
diff --git a/src/app/boards/[...id]/DeleteListConfirmation.tsx b/src/app/boards/[...id]/DeleteListConfirmation.tsx new file mode 100644 index 00000000..9971cb09 --- /dev/null +++ b/src/app/boards/[...id]/DeleteListConfirmation.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { api } from "~/trpc/react"; +import { useBoard } from "~/app/providers/board"; +import { useModal } from "~/app/providers/modal"; + +interface DeleteListConfirmationProps { + listPublicId: string; +} + +export function DeleteListConfirmation({ + listPublicId, +}: DeleteListConfirmationProps) { + const utils = api.useUtils(); + const { boardData } = useBoard(); + const { closeModal } = useModal(); + + const refetchBoard = () => + utils.board.byId.refetch({ id: boardData.publicId }); + + const deleteList = api.list.delete.useMutation({ + onSuccess: async () => { + closeModal(); + await refetchBoard(); + }, + }); + + return ( + <> +
+

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

+

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

+
+
+ + +
+ + ); +} diff --git a/src/app/boards/[...id]/ListDropdown.tsx b/src/app/boards/[...id]/ListDropdown.tsx new file mode 100644 index 00000000..34d35e32 --- /dev/null +++ b/src/app/boards/[...id]/ListDropdown.tsx @@ -0,0 +1,54 @@ +import { Fragment } from "react"; +import { Menu, Transition } from "@headlessui/react"; +import { HiEllipsisHorizontal } from "react-icons/hi2"; +import { useModal } from "~/app/providers/modal"; + +interface ListDropdownProps { + setSelectedPublicListId: () => void; +} + +export default function ListDropdown({ + setSelectedPublicListId, +}: ListDropdownProps) { + const { openModal } = useModal(); + + const handleOpenDeleteListConfirmation = () => { + setSelectedPublicListId(); + openModal("DELETE_LIST"); + }; + + return ( + +
+ + + +
+ + + +
+ + {() => ( + + )} + +
+
+
+
+ ); +} diff --git a/src/app/boards/[...id]/page.tsx b/src/app/boards/[...id]/page.tsx index 929ebb1c..bcad63ef 100644 --- a/src/app/boards/[...id]/page.tsx +++ b/src/app/boards/[...id]/page.tsx @@ -18,6 +18,8 @@ import { useModal } from "~/app/providers/modal"; import Modal from "~/app/_components/modal"; +import { DeleteListConfirmation } from "./DeleteListConfirmation"; +import ListDropdown from "./ListDropdown"; import { NewCardForm } from "./NewCardForm"; import { NewListForm } from "./NewListForm"; @@ -210,15 +212,22 @@ export default function BoardPage() {

{list.name}

- + + setSelectedPublicListId(list.publicId) + } /> - + {(provided) => ( @@ -283,6 +292,9 @@ export default function BoardPage() { + {modalContentType === "DELETE_LIST" && ( + + )} {modalContentType === "NEW_CARD" && ( )} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c25bbc7b..9b20f0df 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -26,7 +26,7 @@ export default function RootLayout({ }) { return ( - + {children} diff --git a/src/server/api/routers/board.ts b/src/server/api/routers/board.ts index d62992aa..27293753 100644 --- a/src/server/api/routers/board.ts +++ b/src/server/api/routers/board.ts @@ -35,6 +35,7 @@ export const boardRouter = createTRPCRouter({ with: { lists: { orderBy: [asc(lists.index)], + where: isNull(cards.deletedAt), columns: { publicId: true, name: true, diff --git a/src/server/api/routers/list.ts b/src/server/api/routers/list.ts index 64e0a1ae..e52d0e23 100644 --- a/src/server/api/routers/list.ts +++ b/src/server/api/routers/list.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { desc, eq, sql } from "drizzle-orm"; -import { boards, lists } from "~/server/db/schema"; +import { boards, cards, lists } from "~/server/db/schema"; import { generateUID } from "~/utils/generateUID"; import { @@ -79,5 +79,29 @@ export const listRouter = createTRPCRouter({ WHERE ${lists.boardId} = ${list.boardId}; `); }) - }) + }), + delete: publicProcedure + .input( + z.object({ + listPublicId: z.string().min(12), + })) + .mutation(({ ctx, input }) => { + const userId = ctx.session?.user.id; + + if (!userId) return; + + return ctx.db.transaction(async (tx) => { + const list = await tx.query.lists.findFirst({ + where: eq(lists.publicId, input.listPublicId), + }) + + if (!list) return; + + await tx.update(lists).set({ deletedAt: new Date(), deletedBy: userId}).where(eq(lists.id, list.id)); + + await tx.update(cards).set({ deletedAt: new Date(), deletedBy: userId}).where(eq(cards.listId, list.id)); + + await tx.execute(sql`UPDATE ${lists} SET ${lists.index} = ${lists.index} - 1 WHERE ${lists.boardId} = ${list.boardId} AND ${lists.index} > ${list.index} AND ${lists.deletedAt} IS NULL;`); + }) + }), }); \ No newline at end of file diff --git a/src/server/db/migrations/0005_hard_colossus.sql b/src/server/db/migrations/0005_hard_colossus.sql new file mode 100644 index 00000000..e8ab0416 --- /dev/null +++ b/src/server/db/migrations/0005_hard_colossus.sql @@ -0,0 +1,2 @@ +ALTER TABLE `list` ADD `deletedAt` timestamp;--> statement-breakpoint +ALTER TABLE `list` ADD `deletedBy` varchar(256); \ No newline at end of file diff --git a/src/server/db/migrations/meta/0005_snapshot.json b/src/server/db/migrations/meta/0005_snapshot.json new file mode 100644 index 00000000..c322eb5e --- /dev/null +++ b/src/server/db/migrations/meta/0005_snapshot.json @@ -0,0 +1,661 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "263f81b0-eac9-47a0-9d24-ed61d6dd6061", + "prevId": "52bf8e38-66ad-4bb6-9924-798177dbd1b5", + "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" + ] + } + } + }, + "card_label": { + "name": "card_label", + "columns": { + "cardId": { + "name": "cardId", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "labelId": { + "name": "labelId", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "card_label_cardId_card_id_fk": { + "name": "card_label_cardId_card_id_fk", + "tableFrom": "card_label", + "tableTo": "card", + "columnsFrom": [ + "cardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "card_label_labelId_label_id_fk": { + "name": "card_label_labelId_label_id_fk", + "tableFrom": "card_label", + "tableTo": "label", + "columnsFrom": [ + "labelId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "card_label_cardId_labelId": { + "name": "card_label_cardId_labelId", + "columns": [ + "cardId", + "labelId" + ] + } + }, + "uniqueConstraints": {} + }, + "label": { + "name": "label", + "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 + }, + "colourCode": { + "name": "colourCode", + "type": "varchar(12)", + "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 + }, + "boardId": { + "name": "boardId", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "label_id": { + "name": "label_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "label_publicId_unique": { + "name": "label_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 + }, + "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": { + "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 82124e39..382539d4 100644 --- a/src/server/db/migrations/meta/_journal.json +++ b/src/server/db/migrations/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1702924641678, "tag": "0004_tidy_lord_hawal", "breakpoints": true + }, + { + "idx": 5, + "version": "5", + "when": 1704193352729, + "tag": "0005_hard_colossus", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index 72e43d06..75d0facd 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -103,7 +103,9 @@ export const lists = mySqlTable( .notNull(), updatedAt: timestamp("updatedAt").onUpdateNow(), boardId: bigint("boardId", { mode: "number" }).notNull(), - index: int("index").notNull() + index: int("index").notNull(), + deletedAt: timestamp("deletedAt"), + deletedBy: varchar("deletedBy", { length: 256 }) } ); @@ -116,7 +118,11 @@ export const listsRelations = relations(lists, ({ one, many }) => ({ fields: [lists.boardId], references: [boards.id], }), - cards: many(cards) + cards: many(cards), + deletedBy: one(users, { + fields: [lists.deletedBy], + references: [users.id], + }), })); export const cards = mySqlTable( @@ -151,7 +157,6 @@ export const cardsRelations = relations(cards, ({ one, many }) => ({ fields: [cards.deletedBy], references: [users.id], }), - labels: many(cardsToLabels) }));