|  | 
| 1 |  | -import { NonIdealState, Spinner, SpinnerSize } from '@blueprintjs/core'; | 
| 2 |  | -import * as React from 'react'; | 
|  | 1 | +import '@tremor/react/dist/esm/tremor.css'; | 
|  | 2 | + | 
|  | 3 | +import { Icon as BpIcon, NonIdealState, Position, Spinner, SpinnerSize } from '@blueprintjs/core'; | 
|  | 4 | +import { IconNames } from '@blueprintjs/icons'; | 
|  | 5 | +import { Button, Card, Col, ColGrid, Flex, Text, Title } from '@tremor/react'; | 
|  | 6 | +import React, { useEffect, useState } from 'react'; | 
| 3 | 7 | import { useDispatch } from 'react-redux'; | 
| 4 | 8 | import { Navigate, useParams } from 'react-router'; | 
| 5 | 9 | import { fetchGradingOverviews } from 'src/commons/application/actions/SessionActions'; | 
| 6 |  | -import { useTypedSelector } from 'src/commons/utils/Hooks'; | 
|  | 10 | +import { Role } from 'src/commons/application/ApplicationTypes'; | 
|  | 11 | +import SimpleDropdown from 'src/commons/SimpleDropdown'; | 
|  | 12 | +import { useSession } from 'src/commons/utils/Hooks'; | 
| 7 | 13 | import { numberRegExp } from 'src/features/academy/AcademyTypes'; | 
|  | 14 | +import { exportGradingCSV, isSubmissionUngraded } from 'src/features/grading/GradingUtils'; | 
| 8 | 15 | 
 | 
| 9 | 16 | import ContentDisplay from '../../../commons/ContentDisplay'; | 
| 10 | 17 | import { convertParamToInt } from '../../../commons/utils/ParamParseHelper'; | 
| 11 |  | -import GradingDashboard from './subcomponents/GradingDashboard'; | 
|  | 18 | +import GradingSubmissionsTable from './subcomponents/GradingSubmissionsTable'; | 
|  | 19 | +import GradingSummary from './subcomponents/GradingSummary'; | 
| 12 | 20 | import GradingWorkspace from './subcomponents/GradingWorkspace'; | 
| 13 | 21 | 
 | 
| 14 | 22 | const Grading: React.FC = () => { | 
| 15 |  | -  const { courseId, gradingOverviews } = useTypedSelector(state => state.session); | 
|  | 23 | +  const { | 
|  | 24 | +    courseId, | 
|  | 25 | +    gradingOverviews, | 
|  | 26 | +    role, | 
|  | 27 | +    group, | 
|  | 28 | +    assessmentOverviews: assessments = [] | 
|  | 29 | +  } = useSession(); | 
| 16 | 30 |   const params = useParams<{ | 
| 17 | 31 |     submissionId: string; | 
| 18 | 32 |     questionId: string; | 
| 19 | 33 |   }>(); | 
| 20 | 34 | 
 | 
|  | 35 | +  const isAdmin = role === Role.Admin; | 
|  | 36 | +  const [showAllGroups, setShowAllGroups] = useState(isAdmin); | 
|  | 37 | +  const handleShowAllGroups = (value: boolean) => { | 
|  | 38 | +    // Admins will always see all groups regardless | 
|  | 39 | +    setShowAllGroups(isAdmin || value); | 
|  | 40 | +  }; | 
|  | 41 | +  const groupOptions = [{ value: true, label: 'all groups' }]; | 
|  | 42 | +  if (!isAdmin) { | 
|  | 43 | +    groupOptions.unshift({ value: false, label: 'my groups' }); | 
|  | 44 | +  } | 
|  | 45 | + | 
| 21 | 46 |   const dispatch = useDispatch(); | 
| 22 |  | -  React.useEffect(() => { | 
| 23 |  | -    dispatch(fetchGradingOverviews(false)); | 
| 24 |  | -  }, [dispatch]); | 
|  | 47 | +  useEffect(() => { | 
|  | 48 | +    dispatch(fetchGradingOverviews(!showAllGroups)); | 
|  | 49 | +  }, [dispatch, role, showAllGroups]); | 
|  | 50 | + | 
|  | 51 | +  const [showAllSubmissions, setShowAllSubmissions] = useState(false); | 
|  | 52 | +  const showOptions = [ | 
|  | 53 | +    { value: false, label: 'ungraded' }, | 
|  | 54 | +    { value: true, label: 'all' } | 
|  | 55 | +  ]; | 
| 25 | 56 | 
 | 
| 26 | 57 |   // If submissionId or questionId is defined but not numeric, redirect back to the Grading overviews page | 
| 27 | 58 |   if ( | 
| @@ -49,79 +80,68 @@ const Grading: React.FC = () => { | 
| 49 | 80 |     /> | 
| 50 | 81 |   ); | 
| 51 | 82 | 
 | 
| 52 |  | -  const data = | 
|  | 83 | +  const submissions = | 
| 53 | 84 |     gradingOverviews?.map(e => | 
| 54 | 85 |       !e.studentName ? { ...e, studentName: '(user has yet to log in)' } : e | 
| 55 | 86 |     ) ?? []; | 
| 56 | 87 | 
 | 
| 57 |  | -  const exportCSV = () => { | 
| 58 |  | -    if (!gradingOverviews) return; | 
| 59 |  | - | 
| 60 |  | -    const win = document.defaultView || window; | 
| 61 |  | -    if (!win) { | 
| 62 |  | -      console.warn('There is no `window` associated with the current `document`'); | 
| 63 |  | -      return; | 
| 64 |  | -    } | 
| 65 |  | - | 
| 66 |  | -    const content = new Blob( | 
| 67 |  | -      [ | 
| 68 |  | -        '"Assessment Number","Assessment Name","Student Name","Student Username","Group","Status","Grading","Question Count","Questions Graded","Initial XP","XP Adjustment","Current XP (excl. bonus)","Max XP","Bonus XP"\n', | 
| 69 |  | -        ...gradingOverviews.map( | 
| 70 |  | -          e => | 
| 71 |  | -            [ | 
| 72 |  | -              e.assessmentNumber, | 
| 73 |  | -              e.assessmentName, | 
| 74 |  | -              e.studentName, | 
| 75 |  | -              e.studentUsername, | 
| 76 |  | -              e.groupName, | 
| 77 |  | -              e.submissionStatus, | 
| 78 |  | -              e.gradingStatus, | 
| 79 |  | -              e.questionCount, | 
| 80 |  | -              e.gradedCount, | 
| 81 |  | -              e.initialXp, | 
| 82 |  | -              e.xpAdjustment, | 
| 83 |  | -              e.currentXp, | 
| 84 |  | -              e.maxXp, | 
| 85 |  | -              e.xpBonus | 
| 86 |  | -            ] | 
| 87 |  | -              .map(field => `"${field}"`) // wrap each field in double quotes in case it contains a comma | 
| 88 |  | -              .join(',') + '\n' | 
| 89 |  | -        ) | 
| 90 |  | -      ], | 
| 91 |  | -      { type: 'text/csv' } | 
| 92 |  | -    ); | 
| 93 |  | -    const fileName = `SA submissions (${new Date().toISOString()}).csv`; | 
| 94 |  | - | 
| 95 |  | -    // code from https://github.com/ag-grid/ag-grid/blob/latest/grid-community-modules/csv-export/src/csvExport/downloader.ts | 
| 96 |  | -    const element = document.createElement('a'); | 
| 97 |  | -    const url = win.URL.createObjectURL(content); | 
| 98 |  | -    element.setAttribute('href', url); | 
| 99 |  | -    element.setAttribute('download', fileName); | 
| 100 |  | -    element.style.display = 'none'; | 
| 101 |  | -    document.body.appendChild(element); | 
| 102 |  | - | 
| 103 |  | -    element.dispatchEvent( | 
| 104 |  | -      new MouseEvent('click', { | 
| 105 |  | -        bubbles: false, | 
| 106 |  | -        cancelable: true, | 
| 107 |  | -        view: win | 
| 108 |  | -      }) | 
| 109 |  | -    ); | 
| 110 |  | - | 
| 111 |  | -    document.body.removeChild(element); | 
| 112 |  | - | 
| 113 |  | -    win.setTimeout(() => { | 
| 114 |  | -      win.URL.revokeObjectURL(url); | 
| 115 |  | -    }, 0); | 
| 116 |  | -  }; | 
| 117 |  | - | 
| 118 | 88 |   return ( | 
| 119 | 89 |     <ContentDisplay | 
| 120 | 90 |       display={ | 
| 121 | 91 |         gradingOverviews === undefined ? ( | 
| 122 | 92 |           loadingDisplay | 
| 123 | 93 |         ) : ( | 
| 124 |  | -          <GradingDashboard submissions={data} handleCsvExport={exportCSV} /> | 
|  | 94 | +          <ColGrid numColsLg={8} gapX="gap-x-4" gapY="gap-y-2"> | 
|  | 95 | +            <Col numColSpanLg={6}> | 
|  | 96 | +              <Card> | 
|  | 97 | +                <Flex justifyContent="justify-between"> | 
|  | 98 | +                  <Flex justifyContent="justify-start" spaceX="space-x-6"> | 
|  | 99 | +                    <Title>Submissions</Title> | 
|  | 100 | +                    <Button | 
|  | 101 | +                      variant="light" | 
|  | 102 | +                      size="xs" | 
|  | 103 | +                      icon={() => ( | 
|  | 104 | +                        <BpIcon icon={IconNames.EXPORT} style={{ marginRight: '0.5rem' }} /> | 
|  | 105 | +                      )} | 
|  | 106 | +                      onClick={() => exportGradingCSV(gradingOverviews)} | 
|  | 107 | +                    > | 
|  | 108 | +                      Export to CSV | 
|  | 109 | +                    </Button> | 
|  | 110 | +                  </Flex> | 
|  | 111 | +                </Flex> | 
|  | 112 | +                <Flex justifyContent="justify-start" marginTop="mt-2" spaceX="space-x-2"> | 
|  | 113 | +                  <Text>Viewing</Text> | 
|  | 114 | +                  <SimpleDropdown | 
|  | 115 | +                    options={showOptions} | 
|  | 116 | +                    defaultValue={showAllSubmissions} | 
|  | 117 | +                    onClick={setShowAllSubmissions} | 
|  | 118 | +                    popoverProps={{ position: Position.BOTTOM }} | 
|  | 119 | +                    buttonProps={{ minimal: true, rightIcon: 'caret-down' }} | 
|  | 120 | +                  /> | 
|  | 121 | +                  <Text>submissions from</Text> | 
|  | 122 | +                  <SimpleDropdown | 
|  | 123 | +                    options={groupOptions} | 
|  | 124 | +                    defaultValue={showAllGroups} | 
|  | 125 | +                    onClick={handleShowAllGroups} | 
|  | 126 | +                    popoverProps={{ position: Position.BOTTOM }} | 
|  | 127 | +                    buttonProps={{ minimal: true, rightIcon: 'caret-down' }} | 
|  | 128 | +                  /> | 
|  | 129 | +                </Flex> | 
|  | 130 | +                <GradingSubmissionsTable | 
|  | 131 | +                  group={group} | 
|  | 132 | +                  submissions={submissions.filter( | 
|  | 133 | +                    s => showAllSubmissions || isSubmissionUngraded(s) | 
|  | 134 | +                  )} | 
|  | 135 | +                /> | 
|  | 136 | +              </Card> | 
|  | 137 | +            </Col> | 
|  | 138 | + | 
|  | 139 | +            <Col numColSpanLg={2}> | 
|  | 140 | +              <Card hFull> | 
|  | 141 | +                <GradingSummary group={group} submissions={submissions} assessments={assessments} /> | 
|  | 142 | +              </Card> | 
|  | 143 | +            </Col> | 
|  | 144 | +          </ColGrid> | 
| 125 | 145 |         ) | 
| 126 | 146 |       } | 
| 127 | 147 |       fullWidth={true} | 
|  | 
0 commit comments