From f5e6dd9367e956b3c6c45132971c763ed6d1a34f Mon Sep 17 00:00:00 2001 From: Yu Han Zhang Date: Mon, 13 Feb 2023 00:31:14 +0800 Subject: [PATCH 01/17] feat: redesign submissions table with basic column filtering --- package.json | 2 + .../subcomponents/GradingStatusBadges.tsx | 56 ++++ .../GradingSubmissionFilters.tsx | 62 ++++ .../subcomponents/GradingSubmissionsTable.tsx | 229 ++++++++++++++ yarn.lock | 295 +++++++++++++++++- 5 files changed, 634 insertions(+), 10 deletions(-) create mode 100644 src/pages/academy/grading/subcomponents/GradingStatusBadges.tsx create mode 100644 src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx create mode 100644 src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx diff --git a/package.json b/package.json index b51475be7c..5a29fda9a1 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,8 @@ "@sourceacademy/sharedb-ace": "^2.0.2", "@sourceacademy/sling-client": "^0.1.0", "@szhsin/react-menu": "^3.2.0", + "@tanstack/react-table": "^8.7.9", + "@tremor/react": "^1.7.0", "ace-builds": "^1.4.14", "acorn": "^8.8.2", "ag-grid-community": "^28.0.2", diff --git a/src/pages/academy/grading/subcomponents/GradingStatusBadges.tsx b/src/pages/academy/grading/subcomponents/GradingStatusBadges.tsx new file mode 100644 index 0000000000..40eca7cf9e --- /dev/null +++ b/src/pages/academy/grading/subcomponents/GradingStatusBadges.tsx @@ -0,0 +1,56 @@ +import { Icon } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { Badge } from '@tremor/react'; +import { GradingStatus } from 'src/commons/assessment/AssessmentTypes'; + +type AssessmentTypeBadgeProps = { + type: string; +}; + +const AssessmentTypeBadge: React.FC = ({ type }) => { + const badgeColor = + type === 'Missions' + ? 'indigo' + : type === 'Quests' + ? 'yellow' + : type === 'Paths' + ? 'sky' + : 'gray'; + return ; +}; + +type SubmissionStatusBadgeProps = { + status: string; +}; + +const SubmissionStatusBadge: React.FC = ({ status }) => { + const badgeColor = status === 'submitted' ? 'green' : 'red'; + const statusText = status.charAt(0).toUpperCase() + status.slice(1); + return ; +}; + +type GradingStatusBadgeProps = { + status: GradingStatus; +}; + +const GradingStatusBadge: React.FC = ({ status }) => { + const badgeColor = status === 'graded' ? 'green' : status === 'grading' ? 'yellow' : 'red'; + const statusText = status.charAt(0).toUpperCase() + status.slice(1); + const badgeIcon = () => ( + + ); + return ; +}; + +export { AssessmentTypeBadge, SubmissionStatusBadge, GradingStatusBadge }; diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx new file mode 100644 index 0000000000..e73cb22691 --- /dev/null +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx @@ -0,0 +1,62 @@ +import { Icon } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { ColumnFilter, ColumnFiltersState } from '@tanstack/react-table'; +import { Badge, Flex } from '@tremor/react'; + +type GradingSubmissinoFiltersProps = { + filters: ColumnFiltersState; + onFilterRemove: (filter: ColumnFilter) => void; +}; + +const GradingSubmissionFilters: React.FC = ({ + filters, + onFilterRemove +}) => { + return ( + + {filters.map(filter => ( + + ))} + + ); +}; + +// TODO: extract to a constants file +const FILTER_COLORS = { + // assessments + missions: 'indigo', + quests: 'yellow', + paths: 'sky', + + // submission status + submitted: 'green', + attempted: 'red', + + // grading status + graded: 'green', + grading: 'yellow', + none: 'red' +}; + +type FilterBadgeProps = { + filter: ColumnFilter; + onRemove: (filter: ColumnFilter) => void; +}; + +const FilterBadge: React.FC = ({ filter, onRemove }) => { + let filterValue = filter.value as string; + const filterColor = FILTER_COLORS[filterValue.toLowerCase()] || 'gray'; + + filterValue = filterValue.charAt(0).toUpperCase() + filterValue.slice(1); + return ( + + ); +}; + +export default GradingSubmissionFilters; diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx new file mode 100644 index 0000000000..4377681970 --- /dev/null +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -0,0 +1,229 @@ +import '@tremor/react/dist/esm/tremor.css'; + +import { Icon as BpIcon } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { + Column, + ColumnFilter, + ColumnFiltersState, + createColumnHelper, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + useReactTable +} from '@tanstack/react-table'; +import { + Bold, + Button, + Flex, + Footer, + Icon, + Table, + TableBody, + TableCell, + TableHead, + TableHeaderCell, + TableRow, + Text +} from '@tremor/react'; +import { useState } from 'react'; +import { GradingOverview } from 'src/features/grading/GradingTypes'; + +import { + AssessmentTypeBadge, + GradingStatusBadge, + SubmissionStatusBadge +} from './GradingStatusBadges'; +import GradingSubmissionFilters from './GradingSubmissionFilters'; + +const columnHelper = createColumnHelper(); + +const columns = [ + columnHelper.accessor('assessmentName', { + header: 'Name', + cell: info => + }), + columnHelper.accessor('assessmentType', { + header: 'Type', + cell: info => ( + + + + ) + }), + columnHelper.accessor('studentName', { + header: 'Student', + cell: info => + }), + columnHelper.accessor('submissionStatus', { + header: 'Progress', + cell: info => ( + + + + ) + }), + columnHelper.accessor('gradingStatus', { + header: 'Grading', + cell: info => ( + + + + ) + }), + columnHelper.accessor(({ currentXp, maxXp }) => ({ currentXp, maxXp }), { + header: 'XP', + enableColumnFilter: false, + cell: info => { + const { currentXp, maxXp } = info.getValue(); + return ( + + {currentXp} + / + {maxXp} + + ); + } + }), + columnHelper.display({ + id: 'actions', + header: 'Actions', + cell: () => + }) +]; + +type GradingSubmissionTableProps = { + submissions: GradingOverview[]; +}; + +const GradingSubmissionTable: React.FC = ({ submissions }) => { + const [columnFilters, setColumnFilters] = useState([]); + + const table = useReactTable({ + data: submissions, + columns, + state: { + columnFilters + }, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel() + }); + + const handleFilterRemove = ({ id, value }: ColumnFilter) => { + const newFilters = columnFilters.filter(filter => filter.id !== id && filter.value !== value); + setColumnFilters(newFilters); + }; + + return ( + <> + {/* TODO: add global filter */} + +
+ + + {columnFilters.length > 0 + ? 'Filters: ' + : 'No filters applied. Click a cell to filter by its value.'}{' '} + +
+ +
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ))} + + ))} + + + {table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + + +
+ +
+
+ + ); +}; + +type FilterableProps = { + column: Column; + value: string; + children?: React.ReactNode; +}; + +const Filterable: React.FC = ({ column, value, children }) => { + const handleFilterChange = () => { + column.setFilterValue(value); + }; + + return ( + + ); +}; + +const GradingActions: React.FC = () => { + return ( + + + + + + ); +}; + +export default GradingSubmissionTable; diff --git a/yarn.lock b/yarn.lock index ca61ec181e..d95a0d198d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1830,7 +1830,7 @@ schema-utils "^3.0.0" source-map "^0.7.3" -"@popperjs/core@^2.11.6": +"@popperjs/core@^2.11.6", "@popperjs/core@^2.9.0": version "2.11.6" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== @@ -2227,6 +2227,18 @@ prop-types "^15.7.2" react-transition-state "^1.1.5" +"@tanstack/react-table@^8.7.9": + version "8.7.9" + resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.7.9.tgz#9efcd168fb0080a7e0bc213b5eac8b55513babf4" + integrity sha512-6MbbQn5AupSOkek1+6IYu+1yZNthAKTRZw9tW92Vi6++iRrD1GbI3lKTjJalf8lEEKOqapPzQPE20nywu0PjCA== + dependencies: + "@tanstack/table-core" "8.7.9" + +"@tanstack/table-core@8.7.9": + version "8.7.9" + resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.7.9.tgz#0e975f8a5079972f1827a569079943d43257c42f" + integrity sha512-4RkayPMV1oS2SKDXfQbFoct1w5k+pvGpmX18tCXMofK/VDRdA2hhxfsQlMvsJ4oTX8b0CI4Y3GDKn5T425jBCw== + "@testing-library/dom@^8.0.0": version "8.20.0" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.20.0.tgz#914aa862cef0f5e89b98cc48e3445c4c921010f6" @@ -2255,6 +2267,13 @@ resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.4.3.tgz#af975e367743fa91989cd666666aec31a8f50591" integrity sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q== +"@tippyjs/react@^4.2.6": + version "4.2.6" + resolved "https://registry.yarnpkg.com/@tippyjs/react/-/react-4.2.6.tgz#971677a599bf663f20bb1c60a62b9555b749cc71" + integrity sha512-91RicDR+H7oDSyPycI13q3b7o4O60wa2oRbjlz2fyRLmHImc4vyDwuUP8NtZaN0VARJY5hybvDYrFzhY9+Lbyw== + dependencies: + tippy.js "^6.3.1" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -2265,6 +2284,16 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== +"@tremor/react@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@tremor/react/-/react-1.7.0.tgz#661b6c20d3369e7facba24367ea855927c17544c" + integrity sha512-PlER8rLiF1Id9Gy9FpO4Z9GSkzqQpts9lY5/4tiedHFoEUvgWH3uOGHwPPWiI91WbQlz+1jJRSctdW2KUcCEBw== + dependencies: + "@tippyjs/react" "^4.2.6" + date-fns "^2.28.0" + react-transition-group "^4.4.5" + recharts "^2.3.2" + "@trysound/sax@0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" @@ -2389,6 +2418,57 @@ dependencies: "@types/node" "*" +"@types/d3-array@^3.0.3": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.4.tgz#44eebe40be57476cad6a0cd6a85b0f57d54185a2" + integrity sha512-nwvEkG9vYOc0Ic7G7kwgviY4AQlTfYGIZ0fqB7CQHXGyYM6nO7kJh5EguSNA3jfh4rq7Sb7eMVq8isuvg2/miQ== + +"@types/d3-color@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.0.tgz#6594da178ded6c7c3842f3cc0ac84b156f12f2d4" + integrity sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA== + +"@types/d3-ease@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.0.tgz#c29926f8b596f9dadaeca062a32a45365681eae0" + integrity sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA== + +"@types/d3-interpolate@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz#e7d17fa4a5830ad56fe22ce3b4fac8541a9572dc" + integrity sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw== + dependencies: + "@types/d3-color" "*" + +"@types/d3-path@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.0.0.tgz#939e3a784ae4f80b1fde8098b91af1776ff1312b" + integrity sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg== + +"@types/d3-scale@^4.0.2": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.3.tgz#7a5780e934e52b6f63ad9c24b105e33dd58102b5" + integrity sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ== + dependencies: + "@types/d3-time" "*" + +"@types/d3-shape@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.1.tgz#15cc497751dac31192d7aef4e67a8d2c62354b95" + integrity sha512-6Uh86YFF7LGg4PQkuO2oG6EMBRLuW9cbavUW46zkIO5kuS2PfTqo2o9SkgtQzguBHbLgNnU90UNsITpsX1My+A== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time@*", "@types/d3-time@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.0.tgz#e1ac0f3e9e195135361fa1a1d62f795d87e6e819" + integrity sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg== + +"@types/d3-timer@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.0.tgz#e2505f1c21ec08bda8915238e397fb71d2fc54ce" + integrity sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g== + "@types/dom4@^2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@types/dom4/-/dom4-2.0.2.tgz#6495303f049689ce936ed328a3e5ede9c51408ee" @@ -4267,7 +4347,7 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== -classnames@^2.3.1, classnames@^2.3.2: +classnames@^2.2.5, classnames@^2.3.1, classnames@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== @@ -4736,6 +4816,11 @@ css-tree@^1.1.2, css-tree@^1.1.3: mdn-data "2.0.14" source-map "^0.6.1" +css-unit-converter@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.2.tgz#4c77f5a1954e6dbff60695ecb214e3270436ab21" + integrity sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA== + css-what@^3.2.1: version "3.4.2" resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" @@ -4834,6 +4919,77 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== +"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6: + version "3.2.2" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.2.tgz#f8ac4705c5b06914a7e0025bbf8d5f1513f6a86e" + integrity sha512-yEEyEAbDrF8C6Ob2myOBLjwBLck1Z89jMGFee0oPsn95GqjerpaOA4ch+vc2l0FNFFwMD5N7OCSEN5eAlsUbgQ== + dependencies: + internmap "1 - 2" + +"d3-color@1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" + integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== + +d3-ease@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + +"d3-format@1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" + integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== + +"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + +d3-path@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" + integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== + +d3-scale@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" + integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== + dependencies: + d3-array "2.10.0 - 3" + d3-format "1 - 3" + d3-interpolate "1.2.0 - 3" + d3-time "2.1.1 - 3" + d3-time-format "2 - 4" + +d3-shape@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" + integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== + dependencies: + d3-path "^3.1.0" + +"d3-time-format@2 - 4": + version "4.1.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" + integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== + dependencies: + d3-time "1 - 3" + +"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" + integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== + dependencies: + d3-array "2 - 3" + +d3-timer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + damerau-levenshtein@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" @@ -4855,6 +5011,11 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +date-fns@^2.28.0: + version "2.29.3" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" + integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== + debug@2.6.9, debug@^2.6.0, debug@^2.6.8, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -4876,6 +5037,11 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +decimal.js-light@^2.4.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" + integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg== + decimal.js@^10.2.1: version "10.4.3" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" @@ -5129,6 +5295,13 @@ dom-converter@^0.2.0: dependencies: utila "~0.4" +dom-helpers@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" + integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA== + dependencies: + "@babel/runtime" "^7.1.2" + dom-helpers@^5.0.1: version "5.2.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" @@ -5811,7 +5984,7 @@ event-emitter-es6@^1.1.5: resolved "https://registry.yarnpkg.com/event-emitter-es6/-/event-emitter-es6-1.1.5.tgz#ef95311b2e17aa39be763b031ce4af7ee9cb7849" integrity sha512-/n7qzkJBySdbe1W9/FBDdO7gzDIaewgj+Rj6Ayc2BdvVcaGP+p40DyViOFudCgV47UU8+cUFmcD3tJgjwY65qQ== -eventemitter3@^4.0.0, eventemitter3@^4.0.7: +eventemitter3@^4.0.0, eventemitter3@^4.0.1, eventemitter3@^4.0.7: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== @@ -5929,6 +6102,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-equals@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-2.0.4.tgz#3add9410585e2d7364c2deeb6a707beadb24b927" + integrity sha512-caj/ZmjHljPrZtbzJ3kfH5ia/k4mTJe/qSiXAGzxZWRZgsgDV0cvNaQULqUX8t0/JVlzzEdYOwCN5DmzTxoD4w== + fast-glob@^3.2.12, fast-glob@^3.2.9: version "3.2.12" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" @@ -6965,6 +7143,11 @@ internal-slot@^1.0.3, internal-slot@^1.0.4: has "^1.0.3" side-channel "^1.0.4" +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + invariant@^2.2.2, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -8228,7 +8411,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== -lodash@^4.17.14, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0: +lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -9813,6 +9996,11 @@ postcss-unique-selectors@^5.1.1: dependencies: postcss-selector-parser "^6.0.5" +postcss-value-parser@^3.3.0: + version "3.3.1" + resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz" + integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== + postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" @@ -10215,19 +10403,19 @@ react-hotkeys@^2.0.0: dependencies: prop-types "^15.6.1" +react-is@^16.10.2, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + "react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.0.0, react-is@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== -react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - react-is@^17.0.0, react-is@^17.0.1, react-is@^17.0.2: version "17.0.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== react-konva@^17.0.2-5: @@ -10246,6 +10434,11 @@ react-latex-next@^2.1.0: dependencies: katex "^0.13.0" +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + react-mde@^11.5.0: version "11.5.0" resolved "https://registry.yarnpkg.com/react-mde/-/react-mde-11.5.0.tgz#3e81a505071aa80287fb23a1c0ce5e8b34c82055" @@ -10315,6 +10508,13 @@ react-refresh@^0.11.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046" integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== +react-resize-detector@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-7.1.2.tgz#8ef975dd8c3d56f9a5160ac382ef7136dcd2d86c" + integrity sha512-zXnPJ2m8+6oq9Nn8zsep/orts9vQv3elrpA+R8XTcW7DVVUJ9vwDwMXaBtykAYjMnkCIaOoK9vObyR7ZgFNlOw== + dependencies: + lodash "^4.17.21" + react-responsive@^9.0.0-beta.10: version "9.0.2" resolved "https://registry.yarnpkg.com/react-responsive/-/react-responsive-9.0.2.tgz#34531ca77a61e7a8775714016d21241df7e4205c" @@ -10421,6 +10621,14 @@ react-simple-keyboard@^3.4.240: resolved "https://registry.yarnpkg.com/react-simple-keyboard/-/react-simple-keyboard-3.5.35.tgz#cebb1a9c389b4f5f122aaba97fc184be11199be7" integrity sha512-jEn4SsnzyrK4T1Yki0Ouj7y7igKku5s+09rj+0UPRfcnKNh9RdRL0DYEq5VNUky0sTEGbEXy41AIRHhc+Z5u7g== +react-smooth@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-2.0.1.tgz#74c7309916d6ccca182c4b30c8992f179e6c5a05" + integrity sha512-Own9TA0GPPf3as4vSwFhDouVfXP15ie/wIHklhyKBH5AN6NFtdk0UpHBnonV11BtqDkAWlt40MOUc+5srmW7NA== + dependencies: + fast-equals "^2.0.0" + react-transition-group "2.9.0" + react-sortable-hoc@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/react-sortable-hoc/-/react-sortable-hoc-2.0.0.tgz#f6780d8aa4b922a21f3e754af542f032677078b7" @@ -10469,6 +10677,16 @@ react-textarea-autosize@*, react-textarea-autosize@^8.3.4: use-composed-ref "^1.3.0" use-latest "^1.2.1" +react-transition-group@2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d" + integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg== + dependencies: + dom-helpers "^3.4.0" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react-lifecycles-compat "^3.0.4" + react-transition-group@^4.4.5: version "4.4.5" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" @@ -10547,6 +10765,28 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +recharts-scale@^0.4.4: + version "0.4.5" + resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9" + integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w== + dependencies: + decimal.js-light "^2.4.1" + +recharts@^2.3.2: + version "2.4.1" + resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.4.1.tgz#b5519fe0179cf8b0860951ff5581167347b3c564" + integrity sha512-mX/R6wUFquFKbvqfa2VUFfrtqydzNp6f2b74NKVmk3Zt3m/uPD6hQYST2/1j/YXv0ZbNWE4xAyVp98tMBVxOsA== + dependencies: + classnames "^2.2.5" + eventemitter3 "^4.0.1" + lodash "^4.17.19" + react-is "^16.10.2" + react-resize-detector "^7.1.2" + react-smooth "^2.0.1" + recharts-scale "^0.4.4" + reduce-css-calc "^2.1.8" + victory-vendor "^36.6.8" + reconnecting-websocket@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783" @@ -10559,6 +10799,14 @@ recursive-readdir@^2.2.2: dependencies: minimatch "^3.0.5" +reduce-css-calc@^2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz#7ef8761a28d614980dc0c982f772c93f7a99de03" + integrity sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg== + dependencies: + css-unit-converter "^1.1.1" + postcss-value-parser "^3.3.0" + redux-mock-store@^1.5.4: version "1.5.4" resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.5.4.tgz#90d02495fd918ddbaa96b83aef626287c9ab5872" @@ -11890,6 +12138,13 @@ tiny-warning@^1.0.0: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== +tippy.js@^6.3.1: + version "6.3.7" + resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c" + integrity sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ== + dependencies: + "@popperjs/core" "^2.9.0" + tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -12368,6 +12623,26 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +victory-vendor@^36.6.8: + version "36.6.8" + resolved "https://registry.yarnpkg.com/victory-vendor/-/victory-vendor-36.6.8.tgz#5a1c555ca99a39fdb66a6c959c8426eb834893a2" + integrity sha512-H3kyQ+2zgjMPvbPqAl7Vwm2FD5dU7/4bCTQakFQnpIsfDljeOMDojRsrmJfwh4oAlNnWhpAf+mbAoLh8u7dwyQ== + dependencies: + "@types/d3-array" "^3.0.3" + "@types/d3-ease" "^3.0.0" + "@types/d3-interpolate" "^3.0.1" + "@types/d3-scale" "^4.0.2" + "@types/d3-shape" "^3.1.0" + "@types/d3-time" "^3.0.0" + "@types/d3-timer" "^3.0.0" + d3-array "^3.1.6" + d3-ease "^3.0.1" + d3-interpolate "^3.0.1" + d3-scale "^4.0.2" + d3-shape "^3.1.0" + d3-time "^3.0.0" + d3-timer "^3.0.1" + w3c-hr-time@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" From 70c5556dce6b2e4a41c6cddbf248e3e89b6c3935 Mon Sep 17 00:00:00 2001 From: Yu Han Zhang Date: Wed, 8 Mar 2023 10:34:12 +0800 Subject: [PATCH 02/17] feat: make new submission action buttons functional --- .../grading/subcomponents/GradingActions.tsx | 68 +++++++++++++++++++ .../subcomponents/GradingSubmissionsTable.tsx | 30 ++------ 2 files changed, 74 insertions(+), 24 deletions(-) create mode 100644 src/pages/academy/grading/subcomponents/GradingActions.tsx diff --git a/src/pages/academy/grading/subcomponents/GradingActions.tsx b/src/pages/academy/grading/subcomponents/GradingActions.tsx new file mode 100644 index 0000000000..167f3d98ff --- /dev/null +++ b/src/pages/academy/grading/subcomponents/GradingActions.tsx @@ -0,0 +1,68 @@ +import { Icon as BpIcon } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { Flex, Icon } from '@tremor/react'; +import { useDispatch } from 'react-redux'; +import { + reautogradeSubmission, + unsubmitSubmission +} from 'src/commons/application/actions/SessionActions'; +import { showSimpleConfirmDialog } from 'src/commons/utils/DialogHelper'; +import { useTypedSelector } from 'src/commons/utils/Hooks'; + +type GradingActionsProps = { + submissionId: number; +}; + +const GradingActions: React.FC = ({ submissionId }) => { + const dispatch = useDispatch(); + const courseId = useTypedSelector(store => store.session.courseId); + + const handleReautogradeClick = async () => { + const confirm = await showSimpleConfirmDialog({ + contents: ( + <> +

Reautograde this submission?

+

Note: all manual adjustments will be reset to 0.

+ + ), + positiveIntent: 'danger', + positiveLabel: 'Reautograde' + }); + if (confirm) { + dispatch(reautogradeSubmission(submissionId)); + } + }; + + const handleUnsubmitClick = async () => { + const confirm = await showSimpleConfirmDialog({ + contents: 'Are you sure you want to unsubmit?', + positiveIntent: 'danger', + positiveLabel: 'Unsubmit' + }); + if (confirm) { + dispatch(unsubmitSubmission(submissionId)); + } + }; + + return ( + + + } variant="light" /> + + + + + + + ); +}; + +export default GradingActions; diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 4377681970..b871cd36db 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -18,7 +18,6 @@ import { Button, Flex, Footer, - Icon, Table, TableBody, TableCell, @@ -30,6 +29,7 @@ import { import { useState } from 'react'; import { GradingOverview } from 'src/features/grading/GradingTypes'; +import GradingActions from './GradingActions'; import { AssessmentTypeBadge, GradingStatusBadge, @@ -86,10 +86,12 @@ const columns = [ ); } }), - columnHelper.display({ - id: 'actions', + columnHelper.accessor(({ submissionId }) => ({ submissionId }), { header: 'Actions', - cell: () => + cell: info => { + const { submissionId } = info.getValue(); + return ; + } }) ]; @@ -206,24 +208,4 @@ const Filterable: React.FC = ({ column, value, children }) => { ); }; -const GradingActions: React.FC = () => { - return ( - - - - - - ); -}; - export default GradingSubmissionTable; From bf899ffcdfd361968880e2ab235d2acf4d1e0c5c Mon Sep 17 00:00:00 2001 From: Yu Han Zhang Date: Wed, 29 Mar 2023 21:31:31 +0800 Subject: [PATCH 03/17] feat: improving submissions table filtering --- .../subcomponents/GradingStatusBadges.tsx | 11 ++-- .../GradingSubmissionFilters.tsx | 1 + .../subcomponents/GradingSubmissionsTable.tsx | 60 ++++++++++++------- 3 files changed, 47 insertions(+), 25 deletions(-) diff --git a/src/pages/academy/grading/subcomponents/GradingStatusBadges.tsx b/src/pages/academy/grading/subcomponents/GradingStatusBadges.tsx index 40eca7cf9e..13ecd48106 100644 --- a/src/pages/academy/grading/subcomponents/GradingStatusBadges.tsx +++ b/src/pages/academy/grading/subcomponents/GradingStatusBadges.tsx @@ -5,18 +5,19 @@ import { GradingStatus } from 'src/commons/assessment/AssessmentTypes'; type AssessmentTypeBadgeProps = { type: string; + size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; }; -const AssessmentTypeBadge: React.FC = ({ type }) => { +const AssessmentTypeBadge: React.FC = ({ type, size = 'sm' }) => { const badgeColor = type === 'Missions' ? 'indigo' : type === 'Quests' - ? 'yellow' + ? 'emerald' : type === 'Paths' ? 'sky' : 'gray'; - return ; + return ; }; type SubmissionStatusBadgeProps = { @@ -24,7 +25,7 @@ type SubmissionStatusBadgeProps = { }; const SubmissionStatusBadge: React.FC = ({ status }) => { - const badgeColor = status === 'submitted' ? 'green' : 'red'; + const badgeColor = status === 'submitted' ? 'green' : status === 'attempting' ? 'yellow' : 'red'; const statusText = status.charAt(0).toUpperCase() + status.slice(1); return ; }; @@ -45,7 +46,7 @@ const GradingStatusBadge: React.FC = ({ status }) => { ? IconNames.TIME : status === 'none' ? IconNames.CROSS - : IconNames.ERROR + : IconNames.DISABLE } style={{ marginRight: '0.5rem' }} /> diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx index e73cb22691..774a204a47 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx @@ -30,6 +30,7 @@ const FILTER_COLORS = { // submission status submitted: 'green', + attempting: 'yellow', attempted: 'red', // grading status diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index b871cd36db..f5f002e3c5 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -24,7 +24,8 @@ import { TableHead, TableHeaderCell, TableRow, - Text + Text, + TextInput } from '@tremor/react'; import { useState } from 'react'; import { GradingOverview } from 'src/features/grading/GradingTypes'; @@ -56,6 +57,10 @@ const columns = [ header: 'Student', cell: info => }), + columnHelper.accessor('groupName', { + header: 'Group', + cell: info => + }), columnHelper.accessor('submissionStatus', { header: 'Progress', cell: info => ( @@ -88,6 +93,7 @@ const columns = [ }), columnHelper.accessor(({ submissionId }) => ({ submissionId }), { header: 'Actions', + enableColumnFilter: false, cell: info => { const { submissionId } = info.getValue(); return ; @@ -96,19 +102,30 @@ const columns = [ ]; type GradingSubmissionTableProps = { + group: string | null; submissions: GradingOverview[]; }; -const GradingSubmissionTable: React.FC = ({ submissions }) => { - const [columnFilters, setColumnFilters] = useState([]); +const GradingSubmissionTable: React.FC = ({ group, submissions }) => { + const defaultFilters = []; + if (group) { + defaultFilters.push({ + id: 'groupName', + value: group + }); + } + const [columnFilters, setColumnFilters] = useState(defaultFilters); + const [globalFilter, setGlobalFilter] = useState(''); const table = useReactTable({ data: submissions, columns, state: { - columnFilters + columnFilters, + globalFilter }, onColumnFiltersChange: setColumnFilters, + onGlobalFilterChange: setGlobalFilter, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), getPaginationRowModel: getPaginationRowModel() @@ -121,22 +138,25 @@ const GradingSubmissionTable: React.FC = ({ submiss return ( <> - {/* TODO: add global filter */} - -
- - - {columnFilters.length > 0 - ? 'Filters: ' - : 'No filters applied. Click a cell to filter by its value.'}{' '} - -
- + + +
+ + + {columnFilters.length > 0 + ? 'Filters: ' + : 'No filters applied. Click on any cell to filter by its value.'}{' '} + +
+ +
+ + } + placeholder="Search for any value here..." + onChange={e => setGlobalFilter(e.target.value)} + />
From e8a04d517623f3a9939e34377ca2e887d56f7ef0 Mon Sep 17 00:00:00 2001 From: Yu Han Zhang Date: Wed, 29 Mar 2023 22:14:54 +0800 Subject: [PATCH 04/17] feat: add submissions table filter state to Redux store --- src/commons/application/ApplicationTypes.ts | 4 +++ src/commons/workspace/WorkspaceActions.ts | 5 ++++ src/commons/workspace/WorkspaceReducer.ts | 9 +++++++ src/commons/workspace/WorkspaceTypes.ts | 7 +++++ .../workspace/__tests__/WorkspaceActions.ts | 26 ++++++++++++++++++ .../subcomponents/GradingSubmissionsTable.tsx | 27 ++++++++++++++++--- 6 files changed, 74 insertions(+), 4 deletions(-) diff --git a/src/commons/application/ApplicationTypes.ts b/src/commons/application/ApplicationTypes.ts index 6dd4de5813..d88e9bf34f 100644 --- a/src/commons/application/ApplicationTypes.ts +++ b/src/commons/application/ApplicationTypes.ts @@ -300,6 +300,10 @@ export const defaultWorkspaceManager: WorkspaceManagerState = { }, grading: { ...createDefaultWorkspace('grading'), + submissionsTableFilters: { + columnFilters: [], + globalFilter: null + }, currentSubmission: undefined, currentQuestion: undefined, hasUnsavedChanges: false diff --git a/src/commons/workspace/WorkspaceActions.ts b/src/commons/workspace/WorkspaceActions.ts index 18c5db06b7..1545068c17 100644 --- a/src/commons/workspace/WorkspaceActions.ts +++ b/src/commons/workspace/WorkspaceActions.ts @@ -44,6 +44,7 @@ import { SEND_REPL_INPUT_TO_OUTPUT, SET_FOLDER_MODE, SHIFT_EDITOR_TAB, + SubmissionsTableFilters, TOGGLE_EDITOR_AUTORUN, TOGGLE_FOLDER_MODE, TOGGLE_USING_SUBST, @@ -56,6 +57,7 @@ import { UPDATE_HAS_UNSAVED_CHANGES, UPDATE_REPL_VALUE, UPDATE_SUBLANGUAGE, + UPDATE_SUBMISSIONS_TABLE_FILTERS, UPDATE_WORKSPACE, WorkspaceLocation, WorkspaceState @@ -308,6 +310,9 @@ export const setIsEditorReadonly = ( isEditorReadonly: isEditorReadonly }); +export const updateSubmissionsTableFilters = (filters: SubmissionsTableFilters) => + action(UPDATE_SUBMISSIONS_TABLE_FILTERS, { filters }); + export const updateCurrentAssessmentId = (assessmentId: number, questionId: number) => action(UPDATE_CURRENT_ASSESSMENT_ID, { assessmentId, diff --git a/src/commons/workspace/WorkspaceReducer.ts b/src/commons/workspace/WorkspaceReducer.ts index d530c78a53..abab92539c 100644 --- a/src/commons/workspace/WorkspaceReducer.ts +++ b/src/commons/workspace/WorkspaceReducer.ts @@ -68,6 +68,7 @@ import { UPDATE_HAS_UNSAVED_CHANGES, UPDATE_REPL_VALUE, UPDATE_SUBLANGUAGE, + UPDATE_SUBMISSIONS_TABLE_FILTERS, UPDATE_WORKSPACE, WorkspaceLocation, WorkspaceManagerState @@ -576,6 +577,14 @@ export const WorkspaceReducer: Reducer = ( } else { return state; } + case UPDATE_SUBMISSIONS_TABLE_FILTERS: + return { + ...state, + grading: { + ...state.grading, + submissionsTableFilters: action.payload.filters + } + }; case UPDATE_CURRENT_ASSESSMENT_ID: return { ...state, diff --git a/src/commons/workspace/WorkspaceTypes.ts b/src/commons/workspace/WorkspaceTypes.ts index 66b06d5bbc..a20c926c37 100644 --- a/src/commons/workspace/WorkspaceTypes.ts +++ b/src/commons/workspace/WorkspaceTypes.ts @@ -35,6 +35,7 @@ export const RESET_WORKSPACE = 'RESET_WORKSPACE'; export const SEND_REPL_INPUT_TO_OUTPUT = 'SEND_REPL_INPUT_TO_OUTPUT'; export const TOGGLE_EDITOR_AUTORUN = 'TOGGLE_EDITOR_AUTORUN'; export const TOGGLE_USING_SUBST = 'TOGGLE_USING_SUBST'; +export const UPDATE_SUBMISSIONS_TABLE_FILTERS = 'UPDATE_SUBMISSIONS_TABLE_FILTERS'; export const UPDATE_CURRENT_ASSESSMENT_ID = 'UPDATE_CURRENT_ASSESSMENT_ID'; export const UPDATE_CURRENT_SUBMISSION_ID = 'UPDATE_CURRENT_SUBMISSION_ID'; export const TOGGLE_FOLDER_MODE = 'TOGGLE_FOLDER_MODE'; @@ -66,6 +67,7 @@ type AssessmentWorkspaceAttr = { type AssessmentWorkspaceState = AssessmentWorkspaceAttr & WorkspaceState; type GradingWorkspaceAttr = { + readonly submissionsTableFilters: SubmissionsTableFilters; readonly currentSubmission?: number; readonly currentQuestion?: number; readonly hasUnsavedChanges: boolean; @@ -137,3 +139,8 @@ export type DebuggerContext = { context: Context; workspaceLocation?: WorkspaceLocation; }; + +export type SubmissionsTableFilters = { + columnFilters: { id: string; value: unknown }[]; + globalFilter: string | null; +}; diff --git a/src/commons/workspace/__tests__/WorkspaceActions.ts b/src/commons/workspace/__tests__/WorkspaceActions.ts index a7e99a0c2c..4a223ac82c 100644 --- a/src/commons/workspace/__tests__/WorkspaceActions.ts +++ b/src/commons/workspace/__tests__/WorkspaceActions.ts @@ -41,6 +41,7 @@ import { toggleUsingSubst, updateActiveEditorTab, updateActiveEditorTabIndex, + updateSubmissionsTableFilters, updateCurrentAssessmentId, updateCurrentSubmissionId, updateEditorValue, @@ -83,6 +84,7 @@ import { TOGGLE_USING_SUBST, UPDATE_ACTIVE_EDITOR_TAB, UPDATE_ACTIVE_EDITOR_TAB_INDEX, + UPDATE_SUBMISSIONS_TABLE_FILTERS, UPDATE_CURRENT_ASSESSMENT_ID, UPDATE_CURRENT_SUBMISSION_ID, UPDATE_EDITOR_BREAKPOINTS, @@ -541,6 +543,30 @@ test('resetWorkspace generates correct action object with provided workspace', ( }); }); +test('updateSubmissionsTableFilters generates correct action object', () => { + const columnFilters = [ + { + id: 'groupName', + value: '1A' + }, + { + id: 'assessmentType', + value: 'Missions' + } + ]; + const globalFilter = 'runes'; + const action = updateSubmissionsTableFilters({ columnFilters, globalFilter }); + expect(action).toEqual({ + type: UPDATE_SUBMISSIONS_TABLE_FILTERS, + payload: { + filters: { + columnFilters, + globalFilter + } + } + }); +}); + test('updateCurrentAssessmentId generates correct action object', () => { const assessmentId = 2; const questionId = 4; diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index f5f002e3c5..5da7994639 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -27,7 +27,10 @@ import { Text, TextInput } from '@tremor/react'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { useTypedSelector } from 'src/commons/utils/Hooks'; +import { updateSubmissionsTableFilters } from 'src/commons/workspace/WorkspaceActions'; import { GradingOverview } from 'src/features/grading/GradingTypes'; import GradingActions from './GradingActions'; @@ -107,15 +110,22 @@ type GradingSubmissionTableProps = { }; const GradingSubmissionTable: React.FC = ({ group, submissions }) => { + const dispatch = useDispatch(); + const tableFilters = useTypedSelector(state => state.workspaces.grading.submissionsTableFilters); + const defaultFilters = []; - if (group) { + if (group && !tableFilters.columnFilters.find(filter => filter.id === 'groupName')) { defaultFilters.push({ id: 'groupName', value: group }); } - const [columnFilters, setColumnFilters] = useState(defaultFilters); - const [globalFilter, setGlobalFilter] = useState(''); + + const [columnFilters, setColumnFilters] = useState([ + ...tableFilters.columnFilters, + ...defaultFilters + ]); + const [globalFilter, setGlobalFilter] = useState(tableFilters.globalFilter); const table = useReactTable({ data: submissions, @@ -136,6 +146,15 @@ const GradingSubmissionTable: React.FC = ({ group, setColumnFilters(newFilters); }; + useEffect(() => { + dispatch( + updateSubmissionsTableFilters({ + columnFilters, + globalFilter + }) + ); + }, [columnFilters, globalFilter, dispatch]); + return ( <> From da707a5849912e44881f3281c701c97a323d22a9 Mon Sep 17 00:00:00 2001 From: Yu Han Zhang Date: Wed, 29 Mar 2023 22:15:15 +0800 Subject: [PATCH 05/17] fix: use React Router's `Link` instead of `a` tag --- src/pages/academy/grading/subcomponents/GradingActions.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/academy/grading/subcomponents/GradingActions.tsx b/src/pages/academy/grading/subcomponents/GradingActions.tsx index 167f3d98ff..93e2400494 100644 --- a/src/pages/academy/grading/subcomponents/GradingActions.tsx +++ b/src/pages/academy/grading/subcomponents/GradingActions.tsx @@ -2,6 +2,7 @@ import { Icon as BpIcon } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import { Flex, Icon } from '@tremor/react'; import { useDispatch } from 'react-redux'; +import { Link } from 'react-router-dom'; import { reautogradeSubmission, unsubmitSubmission @@ -46,9 +47,9 @@ const GradingActions: React.FC = ({ submissionId }) => { return ( - + } variant="light" /> - + - -
- -
-
- -
- - - ); - - const grid = ( -
- -
- ); - - const content = ( -
- {controls} - - {grid} -
- ); + ) || []; - return ( -
- -
- ); - } + const exportCSV = () => { + if (!gradingOverviews) return; - public componentDidUpdate(prevProps: GradingProps, prevState: State) { - // Only update grid data when a notification is acknowledged - if (this.gridApi && this.props.notifications.length !== prevProps.notifications.length) { - // Pass the new reconstructed row data to the grid after fetching the updated notifs - this.gridApi.setRowData(this.sortSubmissionsByNotifications()); + const win = document.defaultView || window; + if (!win) { + console.warn('There is no `window` associated with the current `document`'); + return; } - } - - /** Component to render in table - grading status */ - private GradingStatus = (props: GradingNavLinkProps) => { - return ; - }; - private NotificationBadgeCell = (props: GradingNavLinkProps) => { - return ( - + const content = new Blob( + [ + 'Assessment Name,Student Name,Group,Status,Grading,Question Count,Questions Graded,Initial XP,XP Adjustment,Current XP (excl. bonus),Max XP,Bonus XP\n', + ...gradingOverviews.map( + e => + [ + e.assessmentName, + e.studentName, + e.groupName, + e.submissionStatus, + e.gradingStatus, + e.questionCount, + e.gradedCount, + e.initialXp, + e.xpAdjustment, + e.currentXp, + e.maxXp, + e.xpBonus + ].join(',') + '\n' + ) + ], + { type: 'text/csv' } + ); + const fileName = `SA submissions (${new Date().toISOString()}).csv`; + + // code from https://github.com/ag-grid/ag-grid/blob/latest/grid-community-modules/csv-export/src/csvExport/downloader.ts + const element = document.createElement('a'); + const url = win.URL.createObjectURL(content); + element.setAttribute('href', url); + element.setAttribute('download', fileName); + element.style.display = 'none'; + document.body.appendChild(element); + + element.dispatchEvent( + new MouseEvent('click', { + bubbles: false, + cancelable: true, + view: win + }) ); - }; - - /** Component to render in table - XP */ - private GradingExp = (props: GradingNavLinkProps) => { - return ; - }; - - // Forcibly resizes columns to fit the width of the datagrid - prevents datagrid - // from needing to render a horizontal scrollbar when columns overflow grid width - private resizeGrid = () => { - if (this.gridApi) { - this.gridApi.sizeColumnsToFit(); - } - }; - - private updatePaginationState = () => { - if (this.gridApi) { - const newTotalPages = this.gridApi.paginationGetTotalPages(); - const newCurrPage = newTotalPages === 0 ? 0 : this.gridApi.paginationGetCurrentPage() + 1; - this.setState({ - currPage: newCurrPage, - maxPages: newTotalPages, - rowCountString: this.formatRowCountString( - 25, - newCurrPage, - newTotalPages, - this.gridApi.paginationGetRowCount() - ), - isBackDisabled: newTotalPages === 0 || newCurrPage === 1, - isForwardDisabled: newTotalPages === 0 || newCurrPage === newTotalPages - }); - } - }; - - private formatRowCountString = ( - pageSize: number, - currPage: number, - maxPages: number, - totalRows: number - ) => { - return maxPages === 0 - ? '(none)' - : currPage !== maxPages - ? `(#${pageSize * currPage - 24} - #${pageSize * currPage})` - : `(#${pageSize * currPage - 24} - #${totalRows})`; - }; - - private handleFilterKeypress = (event: React.KeyboardEvent) => { - if (event.key === 'Enter') { - this.applyFilter(event.currentTarget.value); - } - }; - - private handleApplyFilter = (event: React.FocusEvent) => { - this.applyFilter(event.target.value); - }; - - private handleGroupsFilter = () => { - if (this.gridApi) { - this.setState({ groupFilterEnabled: !this.state.groupFilterEnabled }); - this.props.handleFetchGradingOverviews(this.state.groupFilterEnabled); - } - }; - - private applyFilter = (filter: string) => { - this.gridApi?.setQuickFilter(filter); - }; - private onGridReady = (params: GridReadyEvent) => { - this.gridApi = params.api; - this.gridApi.sizeColumnsToFit(); - this.updatePaginationState(); - }; + document.body.removeChild(element); - private exportCSV = () => { - if (this.gridApi) { - this.gridApi.exportDataAsCsv({ - fileName: `SA submissions (${new Date().toISOString()}).csv`, - // Explicitly declare exported columns to avoid exporting trash columns - columnKeys: [ - 'assessmentName', - 'assessmentCategory', - 'studentName', - 'groupName', - 'submissionStatus', - 'gradingStatus', - 'questionCount', - 'gradedCount', - 'initialGrade', - 'gradeAdjustment', - 'currentGrade', - 'maxGrade', - 'initialXp', - 'xpAdjustment', - 'currentXp', - 'maxXp', - 'xpBonus' - ] - }); - } + win.setTimeout(() => { + win.URL.revokeObjectURL(url); + }, 0); }; - private changePaginationView = (type: string) => { - return () => { - if (this.gridApi) { - switch (type) { - case 'first': - return this.gridApi.paginationGoToFirstPage(); - case 'prev': - return this.gridApi.paginationGoToPreviousPage(); - case 'next': - return this.gridApi.paginationGoToNextPage(); - case 'last': - return this.gridApi.paginationGoToLastPage(); - default: - } - } - }; - }; - - /** Constructs data nodes for the datagrid by joining grading overviews with their - * associated notifications. - * @return Returns an array of data nodes, prioritising grading overviews with - * notifications first. - */ - private sortSubmissionsByNotifications = () => { - if (!this.props.gradingOverviews) { - return []; - } - - return (this.props.gradingOverviews as GradingOverviewWithNotifications[]) - .map(overview => ({ - ...overview, - notifications: filterNotificationsBySubmission(overview.submissionId)( - this.props.notifications + return ( + ) - })) - .sort((subX, subY) => subY.notifications.length - subX.notifications.length); - }; -} + } + fullWidth={true} + /> + ); +}; export default Grading; diff --git a/src/pages/academy/grading/GradingContainer.ts b/src/pages/academy/grading/GradingContainer.ts deleted file mode 100644 index 15707705e6..0000000000 --- a/src/pages/academy/grading/GradingContainer.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; - -import { - acknowledgeNotifications, - fetchGradingOverviews, - reautogradeSubmission, - unsubmitSubmission -} from '../../../commons/application/actions/SessionActions'; -import { OverallState } from '../../../commons/application/ApplicationTypes'; -import Grading, { DispatchProps, StateProps } from './Grading'; - -const mapStateToProps: MapStateToProps = state => ({ - gradingOverviews: state.session.gradingOverviews, - courseRegId: state.session.courseRegId, - notifications: state.session.notifications, - role: state.session.role, - courseId: state.session.courseId -}); - -const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch) => - bindActionCreators( - { - handleAcknowledgeNotifications: acknowledgeNotifications, - handleFetchGradingOverviews: fetchGradingOverviews, - handleUnsubmitSubmission: unsubmitSubmission, - handleReautogradeSubmission: reautogradeSubmission - }, - dispatch - ); - -const GradingContainer = connect(mapStateToProps, mapDispatchToProps)(Grading); - -export default GradingContainer; diff --git a/src/pages/academy/grading/subcomponents/GradingActionsCell.tsx b/src/pages/academy/grading/subcomponents/GradingActionsCell.tsx deleted file mode 100644 index 4d1a52e27e..0000000000 --- a/src/pages/academy/grading/subcomponents/GradingActionsCell.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { Button, Position } from '@blueprintjs/core'; -import { Tooltip2 } from '@blueprintjs/popover2'; -import * as React from 'react'; -import AnchorButtonLink from 'src/commons/AnchorButtonLink'; - -import { Role } from '../../../../commons/application/ApplicationTypes'; -import { showSimpleConfirmDialog } from '../../../../commons/utils/DialogHelper'; -import { GradingOverview } from '../../../../features/grading/GradingTypes'; - -export type GradingActionsCellProps = DispatchProps & StateProps; -type DispatchProps = { - handleUnsubmitSubmission: (submissionId: number) => void; - handleReautogradeSubmission: (submissionId: number) => void; -}; - -type StateProps = { - data: GradingOverview; - courseId?: number; - courseRegId?: number; - role?: Role; -}; - -const GradingActionsCell: React.FC = props => { - const handleConfirmUnsubmit = async () => { - const confirm = await showSimpleConfirmDialog({ - contents: 'Are you sure you want to unsubmit?', - positiveIntent: 'danger', - positiveLabel: 'Unsubmit' - }); - if (confirm) { - props.handleUnsubmitSubmission(props.data.submissionId); - } - }; - - const handleConfirmReautograde = async () => { - const confirm = await showSimpleConfirmDialog({ - contents: ( - <> -

Reautograde this submission?

-

Note: all manual adjustments will be reset to 0.

- - ), - positiveIntent: 'danger', - positiveLabel: 'Reautograde' - }); - if (confirm) { - props.handleReautogradeSubmission(props.data.submissionId); - } - }; - - const isOwnSubmission = props.courseRegId && props.courseRegId === props.data.studentId; - const canReautograde = isOwnSubmission || props.data.submissionStatus === 'submitted'; - const canUnsubmit = - props.data.submissionStatus === 'submitted' && - props.courseRegId && - (props.courseRegId === props.data.groupLeaderId || - isOwnSubmission || - props.role === Role.Admin); - - return ( - <> - - - - - -
+ + + Submissions + + + + + + + + + + + + + + + + + ); +}; + +export default GradingDashboard; diff --git a/src/pages/academy/grading/subcomponents/GradingStatusCell.tsx b/src/pages/academy/grading/subcomponents/GradingStatusCell.tsx deleted file mode 100644 index 0e0cf133ba..0000000000 --- a/src/pages/academy/grading/subcomponents/GradingStatusCell.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Icon, IconName, Intent, Position } from '@blueprintjs/core'; -import { IconNames } from '@blueprintjs/icons'; -import { Tooltip2 } from '@blueprintjs/popover2'; -import React from 'react'; - -import { GradingStatuses } from '../../../../commons/assessment/AssessmentTypes'; -import { GradingCellProps } from '../../../../features/grading/GradingTypes'; - -/** - * Used to render the submission grading status details in the table that displays GradingOverviews. - * This is a fully fledged component (not SFC) by specification in - * ag-grid. - * - * See {@link https://www.ag-grid.com/example-react-dynamic} - */ -const GradingStatusCell: React.FC = props => { - const gradingStatus = props.data.gradingStatus; - let iconName: IconName; - let tooltip: string; - let intent: Intent; - - switch (gradingStatus) { - case GradingStatuses.graded: - iconName = IconNames.TICK; - tooltip = `Fully graded: ${props.data.gradedCount} of - ${props.data.questionCount}`; - intent = Intent.SUCCESS; - break; - case GradingStatuses.grading: - iconName = IconNames.TIME; - tooltip = `Partially graded: ${props.data.gradedCount} of - ${props.data.questionCount}`; - intent = Intent.WARNING; - break; - case GradingStatuses.none: - iconName = IconNames.CROSS; - tooltip = `Not graded: ${props.data.gradedCount} of - ${props.data.questionCount}`; - intent = Intent.DANGER; - break; - default: - iconName = IconNames.DISABLE; - tooltip = 'Not applicable'; - intent = Intent.PRIMARY; - } - - return ( -
- - - -
- ); -}; - -export default GradingStatusCell; diff --git a/src/pages/academy/grading/subcomponents/GradingXPCell.tsx b/src/pages/academy/grading/subcomponents/GradingXPCell.tsx deleted file mode 100644 index a56040b847..0000000000 --- a/src/pages/academy/grading/subcomponents/GradingXPCell.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Position } from '@blueprintjs/core'; -import { Tooltip2 } from '@blueprintjs/popover2'; -import React from 'react'; - -import { GradingCellProps } from '../../../../features/grading/GradingTypes'; - -/** - * Used to render the submission XP details in the table that displays GradingOverviews. - * This is a fully fledged component (not SFC) by specification in - * ag-grid. - * - * See {@link https://www.ag-grid.com/example-react-dynamic} - */ -const GradingXPCell: React.FC = props => { - if (props.data.maxXp || props.data.xpBonus) { - const tooltip = `Initial XP: ${props.data.initialXp} - (${props.data.xpBonus > 0 ? `+${props.data.xpBonus} bonus ` : ''} - ${props.data.xpAdjustment >= 0 ? '+' : ''}${props.data.xpAdjustment} adj.)`; - return ( -
- - {`${props.data.currentXp + props.data.xpBonus} / ${props.data.maxXp}`} - -
- ); - } else { - return
No Exp
; - } -}; - -export default GradingXPCell; From 335ccdf64559425949fc85fa733639f70478b31f Mon Sep 17 00:00:00 2001 From: Yu Han Zhang Date: Tue, 11 Apr 2023 23:01:05 +0800 Subject: [PATCH 09/17] feat: move `Export to CSV` button --- .../grading/subcomponents/GradingDashboard.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/pages/academy/grading/subcomponents/GradingDashboard.tsx b/src/pages/academy/grading/subcomponents/GradingDashboard.tsx index 76a9ba3533..b749ba4c5a 100644 --- a/src/pages/academy/grading/subcomponents/GradingDashboard.tsx +++ b/src/pages/academy/grading/subcomponents/GradingDashboard.tsx @@ -1,5 +1,7 @@ import '@tremor/react/dist/esm/tremor.css'; +import { Icon as BpIcon } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; import { Button, Card, Col, ColGrid, Flex, Title, Toggle, ToggleItem } from '@tremor/react'; import { useState } from 'react'; import { GradingStatuses } from 'src/commons/assessment/AssessmentTypes'; @@ -38,8 +40,18 @@ const GradingDashboard: React.FC = ({ submissions, handle
- Submissions - + + Submissions + + + From b84a7bd4654058157865434a775db20247b84fcc Mon Sep 17 00:00:00 2001 From: Yu Han Zhang Date: Wed, 12 Apr 2023 08:50:05 +0800 Subject: [PATCH 10/17] fix: sort imports --- src/commons/workspace/__tests__/WorkspaceActions.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commons/workspace/__tests__/WorkspaceActions.ts b/src/commons/workspace/__tests__/WorkspaceActions.ts index 4a223ac82c..41eaaddb2c 100644 --- a/src/commons/workspace/__tests__/WorkspaceActions.ts +++ b/src/commons/workspace/__tests__/WorkspaceActions.ts @@ -41,13 +41,13 @@ import { toggleUsingSubst, updateActiveEditorTab, updateActiveEditorTabIndex, - updateSubmissionsTableFilters, updateCurrentAssessmentId, updateCurrentSubmissionId, updateEditorValue, updateHasUnsavedChanges, updateReplValue, - updateSublanguage + updateSublanguage, + updateSubmissionsTableFilters } from '../WorkspaceActions'; import { ADD_EDITOR_TAB, @@ -84,7 +84,6 @@ import { TOGGLE_USING_SUBST, UPDATE_ACTIVE_EDITOR_TAB, UPDATE_ACTIVE_EDITOR_TAB_INDEX, - UPDATE_SUBMISSIONS_TABLE_FILTERS, UPDATE_CURRENT_ASSESSMENT_ID, UPDATE_CURRENT_SUBMISSION_ID, UPDATE_EDITOR_BREAKPOINTS, @@ -92,6 +91,7 @@ import { UPDATE_HAS_UNSAVED_CHANGES, UPDATE_REPL_VALUE, UPDATE_SUBLANGUAGE, + UPDATE_SUBMISSIONS_TABLE_FILTERS, WorkspaceLocation } from '../WorkspaceTypes'; From 1b2527f20572075c8fb0300fe57c38a0aff47baa Mon Sep 17 00:00:00 2001 From: Yu Han Zhang Date: Wed, 12 Apr 2023 09:05:04 +0800 Subject: [PATCH 11/17] fix: skip missing assessments instead of throwing an error --- .../grading/subcomponents/GradingSummary.tsx | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/pages/academy/grading/subcomponents/GradingSummary.tsx b/src/pages/academy/grading/subcomponents/GradingSummary.tsx index f12dbfb643..ce5de4ab1d 100644 --- a/src/pages/academy/grading/subcomponents/GradingSummary.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSummary.tsx @@ -21,6 +21,12 @@ type GradingSummaryProps = { assessments: AssessmentOverview[]; }; +type AssessmentSummary = { + id: number; + type: string; + title: string; +}; + const GradingSummary: React.FC = ({ group, submissions, assessments }) => { submissions = submissions.filter(({ assessmentType }) => assessmentType !== 'Paths'); const groupSubmissions = submissions.filter( @@ -29,17 +35,21 @@ const GradingSummary: React.FC = ({ group, submissions, ass const ungraded = groupSubmissions.filter( ({ gradingStatus }) => gradingStatus !== GradingStatuses.graded ); - const ungradedAssessments = [...new Set(ungraded.map(({ assessmentId }) => assessmentId))] - .map(assessmentId => { + const ungradedAssessments = [...new Set(ungraded.map(({ assessmentId }) => assessmentId))].reduce( + (acc: AssessmentSummary[], assessmentId) => { const assessment = assessments.find(assessment => assessment.id === assessmentId); - if (!assessment) throw new Error('Assessment not found'); - return { - id: assessmentId, - type: assessment.type, - title: assessment.title - }; - }) - .filter(({ type }) => type !== 'Path'); + if (!assessment || assessment.type === 'Path') return acc; + return [ + ...acc, + { + id: assessmentId, + type: assessment.type, + title: assessment.title + } + ]; + }, + [] + ); const numSubmissions = groupSubmissions.length; const numGraded = numSubmissions - ungraded.length; From ef91fcbfda2474363989c5ae11ae0412a8f878d5 Mon Sep 17 00:00:00 2001 From: Yu Han Zhang Date: Wed, 12 Apr 2023 09:20:22 +0800 Subject: [PATCH 12/17] refactor: add helper to get badge color --- ...dingStatusBadges.tsx => GradingBadges.tsx} | 15 ++++-------- .../GradingSubmissionFilters.tsx | 24 +++---------------- .../subcomponents/GradingSubmissionsTable.tsx | 6 +---- .../grading/subcomponents/GradingSummary.tsx | 2 +- .../academy/grading/subcomponents/colors.ts | 20 ++++++++++++++++ 5 files changed, 29 insertions(+), 38 deletions(-) rename src/pages/academy/grading/subcomponents/{GradingStatusBadges.tsx => GradingBadges.tsx} (80%) create mode 100644 src/pages/academy/grading/subcomponents/colors.ts diff --git a/src/pages/academy/grading/subcomponents/GradingStatusBadges.tsx b/src/pages/academy/grading/subcomponents/GradingBadges.tsx similarity index 80% rename from src/pages/academy/grading/subcomponents/GradingStatusBadges.tsx rename to src/pages/academy/grading/subcomponents/GradingBadges.tsx index 967196b7ba..fcd3403a8e 100644 --- a/src/pages/academy/grading/subcomponents/GradingStatusBadges.tsx +++ b/src/pages/academy/grading/subcomponents/GradingBadges.tsx @@ -3,25 +3,19 @@ import { IconNames } from '@blueprintjs/icons'; import { Badge } from '@tremor/react'; import { GradingStatus } from 'src/commons/assessment/AssessmentTypes'; +import { badgeColor } from './colors'; + type AssessmentTypeBadgeProps = { type: string; size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; }; const AssessmentTypeBadge: React.FC = ({ type, size = 'sm' }) => { - const badgeColor = - type === 'Missions' - ? 'indigo' - : type === 'Quests' - ? 'emerald' - : type === 'Paths' - ? 'sky' - : 'gray'; return ( ); }; @@ -41,7 +35,6 @@ type GradingStatusBadgeProps = { }; const GradingStatusBadge: React.FC = ({ status }) => { - const badgeColor = status === 'graded' ? 'green' : status === 'grading' ? 'yellow' : 'red'; const statusText = status.charAt(0).toUpperCase() + status.slice(1); const badgeIcon = () => ( = ({ status }) => { style={{ marginRight: '0.5rem' }} /> ); - return ; + return ; }; export { AssessmentTypeBadge, SubmissionStatusBadge, GradingStatusBadge }; diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx index c12a17b5e8..da16a0e505 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx @@ -3,6 +3,8 @@ import { IconNames } from '@blueprintjs/icons'; import { ColumnFilter, ColumnFiltersState } from '@tanstack/react-table'; import { Badge, Flex } from '@tremor/react'; +import { badgeColor } from './colors'; + type GradingSubmissinoFiltersProps = { filters: ColumnFiltersState; onFilterRemove: (filter: ColumnFilter) => void; @@ -21,24 +23,6 @@ const GradingSubmissionFilters: React.FC = ({ ); }; -// TODO: extract to a constants file -const FILTER_COLORS = { - // assessments - missions: 'indigo', - quests: 'emerald', - paths: 'sky', - - // submission status - submitted: 'green', - attempting: 'yellow', - attempted: 'red', - - // grading status - graded: 'green', - grading: 'yellow', - none: 'red' -}; - type FilterBadgeProps = { filter: ColumnFilter; onRemove: (filter: ColumnFilter) => void; @@ -47,14 +31,12 @@ type FilterBadgeProps = { const FilterBadge: React.FC = ({ filter, onRemove }) => { let filterValue = filter.value as string; filterValue = filterValue.charAt(0).toUpperCase() + filterValue.slice(1); - const filterColor = FILTER_COLORS[filterValue.toLowerCase()] || 'gray'; - return ( ); diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 5da7994639..3434cc84aa 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -34,11 +34,7 @@ import { updateSubmissionsTableFilters } from 'src/commons/workspace/WorkspaceAc import { GradingOverview } from 'src/features/grading/GradingTypes'; import GradingActions from './GradingActions'; -import { - AssessmentTypeBadge, - GradingStatusBadge, - SubmissionStatusBadge -} from './GradingStatusBadges'; +import { AssessmentTypeBadge, GradingStatusBadge, SubmissionStatusBadge } from './GradingBadges'; import GradingSubmissionFilters from './GradingSubmissionFilters'; const columnHelper = createColumnHelper(); diff --git a/src/pages/academy/grading/subcomponents/GradingSummary.tsx b/src/pages/academy/grading/subcomponents/GradingSummary.tsx index ce5de4ab1d..d238c33db2 100644 --- a/src/pages/academy/grading/subcomponents/GradingSummary.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSummary.tsx @@ -13,7 +13,7 @@ import { import { AssessmentOverview, GradingStatuses } from 'src/commons/assessment/AssessmentTypes'; import { GradingOverview } from 'src/features/grading/GradingTypes'; -import { AssessmentTypeBadge } from './GradingStatusBadges'; +import { AssessmentTypeBadge } from './GradingBadges'; type GradingSummaryProps = { group: string | null; diff --git a/src/pages/academy/grading/subcomponents/colors.ts b/src/pages/academy/grading/subcomponents/colors.ts new file mode 100644 index 0000000000..fe952433d5 --- /dev/null +++ b/src/pages/academy/grading/subcomponents/colors.ts @@ -0,0 +1,20 @@ +const BADGE_COLORS = { + // assessment types + missions: 'indigo', + quests: 'emerald', + paths: 'sky', + + // submission status + submitted: 'green', + attempting: 'yellow', + attempted: 'red', + + // grading status + graded: 'green', + grading: 'yellow', + none: 'red' +}; + +export function badgeColor(label: string) { + return BADGE_COLORS[label.toLowerCase()] || 'gray'; +} From 77b0651657f523073555693a719a7ec1c8757f70 Mon Sep 17 00:00:00 2001 From: Yu Han Zhang Date: Tue, 18 Apr 2023 13:34:59 +0800 Subject: [PATCH 13/17] refactor: use nullish coallescing Co-authored-by: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> --- src/pages/academy/grading/Grading.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index e6cb0adbcc..a17c8673d3 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -39,7 +39,7 @@ const Grading: React.FC = ({ match }) => { const data = gradingOverviews?.map(e => !e.studentName ? { ...e, studentName: '(user has yet to log in)' } : e - ) || []; + ) ?? []; const exportCSV = () => { if (!gradingOverviews) return; From 4c75f1f0cbff6eed295825c3d9e436a5fbd7f46c Mon Sep 17 00:00:00 2001 From: Yu Han Zhang Date: Tue, 18 Apr 2023 13:40:24 +0800 Subject: [PATCH 14/17] refactor: remove unnecessary check for Paths --- src/pages/academy/grading/subcomponents/GradingSummary.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/academy/grading/subcomponents/GradingSummary.tsx b/src/pages/academy/grading/subcomponents/GradingSummary.tsx index d238c33db2..66a14dbbc4 100644 --- a/src/pages/academy/grading/subcomponents/GradingSummary.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSummary.tsx @@ -38,7 +38,7 @@ const GradingSummary: React.FC = ({ group, submissions, ass const ungradedAssessments = [...new Set(ungraded.map(({ assessmentId }) => assessmentId))].reduce( (acc: AssessmentSummary[], assessmentId) => { const assessment = assessments.find(assessment => assessment.id === assessmentId); - if (!assessment || assessment.type === 'Path') return acc; + if (!assessment) return acc; return [ ...acc, { From c3813954aee96eba9f9be7a3e20d61332dc07473 Mon Sep 17 00:00:00 2001 From: Yu Han Zhang Date: Tue, 18 Apr 2023 14:00:08 +0800 Subject: [PATCH 15/17] refactor: reorganize components and logic for badges --- .../grading/subcomponents/GradingBadges.tsx | 50 ++++++++++++++++--- .../GradingSubmissionFilters.tsx | 29 ++--------- .../academy/grading/subcomponents/colors.ts | 20 -------- 3 files changed, 48 insertions(+), 51 deletions(-) delete mode 100644 src/pages/academy/grading/subcomponents/colors.ts diff --git a/src/pages/academy/grading/subcomponents/GradingBadges.tsx b/src/pages/academy/grading/subcomponents/GradingBadges.tsx index fcd3403a8e..31cdb6a161 100644 --- a/src/pages/academy/grading/subcomponents/GradingBadges.tsx +++ b/src/pages/academy/grading/subcomponents/GradingBadges.tsx @@ -1,9 +1,29 @@ import { Icon } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; +import { ColumnFilter } from '@tanstack/react-table'; import { Badge } from '@tremor/react'; import { GradingStatus } from 'src/commons/assessment/AssessmentTypes'; -import { badgeColor } from './colors'; +const BADGE_COLORS = { + // assessment types + missions: 'indigo', + quests: 'emerald', + paths: 'sky', + + // submission status + submitted: 'green', + attempting: 'yellow', + attempted: 'red', + + // grading status + graded: 'green', + grading: 'yellow', + none: 'red' +}; + +export function getBadgeColorFromLabel(label: string) { + return BADGE_COLORS[label.toLowerCase()] || 'gray'; +} type AssessmentTypeBadgeProps = { type: string; @@ -15,7 +35,7 @@ const AssessmentTypeBadge: React.FC = ({ type, size = ); }; @@ -25,9 +45,8 @@ type SubmissionStatusBadgeProps = { }; const SubmissionStatusBadge: React.FC = ({ status }) => { - const badgeColor = status === 'submitted' ? 'green' : status === 'attempting' ? 'yellow' : 'red'; const statusText = status.charAt(0).toUpperCase() + status.slice(1); - return ; + return ; }; type GradingStatusBadgeProps = { @@ -50,7 +69,26 @@ const GradingStatusBadge: React.FC = ({ status }) => { style={{ marginRight: '0.5rem' }} /> ); - return ; + return ; +}; + +type FilterBadgeProps = { + filter: ColumnFilter; + onRemove: (filter: ColumnFilter) => void; +}; + +const FilterBadge: React.FC = ({ filter, onRemove }) => { + let filterValue = filter.value as string; + filterValue = filterValue.charAt(0).toUpperCase() + filterValue.slice(1); + return ( + + ); }; -export { AssessmentTypeBadge, SubmissionStatusBadge, GradingStatusBadge }; +export { AssessmentTypeBadge, FilterBadge, GradingStatusBadge, SubmissionStatusBadge }; diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx index da16a0e505..8f1091ff06 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx @@ -1,16 +1,14 @@ -import { Icon } from '@blueprintjs/core'; -import { IconNames } from '@blueprintjs/icons'; import { ColumnFilter, ColumnFiltersState } from '@tanstack/react-table'; -import { Badge, Flex } from '@tremor/react'; +import { Flex } from '@tremor/react'; -import { badgeColor } from './colors'; +import { FilterBadge } from './GradingBadges'; -type GradingSubmissinoFiltersProps = { +type GradingSubmissionFiltersProps = { filters: ColumnFiltersState; onFilterRemove: (filter: ColumnFilter) => void; }; -const GradingSubmissionFilters: React.FC = ({ +const GradingSubmissionFilters: React.FC = ({ filters, onFilterRemove }) => { @@ -23,23 +21,4 @@ const GradingSubmissionFilters: React.FC = ({ ); }; -type FilterBadgeProps = { - filter: ColumnFilter; - onRemove: (filter: ColumnFilter) => void; -}; - -const FilterBadge: React.FC = ({ filter, onRemove }) => { - let filterValue = filter.value as string; - filterValue = filterValue.charAt(0).toUpperCase() + filterValue.slice(1); - return ( - - ); -}; - export default GradingSubmissionFilters; diff --git a/src/pages/academy/grading/subcomponents/colors.ts b/src/pages/academy/grading/subcomponents/colors.ts deleted file mode 100644 index fe952433d5..0000000000 --- a/src/pages/academy/grading/subcomponents/colors.ts +++ /dev/null @@ -1,20 +0,0 @@ -const BADGE_COLORS = { - // assessment types - missions: 'indigo', - quests: 'emerald', - paths: 'sky', - - // submission status - submitted: 'green', - attempting: 'yellow', - attempted: 'red', - - // grading status - graded: 'green', - grading: 'yellow', - none: 'red' -}; - -export function badgeColor(label: string) { - return BADGE_COLORS[label.toLowerCase()] || 'gray'; -} From da4be57d0c83d1713c680af5884008cfa024b78c Mon Sep 17 00:00:00 2001 From: Yu Han Zhang Date: Tue, 18 Apr 2023 14:08:10 +0800 Subject: [PATCH 16/17] fix: wrap csv fields in quotes --- src/pages/academy/grading/Grading.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index a17c8673d3..bafe6a6968 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -68,7 +68,9 @@ const Grading: React.FC = ({ match }) => { e.currentXp, e.maxXp, e.xpBonus - ].join(',') + '\n' + ] + .map(field => `"${field}"`) // wrap each field in double quotes in case it contains a comma + .join(',') + '\n' ) ], { type: 'text/csv' } From 35bfde6e92137ae9998ad70b95146dd8b3223651 Mon Sep 17 00:00:00 2001 From: Yu Han Zhang Date: Thu, 20 Apr 2023 15:40:00 +0800 Subject: [PATCH 17/17] fix: wraps column headers in quotes too --- src/pages/academy/grading/Grading.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index bafe6a6968..3bb4a885ea 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -52,7 +52,7 @@ const Grading: React.FC = ({ match }) => { const content = new Blob( [ - 'Assessment Name,Student Name,Group,Status,Grading,Question Count,Questions Graded,Initial XP,XP Adjustment,Current XP (excl. bonus),Max XP,Bonus XP\n', + '"Assessment Name","Student Name","Group","Status","Grading","Question Count","Questions Graded","Initial XP","XP Adjustment","Current XP (excl. bonus)","Max XP","Bonus XP"\n', ...gradingOverviews.map( e => [