|
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