From 5aa5bc766df23e4d672eb776c3163465ea8b3d5c Mon Sep 17 00:00:00 2001 From: Nick Doan Date: Mon, 15 Apr 2024 14:29:29 -0400 Subject: [PATCH] Add file edit options on frontend (#164) * Add file edit options on frontend * Add duplicate check for file creation * Update logic to check duplicates * Fix padding * Fix query logic --- prisma/schema.prisma | 4 + src/app/api/file/[fileId]/route.client.ts | 6 - src/app/api/file/[fileId]/route.ts | 150 ++++-------------- src/app/api/file/route.ts | 26 +-- src/app/api/senior/[id]/route.ts | 7 +- src/app/api/user/[uid]/edit-seniors/route.ts | 4 +- .../seniors/[seniorId]/page.tsx | 8 +- src/components/AddSenior.tsx | 2 +- src/components/container/HeaderContainer.tsx | 2 +- src/components/file/AddFile.tsx | 13 ++ src/components/file/File.tsx | 73 ++++++++- src/components/file/FileTile.tsx | 2 +- src/components/senior/DisplaySenior.tsx | 16 +- src/components/user/AddFile.tsx | 101 ++++++++---- 14 files changed, 231 insertions(+), 183 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b82554e2..35e32620 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -105,7 +105,11 @@ model File { id String @id @default(auto()) @map("_id") @db.ObjectId date DateTime // will zero out the hours filetype String + + // Use with Google API + fileId String url String + seniorId String @db.ObjectId senior Senior @relation(fields: [seniorId], references: [id], onDelete: Cascade) Tags String[] diff --git a/src/app/api/file/[fileId]/route.client.ts b/src/app/api/file/[fileId]/route.client.ts index 16461c1e..647b8275 100644 --- a/src/app/api/file/[fileId]/route.client.ts +++ b/src/app/api/file/[fileId]/route.client.ts @@ -2,14 +2,8 @@ import { z } from "zod"; import { FileResponse } from "./route.schema"; import { File } from "@server/model"; -/** - * Describe the interface of SignInRequest. - */ type IFile = z.infer; -/** - * Extends the parameters of fetch() function to give types to the RequestBody. - */ interface IUpdateRequest extends Omit { fileId: string; body: IFile; diff --git a/src/app/api/file/[fileId]/route.ts b/src/app/api/file/[fileId]/route.ts index d8edae36..c7beea90 100644 --- a/src/app/api/file/[fileId]/route.ts +++ b/src/app/api/file/[fileId]/route.ts @@ -9,14 +9,13 @@ import { withSession } from "@server/decorator"; import { driveV3 } from "@server/service"; import moment from "moment"; -export const PATCH = withSession(async (request) => { - const body = await request.req.json(); - const nextParams: { fileId: string } = request.params.params; +export const PATCH = withSession(async ({ params, session, req }) => { + const body = await req.json(); + const nextParams: { fileId: string } = params.params; const { fileId } = nextParams; const fileRequest = File.safeParse(body); if (!fileRequest.success) { - console.log(fileRequest.error); return NextResponse.json( FileResponse.parse({ code: "INVALID_REQUEST", @@ -26,57 +25,31 @@ export const PATCH = withSession(async (request) => { ); } else { const fileData = fileRequest.data; - - // Check that user has this senior assigned to them - const user = await prisma.user.findFirst({ + const maybeFile = await prisma.file.findFirst({ where: { - id: request.session.user.id, + id: fileId, }, + include: { senior: true }, }); - if (user === null || user.SeniorIDs === null) { - return NextResponse.json( - FileResponse.parse({ - code: "INVALID_REQUEST", - message: "Not a valid request", - }), - { status: 400 } - ); - } + const otherFiles = await prisma.file.findMany({ + where: { + date: fileData.date, + seniorId: fileData.seniorId, + id: { + not: fileId, + }, + }, + }); if ( - !user.SeniorIDs.some((seniorId: string) => seniorId === fileData.seniorId) + otherFiles.length > 0 || + maybeFile == null || + !maybeFile.senior.StudentIDs.includes(session.user.id) ) { - return NextResponse.json( - FileResponse.parse({ - code: "INVALID_REQUEST", - message: "Not a valid request", - }), - { status: 400 } - ); - } - - // Get senior from database - const foundSenior = await prisma.senior.findUnique({ - where: { id: fileData.seniorId }, - }); - - if (foundSenior === null) { - return NextResponse.json( - FileResponse.parse({ - code: "INVALID_REQUEST", - message: "Not a valid request", - }), - { status: 400 } - ); - } - - // query that date doesn't already exist - const foundFile = await prisma.file.findFirst({ - where: { date: fileData.date, seniorId: fileData.seniorId }, - }); - - if (foundFile !== null) { + // If there are other files with the same date, then the user can't take it + // If file is not found then file is deleted + // If senior doesn't include current user then they have been deselected return NextResponse.json( FileResponse.parse({ code: "INVALID_REQUEST", @@ -91,30 +64,13 @@ export const PATCH = withSession(async (request) => { const body = { name: formatted_date }; const fileUpdateData = { - fileId: fileId, + fileId: maybeFile.fileId, resource: body, }; await driveV3.files.update(fileUpdateData); - - const file = await prisma.file.findFirst({ - where: { - url: fileData.url, - }, - }); - - if (file === null) { - return NextResponse.json( - FileResponse.parse({ - code: "INVALID_REQUEST", - message: "Not a valid request", - }), - { status: 400 } - ); - } - await prisma.file.update({ - where: { id: file.id }, + where: { id: maybeFile.id }, data: { date: fileData.date, Tags: fileData.Tags }, }); @@ -128,68 +84,32 @@ export const PATCH = withSession(async (request) => { } }); -export const DELETE = withSession(async (request) => { - const nextParams: { fileId: string } = request.params.params; +export const DELETE = withSession(async ({ params, session }) => { + const nextParams: { fileId: string } = params.params; const { fileId } = nextParams; - // Check that user has this senior assigned to them - const user = await prisma.user.findFirst({ - where: { - id: request.session.user.id, - }, - }); - - if (user === null || user.SeniorIDs === null) { - return NextResponse.json( - FileResponse.parse({ - code: "INVALID_REQUEST", - message: "Not a valid request", - }), - { status: 400 } - ); - } const file = await prisma.file.findFirst({ where: { - url: `https://docs.google.com/document/d/${fileId}`, + id: fileId, + }, + include: { + senior: true, }, }); - if (file === null) { + if (file == null || !file.senior.StudentIDs.includes(session.user.id)) { + // File has been deleted or student has been deselected return NextResponse.json( FileResponse.parse({ - code: "INVALID_REQUEST", - message: "Not a valid request", - }), - { status: 400 } - ); - } - - if (!user.SeniorIDs.some((seniorId: string) => seniorId === file.seniorId)) { - return NextResponse.json( - FileResponse.parse({ - code: "NOT_AUTHORIZED", - message: "Senior not assigned to user", - }), - { status: 404 } - ); - } - - // Get senior from database - const foundSenior = await prisma.senior.findUnique({ - where: { id: file.seniorId }, - }); - if (foundSenior === null) { - return NextResponse.json( - FileResponse.parse({ - code: "INVALID_REQUEST", - message: "Not a valid request", + code: "SUCCESS_DELETE", + message: "File successfully deleted", }), - { status: 400 } + { status: 200 } ); } await driveV3.files.delete({ - fileId: fileId, + fileId: file.fileId, }); await prisma.file.delete({ diff --git a/src/app/api/file/route.ts b/src/app/api/file/route.ts index 37d91361..ef78c1f7 100644 --- a/src/app/api/file/route.ts +++ b/src/app/api/file/route.ts @@ -27,8 +27,19 @@ export const POST = withSession(async (request) => { id: request.session.user.id, }, }); + const otherFiles = await prisma.file.findMany({ + where: { + date: fileData.date, + seniorId: fileData.seniorId, + }, + }); - if (user === null || user.SeniorIDs === null) { + if ( + user === null || + user.SeniorIDs === null || + !user.SeniorIDs.some((seniorId) => seniorId === fileData.seniorId) || + otherFiles.length > 0 + ) { return NextResponse.json( FileResponse.parse({ code: "INVALID_REQUEST", @@ -38,18 +49,6 @@ export const POST = withSession(async (request) => { ); } - if ( - !user.SeniorIDs.some((seniorId: string) => seniorId === fileData.seniorId) - ) { - return NextResponse.json( - FileResponse.parse({ - code: "NOT_AUTHORIZED", - message: "Senior not assigned to user", - }), - { status: 404 } - ); - } - // Get senior from database const foundSenior = await prisma.senior.findUnique({ where: { id: fileData.seniorId }, @@ -88,6 +87,7 @@ export const POST = withSession(async (request) => { data: { date: fileData.date, filetype: fileData.filetype, + fileId: googleFileId ?? "", url: `https://docs.google.com/document/d/${googleFileId}`, seniorId: fileData.seniorId, Tags: fileData.Tags, diff --git a/src/app/api/senior/[id]/route.ts b/src/app/api/senior/[id]/route.ts index 66d4ca62..63e289dd 100644 --- a/src/app/api/senior/[id]/route.ts +++ b/src/app/api/senior/[id]/route.ts @@ -106,13 +106,14 @@ export const PATCH = withSessionAndRole( ); const studentIDsToAdd = seniorBody.StudentIDs; + const { StudentIDs: _, ...other } = seniorBody; const senior = await prisma.senior.update({ where: { id: seniorId }, data: { - ...seniorBody, + ...other, Students: { - connect: studentIDsToAdd.map((id) => ({ id })), - disconnect: studentIDsToRemove.map((id) => ({ id })), + connect: studentIDsToAdd.map((id) => ({ id: id })), + disconnect: studentIDsToRemove.map((id) => ({ id: id })), }, }, }); diff --git a/src/app/api/user/[uid]/edit-seniors/route.ts b/src/app/api/user/[uid]/edit-seniors/route.ts index b80f9060..c01a4b57 100644 --- a/src/app/api/user/[uid]/edit-seniors/route.ts +++ b/src/app/api/user/[uid]/edit-seniors/route.ts @@ -31,8 +31,8 @@ export const PATCH = withSession( where: { id: uid }, data: { Seniors: { - connect: seniorIDsToAdd.map((id) => ({ id })), - disconnect: seniorIDsToRemove.map((id) => ({ id })), + connect: seniorIDsToAdd.map((id) => ({ id: id })), + disconnect: seniorIDsToRemove.map((id) => ({ id: id })), }, }, }); diff --git a/src/app/private/chapter-leader/seniors/[seniorId]/page.tsx b/src/app/private/chapter-leader/seniors/[seniorId]/page.tsx index fbbf5e77..e0886ed4 100644 --- a/src/app/private/chapter-leader/seniors/[seniorId]/page.tsx +++ b/src/app/private/chapter-leader/seniors/[seniorId]/page.tsx @@ -1,6 +1,7 @@ import PathNav from "@components/PathNav"; import { DisplaySenior } from "@components/senior"; import { prisma } from "@server/db/client"; +import { getServerSessionOrRedirect } from "@server/utils"; import { seniorFullName } from "@utils"; interface LayoutProps { @@ -10,6 +11,7 @@ interface LayoutProps { } const Page = async ({ params }: LayoutProps) => { + const session = await getServerSessionOrRedirect(); const senior = await prisma.senior.findFirstOrThrow({ where: { id: params.seniorId }, include: { @@ -33,7 +35,11 @@ const Page = async ({ params }: LayoutProps) => { }, ]} /> - + ); }; diff --git a/src/components/AddSenior.tsx b/src/components/AddSenior.tsx index 7d664eb2..0a00206a 100644 --- a/src/components/AddSenior.tsx +++ b/src/components/AddSenior.tsx @@ -78,7 +78,7 @@ const StudentSelector = ({ return (
- Assign students + Assign members
items={students} diff --git a/src/components/container/HeaderContainer.tsx b/src/components/container/HeaderContainer.tsx index 2e71277a..6f5814da 100644 --- a/src/components/container/HeaderContainer.tsx +++ b/src/components/container/HeaderContainer.tsx @@ -20,7 +20,7 @@ const HeaderContainer = (props: IHeaderContainer) => { {showHorizontalLine && (
)} -
{children}
+
{children}
); }; diff --git a/src/components/file/AddFile.tsx b/src/components/file/AddFile.tsx index 20db76ff..8a27c46c 100644 --- a/src/components/file/AddFile.tsx +++ b/src/components/file/AddFile.tsx @@ -6,15 +6,20 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useState } from "react"; import AddFilePopup from "@components/user/AddFile"; import { File as PrismaFile } from "@prisma/client"; +import React from "react"; const AddFile = ({ seniorId, seniorFolder, files, + editFile, + setEditFile, }: { seniorId: string; seniorFolder: string; files: PrismaFile[]; + editFile?: PrismaFile; + setEditFile: React.Dispatch>; }) => { const [showAddFilePopUp, setShowAddFilePopUp] = useState(false); @@ -22,6 +27,12 @@ const AddFile = ({ setShowAddFilePopUp(!showAddFilePopUp); }; + React.useEffect(() => { + if (editFile != null) { + setShowAddFilePopUp(true); + } + }, [editFile]); + return (
{showAddFilePopUp ? ( @@ -31,6 +42,8 @@ const AddFile = ({ seniorId={seniorId} files={files} folder={seniorFolder} + editFile={editFile} + setEditFile={setEditFile} /> ) : null} @@ -168,19 +200,28 @@ const AddFile = ({ selectedTags.length == 0 || excludedDatesString.includes(startDate.toDateString()) } - onClick={() => { - throttledCreateFile({ - body: { - date: startDate, - filetype: "Google Document", - url: "", - Tags: selectedTags.map((tagProp) => tagProp.name), - seniorId: seniorId, - }, - }); + onClick={async () => { + editFile == null + ? await throttledCreateFile({ + body: { + date: startDate, + filetype: "Google Document", + url: "", + Tags: selectedTags.map((tagProp) => tagProp.name), + seniorId: seniorId, + }, + }) + : await throttleUpdateFile({ + fileId: editFile.id, + body: { + ...editFile, + date: startDate, + Tags: selectedTags.map((tagProp) => tagProp.name), + }, + }); }} > - {!fetching ? "Create" : "Loading..."} + {fetching ? "Loading..." : editFile == null ? "Create" : "Update"}
@@ -194,7 +235,7 @@ const AddFile = ({