feat: delete lists

This commit is contained in:
Henry
2024-01-02 11:24:08 +00:00
parent 0a0bfe9c76
commit 8fdacd1388
11 changed files with 852 additions and 18 deletions

View File

@@ -29,7 +29,7 @@ export default async function Layout(props: { children: React.ReactNode }) {
<div className="flex h-full w-full">
<nav className="flex w-72 flex-col justify-between border-r border-dark-600 px-5 py-5">
<div>
<ul role="list" className="-mx-2 my-6 space-y-1">
<ul role="list" className="-mx-2 my-3 space-y-1">
{navigation.map((item) => (
<li key={item.name}>
<ReactiveButton
@@ -42,7 +42,6 @@ export default async function Layout(props: { children: React.ReactNode }) {
))}
</ul>
</div>
<button className="flex items-center">
{session?.user.image ? (
<Image
@@ -52,12 +51,23 @@ export default async function Layout(props: { children: React.ReactNode }) {
height={30}
alt=""
/>
) : null}
<p className="ml-2 truncate text-sm text-dark-1000">
) : (
<span className="inline-block h-6 w-6 overflow-hidden rounded-full bg-dark-400">
<svg
className="h-full w-full text-dark-700"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</span>
)}
<p className="ml-2 truncate text-sm text-dark-900">
{session?.user.email}
</p>
</button>
</nav>
<div className="w-full overflow-hidden">{props.children}</div>
</div>
</main>

View File

@@ -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 (
<>
<div className="flex w-full flex-col justify-between pb-4">
<h2 className="text-md pb-4 font-medium text-dark-1000">
Are you sure you want to delete this list?
</h2>
<p className="text-sm font-medium text-dark-900">
{"This action can't be undone."}
</p>
</div>
<div className="mt-5 flex justify-end sm:mt-6">
<button
className="mr-4 inline-flex justify-center rounded-md border-[1px] border-dark-600 bg-dark-300 px-3 py-2 text-sm font-semibold text-dark-1000 shadow-sm focus-visible:outline-none"
onClick={() => closeModal()}
>
Cancel
</button>
<button
onClick={() =>
deleteList.mutate({
listPublicId,
})
}
className="inline-flex justify-center rounded-md bg-dark-1000 px-3 py-2 text-sm font-semibold text-dark-50 shadow-sm focus-visible:outline-none"
>
Delete
</button>
</div>
</>
);
}

View File

@@ -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 (
<Menu as="div" className="relative inline-block text-left">
<div>
<Menu.Button className="mr-1 inline-flex h-fit items-center rounded-md p-1 px-1 text-sm font-semibold text-dark-50 hover:bg-dark-400">
<HiEllipsisHorizontal className="h-5 w-5 text-dark-900" />
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md border border-dark-400 bg-dark-300 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="flex">
<Menu.Item>
{() => (
<button
onClick={handleOpenDeleteListConfirmation}
className="m-1 w-full rounded-[5px] px-3 py-2 text-left text-sm text-dark-1000 hover:bg-dark-400"
>
Delete list
</button>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
);
}

View File

@@ -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() {
<p className="mb-4 px-4 pt-1 text-sm font-medium text-dark-1000">
{list.name}
</p>
<button
className="mx-1 inline-flex h-fit items-center rounded-md p-1 px-1 text-sm font-semibold text-dark-50 hover:bg-dark-400"
onClick={() => openNewCardForm(list.publicId)}
>
<HiOutlinePlusSmall
className="h-5 w-5 text-dark-900"
aria-hidden="true"
<div>
<button
className="mx-1 inline-flex h-fit items-center rounded-md p-1 px-1 text-sm font-semibold text-dark-50 hover:bg-dark-400"
onClick={() => openNewCardForm(list.publicId)}
>
<HiOutlinePlusSmall
className="h-5 w-5 text-dark-900"
aria-hidden="true"
/>
</button>
<ListDropdown
setSelectedPublicListId={() =>
setSelectedPublicListId(list.publicId)
}
/>
</button>
</div>
</div>
<Droppable droppableId={`${list.publicId}`} type="CARD">
{(provided) => (
@@ -283,6 +292,9 @@ export default function BoardPage() {
</DragDropContext>
</div>
<Modal>
{modalContentType === "DELETE_LIST" && (
<DeleteListConfirmation listPublicId={selectedPublicListId} />
)}
{modalContentType === "NEW_CARD" && (
<NewCardForm listPublicId={selectedPublicListId} />
)}

View File

@@ -26,7 +26,7 @@ export default function RootLayout({
}) {
return (
<html lang="en" className={`${jakarta.variable}`}>
<body>
<body className="h-screen overflow-hidden">
<TRPCReactProvider headers={headers()}>
<ModalProvider>
<BoardProvider>{children}</BoardProvider>

View File

@@ -35,6 +35,7 @@ export const boardRouter = createTRPCRouter({
with: {
lists: {
orderBy: [asc(lists.index)],
where: isNull(cards.deletedAt),
columns: {
publicId: true,
name: true,

View File

@@ -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;`);
})
}),
});

View File

@@ -0,0 +1,2 @@
ALTER TABLE `list` ADD `deletedAt` timestamp;--> statement-breakpoint
ALTER TABLE `list` ADD `deletedBy` varchar(256);

View File

@@ -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": {}
}
}

View File

@@ -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
}
]
}

View File

@@ -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)
}));