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