diff --git a/src/app/boards/[...id]/components/BoardDropdown.tsx b/src/app/boards/[...id]/components/BoardDropdown.tsx new file mode 100644 index 00000000..9745f590 --- /dev/null +++ b/src/app/boards/[...id]/components/BoardDropdown.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 BoardDropdown() { + const { openModal } = useModal(); + + return ( + +
+ + + +
+ + + +
+ + {() => ( + + )} + +
+
+
+
+ ); +} diff --git a/src/app/boards/[...id]/components/DeleteBoardConfirmation.tsx b/src/app/boards/[...id]/components/DeleteBoardConfirmation.tsx new file mode 100644 index 00000000..aafd45bf --- /dev/null +++ b/src/app/boards/[...id]/components/DeleteBoardConfirmation.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useRouter } from "next/navigation"; + +import { api } from "~/trpc/react"; +import { useBoard } from "~/app/providers/board"; +import { useModal } from "~/app/providers/modal"; + +export function DeleteBoardConfirmation() { + const router = useRouter(); + const { boardData } = useBoard(); + const { closeModal } = useModal(); + + const deleteBoard = api.board.delete.useMutation({ + onSuccess: () => { + closeModal(); + router.push(`/boards`); + }, + }); + + return ( + <> +
+

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

+

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

+
+
+ + +
+ + ); +} diff --git a/src/app/boards/[...id]/DeleteListConfirmation.tsx b/src/app/boards/[...id]/components/DeleteListConfirmation.tsx similarity index 100% rename from src/app/boards/[...id]/DeleteListConfirmation.tsx rename to src/app/boards/[...id]/components/DeleteListConfirmation.tsx diff --git a/src/app/boards/[...id]/ListDropdown.tsx b/src/app/boards/[...id]/components/ListDropdown.tsx similarity index 100% rename from src/app/boards/[...id]/ListDropdown.tsx rename to src/app/boards/[...id]/components/ListDropdown.tsx diff --git a/src/app/boards/[...id]/NewCardForm.tsx b/src/app/boards/[...id]/components/NewCardForm.tsx similarity index 100% rename from src/app/boards/[...id]/NewCardForm.tsx rename to src/app/boards/[...id]/components/NewCardForm.tsx diff --git a/src/app/boards/[...id]/NewListForm.tsx b/src/app/boards/[...id]/components/NewListForm.tsx similarity index 100% rename from src/app/boards/[...id]/NewListForm.tsx rename to src/app/boards/[...id]/components/NewListForm.tsx diff --git a/src/app/boards/[...id]/page.tsx b/src/app/boards/[...id]/page.tsx index bcad63ef..f55c0442 100644 --- a/src/app/boards/[...id]/page.tsx +++ b/src/app/boards/[...id]/page.tsx @@ -18,10 +18,12 @@ 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"; +import BoardDropdown from "./components/BoardDropdown"; +import { DeleteBoardConfirmation } from "./components/DeleteBoardConfirmation"; +import { DeleteListConfirmation } from "./components/DeleteListConfirmation"; +import ListDropdown from "./components/ListDropdown"; +import { NewCardForm } from "./components/NewCardForm"; +import { NewListForm } from "./components/NewListForm"; interface List { publicId: string; @@ -169,10 +171,10 @@ export default function BoardPage() { className="block border-0 bg-transparent p-0 py-1.5 font-medium tracking-tight text-dark-1000 focus:ring-0 focus-visible:outline-none sm:text-[1.2rem] sm:leading-6" /> -
+
+
@@ -292,6 +295,7 @@ export default function BoardPage() { + {modalContentType === "DELETE_BOARD" && } {modalContentType === "DELETE_LIST" && ( )} diff --git a/src/app/boards/boards.tsx b/src/app/boards/components/BoardsList.tsx similarity index 95% rename from src/app/boards/boards.tsx rename to src/app/boards/components/BoardsList.tsx index af7ca448..9c682f40 100644 --- a/src/app/boards/boards.tsx +++ b/src/app/boards/components/BoardsList.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { api } from "~/trpc/react"; -export function Boards() { +export function BoardsList() { const { data } = api.board.all.useQuery(); if (data?.length === 0) return <>; diff --git a/src/app/boards/create.tsx b/src/app/boards/components/NewBoardForm.tsx similarity index 92% rename from src/app/boards/create.tsx rename to src/app/boards/components/NewBoardForm.tsx index 4c3df173..2c9f89c6 100644 --- a/src/app/boards/create.tsx +++ b/src/app/boards/components/NewBoardForm.tsx @@ -13,11 +13,15 @@ interface FormValues { } export function NewBoardForm() { + const utils = api.useUtils(); const { closeModal } = useModal(); + const refetchBoards = () => utils.board.all.refetch(); + const createBoard = api.board.create.useMutation({ - onSuccess() { + onSuccess: async () => { closeModal(); + await refetchBoards(); }, }); diff --git a/src/app/boards/page.tsx b/src/app/boards/page.tsx index 710c3dc2..9f77aead 100644 --- a/src/app/boards/page.tsx +++ b/src/app/boards/page.tsx @@ -1,12 +1,12 @@ "use client"; import { HiOutlinePlusSmall } from "react-icons/hi2"; -import { Boards } from "./boards"; +import { BoardsList } from "./components/BoardsList"; import { useModal } from "~/app/providers/modal"; import Modal from "~/app/_components/modal"; -import { NewBoardForm } from "~/app/boards/create"; +import { NewBoardForm } from "~/app/boards/components/NewBoardForm"; export default function BoardsPage() { const { openModal } = useModal(); @@ -37,7 +37,7 @@ export default function BoardsPage() {
- +
); diff --git a/src/server/api/routers/board.ts b/src/server/api/routers/board.ts index 27293753..ebc7d895 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, isNull } from "drizzle-orm"; +import { and, eq, asc, isNull, inArray } from "drizzle-orm"; import { boards, cards, lists } from "~/server/db/schema"; import { generateUID } from "~/utils/generateUID"; @@ -16,7 +16,7 @@ export const boardRouter = createTRPCRouter({ if (!userId) return; return ctx.db.query.boards.findMany({ - where: eq(boards.createdBy, userId), + where: and(eq(boards.createdBy, userId), isNull(boards.deletedAt)), columns: { publicId: true, name: true, @@ -27,7 +27,7 @@ export const boardRouter = createTRPCRouter({ .input(z.object({ id: z.string().min(12) })) .query(({ ctx, input }) => ctx.db.query.boards.findFirst({ - where: eq(boards.publicId, input.id), + where: and(eq(boards.publicId, input.id), isNull(boards.deletedAt)), columns: { publicId: true, name: true, @@ -106,4 +106,34 @@ export const boardRouter = createTRPCRouter({ return ctx.db.update(boards).set({ name: input.name }).where(eq(boards.publicId, input.boardId)); }), + delete: publicProcedure + .input( + z.object({ + boardPublicId: z.string().min(12), + })) + .mutation(({ ctx, input }) => { + const userId = ctx.session?.user.id; + + if (!userId) return; + + return ctx.db.transaction(async (tx) => { + const board = await tx.query.boards.findFirst({ + where: eq(boards.publicId, input.boardPublicId), + with: { + lists: true, + } + }) + + if (!board) return; + + const listIds = board.lists.map((list) => list.id) + + await tx.update(boards).set({ deletedAt: new Date(), deletedBy: userId }).where(eq(boards.id, board.id)); + + if (listIds.length) { + await tx.update(lists).set({ deletedAt: new Date(), deletedBy: userId }).where(eq(lists.boardId, board.id)); + await tx.update(cards).set({ deletedAt: new Date(), deletedBy: userId }).where(inArray(cards.listId, listIds)); + } + }) + }), }); diff --git a/src/server/db/migrations/0006_lame_pepper_potts.sql b/src/server/db/migrations/0006_lame_pepper_potts.sql new file mode 100644 index 00000000..a3b93dbf --- /dev/null +++ b/src/server/db/migrations/0006_lame_pepper_potts.sql @@ -0,0 +1,2 @@ +ALTER TABLE `board` ADD `deletedAt` timestamp;--> statement-breakpoint +ALTER TABLE `board` ADD `deletedBy` varchar(256); \ No newline at end of file diff --git a/src/server/db/migrations/meta/0006_snapshot.json b/src/server/db/migrations/meta/0006_snapshot.json new file mode 100644 index 00000000..62ab0bb1 --- /dev/null +++ b/src/server/db/migrations/meta/0006_snapshot.json @@ -0,0 +1,675 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "dbc12511-6c1b-4580-b95b-eb8365a5b090", + "prevId": "263f81b0-eac9-47a0-9d24-ed61d6dd6061", + "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 + }, + "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": { + "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 382539d4..c5aa0e56 100644 --- a/src/server/db/migrations/meta/_journal.json +++ b/src/server/db/migrations/meta/_journal.json @@ -43,6 +43,13 @@ "when": 1704193352729, "tag": "0005_hard_colossus", "breakpoints": true + }, + { + "idx": 6, + "version": "5", + "when": 1704196792662, + "tag": "0006_lame_pepper_potts", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index 75d0facd..df7e87e6 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -30,6 +30,8 @@ export const boards = mySqlTable( .default(sql`CURRENT_TIMESTAMP`) .notNull(), updatedAt: timestamp("updatedAt").onUpdateNow(), + deletedAt: timestamp("deletedAt"), + deletedBy: varchar("deletedBy", { length: 256 }) }, ); @@ -39,7 +41,11 @@ export const boardsRelations = relations(boards, ({ one, many }) => ({ references: [users.id], }), lists: many(lists), - labels: many(labels) + labels: many(labels), + deletedBy: one(users, { + fields: [boards.deletedBy], + references: [users.id], + }), })); export const labels = mySqlTable(