diff --git a/.env-template b/.env-template new file mode 100644 index 00000000..6f466741 --- /dev/null +++ b/.env-template @@ -0,0 +1,8 @@ +backend/app/src/main/resources/application-secrets.properties,spring.datasource.username= +backend/app/src/main/resources/application-secrets.properties,spring.datasource.password= +backend/app/src/main/resources/application-secrets.properties,azure.activedirectory.client-id= +backend/app/src/main/resources/application-secrets.properties,azure.activedirectory.b2c.client-secret= +backend/app/src/main/resources/application-secrets.properties,azure.activedirectory.tenant-id= +docker.env,PGU= +docker.env,PGP= +docker.env,POSTGRES_USER=${PGU} diff --git a/.gitignore b/.gitignore index 9d406eb9..9a182e36 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,4 @@ docker.env ./startBackend.sh startBackend.sh +/.env diff --git a/backend/app/src/main/java/com/ugent/pidgeon/GlobalErrorHandler.java b/backend/app/src/main/java/com/ugent/pidgeon/GlobalErrorHandler.java index 47371b60..6e5b68b9 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/GlobalErrorHandler.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/GlobalErrorHandler.java @@ -51,6 +51,15 @@ public ResponseEntity handleNoHandlerFoundException(HttpServlet "Resource/endpoint doesn't exist", path)); } + @ExceptionHandler(NoResourceFoundException.class) + public ResponseEntity handleNoResourceFoundException(HttpServletRequest request, Exception ex) { + logError(ex); + String path = request.getRequestURI(); + HttpStatus status = HttpStatus.NOT_FOUND; + return ResponseEntity.status(status).body(new ApiErrorReponse(OffsetDateTime.now(), status.value(), status.getReasonPhrase(), + "Resource/endpoint doesn't exist", path)); + } + /* Gets thrown when the method is not allowed */ @ExceptionHandler(HttpRequestMethodNotSupportedException.class) public ResponseEntity handleMethodNotSupportedException(HttpServletRequest request, Exception ex) { diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubmissionTestModel.java b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubmissionTestModel.java index f55385d5..f336ff03 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubmissionTestModel.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubmissionTestModel.java @@ -14,6 +14,8 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; @@ -312,13 +314,24 @@ public static void removeDockerImage(String imageName) { } public static boolean imageExists(String image) { - DockerClient dockerClient = DockerClientInstance.getInstance(); try { - dockerClient.inspectImageCmd(image).exec(); - } catch (Exception e) { + // Split the image into repository and tag + String[] parts = image.split(":"); + String repository = parts[0]; + String tag = parts.length > 1 ? parts[1] : "latest"; + + // Construct the URL for the Docker Hub API + String apiUrl = "https://hub.docker.com/v2/repositories/library/" + repository + "/tags/" + tag; + URL url = new URL(apiUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.connect(); + int responseCode = connection.getResponseCode(); + + return (responseCode == 200); + } catch (IOException e) { return false; } - return true; } public static boolean isValidTemplate(String template) { diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/TestUtil.java b/backend/app/src/main/java/com/ugent/pidgeon/util/TestUtil.java index 08d909ce..13c28bc0 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/TestUtil.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/TestUtil.java @@ -80,9 +80,9 @@ public CheckResult> checkForTestUpdate( } // This returns false if the image isn't pullt yet! FIX PLS -// if(dockerImage != null && !DockerSubmissionTestModel.imageExists(dockerImage)) { -// return new CheckResult<>(HttpStatus.BAD_REQUEST, "A valid docker image is required in a docker test.", null); -// } + if(dockerImage != null && !DockerSubmissionTestModel.imageExists(dockerImage)) { + return new CheckResult<>(HttpStatus.BAD_REQUEST, "A valid docker image is required in a docker test.", null); + } if (!httpMethod.equals(HttpMethod.PATCH) && dockerTemplate != null && dockerImage == null) { return new CheckResult<>(HttpStatus.BAD_REQUEST, "A test script and image are required in a docker template test.", null); diff --git a/backend/app/src/test/java/com/ugent/pidgeon/docker/DockerSubmissionTestTest.java b/backend/app/src/test/java/com/ugent/pidgeon/docker/DockerSubmissionTestTest.java index e99e185f..688b3157 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/docker/DockerSubmissionTestTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/docker/DockerSubmissionTestTest.java @@ -196,7 +196,8 @@ void zipFileInputTest() throws IOException { } @Test void dockerImageDoesNotExist(){ - assertFalse(DockerSubmissionTestModel.imageExists("BADUBADUBADUBADUBADUBADUB")); + assertFalse(DockerSubmissionTestModel.imageExists("BADUBADUBADUBADUBADUBADUB - miauw :3")); + assertFalse(DockerSubmissionTestModel.imageExists("alpine:v69696969")); assertTrue(DockerSubmissionTestModel.imageExists("alpine:latest")); } diff --git a/envBuilder.bat b/envBuilder.bat new file mode 100644 index 00000000..fa93db80 --- /dev/null +++ b/envBuilder.bat @@ -0,0 +1,28 @@ +@echo off +setlocal EnableDelayedExpansion + +set "prevFile=" + +for /F "tokens=1,2 delims=," %%a in (.env) do ( + echo Processing line: %%a,%%b + for /F "tokens=1,2 delims==" %%c in ("%%b") do ( + echo File: %%a + echo Variable: %%c + echo Value: %%d + + if not "%%a"=="!prevFile!" ( + if exist %%a ( + del %%a + echo Deleted file: %%a + ) + type nul > %%a + echo Created file: %%a + ) + + echo. >> %%a + echo %%c=%%d >> %%a + echo Added variable to file + + set "prevFile=%%a" + ) +) diff --git a/envBuilder.sh b/envBuilder.sh new file mode 100644 index 00000000..99413a31 --- /dev/null +++ b/envBuilder.sh @@ -0,0 +1,18 @@ +ENV_FILE=".env" + +while IFS= read -r line +do + echo "Processing line: $line" + IFS=',' read -r full_addr var <<< "$line" + IFS='=' read -r file env <<< "$full_addr" + echo "File: $file" + echo "Variable: $var" + echo "Value: $env" + touch "$file" + if ! grep -q "${var}=" "$file"; then + echo "Variable not set, appending to file..." + echo "${var}=${env}" >> "$file" + else + echo "Variable already set in file." + fi +done < "$ENV_FILE" \ No newline at end of file diff --git a/frontend/src/@types/requests.d.ts b/frontend/src/@types/requests.d.ts index 953fdda1..97cb167c 100644 --- a/frontend/src/@types/requests.d.ts +++ b/frontend/src/@types/requests.d.ts @@ -245,6 +245,7 @@ export type GET_Responses = { testsUrl: string maxScore: number | null visible: boolean + visibleAfter?: Timestamp status?: ProjectStatus progress: { completed: number diff --git a/frontend/src/components/forms/projectFormTabs/GeneralFormTab.tsx b/frontend/src/components/forms/projectFormTabs/GeneralFormTab.tsx index f81c237b..f81fd4c7 100644 --- a/frontend/src/components/forms/projectFormTabs/GeneralFormTab.tsx +++ b/frontend/src/components/forms/projectFormTabs/GeneralFormTab.tsx @@ -4,56 +4,72 @@ import { FC } from "react" import MarkdownEditor from "../../input/MarkdownEditor" const GeneralFormTab: FC<{ form: FormInstance }> = ({ form }) => { - const { t } = useTranslation() - const description = Form.useWatch("description", form) - - return ( - <> - - - - - - {t("project.change.description")} - - - - - - - - - - - - - - ) + const { t } = useTranslation() + const description = Form.useWatch("description", form) + const visible = Form.useWatch("visible", form) + + return ( + <> + + + + + + {t("project.change.description")} + + + + + + + + {!visible && ( + + + + )} + + + + + + + + + + ) } export default GeneralFormTab diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 5e6d07b3..15ada75a 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -49,6 +49,13 @@ "showMore": "Show more", "submit": "Submit", "projectStatus": "Status", + "visibility": "Visibility", + "visibleStatus": { + "visible": "Visible", + "invisible": "Invisible", + "visibleFrom": "Visible from ", + "scheduled": "Scheduled" + }, "status": { "completed": "Completed", "failed": "Failed", @@ -135,6 +142,7 @@ "groupClusterId": "Groups", "groupClusterIdMessage": "Please enter the group cluster", "visible": "Make the project visible", + "visibleAfter": "Choose when the project will be made visible to students, leaving this empty will keep the project invisible", "maxScore": "Maximum score", "maxScoreMessage": "Please enter the maximum score for the project", "maxScoreHelp": "What is the maximum achievable score for this project? Leaving it empty means the project won't be graded.", diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index bb1ec1df..6315beda 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -47,8 +47,14 @@ "deadline": "Deadline", "deadlineNotPassed": "Toon enkel actieve projecten", "showMore": "Toon meer", - "projectStatus": "Status", + "visibility": "Zichtbaarheid", + "visibleStatus": { + "visible": "Zichtbaar", + "invisible": "Onzichtbaar", + "visibleFrom": "Zichtbaar vanaf ", + "scheduled" : "gepland" + }, "status": { "completed": "Voltooid", "failed": "Verkeerd", @@ -139,6 +145,7 @@ "groupClusterId": "Groepen", "groupClusterIdMessage": "Vul de Groep cluster in", "visible": "Project zichtbaar maken", + "visibleAfter": "Kies wanneer het project automatisch zichtbaar wordt voor studenten. Als je niets invult, blijft het project onzichtbaar", "maxScore": "Maximum score", "maxScoreMessage": "Vul de maximum score van het project in", "maxScoreHelp": "Wat is de maximale score die je kunt behalen voor dit project? Als je het leeg laat, wordt het project niet beoordeeld", diff --git a/frontend/src/pages/editProject/EditProject.tsx b/frontend/src/pages/editProject/EditProject.tsx index a96ecf94..ef7c6b62 100644 --- a/frontend/src/pages/editProject/EditProject.tsx +++ b/frontend/src/pages/editProject/EditProject.tsx @@ -1,6 +1,6 @@ import React, { useContext, useEffect, useState } from "react" import { useParams, useNavigate, useLocation } from "react-router-dom" -import { Button, Form, Card, UploadProps } from "antd" +import { Button, Form, UploadProps } from "antd" import { useTranslation } from "react-i18next" import ProjectForm from "../../components/forms/ProjectForm" import { EditFilled } from "@ant-design/icons" @@ -15,155 +15,159 @@ import useApi from "../../hooks/useApi" import saveDockerForm, { DockerFormData } from "../../components/common/saveDockerForm" const EditProject: React.FC = () => { - const [form] = Form.useForm() - const { t } = useTranslation() - const { courseId, projectId } = useParams() - const [loading, setLoading] = useState(false) - const API = useApi() - const [error, setError] = useState(null) // Gebruik ProjectError type voor error state - const navigate = useNavigate() - const project = useProject() - const { updateProject } = useContext(ProjectContext) - const [initialDockerValues, setInitialDockerValues] = useState(null) - const location = useLocation() - - const updateDockerForm = async () => { - if (!projectId) return - const response = await API.GET(ApiRoutes.PROJECT_TESTS, { pathValues: { id: projectId } }) - if (!response.success) return setInitialDockerValues(null) - - let formVals: POST_Requests[ApiRoutes.PROJECT_TESTS] = { - structureTest: null, - dockerTemplate: null, - dockerScript: null, - dockerImage: null, - } - if (response.success) { - const tests = response.response.data - console.log(tests) - - if(tests.extraFilesName) { - const downloadLink = AppRoutes.DOWNLOAD_PROJECT_TESTS.replace(":projectId", projectId).replace(":courseId", courseId!) - - const uploadVal:UploadProps["defaultFileList"] = [{ - uid: '1', - name: tests.extraFilesName, - status: 'done', - url: downloadLink, - type: "file", - }] - - form.setFieldValue("dockerTestDir", uploadVal) - } - - - formVals = { - structureTest: tests.structureTest ?? "", - dockerTemplate: tests.dockerTemplate ?? "", - dockerScript: tests.dockerScript ?? "", - dockerImage: tests.dockerImage ?? "", - } + const [form] = Form.useForm() + const { t } = useTranslation() + const { courseId, projectId } = useParams() + const [loading, setLoading] = useState(false) + const API = useApi() + const [error, setError] = useState(null) + const navigate = useNavigate() + const project = useProject() + const { updateProject } = useContext(ProjectContext) + const [initialDockerValues, setInitialDockerValues] = useState(null) + const location = useLocation() + + const updateDockerForm = async () => { + if (!projectId) return + const response = await API.GET(ApiRoutes.PROJECT_TESTS, { pathValues: { id: projectId } }) + if (!response.success) return setInitialDockerValues(null) + + let formVals: POST_Requests[ApiRoutes.PROJECT_TESTS] = { + structureTest: null, + dockerTemplate: null, + dockerScript: null, + dockerImage: null, + } + if (response.success) { + const tests = response.response.data + console.log(tests) + + if (tests.extraFilesName) { + const downloadLink = AppRoutes.DOWNLOAD_PROJECT_TESTS.replace(":projectId", projectId).replace(":courseId", courseId!) + + const uploadVal: UploadProps["defaultFileList"] = [{ + uid: '1', + name: tests.extraFilesName, + status: 'done', + url: downloadLink, + type: "file", + }] + + form.setFieldValue("dockerTestDir", uploadVal) + } + + formVals = { + structureTest: tests.structureTest ?? "", + dockerTemplate: tests.dockerTemplate ?? "", + dockerScript: tests.dockerScript ?? "", + dockerImage: tests.dockerImage ?? "", + } + } + + form.setFieldsValue(formVals) + + setInitialDockerValues(formVals) } - form.setFieldsValue(formVals) + console.log(initialDockerValues) - setInitialDockerValues(formVals) - } + useEffect(() => { + if (!project) return - console.log(initialDockerValues) + updateDockerForm() + }, [project?.projectId]) - useEffect(() => { - if (!project) return + const handleCreation = async () => { + const values: ProjectFormData & DockerFormData = form.getFieldsValue() + if (values.visible) { + values.visibleAfter = null + } - updateDockerForm() - }, [project?.projectId]) + console.log(values) - const handleCreation = async () => { - const values: ProjectFormData & DockerFormData = form.getFieldsValue() - console.log(values) + if (!courseId || !projectId) return console.error("courseId or projectId is undefined") + setLoading(true) - if (!courseId || !projectId) return console.error("courseId or projectId is undefined") - setLoading(true) + const response = await API.PUT( + ApiRoutes.PROJECT, + { + body: values, + pathValues: { id: projectId }, + }, + "alert" + ) + if (!response.success) { + setError(response.alert || null) + setLoading(false) + return + } - const response = await API.PUT( - ApiRoutes.PROJECT, - { - body: values, - pathValues: { id: projectId }, - }, - "alert" - ) - if (!response.success) { - setError(response.alert || null) - setLoading(false) - return - } + let promises = [] + + promises.push(saveDockerForm(form, initialDockerValues, API, projectId)) - let promisses = [] + if (form.isFieldTouched("groups") && values.groupClusterId && values.groups) { + promises.push(API.PUT(ApiRoutes.CLUSTER_FILL, { body: values.groups, pathValues: { id: values.groupClusterId } }, "message")) + } - promisses.push(saveDockerForm(form, initialDockerValues, API, projectId)) + await Promise.all(promises) - if (form.isFieldTouched("groups") && values.groupClusterId && values.groups) { - promisses.push(API.PUT(ApiRoutes.CLUSTER_FILL, { body: values.groups, pathValues: { id: values.groupClusterId } }, "message")) + const result = response.response.data + updateProject(result) + navigate(AppRoutes.PROJECT.replace(":projectId", result.projectId.toString()).replace(":courseId", courseId)) // Navigeer naar het nieuwe project } - await Promise.all(promisses) - - const result = response.response.data - updateProject(result) - navigate(AppRoutes.PROJECT.replace(":projectId", result.projectId.toString()).replace(":courseId", courseId)) // Navigeer naar het nieuwe project - } - - const onInvalid: FormProps["onFinishFailed"] = (e) => { - const errField = e.errorFields[0].name[0] - if (errField === "groupClusterId") navigate("#groups") - else if (errField === "structureTest") navigate("#structure") - else if (errField === "dockerScript" || errField === "dockerImage" || errField === "dockerTemplate") navigate("#tests") - else navigate("#general") - } - - if (!project) return <> - return ( - <> -
-
- - - - ), - }} - /> -
-
- - ) + const onInvalid: FormProps["onFinishFailed"] = (e) => { + const errField = e.errorFields[0].name[0] + if (errField === "groupClusterId") navigate("#groups") + else if (errField === "structureTest") navigate("#structure") + else if (errField === "dockerScript" || errField === "dockerImage" || errField === "dockerTemplate") navigate("#tests") + else navigate("#general") + } + + if (!project) return <> + return ( + <> +
+
+ + + + ), + }} + /> +
+
+ + ) } export default EditProject diff --git a/frontend/src/pages/index/components/ProjectCard.tsx b/frontend/src/pages/index/components/ProjectCard.tsx index b092a439..e5244e3c 100644 --- a/frontend/src/pages/index/components/ProjectCard.tsx +++ b/frontend/src/pages/index/components/ProjectCard.tsx @@ -1,5 +1,6 @@ import { FC, useEffect, useState } from "react" -import ProjectTable, { ProjectType } from "./ProjectTable" +import ProjectTableCourse, { ProjectType } from "./ProjectTableCourse" +import ProjectTable, { ProjectType as NormalProjectType } from "./ProjectTable" import { Button, Card } from "antd" import { ApiRoutes } from "../../../@types/requests.d" import { useTranslation } from "react-i18next" @@ -8,53 +9,64 @@ import { useNavigate } from "react-router-dom" import CourseAdminView from "../../../hooks/CourseAdminView" import { PlusOutlined } from "@ant-design/icons" import useApi from "../../../hooks/useApi" +import useIsCourseAdmin from "../../../hooks/useIsCourseAdmin"; const ProjectCard: FC<{ courseId?: number }> = ({ courseId }) => { - const [projects, setProjects] = useState(null) - const { t } = useTranslation() - const navigate = useNavigate() - const API = useApi() + const [projects, setProjects] = useState(null) + const { t } = useTranslation() + const navigate = useNavigate() + const API = useApi() + const isCourseAdmin = useIsCourseAdmin() - useEffect(() => { - if (courseId) { - API.GET(ApiRoutes.COURSE_PROJECTS, { pathValues: { id: courseId } }).then((res) => { - if (!res.success) return - setProjects(res.response.data) - }) - } - }, [courseId]) + useEffect(() => { + if (courseId) { + API.GET(ApiRoutes.COURSE_PROJECTS, { pathValues: { id: courseId } }).then((res) => { + if (!res.success) return + setProjects(res.response.data) + }) + } + }, [courseId]) - return ( - <> - -
- -
-
- - - - - ) + return ( + <> + {isCourseAdmin && ( + +
+ +
+
+ )} + + {isCourseAdmin ? ( + + ) : ( + + )} + + + ) } export default ProjectCard diff --git a/frontend/src/pages/index/components/ProjectTableCourse.tsx b/frontend/src/pages/index/components/ProjectTableCourse.tsx new file mode 100644 index 00000000..861c0c66 --- /dev/null +++ b/frontend/src/pages/index/components/ProjectTableCourse.tsx @@ -0,0 +1,138 @@ +import { Button, Table, TableProps, Tag, Tooltip } from "antd" +import { FC, useMemo } from "react" +import { ApiRoutes, GET_Responses } from "../../../@types/requests.d" +import { useTranslation } from "react-i18next" +import i18n from 'i18next' +import useAppApi from "../../../hooks/useAppApi" +import ProjectStatusTag from "./ProjectStatusTag" +import GroupProgress from "./GroupProgress" +import { Link } from "react-router-dom" +import { AppRoutes } from "../../../@types/routes" +import { ClockCircleOutlined } from "@ant-design/icons" +import useIsCourseAdmin from "../../../hooks/useIsCourseAdmin"; + +export type ProjectType = GET_Responses[ApiRoutes.PROJECT] + +const ProjectTableCourse: FC<{ projects: ProjectType[] | null, ignoreColumns?: string[] }> = ({ projects, ignoreColumns }) => { + const { t } = useTranslation() + const { modal } = useAppApi() + const isCourseAdmin = useIsCourseAdmin() + + const columns: TableProps["columns"] = useMemo( + () => { + let columns: TableProps["columns"] = [ + { + title: t("home.projects.name"), + key: "name", + render: (project: ProjectType) => ( + + + + ) + }, + { + title: t("home.projects.course"), + dataIndex: "course", + key: "course", + sorter: (a: ProjectType, b: ProjectType) => a.course.name.localeCompare(b.course.name), + sortDirections: ['ascend', 'descend'], + render: (course: ProjectType["course"]) => course.name + }, + { + title: t("home.projects.deadline"), + dataIndex: "deadline", + key: "deadline", + sorter: (a: ProjectType, b: ProjectType) => new Date(a.deadline).getTime() - new Date(b.deadline).getTime(), + sortDirections: ['ascend', "descend"], + defaultSortOrder: "ascend", + filters: [{ text: t('home.projects.deadlineNotPassed'), value: 'notPassed' }], + onFilter: (value: any, record: any) => { + const currentTimestamp = new Date().getTime(); + const deadlineTimestamp = new Date(record.deadline).getTime(); + return value === 'notPassed' ? deadlineTimestamp >= currentTimestamp : true; + }, + defaultFilteredValue: ["notPassed"], + render: (text: string) => + new Date(text).toLocaleString(i18n.language, { + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }), + }, + { + title: t("home.projects.groupProgress"), + key: "progress", + render: (project: ProjectType) => ( + + ), + } + ] + + if (ignoreColumns) { + columns = columns.filter((c) => !ignoreColumns.includes(c.key as string)) + } + + if (isCourseAdmin) { + columns = columns.filter((c) => c.key !== "status") + columns.push({ + title: t("home.projects.visibility"), + key: "visible", + render: (project: ProjectType) => { + if (project.visible) { + return {t("home.projects.visibleStatus.visible")} + } else if (project.visibleAfter) { + return ( + + } color="default">{t("home.projects.visibleStatus.scheduled")} + + ) + } else { + return {t("home.projects.visibleStatus.invisible")} + } + } + }) + } else { + columns.push({ + title: t("home.projects.projectStatus"), + key: "status", + render: (project: ProjectType) => + project.status && , + }) + } + + return columns + }, + [t, modal, projects, isCourseAdmin] + ) + + return ( + project.projectId} + /> + ) +} + +export default ProjectTableCourse diff --git a/frontend/src/pages/projectCreate/ProjectCreate.tsx b/frontend/src/pages/projectCreate/ProjectCreate.tsx index a775b93c..2abc3bea 100644 --- a/frontend/src/pages/projectCreate/ProjectCreate.tsx +++ b/frontend/src/pages/projectCreate/ProjectCreate.tsx @@ -13,101 +13,107 @@ import useApi from "../../hooks/useApi" import { ApiRoutes } from "../../@types/requests.d" const ProjectCreate: React.FC = () => { - const [form] = Form.useForm() - const { t } = useTranslation() - const navigate = useNavigate() - const { courseId } = useParams<{ courseId: string }>() - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) // Gebruik ProjectError type voor error state - const API = useApi() - const { message } = useAppApi() + const [form] = Form.useForm() + const { t } = useTranslation() + const navigate = useNavigate() + const { courseId } = useParams<{ courseId: string }>() + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) // Gebruik ProjectError type voor error state + const API = useApi() + const { message } = useAppApi() - const handleCreation = async () => { - const values: ProjectFormData & DockerFormData = form.getFieldsValue() - const project: Omit = { - name: values.name, - description: values.description, - groupClusterId: values.groupClusterId, - deadline: values.deadline, - maxScore: values.maxScore, - testId: values.testId, - visible: values.visible, - visibleAfter: values.visibleAfter, - } - console.log(values) + const handleCreation = async () => { + const values: ProjectFormData & DockerFormData = form.getFieldsValue() + if (values.visible) { + values.visibleAfter = null + } - if (!courseId) return console.error("courseId is undefined") - setLoading(true) + const project: Omit = { + name: values.name, + description: values.description, + groupClusterId: values.groupClusterId, + deadline: values.deadline, + maxScore: values.maxScore, + testId: values.testId, + visible: values.visible, + visibleAfter: values.visibleAfter, + } - const response = await API.POST(ApiRoutes.PROJECT_CREATE, { body: project, pathValues: { courseId } }, "alert") - if (!response.success) { - setError(response.alert || null) - return setLoading(false) - } - const result = response.response.data - let promisses: Promise[] = [] + console.log(values) - promisses.push(saveDockerForm(form, null, API, result.projectId.toString())) - if (form.isFieldTouched("groups") && values.groupClusterId && values.groups) { - promisses.push(API.PUT(ApiRoutes.CLUSTER_FILL, { body: values.groups, pathValues: { id: values.groupClusterId } }, "message")) - } + if (!courseId) return console.error("courseId is undefined") + setLoading(true) + + const response = await API.POST(ApiRoutes.PROJECT_CREATE, { body: project, pathValues: { courseId } }, "alert") + if (!response.success) { + setError(response.alert || null) + return setLoading(false) + } + const result = response.response.data + let promises: Promise[] = [] - await Promise.all(promisses) + promises.push(saveDockerForm(form, null, API, result.projectId.toString())) + if (form.isFieldTouched("groups") && values.groupClusterId && values.groups) { + promises.push(API.PUT(ApiRoutes.CLUSTER_FILL, { body: values.groups, pathValues: { id: values.groupClusterId } }, "message")) + } - message.success(t("project.change.success")) // Toon een succesbericht - navigate(AppRoutes.PROJECT.replace(":projectId", result.projectId.toString()).replace(":courseId", courseId)) // Navigeer naar het nieuwe project - } + await Promise.all(promises) - const onInvalid: FormProps["onFinishFailed"] = (e) => { - const errField = e.errorFields[0].name[0] - if (errField === "groupClusterId") navigate("#groups") - else if (errField === "structureTest") navigate("#structure") - else if (errField === "dockerScript" || errField === "dockerImage" || errField === "dockerTemplate") navigate("#tests") - else navigate("#general") - } + message.success(t("project.change.success")) // Toon een succesbericht + navigate(AppRoutes.PROJECT.replace(":projectId", result.projectId.toString()).replace(":courseId", courseId)) // Navigeer naar het nieuwe project + } + + const onInvalid: FormProps["onFinishFailed"] = (e) => { + const errField = e.errorFields[0].name[0] + if (errField === "groupClusterId") navigate("#groups") + else if (errField === "structureTest") navigate("#structure") + else if (errField === "dockerScript" || errField === "dockerImage" || errField === "dockerTemplate") navigate("#tests") + else navigate("#general") + } - return ( - <> -
-
- - - - ), - }} - /> -
- - - ) + return ( + <> +
+
+ + + + ), + }} + /> +
+ + + ) } export default ProjectCreate