diff --git a/src/app/api/senior/[id]/route.schema.ts b/src/app/api/senior/[id]/route.schema.ts index 2d570a65..5027630f 100644 --- a/src/app/api/senior/[id]/route.schema.ts +++ b/src/app/api/senior/[id]/route.schema.ts @@ -6,7 +6,7 @@ import { import { seniorSchema } from "@server/model"; export const seniorDeleteResponse = z.discriminatedUnion("code", [ - z.object({ code: z.literal("SUCCESS") }), + z.object({ code: z.literal("SUCCESS"), seniorId: z.string() }), z.object({ code: z.literal("NOT_FOUND"), message: z.string(), diff --git a/src/app/api/senior/[id]/route.ts b/src/app/api/senior/[id]/route.ts index 63e289dd..38e33072 100644 --- a/src/app/api/senior/[id]/route.ts +++ b/src/app/api/senior/[id]/route.ts @@ -6,10 +6,9 @@ import { patchSeniorSchema, } from "./route.schema"; import { prisma } from "@server/db/client"; +import { driveV3 } from "@server/service"; +import { Senior } from "@prisma/client"; -/** - * @TODO - Delete folder belonging to the senior - */ export const DELETE = withSessionAndRole( ["CHAPTER_LEADER"], async ({ session, params }) => { @@ -39,23 +38,26 @@ export const DELETE = withSessionAndRole( ); } - const disconnectSenior = await prisma.senior.update({ - where: { - id: seniorId, - }, - data: { - Students: { - set: [], + await driveV3.files.delete({ fileId: maybeSenior.folder }); + await prisma.$transaction([ + prisma.senior.update({ + where: { + id: seniorId, }, - }, - }); - const deleteSenior = await prisma.senior.delete({ - where: { - id: seniorId, - }, - }); + data: { + Students: { + set: [], + }, + }, + }), + prisma.senior.delete({ + where: { + id: seniorId, + }, + }), + ]); - return NextResponse.json({ code: "SUCCESS" }); + return NextResponse.json({ code: "SUCCESS", seniorId: seniorId }); } ); @@ -118,6 +120,18 @@ export const PATCH = withSessionAndRole( }, }); + // TODO(nickbar01234) - Refactor for to sync with POST /senior + const toFolderName = (senior: Pick) => + `${senior.firstname}_${senior.lastname}-${seniorId}`; + + if (toFolderName(seniorBody) != toFolderName(maybeSenior)) { + const params = { + fileId: maybeSenior.folder, + resource: { name: toFolderName(seniorBody) }, + }; + await driveV3.files.update(params); + } + return NextResponse.json( seniorPatchResponse.parse({ code: "SUCCESS", diff --git a/src/app/api/senior/route.ts b/src/app/api/senior/route.ts index 81f6a2d3..30cb0b71 100644 --- a/src/app/api/senior/route.ts +++ b/src/app/api/senior/route.ts @@ -74,7 +74,7 @@ export const POST = withSessionAndRole( const baseFolder = chapter.chapterFolder; // TODO: make env variable const fileMetadata = { - name: [`${seniorBody.firstname}_${seniorBody.lastname}_${senior.id}`], + name: [`${seniorBody.firstname}_${seniorBody.lastname}-${senior.id}`], mimeType: "application/vnd.google-apps.folder", parents: [baseFolder], }; diff --git a/src/components/AdminHomePage.tsx b/src/components/AdminHomePage.tsx index 89edabe2..b9416068 100644 --- a/src/components/AdminHomePage.tsx +++ b/src/components/AdminHomePage.tsx @@ -11,6 +11,9 @@ import { useRouter } from "next/navigation"; import SearchableContainer from "./SearchableContainer"; import ChapterRequest from "./ChapterRequest"; import DropDownContainer from "./container/DropDownContainer"; +import { useApiThrottle } from "@hooks"; +import React from "react"; +import { Spinner } from "./skeleton"; type ChapterWithUserAndChapterRequest = Prisma.ChapterGetPayload<{ include: { students: true; chapterRequest: true }; @@ -23,6 +26,15 @@ type AdminHomePageProps = { const AdminHomePage = ({ chapters }: AdminHomePageProps) => { const router = useRouter(); + const [deleteChapterId, setDeleteChapterId] = React.useState(""); + const { fetching, fn: throttleDeleteChapter } = useApiThrottle({ + fn: deleteChapter, + callback: () => { + setDeleteChapterId(""); + router.refresh(); + }, + }); + return ( { options.push({ name: "Remove Chapter", - onClick: async () => { - const response = await deleteChapter(chapter.id); - router.refresh(); + onClick: () => { + setDeleteChapterId(chapter.id); + throttleDeleteChapter(chapter.id); }, color: "#ef6767", icon: , @@ -69,15 +81,19 @@ const AdminHomePage = ({ chapters }: AdminHomePageProps) => { }, ]} topRightButton={ - - } - /> + fetching && chapter.id === deleteChapterId ? ( + + ) : !fetching ? ( + + } + /> + ) : null } moreInformation={ diff --git a/src/components/ChapterRequest.tsx b/src/components/ChapterRequest.tsx index bdc5e3e9..f384a36e 100644 --- a/src/components/ChapterRequest.tsx +++ b/src/components/ChapterRequest.tsx @@ -128,7 +128,7 @@ const ChapterRequest = (props: ChapterRequestProps) => { ) : (
- +
)} diff --git a/src/components/SeniorView.tsx b/src/components/SeniorView.tsx index 68f5c9e4..3b08424a 100644 --- a/src/components/SeniorView.tsx +++ b/src/components/SeniorView.tsx @@ -4,9 +4,13 @@ import { Senior, User } from "@prisma/client"; import SearchableContainer from "./SearchableContainer"; import { UserTile, TileEdit } from "./TileGrid"; import AddSenior from "./AddSenior"; -import { useState } from "react"; +import React, { useState } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faPencil, faTrashCan } from "@fortawesome/free-solid-svg-icons"; +import { useApiThrottle } from "@hooks"; +import { deleteSenior } from "@api/senior/[id]/route.client"; +import { Spinner } from "./skeleton"; +import { seniorFullName } from "@utils"; type SeniorViewProps = { seniors: Senior[]; @@ -26,9 +30,26 @@ export const SeniorView = ({ seniors, students }: SeniorViewProps) => { const [yearsClicked, setYearsClicked] = useState([]); + const [deleteSeniorId, setDeleteSeniorId] = React.useState(""); + + const { fn: throttleDeleteSenior, fetching } = useApiThrottle({ + fn: deleteSenior, + callback: (res) => { + setDeleteSeniorId(""); + if (res.code === "SUCCESS") { + setSeniorsState((seniors) => + seniors.filter((senior) => senior.id !== res.seniorId) + ); + } else { + // Caught by error.tsx + throw new Error("Fail to delete senior"); + } + }, + }); + return ( <> -
+
Seniors {`(${seniors.length})`}
{ options.push({ name: "Delete", - onClick: (e) => { - e.stopPropagation(); - e.preventDefault(); - fetch(`/api/senior/${senior.id}`, { - method: "DELETE", - }).then(() => { - window.location.reload(); - }); + onClick: () => { + setDeleteSeniorId(senior.id); + throttleDeleteSenior({ seniorId: senior.id }); }, color: "#EF6767", icon: , @@ -82,14 +98,18 @@ export const SeniorView = ({ seniors, students }: SeniorViewProps) => { senior={senior} link={`/private/chapter-leader/seniors/${senior.id}`} key={senior.id} - dropdownComponent={} + dropdownComponent={ + fetching && senior.id === deleteSeniorId ? ( + + ) : !fetching ? ( + + ) : null + } /> ); }} search={(senior, key) => - (senior.firstname + " " + senior.lastname) - .toLowerCase() - .includes(key.toLowerCase()) + seniorFullName(senior).toLowerCase().includes(key.toLowerCase()) } filterField={