github.com/quickfeed/quickfeed@v0.0.0-20240507093252-ed8ca812a09c/public/src/components/Results.tsx (about) 1 import React, { useCallback, useEffect, useMemo } from "react" 2 import { useHistory, useLocation } from 'react-router-dom' 3 import { Enrollment, Group, Submission } from "../../proto/qf/types_pb" 4 import { Color, getCourseID, getSubmissionCellColor } from "../Helpers" 5 import { useActions, useAppState } from "../overmind" 6 import Button, { ButtonType } from "./admin/Button" 7 import { generateAssignmentsHeader, generateSubmissionRows } from "./ComponentsHelpers" 8 import DynamicTable, { CellElement, RowElement } from "./DynamicTable" 9 import TableSort from "./forms/TableSort" 10 import LabResult from "./LabResult" 11 import ReviewForm from "./manual-grading/ReviewForm" 12 import Release from "./Release" 13 import Search from "./Search" 14 15 16 const Results = ({ review }: { review: boolean }): JSX.Element => { 17 const state = useAppState() 18 const actions = useActions() 19 const courseID = getCourseID() 20 const history = useHistory() 21 const location = useLocation() 22 23 const members = useMemo(() => { return state.courseMembers }, [state.courseMembers, state.groupView]) 24 const assignments = useMemo(() => { 25 // Filter out all assignments that are not the selected assignment, if any assignment is selected 26 return state.assignments[courseID.toString()]?.filter(a => state.review.assignmentID <= 0 || a.ID === state.review.assignmentID) ?? [] 27 }, [state.assignments, courseID, state.review.assignmentID]) 28 29 useEffect(() => { 30 if (!state.loadedCourse[courseID.toString()]) { 31 actions.loadCourseSubmissions(courseID) 32 } 33 return () => { 34 actions.setGroupView(false) 35 actions.review.setAssignmentID(-1n) 36 actions.setActiveEnrollment(null) 37 } 38 }, []) 39 40 useEffect(() => { 41 if (!state.selectedSubmission) { 42 // If no submission is selected, check if there is a selected lab in the URL 43 // and select it if it exists 44 const selectedLab = new URLSearchParams(location.search).get('id') 45 if (selectedLab) { 46 const submission = state.submissionsForCourse.ByID(BigInt(selectedLab)) 47 if (submission) { 48 actions.setSelectedSubmission(submission) 49 actions.updateSubmissionOwner(state.submissionsForCourse.OwnerByID(submission.ID)) 50 } 51 } 52 } 53 }, []) 54 55 const handleLabClick = useCallback((labId: bigint) => { 56 // Update the URL with the selected lab 57 history.replace({ 58 pathname: location.pathname, 59 search: `?id=${labId}` 60 }) 61 }, [history]) 62 63 if (!state.loadedCourse[courseID.toString()]) { 64 return <h1>Fetching Submissions...</h1> 65 } 66 67 const generateReviewCell = (submission: Submission, owner: Enrollment | Group): RowElement => { 68 if (!state.isManuallyGraded(submission)) { 69 return { value: "N/A" } 70 } 71 const reviews = state.review.reviews.get(submission.ID) ?? [] 72 // Check if the current user has any pending reviews for this submission 73 // Used to give cell a box shadow to indicate that the user has a pending review 74 const pending = reviews.some((r) => !r.ready && r.ReviewerID === state.self.ID) 75 // Check if the this submission is the currently selected submission 76 // Used to highlight the cell 77 const isSelected = state.selectedSubmission?.ID === submission.ID 78 const score = reviews.reduce((acc, theReview) => acc + theReview.score, 0) / reviews.length 79 // willBeReleased is true if the average score of all of this submission's reviews is greater than the set minimum score 80 // Used to visually indicate that the submission will be released for the given minimum score 81 const willBeReleased = state.review.minimumScore > 0 && score >= state.review.minimumScore 82 const numReviewers = state.assignments[state.activeCourse.toString()]?.find((a) => a.ID === submission.AssignmentID)?.reviewers ?? 0 83 return ({ 84 // TODO: Figure out a better way to visualize released submissions than '(r)' 85 value: `${reviews.length}/${numReviewers} ${submission.released ? "(r)" : ""}`, 86 className: `${getSubmissionCellColor(submission)} ${isSelected ? "selected" : ""} ${willBeReleased ? "release" : ""} ${pending ? "pending-review" : ""}`, 87 onClick: () => { 88 actions.setSelectedSubmission(submission) 89 if (owner instanceof Enrollment) { 90 actions.setActiveEnrollment(owner.clone()) 91 } 92 actions.setSubmissionOwner(owner) 93 actions.review.setSelectedReview(-1) 94 handleLabClick(submission.ID) 95 } 96 }) 97 } 98 99 const getSubmissionCell = (submission: Submission, owner: Enrollment | Group): CellElement => { 100 // Check if the this submission is the currently selected submission 101 // Used to highlight the cell 102 const isSelected = state.selectedSubmission?.ID === submission.ID 103 return ({ 104 value: `${submission.score} %`, 105 className: `${getSubmissionCellColor(submission)} ${isSelected ? "selected" : ""}`, 106 onClick: () => { 107 actions.setSelectedSubmission(submission) 108 if (owner instanceof Enrollment) { 109 actions.setActiveEnrollment(owner.clone()) 110 } 111 actions.setSubmissionOwner(owner) 112 handleLabClick(submission.ID) 113 actions.getSubmission({ submission: submission, owner: state.submissionOwner, courseID: state.activeCourse }) 114 } 115 }) 116 } 117 118 const groupView = state.groupView 119 const header = generateAssignmentsHeader(assignments, groupView) 120 121 const generator = review ? generateReviewCell : getSubmissionCell 122 const rows = generateSubmissionRows(members, generator) 123 124 125 return ( 126 <div className="row"> 127 <div className={`p-0 ${state.review.assignmentID >= 0 ? "col-md-4" : "col-md-6"}`}> 128 {review ? <Release /> : null} 129 <Search placeholder={"Search by name ..."} className="mb-2" > 130 <Button 131 text={`View by ${groupView ? "student" : "group"}`} 132 color={groupView ? Color.BLUE : Color.GREEN} 133 type={ButtonType.BUTTON} 134 className="ml-2" 135 onClick={() => { actions.setGroupView(!groupView); actions.review.setAssignmentID(BigInt(-1)) }} 136 /> 137 </Search> 138 <TableSort review={review} /> 139 <DynamicTable header={header} data={rows} /> 140 </div> 141 <div className="col"> 142 {review ? <ReviewForm /> : <LabResult />} 143 </div> 144 </div> 145 ) 146 } 147 148 export default Results