From ec3c10d181314fa6d144399c624073f2f237f0dc Mon Sep 17 00:00:00 2001 From: Arthur Werbrouck Date: Wed, 1 May 2024 00:37:33 +0200 Subject: [PATCH 01/12] skelet for submitting files --- frontend/src/pages/submit/Submit.tsx | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/frontend/src/pages/submit/Submit.tsx b/frontend/src/pages/submit/Submit.tsx index 5d4399da..396df722 100644 --- a/frontend/src/pages/submit/Submit.tsx +++ b/frontend/src/pages/submit/Submit.tsx @@ -3,6 +3,8 @@ import { useTranslation } from "react-i18next" import SubmitForm from "./components/SubmitForm" import SubmitStructure from "./components/SubmitStructure" import { useNavigate } from "react-router-dom" +import React, { useState, useRef} from 'react'; + const Submit = () => { const { t } = useTranslation() @@ -10,7 +12,34 @@ const Submit = () => { const navigate = useNavigate() + // file upload system + const [selectedFile, setSelectedFile] = useState(undefined); + const fileInputRef = useRef(null); + + const handleFileChange = (event: React.ChangeEvent) => { + setSelectedFile(event.target.files?.[0]); + } + + const handleFileUpload = async () => { + if (!selectedFile){ + return alert("Please select a file to upload"); + } + const formData = new FormData(); + formData.append("file", selectedFile as Blob); // Blob atm ma mss is er iets da het moet zijn voor de backend + + const response = await fetch('https://selab2-6.ugent.be/api/submissions/submit', { // juiste url nog toevoegen en body info enzo + method: 'POST', + body: formData, + }); + + if (response.ok) { + alert("File uploaded successfully"); + } else { + alert("Failed to upload file"); + } + + } return ( <>
From ef647bb5c8ccfff00aedae63bb5275745d621600 Mon Sep 17 00:00:00 2001 From: Arthur Werbrouck Date: Sun, 5 May 2024 17:39:29 +0200 Subject: [PATCH 02/12] support multipart file --- frontend/src/util/apiFetch.ts | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/frontend/src/util/apiFetch.ts b/frontend/src/util/apiFetch.ts index 66e6162c..d3544be7 100644 --- a/frontend/src/util/apiFetch.ts +++ b/frontend/src/util/apiFetch.ts @@ -21,7 +21,7 @@ type ApiCallPathValues = {[param: string]: string | number} * const newCourse = await apiFetch("POST", ApiRoutes.COURSES, { name: "New Course" }); * */ -async function apiFetch(method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", route: string, body?: any, pathValues?:ApiCallPathValues): Promise> { +async function apiFetch(method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", route: string, body?: any, pathValues?:ApiCallPathValues, headers?: {[header: string]: string}): Promise> { const account = msalInstance.getActiveAccount() if (!account) { @@ -47,30 +47,31 @@ async function apiFetch(method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", rou tokenExpiry = response.expiresOn // convert expiry time to JavaScript Date } - const headers = { + const defaultHeaders = { Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", + "Content-Type": body instanceof FormData ? undefined : "application/json", } + const finalHeaders = headers ? {...defaultHeaders, ...headers} : defaultHeaders; + const url = new URL(route, serverHost) - const config: AxiosRequestConfig = { - method: method, - url: url.toString(), - headers: headers, - data: body, - } - +const config: AxiosRequestConfig = { + method: method, + url: url.toString(), + headers: finalHeaders, + data: body instanceof FormData ? body : JSON.stringify(body), +} return axios(config) } const apiCall = { - get: async (route: T, pathValues?:ApiCallPathValues) => apiFetch("GET", route,undefined,pathValues) as Promise>, - post: async (route: T, body: POST_Requests[T], pathValues?:ApiCallPathValues) => apiFetch("POST", route, body,pathValues) as Promise>, - put: async (route: T, body: PUT_Requests[T], pathValues?:ApiCallPathValues) => apiFetch("PUT", route, body,pathValues) as Promise>, - delete: async (route: T, body: DELETE_Requests[T], pathValues?:ApiCallPathValues) => apiFetch("DELETE", route, body,pathValues), - patch: async (route: T, body: Partial, pathValues?:ApiCallPathValues) => apiFetch("PATCH", route, body,pathValues) as Promise>, + get: async (route: T, pathValues?:ApiCallPathValues, headers?: {[header: string]: string}) => apiFetch("GET", route, undefined, pathValues, headers) as Promise>, + post: async (route: T, body: POST_Requests[T] | FormData, pathValues?:ApiCallPathValues, headers?: {[header: string]: string}) => apiFetch("POST", route, body, pathValues, headers) as Promise>, + put: async (route: T, body: PUT_Requests[T], pathValues?:ApiCallPathValues, headers?: {[header: string]: string}) => apiFetch("PUT", route, body, pathValues, headers) as Promise>, + delete: async (route: T, body: DELETE_Requests[T], pathValues?:ApiCallPathValues, headers?: {[header: string]: string}) => apiFetch("DELETE", route, body, pathValues, headers), + patch: async (route: T, body: Partial, pathValues?:ApiCallPathValues, headers?: {[header: string]: string}) => apiFetch("PATCH", route, body, pathValues, headers) as Promise>, } const apiCallInit = async () => { From 25cff9251798922cf432de6a0d56066778887687 Mon Sep 17 00:00:00 2001 From: Arthur Werbrouck Date: Sun, 5 May 2024 17:39:41 +0200 Subject: [PATCH 03/12] submission support route --- frontend/src/@types/requests.d.ts | 415 +++++++++++++++--------------- 1 file changed, 209 insertions(+), 206 deletions(-) diff --git a/frontend/src/@types/requests.d.ts b/frontend/src/@types/requests.d.ts index 40104f52..6c0c3c67 100644 --- a/frontend/src/@types/requests.d.ts +++ b/frontend/src/@types/requests.d.ts @@ -4,43 +4,44 @@ import type {ProjectFormData} from "../pages/projectCreate/components/ProjectCre * Routes used to make API calls */ export enum ApiRoutes { - USER_COURSES = "api/courses", - COURSES = "api/courses", - - COURSE = "api/courses/:courseId", - COURSE_MEMBERS = "api/courses/:courseId/members", - COURSE_MEMBER = "api/courses/:courseId/members/:userId", - COURSE_PROJECTS = "api/courses/:id/projects", - COURSE_CLUSTERS = "api/courses/:id/clusters", - COURSE_GRADES = '/api/courses/:id/grades', - COURSE_LEAVE = "api/courses/:courseId/leave", + USER_COURSES = "api/courses", + COURSES = "api/courses", - PROJECTS = "api/projects", - PROJECT = "api/projects/:id", - PROJECT_CREATE = "api/courses/:courseId/projects", - PROJECT_TESTS = "api/projects/:id/tests", - PROJECT_SUBMISSIONS = "api/projects/:id/submissions", - PROJECT_SCORE = "api/projects/:id/groups/:groupId/score", - PROJECT_GROUP = "api/projects/:id/groups/:groupId", - PROJECT_GROUPS = "api/projects/:id/groups", - PROJECT_GROUP_SUBMISSIONS = "api/projects/:projectId/submissions/:groupId", + COURSE = "api/courses/:courseId", + COURSE_MEMBERS = "api/courses/:courseId/members", + COURSE_MEMBER = "api/courses/:courseId/members/:userId", + COURSE_PROJECTS = "api/courses/:id/projects", + COURSE_CLUSTERS = "api/courses/:id/clusters", + COURSE_GRADES = '/api/courses/:id/grades', + COURSE_LEAVE = "api/courses/:courseId/leave", - SUBMISSION = "api/submissions/:id", - SUBMISSION_FILE = "api/submissions/:id/file", - SUBMISSION_STRUCTURE_FEEDBACK= "/api/submissions/:id/structurefeedback", - SUBMISSION_DOCKER_FEEDBACK= "/api/submissions/:id/dockerfeedback", + PROJECTS = "api/projects", + PROJECT = "api/projects/:id", + PROJECT_CREATE = "api/courses/:courseId/projects", + PROJECT_TESTS = "api/projects/:id/tests", + PROJECT_SUBMISSIONS = "api/projects/:id/submissions", + PROJECT_SUBMIT = "api/projects/:id/submit", + PROJECT_SCORE = "api/projects/:id/groups/:groupId/score", + PROJECT_GROUP = "api/projects/:id/groups/:groupId", + PROJECT_GROUPS = "api/projects/:id/groups", + PROJECT_GROUP_SUBMISSIONS = "api/projects/:projectId/submissions/:groupId", - CLUSTER = "api/clusters/:id", + SUBMISSION = "api/submissions/:id", + SUBMISSION_FILE = "api/submissions/:id/file", + SUBMISSION_STRUCTURE_FEEDBACK = "/api/submissions/:id/structurefeedback", + SUBMISSION_DOCKER_FEEDBACK = "/api/submissions/:id/dockerfeedback", - GROUP = "api/groups/:id", - GROUP_MEMBERS = "api/groups/:id/members", - GROUP_MEMBER = "api/groups/:id/members/:userId", - GROUP_SUBMISSIONS = "api/projects/:id/groups/:id/submissions", + CLUSTER = "api/clusters/:id", - TEST = "api/test", - USER = "api/users/:id", - USERS = "api/users", - USER_AUTH = "api/user", + GROUP = "api/groups/:id", + GROUP_MEMBERS = "api/groups/:id/members", + GROUP_MEMBER = "api/groups/:id/members/:userId", + GROUP_SUBMISSIONS = "api/projects/:id/groups/:id/submissions", + + TEST = "api/test", + USER = "api/users/:id", + USERS = "api/users", + USER_AUTH = "api/user", } export type Timestamp = string @@ -49,22 +50,25 @@ export type Timestamp = string * the body of the POST requests */ export type POST_Requests = { - [ApiRoutes.COURSES]: { - name: string - description:string - } - [ApiRoutes.PROJECT_CREATE]: - ProjectFormData + [ApiRoutes.COURSES]: { + name: string + description: string + } + [ApiRoutes.PROJECT_CREATE]: + ProjectFormData [ApiRoutes.GROUP_MEMBERS]: { - id: number + id: number + } + [ApiRoutes.PROJECT_SUBMIT]: { + file: FormData } - [ApiRoutes.COURSE_CLUSTERS]: { - name: string - capacity: number - groupCount: number - } + [ApiRoutes.COURSE_CLUSTERS]: { + name: string + capacity: number + groupCount: number + } } /** @@ -72,22 +76,22 @@ export type POST_Requests = { */ export type POST_Responses = { - [ApiRoutes.COURSES]: GET_Responses[ApiRoutes.COURSE], - [ApiRoutes.PROJECT_CREATE]: GET_Responses[ApiRoutes.PROJECT] - [ApiRoutes.GROUP_MEMBERS]: GET_Responses[ApiRoutes.GROUP_MEMBERS] - [ApiRoutes.COURSE_CLUSTERS]: GET_Responses[ApiRoutes.CLUSTER] - + [ApiRoutes.COURSES]: GET_Responses[ApiRoutes.COURSE], + [ApiRoutes.PROJECT_CREATE]: GET_Responses[ApiRoutes.PROJECT] + [ApiRoutes.GROUP_MEMBERS]: GET_Responses[ApiRoutes.GROUP_MEMBERS] + [ApiRoutes.COURSE_CLUSTERS]: GET_Responses[ApiRoutes.CLUSTER] + [ApiRoutes.PROJECT_SUBMIT]: GET_Responses[ApiRoutes.SUBMISSION] } /** * the body of the DELETE requests */ export type DELETE_Requests = { - [ApiRoutes.COURSE]: undefined - [ApiRoutes.PROJECT]: undefined - [ApiRoutes.GROUP_MEMBER]: undefined - [ApiRoutes.COURSE_LEAVE]: undefined - [ApiRoutes.COURSE_MEMBER]: undefined + [ApiRoutes.COURSE]: undefined + [ApiRoutes.PROJECT]: undefined + [ApiRoutes.GROUP_MEMBER]: undefined + [ApiRoutes.COURSE_LEAVE]: undefined + [ApiRoutes.COURSE_MEMBER]: undefined } @@ -95,31 +99,30 @@ export type DELETE_Requests = { * the body of the PUT & PATCH requests */ export type PUT_Requests = { - [ApiRoutes.COURSE]: POST_Requests[ApiRoutes.COURSE] - [ApiRoutes.PROJECT]: ProjectFormData - [ApiRoutes.COURSE_MEMBER]: { relation: CourseRelation } - [ApiRoutes.PROJECT_SCORE]: { score: number | null , feedback: string} + [ApiRoutes.COURSE]: POST_Requests[ApiRoutes.COURSE] + [ApiRoutes.PROJECT]: ProjectFormData + [ApiRoutes.COURSE_MEMBER]: { relation: CourseRelation } + [ApiRoutes.PROJECT_SCORE]: { score: number | null, feedback: string } } - export type PUT_Responses = { - [ApiRoutes.COURSE]: GET_Responses[ApiRoutes.COURSE] - [ApiRoutes.PROJECT]: GET_Responses[ApiRoutes.PROJECT] - [ApiRoutes.COURSE_MEMBER]: GET_Responses[ApiRoutes.COURSE_MEMBERS] - [ApiRoutes.PROJECT_SCORE]: GET_Responses[ApiRoutes.PROJECT_SCORE] + [ApiRoutes.COURSE]: GET_Responses[ApiRoutes.COURSE] + [ApiRoutes.PROJECT]: GET_Responses[ApiRoutes.PROJECT] + [ApiRoutes.COURSE_MEMBER]: GET_Responses[ApiRoutes.COURSE_MEMBERS] + [ApiRoutes.PROJECT_SCORE]: GET_Responses[ApiRoutes.PROJECT_SCORE] } type CourseTeacher = { - name: string - surname: string - url: string, + name: string + surname: string + url: string, } type Course = { - courseUrl: string - name: string + courseUrl: string + name: string } export type ProjectStatus = "correct" | "incorrect" | "not started" @@ -131,148 +134,148 @@ export type UserRole = "student" | "teacher" | "admin" */ export type GET_Responses = { - [ApiRoutes.TEST]: { - name: string - firstName: string - lastName: string - email: string - oid: string - } - [ApiRoutes.PROJECT_SUBMISSIONS]: { - feedback: GET_Responses[ApiRoutes.PROJECT_SCORE] | null, - group: GET_Responses[ApiRoutes.GROUP], - submission: GET_Responses[ApiRoutes.SUBMISSION] | null // null if no submission yet - }[], - [ApiRoutes.PROJECT_GROUP_SUBMISSIONS]: GET_Responses[ApiRoutes.SUBMISSION][] - [ApiRoutes.GROUP_SUBMISSIONS]: GET_Responses[ApiRoutes.SUBMISSION] - [ApiRoutes.SUBMISSION]: { - submissionId: number - projectId: number - groupId: number - structureAccepted: boolean - dockerAccepted: boolean - submissionTime: Timestamp - projectUrl: ApiRoutes.PROJECT - groupUrl: ApiRoutes.GROUP - fileUrl: ApiRoutes.SUBMISSION_FILE - structureFeedbackUrl: ApiRoutes.SUBMISSION_STRUCTURE_FEEDBACK - dockerFeedbackUrl: ApiRoutes.SUBMISSION_DOCKER_FEEDBACK - } - [ApiRoutes.SUBMISSION_FILE]: BlobPart - [ApiRoutes.COURSE_PROJECTS]: GET_Responses[ApiRoutes.PROJECT][] - [ApiRoutes.PROJECT]: { - course: { - name: string - url: string - courseId: number + [ApiRoutes.TEST]: { + name: string + firstName: string + lastName: string + email: string + oid: string } - deadline: Timestamp - description: string - clusterId: number | null; - projectId: number - name: string - submissionUrl: ApiRoutes.PROJECT_GROUP_SUBMISSIONS - testsUrl: string - maxScore:number - visible: boolean - status?: ProjectStatus - progress: { - completed: number - total: number + [ApiRoutes.PROJECT_SUBMISSIONS]: { + feedback: GET_Responses[ApiRoutes.PROJECT_SCORE] | null, + group: GET_Responses[ApiRoutes.GROUP], + submission: GET_Responses[ApiRoutes.SUBMISSION] | null // null if no submission yet + }[], + [ApiRoutes.PROJECT_GROUP_SUBMISSIONS]: GET_Responses[ApiRoutes.SUBMISSION][] + [ApiRoutes.GROUP_SUBMISSIONS]: GET_Responses[ApiRoutes.SUBMISSION] + [ApiRoutes.SUBMISSION]: { + submissionId: number + projectId: number + groupId: number + structureAccepted: boolean + dockerAccepted: boolean + submissionTime: Timestamp + projectUrl: ApiRoutes.PROJECT + groupUrl: ApiRoutes.GROUP + fileUrl: ApiRoutes.SUBMISSION_FILE + structureFeedbackUrl: ApiRoutes.SUBMISSION_STRUCTURE_FEEDBACK + dockerFeedbackUrl: ApiRoutes.SUBMISSION_DOCKER_FEEDBACK + } + [ApiRoutes.SUBMISSION_FILE]: BlobPart + [ApiRoutes.COURSE_PROJECTS]: GET_Responses[ApiRoutes.PROJECT][] + [ApiRoutes.PROJECT]: { + course: { + name: string + url: string + courseId: number + } + deadline: Timestamp + description: string + clusterId: number | null; + projectId: number + name: string + submissionUrl: ApiRoutes.PROJECT_GROUP_SUBMISSIONS + testsUrl: string + maxScore: number + visible: boolean + status?: ProjectStatus + progress: { + completed: number + total: number + }, + groupId: number | null // null if not in a group + } + [ApiRoutes.PROJECT_TESTS]: {} // ?? + [ApiRoutes.GROUP]: { + groupId: number, + capacity: number, + name: string + groupClusterUrl: ApiRoutes.CLUSTER + members: GET_Responses[ApiRoutes.GROUP_MEMBER][] + } + [ApiRoutes.PROJECT_SCORE]: { + score: number | null, + feedback: string | null, + projectId: number, + groupId: number }, - groupId: number | null // null if not in a group - } - [ApiRoutes.PROJECT_TESTS]: {} // ?? - [ApiRoutes.GROUP]: { - groupId: number, - capacity: number, - name: string - groupClusterUrl: ApiRoutes.CLUSTER - members: GET_Responses[ApiRoutes.GROUP_MEMBER][] - } - [ApiRoutes.PROJECT_SCORE]: { - score: number | null, - feedback:string | null, - projectId: number, - groupId: number - }, - [ApiRoutes.GROUP_MEMBER]: { - email: string - name: string - userId: number - } - [ApiRoutes.USERS]: { - name: string - userId: number - url: string - email: string - role: UserRole - } - [ApiRoutes.GROUP_MEMBERS]: GET_Responses[ApiRoutes.GROUP_MEMBER][] + [ApiRoutes.GROUP_MEMBER]: { + email: string + name: string + userId: number + } + [ApiRoutes.USERS]: { + name: string + userId: number + url: string + email: string + role: UserRole + } + [ApiRoutes.GROUP_MEMBERS]: GET_Responses[ApiRoutes.GROUP_MEMBER][] - [ApiRoutes.COURSE_CLUSTERS]: GET_Responses[ApiRoutes.CLUSTER][] - - [ApiRoutes.CLUSTER]: { - clusterId: number; - name: string; - capacity: number; - groupCount: number; - createdAt: Timestamp; - groups: GET_Responses[ApiRoutes.GROUP][] - courseUrl: ApiRoutes.COURSE - } - [ApiRoutes.COURSE]: { - description: string - courseId: number - memberUrl: ApiRoutes.COURSE_MEMBERS - name: string - teacher: CourseTeacher - assistents: CourseTeacher[] - joinUrl: string - archivedAt: Timestamp | null // null if not archived - year: number - createdAt: Timestamp - } - [ApiRoutes.COURSE_MEMBERS]: { - relation: CourseRelation, - user: GET_Responses[ApiRoutes.GROUP_MEMBER] - }[], - [ApiRoutes.USER]: { - courseUrl: string - projects_url: string - url: string - role: UserRole - email: string - id: number - name: string - surname: string - }, - [ApiRoutes.USER_AUTH]: GET_Responses[ApiRoutes.USER], - [ApiRoutes.USER_COURSES]: { - courseId:number, - name:string, - relation: CourseRelation, - memberCount: number, - archivedAt: Timestamp | null, // null if not archived - year: number // Year of the course - url:string - }[], - //[ApiRoutes.PROJECT_GROUP]: GET_Responses[ApiRoutes.CLUSTER_GROUPS][number] - [ApiRoutes.PROJECT_GROUPS]: GET_Responses[ApiRoutes.GROUP][] //GET_Responses[ApiRoutes.PROJECT_GROUP][] + [ApiRoutes.COURSE_CLUSTERS]: GET_Responses[ApiRoutes.CLUSTER][] - [ApiRoutes.PROJECTS]: { - enrolledProjects: {project: GET_Responses[ApiRoutes.PROJECT], status: ProjectStatus}[], - adminProjects: Omit[] - }, + [ApiRoutes.CLUSTER]: { + clusterId: number; + name: string; + capacity: number; + groupCount: number; + createdAt: Timestamp; + groups: GET_Responses[ApiRoutes.GROUP][] + courseUrl: ApiRoutes.COURSE + } + [ApiRoutes.COURSE]: { + description: string + courseId: number + memberUrl: ApiRoutes.COURSE_MEMBERS + name: string + teacher: CourseTeacher + assistents: CourseTeacher[] + joinUrl: string + archivedAt: Timestamp | null // null if not archived + year: number + createdAt: Timestamp + } + [ApiRoutes.COURSE_MEMBERS]: { + relation: CourseRelation, + user: GET_Responses[ApiRoutes.GROUP_MEMBER] + }[], + [ApiRoutes.USER]: { + courseUrl: string + projects_url: string + url: string + role: UserRole + email: string + id: number + name: string + surname: string + }, + [ApiRoutes.USER_AUTH]: GET_Responses[ApiRoutes.USER], + [ApiRoutes.USER_COURSES]: { + courseId: number, + name: string, + relation: CourseRelation, + memberCount: number, + archivedAt: Timestamp | null, // null if not archived + year: number // Year of the course + url: string + }[], + //[ApiRoutes.PROJECT_GROUP]: GET_Responses[ApiRoutes.CLUSTER_GROUPS][number] + [ApiRoutes.PROJECT_GROUPS]: GET_Responses[ApiRoutes.GROUP][] //GET_Responses[ApiRoutes.PROJECT_GROUP][] + + [ApiRoutes.PROJECTS]: { + enrolledProjects: { project: GET_Responses[ApiRoutes.PROJECT], status: ProjectStatus }[], + adminProjects: Omit[] + }, - [ApiRoutes.COURSE_GRADES]: { - projectName: string, - projectUrl: string, - projectId: number, - maxScore: number, - groupFeedback: GET_Responses[ApiRoutes.PROJECT_SCORE] | null - }[] + [ApiRoutes.COURSE_GRADES]: { + projectName: string, + projectUrl: string, + projectId: number, + maxScore: number, + groupFeedback: GET_Responses[ApiRoutes.PROJECT_SCORE] | null + }[] - [ApiRoutes.SUBMISSION_STRUCTURE_FEEDBACK]: string | null // Null if no feedback is given - [ApiRoutes.SUBMISSION_DOCKER_FEEDBACK]: string | null // Null if no feedback is given + [ApiRoutes.SUBMISSION_STRUCTURE_FEEDBACK]: string | null // Null if no feedback is given + [ApiRoutes.SUBMISSION_DOCKER_FEEDBACK]: string | null // Null if no feedback is given } From c47bb93f349bc4e50b97c139799bfd6cb2ed8265 Mon Sep 17 00:00:00 2001 From: Arthur Werbrouck Date: Sun, 5 May 2024 17:39:49 +0200 Subject: [PATCH 04/12] support file submitting --- frontend/src/pages/submit/Submit.tsx | 169 ++++++++---------- .../pages/submit/components/SubmitForm.tsx | 92 +++++----- 2 files changed, 124 insertions(+), 137 deletions(-) diff --git a/frontend/src/pages/submit/Submit.tsx b/frontend/src/pages/submit/Submit.tsx index 396df722..dd4f2f79 100644 --- a/frontend/src/pages/submit/Submit.tsx +++ b/frontend/src/pages/submit/Submit.tsx @@ -1,103 +1,88 @@ -import { Affix, Button, Card, Col, Form, Row, Typography } from "antd" -import { useTranslation } from "react-i18next" +import {Affix, Button, Card, Col, Form, Row, Typography} from "antd" +import {useTranslation} from "react-i18next" import SubmitForm from "./components/SubmitForm" import SubmitStructure from "./components/SubmitStructure" -import { useNavigate } from "react-router-dom" -import React, { useState, useRef} from 'react'; - +import {useNavigate, useParams} from "react-router-dom" +import React, {useState, useRef} from 'react'; +import apiCall from "../../util/apiFetch"; +import {ApiRoutes} from "../../@types/requests.d"; const Submit = () => { - const { t } = useTranslation() - const [form] = Form.useForm() - - const navigate = useNavigate() + const {t} = useTranslation() + const [form] = Form.useForm() + const {projectId} = useParams<{ projectId: string }>() + const [fileAdded, setFileAdded] = useState(false); + const navigate = useNavigate() - // file upload system - const [selectedFile, setSelectedFile] = useState(undefined); - const fileInputRef = useRef(null); - - const handleFileChange = (event: React.ChangeEvent) => { - setSelectedFile(event.target.files?.[0]); +const onSubmit = async (values: any) => { + console.log("Received values of form: ", values) + const file = values[t("project.addFiles")][0].originFileObj + if (!file) { + console.error("No file selected") + return } + const formData = new FormData() + formData.append("file", file) + if (!projectId) return; + const response = await apiCall.post(ApiRoutes.PROJECT_SUBMIT, formData, {id: projectId}) +} + return ( + <> +
+ + + + + - const handleFileUpload = async () => { - if (!selectedFile){ - return alert("Please select a file to upload"); - } - - const formData = new FormData(); - formData.append("file", selectedFile as Blob); // Blob atm ma mss is er iets da het moet zijn voor de backend - - const response = await fetch('https://selab2-6.ugent.be/api/submissions/submit', { // juiste url nog toevoegen en body info enzo - method: 'POST', - body: formData, - }); - - if (response.ok) { - alert("File uploaded successfully"); - } else { - alert("Failed to upload file"); - } - - } - return ( - <> -
- - - - - - + + + + + + + + + + + + +
- - - - - -
- - - - - - -
- - - ) + + ) } export default Submit diff --git a/frontend/src/pages/submit/components/SubmitForm.tsx b/frontend/src/pages/submit/components/SubmitForm.tsx index 32c6f9ad..87022133 100644 --- a/frontend/src/pages/submit/components/SubmitForm.tsx +++ b/frontend/src/pages/submit/components/SubmitForm.tsx @@ -1,51 +1,53 @@ -import { InboxOutlined } from "@ant-design/icons" -import { Form, FormInstance, Upload } from "antd" -import { FC } from "react" -import { useTranslation } from "react-i18next" - - - -const SubmitForm:FC<{form:FormInstance}> = ({form}) => { - - const {t} = useTranslation() - const normFile = (e: any) => { - console.log("Upload event:", e) - if (Array.isArray(e)) { - return e +import {InboxOutlined} from "@ant-design/icons" +import {Form, FormInstance, Upload} from "antd" +import {FC} from "react" +import {useTranslation} from "react-i18next" + + +const SubmitForm: FC<{ form: FormInstance, setFileAdded: (added: boolean) => void, onSubmit: (values: any) => void }> = ({form, setFileAdded, onSubmit}) => { + + const {t} = useTranslation() + const normFile = (e: any) => { + console.log("Upload event:", e) + console.log("ye") + if (Array.isArray(e)) { + return e + } + return e?.fileList } - return e?.fileList - } const onFinish = (values: any) => { - console.log("Received values of form: ", values) - - // TODO: make api call - } - - return ( -
- - - -

- -

-

{t("project.uploadAreaTitle")}

-

{t("project.uploadAreaSubtitle")}

-
-
-
- ) + onSubmit(values); + }; + + return ( +
+ + + { + if (file.status !== 'uploading') { + setFileAdded(true); + } + }} + > +

+ +

+

{t("project.uploadAreaTitle")}

+

{t("project.uploadAreaSubtitle")}

+
+
+
+ ) } export default SubmitForm From fe57b89dcd87b9ae6e1f9ae1f0548c51ece0251d Mon Sep 17 00:00:00 2001 From: Arthur Werbrouck Date: Sun, 5 May 2024 17:41:44 +0200 Subject: [PATCH 05/12] remove debug log --- frontend/src/pages/submit/components/SubmitForm.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/pages/submit/components/SubmitForm.tsx b/frontend/src/pages/submit/components/SubmitForm.tsx index 87022133..9d258b5d 100644 --- a/frontend/src/pages/submit/components/SubmitForm.tsx +++ b/frontend/src/pages/submit/components/SubmitForm.tsx @@ -9,7 +9,6 @@ const SubmitForm: FC<{ form: FormInstance, setFileAdded: (added: boolean) => voi const {t} = useTranslation() const normFile = (e: any) => { console.log("Upload event:", e) - console.log("ye") if (Array.isArray(e)) { return e } From c7b6b1b5eda9f46654696539b592fce6f7daf3cb Mon Sep 17 00:00:00 2001 From: Arthur Werbrouck Date: Sun, 5 May 2024 18:35:13 +0200 Subject: [PATCH 06/12] added zip support --- frontend/package-lock.json | 85 +++++++++++++++++++++++++++- frontend/package.json | 1 + frontend/src/pages/submit/Submit.tsx | 35 ++++++++---- 3 files changed, 107 insertions(+), 14 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4a781014..71f887d7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -26,6 +26,7 @@ "framer-motion": "^11.0.24", "highlight.js": "^11.9.0", "i18next-localstorage-cache": "^1.1.1", + "jszip": "^3.10.1", "lowlight": "^3.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -3545,6 +3546,11 @@ "toggle-selection": "^1.0.6" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -4852,6 +4858,11 @@ "node": ">=4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -4901,8 +4912,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/inline-style-parser": { "version": "0.2.3", @@ -7464,6 +7474,17 @@ "node": ">=6" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -7482,6 +7503,14 @@ "node": ">=6" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -8435,6 +8464,11 @@ "node": ">=6" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "node_modules/parse-entities": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", @@ -8627,6 +8661,11 @@ "node": ">=6" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -9430,6 +9469,25 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -9699,6 +9757,11 @@ "fsevents": "~2.3.2" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -9772,6 +9835,11 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9895,6 +9963,14 @@ "node": ">= 0.4" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/string-convert": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", @@ -10425,6 +10501,11 @@ "react": "^16.8.0 || ^17 || ^18" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/v8-to-istanbul": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0d0e5eec..165c39b5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,7 @@ "framer-motion": "^11.0.24", "highlight.js": "^11.9.0", "i18next-localstorage-cache": "^1.1.1", + "jszip": "^3.10.1", "lowlight": "^3.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/frontend/src/pages/submit/Submit.tsx b/frontend/src/pages/submit/Submit.tsx index dd4f2f79..cfd023ae 100644 --- a/frontend/src/pages/submit/Submit.tsx +++ b/frontend/src/pages/submit/Submit.tsx @@ -6,6 +6,7 @@ import {useNavigate, useParams} from "react-router-dom" import React, {useState, useRef} from 'react'; import apiCall from "../../util/apiFetch"; import {ApiRoutes} from "../../@types/requests.d"; +import JSZip from 'jszip'; const Submit = () => { const {t} = useTranslation() @@ -14,18 +15,28 @@ const Submit = () => { const [fileAdded, setFileAdded] = useState(false); const navigate = useNavigate() -const onSubmit = async (values: any) => { - console.log("Received values of form: ", values) - const file = values[t("project.addFiles")][0].originFileObj - if (!file) { - console.error("No file selected") - return - } - const formData = new FormData() - formData.append("file", file) - if (!projectId) return; - const response = await apiCall.post(ApiRoutes.PROJECT_SUBMIT, formData, {id: projectId}) -} + const onSubmit = async (values: any) => { + console.log("Received values of form: ", values) + const file = values[t("project.addFiles")][0].originFileObj + if (!file) { + console.error("No file selected") + return + } + const formData = new FormData() + + if (file.type === 'application/zip') { + formData.append("file", file); + } else { + const zip = new JSZip(); + zip.file(file.name, file); + const content = await zip.generateAsync({type: "blob"}); + formData.append("file", content, "files.zip"); + } + + if (!projectId) return; + const response = await apiCall.post(ApiRoutes.PROJECT_SUBMIT, formData, {id: projectId}) + console.log(response) + } return ( <>
From 60986e029f552b2653eba77e5067db3065c8888e Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Mon, 6 May 2024 22:35:06 +0200 Subject: [PATCH 07/12] Small bug fixes --- .gitignore | 1 + frontend/src/i18n/en/translation.json | 2 +- frontend/src/i18n/nl/translation.json | 2 +- frontend/src/pages/submit/Submit.tsx | 19 +++++++++---------- .../pages/submit/components/SubmitForm.tsx | 5 ++++- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 991023f6..0a6bb2f9 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ out/ .vscode/ backend/app/data/* backend/data/* +data/* ### Secrets ### backend/app/src/main/resources/application-secrets.properties diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index fb0ba65a..228adcfd 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -84,7 +84,7 @@ "submit": "Submit", "back": "Cancel", "uploadAreaTitle": "Click or drag file to this area to upload", - "uploadAreaSubtitle": "Maximum file size is 10MB", + "uploadAreaSubtitle": "Maximum file size is 100MB", "deadlinePassed": "Deadline passed", "downloadSubmissions": "Download all submissions", "group": "Group", diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index 95cf948e..475c43ad 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -88,7 +88,7 @@ "addFiles": "Bestanden toevoegen", "submit": "Indienen", "uploadAreaTitle": "Bestanden slepen of klikken om bestanden toe te voegen", - "uploadAreaSubtitle": "Maximum bestandsgrootte is 10MB", + "uploadAreaSubtitle": "Maximum bestandsgrootte is 100MB", "deadlinePassed": "Deadline is verstreken", "downloadSubmissions": "Download alle indieningen", "group": "Groep", diff --git a/frontend/src/pages/submit/Submit.tsx b/frontend/src/pages/submit/Submit.tsx index cfd023ae..2b887e74 100644 --- a/frontend/src/pages/submit/Submit.tsx +++ b/frontend/src/pages/submit/Submit.tsx @@ -17,21 +17,20 @@ const Submit = () => { const onSubmit = async (values: any) => { console.log("Received values of form: ", values) - const file = values[t("project.addFiles")][0].originFileObj - if (!file) { - console.error("No file selected") + const files = values.files.map((file: any) => file.originFileObj); + if (files.length === 0) { + console.error("No files selected") return } + console.log(files); const formData = new FormData() - if (file.type === 'application/zip') { - formData.append("file", file); - } else { - const zip = new JSZip(); + const zip = new JSZip(); + files.forEach((file: any) => { zip.file(file.name, file); - const content = await zip.generateAsync({type: "blob"}); - formData.append("file", content, "files.zip"); - } + }); + const content = await zip.generateAsync({type: "blob"}); + formData.append("file", content, "files.zip"); if (!projectId) return; const response = await apiCall.post(ApiRoutes.PROJECT_SUBMIT, formData, {id: projectId}) diff --git a/frontend/src/pages/submit/components/SubmitForm.tsx b/frontend/src/pages/submit/components/SubmitForm.tsx index 9d258b5d..3cbd1b9d 100644 --- a/frontend/src/pages/submit/components/SubmitForm.tsx +++ b/frontend/src/pages/submit/components/SubmitForm.tsx @@ -23,14 +23,17 @@ const SubmitForm: FC<{ form: FormInstance, setFileAdded: (added: boolean) => voi
false} multiple={false} + directory={true} style={{height: "100%"}} onChange={({file}) => { if (file.status !== 'uploading') { From ef0dabfd6d0ed13c9aa8b74de83776ac5c3adb2f Mon Sep 17 00:00:00 2001 From: Arthur Werbrouck Date: Thu, 9 May 2024 18:42:36 +0200 Subject: [PATCH 08/12] allow folders to be uploaded --- frontend/src/pages/submit/components/SubmitForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/submit/components/SubmitForm.tsx b/frontend/src/pages/submit/components/SubmitForm.tsx index 3cbd1b9d..0cb1ec7c 100644 --- a/frontend/src/pages/submit/components/SubmitForm.tsx +++ b/frontend/src/pages/submit/components/SubmitForm.tsx @@ -33,7 +33,7 @@ const SubmitForm: FC<{ form: FormInstance, setFileAdded: (added: boolean) => voi name="file" beforeUpload={() => false} multiple={false} - directory={true} + directory={false} style={{height: "100%"}} onChange={({file}) => { if (file.status !== 'uploading') { From 089a961085b40e9c3983b49f239610e0f4da05f8 Mon Sep 17 00:00:00 2001 From: Arthur Werbrouck Date: Thu, 9 May 2024 22:13:56 +0200 Subject: [PATCH 09/12] better upload & download --- .../com/ugent/pidgeon/config/WebConfig.java | 2 + .../controllers/SubmissionController.java | 2 + frontend/src/i18n/en/translation.json | 1 + frontend/src/i18n/nl/translation.json | 1 + .../submission/components/SubmissionCard.tsx | 245 ++++++++++-------- frontend/src/pages/submit/Submit.tsx | 13 +- .../pages/submit/components/SubmitForm.tsx | 207 ++++++++++++++- frontend/src/util/apiFetch.ts | 19 +- 8 files changed, 359 insertions(+), 131 deletions(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/config/WebConfig.java b/backend/app/src/main/java/com/ugent/pidgeon/config/WebConfig.java index 0009ac5d..ea2dc330 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/config/WebConfig.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/config/WebConfig.java @@ -23,7 +23,9 @@ public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedMethods("*") .allowedOrigins("*") + .exposedHeaders("Content-Disposition") .allowedHeaders("*"); + } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java index a943ce41..4f9d6d15 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java @@ -17,6 +17,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -159,6 +160,7 @@ public ResponseEntity getSubmissions(@PathVariable("projectid") long projecti */ @PostMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/submit") //Route to submit a file, it accepts a multiform with the file and submissionTime + @Transactional @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity submitFile(@RequestParam("file") MultipartFile file, @PathVariable("projectid") long projectid, Auth auth) { long userId = auth.getUserEntity().getId(); diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 228adcfd..6349a566 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -93,6 +93,7 @@ "groupEmpty": "No members in this group", "testFailed": "Tests failed", "structureFailed": "Structure failed", + "uploadDirectory": "Upload Directory", "submission": "Submission", "passed": "Passed", "notSubmitted": "Not submitted", diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index 475c43ad..64f5d73c 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -90,6 +90,7 @@ "uploadAreaTitle": "Bestanden slepen of klikken om bestanden toe te voegen", "uploadAreaSubtitle": "Maximum bestandsgrootte is 100MB", "deadlinePassed": "Deadline is verstreken", + "uploadDirectory": "folder uploaden", "downloadSubmissions": "Download alle indieningen", "group": "Groep", "status": "Status", diff --git a/frontend/src/pages/submission/components/SubmissionCard.tsx b/frontend/src/pages/submission/components/SubmissionCard.tsx index 5e4022d1..f218535a 100644 --- a/frontend/src/pages/submission/components/SubmissionCard.tsx +++ b/frontend/src/pages/submission/components/SubmissionCard.tsx @@ -1,130 +1,155 @@ -import { Card, Spin, theme, Input, Button, Typography } from "antd" -import { useTranslation } from "react-i18next" -import { GET_Responses } from "../../../@types/requests" -import { ApiRoutes } from "../../../@types/requests" -import { ArrowLeftOutlined } from "@ant-design/icons" -import { useNavigate } from "react-router-dom" +import {Card, Spin, theme, Input, Button, Typography} from "antd" +import {useTranslation} from "react-i18next" +import {GET_Responses} from "../../../@types/requests" +import {ApiRoutes} from "../../../@types/requests" +import {ArrowLeftOutlined} from "@ant-design/icons" +import {useNavigate} from "react-router-dom" import "@fontsource/jetbrains-mono" -import { useEffect, useState } from "react" +import {useEffect, useState} from "react" import apiCall from "../../../util/apiFetch" export type SubmissionType = GET_Responses[ApiRoutes.SUBMISSION] -const SubmissionCard: React.FC<{ submission: SubmissionType }> = ({ submission }) => { - const { token } = theme.useToken() - const { t } = useTranslation() - const [structureFeedback, setStructureFeedback] = useState(null) - const [dockerFeedback, setDockerFeedback] = useState(null) - const navigate = useNavigate() - useEffect(() => { - if (!submission.dockerAccepted) apiCall.get(submission.dockerFeedbackUrl).then((res) => setDockerFeedback(res.data ? res.data : "")) - if (!submission.structureAccepted) apiCall.get(submission.structureFeedbackUrl).then((res) => setStructureFeedback(res.data ? res.data : "")) - }, [submission.dockerFeedbackUrl, submission.structureFeedbackUrl]) +const SubmissionCard: React.FC<{ submission: SubmissionType }> = ({submission}) => { + const {token} = theme.useToken() + const {t} = useTranslation() + const [structureFeedback, setStructureFeedback] = useState(null) + const [dockerFeedback, setDockerFeedback] = useState(null) + const navigate = useNavigate() + useEffect(() => { + if (!submission.dockerAccepted) apiCall.get(submission.dockerFeedbackUrl).then((res) => setDockerFeedback(res.data ? res.data : "")) + if (!submission.structureAccepted) apiCall.get(submission.structureFeedbackUrl).then((res) => setStructureFeedback(res.data ? res.data : "")) + }, [submission.dockerFeedbackUrl, submission.structureFeedbackUrl]) - const downloadSubmission = async () => { - //TODO: testen of dit wel echt werkt - try { - const fileContent = await apiCall.get(submission.fileUrl) - console.log(fileContent) - const blob = new Blob([fileContent.data], { type: "text/plain" }) - const url = URL.createObjectURL(blob) - const link = document.createElement("a") - link.href = url - link.download = "indiening.zip" - document.body.appendChild(link) - link.click() - URL.revokeObjectURL(url) - document.body.removeChild(link) - } catch (err) { - // TODO: handle error + const downloadSubmission = async () => { + try { + const response = await apiCall.get(submission.fileUrl, undefined, undefined, { + responseType: 'blob', + transformResponse: [(data) => data], + }); + console.log(response); + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement('a'); + link.href = url; + const contentDisposition = response.headers['content-disposition']; + console.log(contentDisposition); + let fileName = 'file.zip'; // default filename + if (contentDisposition) { + const fileNameMatch = contentDisposition.match(/filename=([^;]+)/); + console.log(fileNameMatch); + if (fileNameMatch && fileNameMatch[1]) { + fileName = fileNameMatch[1]; // use the filename from the headers + } + } + link.setAttribute('download', fileName); + document.body.appendChild(link); + link.click(); + } catch (err) { + console.error(err); + } } - } - return ( - + return ( + - {t("submission.submission")} + {t("submission.submission")} - } - > - {t("submission.submittedFiles")} + } + > + {t("submission.submittedFiles")} -
    -
  • - -
  • -
+
    +
  • + +
  • +
- {t("submission.structuretest")} + {t("submission.structuretest")} -
    -
  • - {submission.structureAccepted ? t("submission.status.accepted") : t("submission.status.failed")} - {submission.structureAccepted ? null : ( -
    - {structureFeedback === null ? ( - - ) : ( - - )} -
    - )} -
  • -
+
    +
  • + {submission.structureAccepted ? t("submission.status.accepted") : t("submission.status.failed")} + {submission.structureAccepted ? null : ( +
    + {structureFeedback === null ? ( + + ) : ( + + )} +
    + )} +
  • +
- {t("submission.dockertest")} + {t("submission.dockertest")} -
    -
  • - {submission.dockerAccepted ? t("submission.status.accepted") : t("submission.status.failed")} - {submission.dockerAccepted ? null : ( -
    - {dockerFeedback === null ? ( - - ) : ( - - )} -
    - )} -
  • -
-
- ) +
    +
  • + {submission.dockerAccepted ? t("submission.status.accepted") : t("submission.status.failed")} + {submission.dockerAccepted ? null : ( +
    + {dockerFeedback === null ? ( + + ) : ( + + )} +
    + )} +
  • +
+
+ ) } export default SubmissionCard diff --git a/frontend/src/pages/submit/Submit.tsx b/frontend/src/pages/submit/Submit.tsx index 2b887e74..950fa72e 100644 --- a/frontend/src/pages/submit/Submit.tsx +++ b/frontend/src/pages/submit/Submit.tsx @@ -27,7 +27,7 @@ const Submit = () => { const zip = new JSZip(); files.forEach((file: any) => { - zip.file(file.name, file); + zip.file(file.webkitRelativePath || file.name, file); }); const content = await zip.generateAsync({type: "blob"}); formData.append("file", content, "files.zip"); @@ -35,7 +35,16 @@ const Submit = () => { if (!projectId) return; const response = await apiCall.post(ApiRoutes.PROJECT_SUBMIT, formData, {id: projectId}) console.log(response) + + const projectUrl = new URL(response.data.projectUrl, 'http://localhost:3001'); + + const courseUrl = new URL(projectUrl.origin + projectUrl.pathname.split('/').slice(0, 3).join('/'), 'http://localhost:3001'); + const courseId = courseUrl.pathname.split('/')[2]; + + const submissionId = response.data.submissionId; + navigate(`/courses/${courseId}/projects/${projectId}/submissions/${submissionId}`); } + return ( <>
@@ -95,4 +104,4 @@ const Submit = () => { ) } -export default Submit +export default Submit \ No newline at end of file diff --git a/frontend/src/pages/submit/components/SubmitForm.tsx b/frontend/src/pages/submit/components/SubmitForm.tsx index 0cb1ec7c..5216d8e9 100644 --- a/frontend/src/pages/submit/components/SubmitForm.tsx +++ b/frontend/src/pages/submit/components/SubmitForm.tsx @@ -1,12 +1,27 @@ import {InboxOutlined} from "@ant-design/icons" import {Form, FormInstance, Upload} from "antd" -import {FC} from "react" +import {FC, useRef, useState} from "react" import {useTranslation} from "react-i18next" +import {Button} from "antd"; +import {Tree} from 'antd'; +import {CloseOutlined} from '@ant-design/icons'; - -const SubmitForm: FC<{ form: FormInstance, setFileAdded: (added: boolean) => void, onSubmit: (values: any) => void }> = ({form, setFileAdded, onSubmit}) => { +const SubmitForm: FC<{ + form: FormInstance, + setFileAdded: (added: boolean) => void, + onSubmit: (values: any) => void +}> = ({form, setFileAdded, onSubmit}) => { const {t} = useTranslation() + const directoryInputRef = useRef(null); + const [directoryTree, setDirectoryTree] = useState([]); + type TreeNode = { + type: string; + title: string; + key: string; + children: TreeNode[]; + }; + const normFile = (e: any) => { console.log("Upload event:", e) if (Array.isArray(e)) { @@ -15,12 +30,141 @@ const SubmitForm: FC<{ form: FormInstance, setFileAdded: (added: boolean) => voi return e?.fileList } - const onFinish = (values: any) => { - onSubmit(values); - }; + const onFinish = (values: any) => { + onSubmit(values); + }; + + + const removeEmptyParentNodes = (nodes: TreeNode[]) => { + for (let i = nodes.length - 1; i >= 0; i--) { + if (nodes[i].type === 'folder') { + if (!nodes[i].children || nodes[i].children.length === 0) { + nodes.splice(i, 1); + } else { + removeEmptyParentNodes(nodes[i].children); + } + } + } + }; + + const removeNode = (key: string) => { + const removeNodeRecursive = (nodes: TreeNode[]): boolean => { + for (let i = 0; i < nodes.length; i++) { + if (nodes[i].key === key) { + nodes.splice(i, 1); + return true; + } else if (nodes[i].children) { + const childRemoved = removeNodeRecursive(nodes[i].children); + if (childRemoved) { + removeEmptyParentNodes(nodes); + return true; + } + } + } + return false; + }; + + const newDirectoryTree = [...directoryTree]; + removeNodeRecursive(newDirectoryTree); + setDirectoryTree(newDirectoryTree); + + + const newFileList = form.getFieldValue('files').filter((file: any) => !file.uid.startsWith(key)); + form.setFieldsValue({ + files: newFileList + }); + + if (newDirectoryTree.length === 0) { + setFileAdded(false); + } + }; + const markFolders = (nodes: TreeNode[]) => { + for (let i = 0; i < nodes.length; i++) { + if (nodes[i].children && nodes[i].children.length > 0) { + nodes[i].type = 'folder'; + markFolders(nodes[i].children); + } + } + }; + const onDirectoryUpload = (event: React.ChangeEvent) => { + const files = event.target.files; + if (files) { + const currentFileList = form.getFieldValue('files') || []; + const newDirectoryTree: TreeNode[] = [...directoryTree]; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + currentFileList.push({ + uid: file.webkitRelativePath, + name: file.name, + status: 'done', + originFileObj: file + }); + + + const pathParts = file.webkitRelativePath.split('/'); + let currentNode = newDirectoryTree; + for (let j = 0; j < pathParts.length; j++) { + let foundNode = currentNode.find(node => node.title === pathParts[j]); + if (!foundNode) { + foundNode = { + title: pathParts[j], + key: pathParts.slice(0, j + 1).join('/'), + children: [], + type: 'file' + }; + currentNode.push(foundNode); + } + currentNode = foundNode.children; + } + } + markFolders(newDirectoryTree); + + form.setFieldsValue({ + files: currentFileList + }); + setDirectoryTree(newDirectoryTree); + setFileAdded(true); + } + } + const renderTreeNodes = (data: TreeNode[]) => + data.map((item) => { + if (item.children) { + return { + title: ( +
+ {item.title} +
+ ), + key: item.key, + children: renderTreeNodes(item.children), + }; + } + + return { + title: ( +
+ {item.title} +
+ ), + key: item.key, + }; + }); return ( - + voi false} - multiple={false} - directory={false} + multiple={true} style={{height: "100%"}} + showUploadList={false} onChange={({file}) => { if (file.status !== 'uploading') { + const currentFileList = form.getFieldValue('files') || []; + currentFileList.push({ + uid: file.uid, + name: file.name, + status: 'done', + originFileObj: file + }); + form.setFieldsValue({ + files: currentFileList + }); + + + const newDirectoryTree: TreeNode[] = [...directoryTree]; + newDirectoryTree.push({ + title: file.name, + key: file.uid, + children: [], + type: "" + }); + setDirectoryTree(newDirectoryTree); + setFileAdded(true); } }} @@ -47,9 +212,31 @@ const SubmitForm: FC<{ form: FormInstance, setFileAdded: (added: boolean) => voi

{t("project.uploadAreaTitle")}

{t("project.uploadAreaSubtitle")}

+ + +
+ +
+
) } -export default SubmitForm +export default SubmitForm \ No newline at end of file diff --git a/frontend/src/util/apiFetch.ts b/frontend/src/util/apiFetch.ts index d3544be7..8985fb15 100644 --- a/frontend/src/util/apiFetch.ts +++ b/frontend/src/util/apiFetch.ts @@ -21,7 +21,7 @@ type ApiCallPathValues = {[param: string]: string | number} * const newCourse = await apiFetch("POST", ApiRoutes.COURSES, { name: "New Course" }); * */ -async function apiFetch(method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", route: string, body?: any, pathValues?:ApiCallPathValues, headers?: {[header: string]: string}): Promise> { +async function apiFetch(method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", route: string, body?: any, pathValues?:ApiCallPathValues, headers?: {[header: string]: string}, config?: AxiosRequestConfig): Promise> { const account = msalInstance.getActiveAccount() if (!account) { @@ -56,18 +56,19 @@ async function apiFetch(method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", rou const url = new URL(route, serverHost) -const config: AxiosRequestConfig = { - method: method, - url: url.toString(), - headers: finalHeaders, - data: body instanceof FormData ? body : JSON.stringify(body), -} - return axios(config) + const finalConfig: AxiosRequestConfig = { + method: method, + url: url.toString(), + headers: finalHeaders, + data: body instanceof FormData ? body : JSON.stringify(body), + ...config, // spread the config object to merge it with the existing configuration + } + return axios(finalConfig) } const apiCall = { - get: async (route: T, pathValues?:ApiCallPathValues, headers?: {[header: string]: string}) => apiFetch("GET", route, undefined, pathValues, headers) as Promise>, + get: async (route: T, pathValues?:ApiCallPathValues, headers?: {[header: string]: string}, config?: AxiosRequestConfig) => apiFetch("GET", route, undefined, pathValues, headers, config) as Promise>, post: async (route: T, body: POST_Requests[T] | FormData, pathValues?:ApiCallPathValues, headers?: {[header: string]: string}) => apiFetch("POST", route, body, pathValues, headers) as Promise>, put: async (route: T, body: PUT_Requests[T], pathValues?:ApiCallPathValues, headers?: {[header: string]: string}) => apiFetch("PUT", route, body, pathValues, headers) as Promise>, delete: async (route: T, body: DELETE_Requests[T], pathValues?:ApiCallPathValues, headers?: {[header: string]: string}) => apiFetch("DELETE", route, body, pathValues, headers), From f75bebab8414c5913532a27eb6c8323464e15e43 Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Fri, 10 May 2024 01:25:22 +0200 Subject: [PATCH 10/12] Fixed ts errors + set tree to DirectoryTree --- frontend/src/@types/types.d.ts | 7 ++++ frontend/src/i18n/en/translation.json | 2 +- frontend/src/i18n/nl/translation.json | 2 +- .../pages/submit/components/SubmitForm.tsx | 37 ++++++++++++------- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/frontend/src/@types/types.d.ts b/frontend/src/@types/types.d.ts index daec4a4b..719ad8de 100644 --- a/frontend/src/@types/types.d.ts +++ b/frontend/src/@types/types.d.ts @@ -1,4 +1,11 @@ +declare module "react" { + interface InputHTMLAttributes extends HTMLAttributes { + webkitdirectory?: string; + directory?:string + mozdirectory?: string + } +} diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 6349a566..598ba661 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -93,7 +93,7 @@ "groupEmpty": "No members in this group", "testFailed": "Tests failed", "structureFailed": "Structure failed", - "uploadDirectory": "Upload Directory", + "uploadDirectory": "Upload directory", "submission": "Submission", "passed": "Passed", "notSubmitted": "Not submitted", diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index 64f5d73c..84ea7743 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -90,7 +90,7 @@ "uploadAreaTitle": "Bestanden slepen of klikken om bestanden toe te voegen", "uploadAreaSubtitle": "Maximum bestandsgrootte is 100MB", "deadlinePassed": "Deadline is verstreken", - "uploadDirectory": "folder uploaden", + "uploadDirectory": "Folder uploaden", "downloadSubmissions": "Download alle indieningen", "group": "Groep", "status": "Status", diff --git a/frontend/src/pages/submit/components/SubmitForm.tsx b/frontend/src/pages/submit/components/SubmitForm.tsx index 5216d8e9..0cdd2993 100644 --- a/frontend/src/pages/submit/components/SubmitForm.tsx +++ b/frontend/src/pages/submit/components/SubmitForm.tsx @@ -5,6 +5,15 @@ import {useTranslation} from "react-i18next" import {Button} from "antd"; import {Tree} from 'antd'; import {CloseOutlined} from '@ant-design/icons'; +import { DataNode } from "antd/es/tree"; + +type TreeNode = { + type: string; + title: string; + key: string; + children: TreeNode[]; +}; + const SubmitForm: FC<{ form: FormInstance, @@ -14,13 +23,7 @@ const SubmitForm: FC<{ const {t} = useTranslation() const directoryInputRef = useRef(null); - const [directoryTree, setDirectoryTree] = useState([]); - type TreeNode = { - type: string; - title: string; - key: string; - children: TreeNode[]; - }; + const [directoryTree, setDirectoryTree] = useState([]); const normFile = (e: any) => { console.log("Upload event:", e) @@ -127,37 +130,41 @@ const SubmitForm: FC<{ setFileAdded(true); } } - const renderTreeNodes = (data: TreeNode[]) => + const renderTreeNodes = (data: TreeNode[]): DataNode[] => data.map((item) => { - if (item.children) { + if (item.children?.length) { return { title: ( -
+ {item.title}
+ ), key: item.key, children: renderTreeNodes(item.children), + isLeaf:false }; } return { + isLeaf:true, title: ( -
+ <> {item.title}
+ ), key: item.key, }; @@ -215,6 +222,7 @@ const SubmitForm: FC<{
-
From 958a579093415cdedc78336131be246528eda5e2 Mon Sep 17 00:00:00 2001 From: Arthur Werbrouck Date: Mon, 13 May 2024 16:31:44 +0200 Subject: [PATCH 11/12] Popup als indiening succesvol is --- frontend/src/i18n/en/translation.json | 1 + frontend/src/i18n/nl/translation.json | 2 ++ frontend/src/pages/submit/Submit.tsx | 4 ++++ 3 files changed, 7 insertions(+) diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 598ba661..bed877dd 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -97,6 +97,7 @@ "submission": "Submission", "passed": "Passed", "notSubmitted": "Not submitted", + "submitSuccess": "Submission successful", "submissionTime": "Submission time", "noSubmissions": "No submissions", "loadingSubmissions": "Loading submissions...", diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index 84ea7743..669841d2 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -99,6 +99,8 @@ "testFailed": "Testen gefaald", "structureFailed": "Structuur testen gefaald", "passed": "Geslaagd", + "submitSuccess": "Indiening succesvol", + "notSubmitted": "Niet ingediend", "submissionTime": "Indienings tijd", "noSubmissions": "Geen indieningen", diff --git a/frontend/src/pages/submit/Submit.tsx b/frontend/src/pages/submit/Submit.tsx index 950fa72e..231f2bd8 100644 --- a/frontend/src/pages/submit/Submit.tsx +++ b/frontend/src/pages/submit/Submit.tsx @@ -7,6 +7,7 @@ import React, {useState, useRef} from 'react'; import apiCall from "../../util/apiFetch"; import {ApiRoutes} from "../../@types/requests.d"; import JSZip from 'jszip'; +import { Popconfirm, message } from 'antd'; const Submit = () => { const {t} = useTranslation() @@ -36,6 +37,9 @@ const Submit = () => { const response = await apiCall.post(ApiRoutes.PROJECT_SUBMIT, formData, {id: projectId}) console.log(response) + if (response.status === 200) { // Check if the submission was successful + message.success(t("project.submitSuccess")); + } const projectUrl = new URL(response.data.projectUrl, 'http://localhost:3001'); const courseUrl = new URL(projectUrl.origin + projectUrl.pathname.split('/').slice(0, 3).join('/'), 'http://localhost:3001'); From 3cdd90ecb5bec2c3d4e75418a89027ec5f367e63 Mon Sep 17 00:00:00 2001 From: Arthur Werbrouck Date: Mon, 13 May 2024 16:52:50 +0200 Subject: [PATCH 12/12] better navigation --- frontend/src/pages/submit/Submit.tsx | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/frontend/src/pages/submit/Submit.tsx b/frontend/src/pages/submit/Submit.tsx index 231f2bd8..b5bab401 100644 --- a/frontend/src/pages/submit/Submit.tsx +++ b/frontend/src/pages/submit/Submit.tsx @@ -8,11 +8,13 @@ import apiCall from "../../util/apiFetch"; import {ApiRoutes} from "../../@types/requests.d"; import JSZip from 'jszip'; import { Popconfirm, message } from 'antd'; +import {AppRoutes} from "../../@types/routes"; +import submission from "../submission/Submission"; const Submit = () => { const {t} = useTranslation() const [form] = Form.useForm() - const {projectId} = useParams<{ projectId: string }>() + const {projectId, courseId} = useParams<{ projectId: string, courseId: string}>() const [fileAdded, setFileAdded] = useState(false); const navigate = useNavigate() @@ -36,17 +38,21 @@ const Submit = () => { if (!projectId) return; const response = await apiCall.post(ApiRoutes.PROJECT_SUBMIT, formData, {id: projectId}) console.log(response) - + const submissionId:string = response.data.submissionId.toString(); if (response.status === 200) { // Check if the submission was successful message.success(t("project.submitSuccess")); } - const projectUrl = new URL(response.data.projectUrl, 'http://localhost:3001'); - - const courseUrl = new URL(projectUrl.origin + projectUrl.pathname.split('/').slice(0, 3).join('/'), 'http://localhost:3001'); - const courseId = courseUrl.pathname.split('/')[2]; - - const submissionId = response.data.submissionId; - navigate(`/courses/${courseId}/projects/${projectId}/submissions/${submissionId}`); + else { + message.error(t("project.submitError")); + } + if (courseId != null && submissionId != null) { + navigate(AppRoutes.SUBMISSION.replace(':courseId', courseId).replace(':projectId', projectId).replace(':submissionId', submissionId)); + }else{ + console.log(projectId) + console.log(courseId) + console.log(submissionId) + message.error(t("project.submitError")); + } } return (