From 97a1ae473bf1f52aaa2cb627c4ffb769e3884960 Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Wed, 28 Feb 2024 18:09:51 +0800 Subject: [PATCH 01/69] added filterable columns in grading overview --- src/commons/application/ApplicationTypes.ts | 3 + src/commons/workspace/WorkspaceActions.ts | 7 ++ src/commons/workspace/WorkspaceReducer.ts | 9 ++ src/commons/workspace/WorkspaceTypes.ts | 6 + src/pages/academy/grading/Grading.tsx | 2 +- .../grading/subcomponents/GradingBadges.tsx | 30 ++++- .../subcomponents/GradingColumnFilters.tsx | 30 +++++ .../subcomponents/GradingSubmissionsTable.tsx | 116 +++++++++++++++--- 8 files changed, 187 insertions(+), 16 deletions(-) create mode 100644 src/pages/academy/grading/subcomponents/GradingColumnFilters.tsx diff --git a/src/commons/application/ApplicationTypes.ts b/src/commons/application/ApplicationTypes.ts index 662099705c..042b7dc8db 100644 --- a/src/commons/application/ApplicationTypes.ts +++ b/src/commons/application/ApplicationTypes.ts @@ -410,6 +410,9 @@ export const defaultWorkspaceManager: WorkspaceManagerState = { submissionsTableFilters: { columnFilters: [] }, + columnVisiblity: { + columns: [] + }, currentSubmission: undefined, currentQuestion: undefined, hasUnsavedChanges: false diff --git a/src/commons/workspace/WorkspaceActions.ts b/src/commons/workspace/WorkspaceActions.ts index dd7e5c5de6..6d7a11ce67 100644 --- a/src/commons/workspace/WorkspaceActions.ts +++ b/src/commons/workspace/WorkspaceActions.ts @@ -34,6 +34,7 @@ import { EVAL_EDITOR_AND_TESTCASES, EVAL_REPL, EVAL_TESTCASE, + GradingColumnVisibility, MOVE_CURSOR, NAV_DECLARATION, PLAYGROUND_EXTERNAL_SELECT, @@ -64,6 +65,7 @@ import { UPDATE_EDITOR_VALUE, UPDATE_ENVSTEPS, UPDATE_ENVSTEPSTOTAL, + UPDATE_GRADING_COLUMN_VISIBILITY, UPDATE_HAS_UNSAVED_CHANGES, UPDATE_REPL_VALUE, UPDATE_SUBLANGUAGE, @@ -398,6 +400,11 @@ export const updateSubmissionsTableFilters = createAction( (filters: SubmissionsTableFilters) => ({ payload: { filters } }) ); +export const updateGradingColumnVisibility = createAction( + UPDATE_GRADING_COLUMN_VISIBILITY, + (filters: GradingColumnVisibility) => ({ payload: { filters } }) +); + export const updateCurrentAssessmentId = createAction( UPDATE_CURRENT_ASSESSMENT_ID, (assessmentId: number, questionId: number) => ({ payload: { assessmentId, questionId } }) diff --git a/src/commons/workspace/WorkspaceReducer.ts b/src/commons/workspace/WorkspaceReducer.ts index 47c3f4251c..2e11a73e70 100644 --- a/src/commons/workspace/WorkspaceReducer.ts +++ b/src/commons/workspace/WorkspaceReducer.ts @@ -77,6 +77,7 @@ import { UPDATE_EDITOR_VALUE, UPDATE_ENVSTEPS, UPDATE_ENVSTEPSTOTAL, + UPDATE_GRADING_COLUMN_VISIBILITY, UPDATE_HAS_UNSAVED_CHANGES, UPDATE_REPL_VALUE, UPDATE_SUBLANGUAGE, @@ -631,6 +632,14 @@ const oldWorkspaceReducer: Reducer = ( submissionsTableFilters: action.payload.filters } }; + case UPDATE_GRADING_COLUMN_VISIBILITY: + return { + ...state, + grading: { + ...state.grading, + columnVisiblity: 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 e011734067..537e2849e7 100644 --- a/src/commons/workspace/WorkspaceTypes.ts +++ b/src/commons/workspace/WorkspaceTypes.ts @@ -41,6 +41,7 @@ export const TOGGLE_USING_SUBST = 'TOGGLE_USING_SUBST'; export const TOGGLE_USING_ENV = 'TOGGLE_USING_ENV'; export const TOGGLE_UPDATE_ENV = 'TOGGLE_UPDATE_ENV'; export const UPDATE_SUBMISSIONS_TABLE_FILTERS = 'UPDATE_SUBMISSIONS_TABLE_FILTERS'; +export const UPDATE_GRADING_COLUMN_VISIBILITY = 'UPDATE_GRADING_COLUMN_VISIBILITY'; 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'; @@ -77,6 +78,7 @@ type AssessmentWorkspaceState = AssessmentWorkspaceAttr & WorkspaceState; type GradingWorkspaceAttr = { readonly submissionsTableFilters: SubmissionsTableFilters; + readonly columnVisiblity: GradingColumnVisibility; readonly currentSubmission?: number; readonly currentQuestion?: number; readonly hasUnsavedChanges: boolean; @@ -166,3 +168,7 @@ export type DebuggerContext = { export type SubmissionsTableFilters = { columnFilters: { id: string; value: unknown }[]; }; + +export type GradingColumnVisibility = { + columns: string[]; +}; diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index 8f4ae43087..82e24ce06b 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -141,7 +141,7 @@ const Grading: React.FC = () => { popoverProps={{ position: Position.BOTTOM }} buttonProps={{ minimal: true, rightIcon: 'caret-down' }} /> - entries per page. + entries per page = ({ filter, onRemove }) => { ); }; -export { AssessmentTypeBadge, FilterBadge, GradingStatusBadge, SubmissionStatusBadge }; +type ColumnFilterBadgeProps = { + filter: string; + onRemove: (toRemove: string) => void; + filtersName: string; +}; + +const ColumnFilterBadge: React.FC = ({ filter, onRemove, filtersName }) => { + return ( + + ); +}; + +export { + AssessmentTypeBadge, + ColumnFilterBadge, + FilterBadge, + GradingStatusBadge, + SubmissionStatusBadge +}; diff --git a/src/pages/academy/grading/subcomponents/GradingColumnFilters.tsx b/src/pages/academy/grading/subcomponents/GradingColumnFilters.tsx new file mode 100644 index 0000000000..4abaf8b1bc --- /dev/null +++ b/src/pages/academy/grading/subcomponents/GradingColumnFilters.tsx @@ -0,0 +1,30 @@ +import { Flex } from '@tremor/react'; + +import { ColumnFilterBadge } from './GradingBadges'; + +type GradingSubmissionFiltersProps = { + filters: string[]; + onFilterRemove: (toRemove: string) => void; + filtersName: string[]; +}; + +const GradingColumnFilters: React.FC = ({ + filters, + onFilterRemove, + filtersName +}) => { + return ( + + {filters.map((filter, index) => ( + + ))} + + ); +}; + +export default GradingColumnFilters; diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 353d414758..554953d613 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -31,12 +31,17 @@ import { debounce } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useTypedSelector } from 'src/commons/utils/Hooks'; -import { updateSubmissionsTableFilters } from 'src/commons/workspace/WorkspaceActions'; +import { + updateGradingColumnVisibility, + updateSubmissionsTableFilters +} from 'src/commons/workspace/WorkspaceActions'; +import { GradingColumnVisibility } from 'src/commons/workspace/WorkspaceTypes'; import { GradingOverview } from 'src/features/grading/GradingTypes'; import { convertFilterToBackendParams } from 'src/features/grading/GradingUtils'; import GradingActions from './GradingActions'; import { AssessmentTypeBadge, GradingStatusBadge, SubmissionStatusBadge } from './GradingBadges'; +import GradingColumnFilters from './GradingColumnFilters'; import GradingSubmissionFilters from './GradingSubmissionFilters'; const columnHelper = createColumnHelper(); @@ -119,11 +124,16 @@ const GradingSubmissionTable: React.FC = ({ }) => { const dispatch = useDispatch(); const tableFilters = useTypedSelector(state => state.workspaces.grading.submissionsTableFilters); + const columnVisibility = useTypedSelector(state => state.workspaces.grading.columnVisiblity); const [columnFilters, setColumnFilters] = useState([ ...tableFilters.columnFilters ]); + const [hiddenColumns, setHiddenColumns] = useState( + columnVisibility ? columnVisibility : { columns: [] } + ); + const [page, setPage] = useState(0); const maxPage = useMemo(() => Math.ceil(totalRows / pageSize) - 1, [totalRows, pageSize]); const resetPage = useCallback(() => setPage(0), [setPage]); @@ -185,10 +195,30 @@ const GradingSubmissionTable: React.FC = ({ resetPage(); }; + const handleColumnFilterRemove = (toRemove: string) => { + setHiddenColumns((prev: GradingColumnVisibility) => { + return { + columns: prev.columns.filter(column => column !== toRemove) + }; + }); + }; + + const handleColumnFilterAdd = (toAdd: string) => { + setHiddenColumns((prev: GradingColumnVisibility) => { + return { + columns: [...prev.columns, toAdd] + }; + }); + }; + useEffect(() => { dispatch(updateSubmissionsTableFilters({ columnFilters })); }, [columnFilters, dispatch]); + useEffect(() => { + dispatch(updateGradingColumnVisibility(hiddenColumns)); + }, [hiddenColumns, dispatch]); + useEffect(() => { resetPage(); }, [updateEntries, resetPage, searchValue]); @@ -206,7 +236,10 @@ const GradingSubmissionTable: React.FC = ({ {columnFilters.length > 0 ? 'Filters: ' - : 'No filters applied. Click on any cell to filter by its value.'}{' '} + : 'No filters applied. Click on any cell to filter by its value.' + + (hiddenColumns.columns.length === 0 + ? ' Click on any column header to hide it.' + : '')}{' '} @@ -220,28 +253,83 @@ const GradingSubmissionTable: React.FC = ({ onChange={handleSearchQueryUpdate} /> + + {hiddenColumns.columns.length > 0 ? ( + + +
+ Columns Hidden: +
+ { + const headerTexts = columns.filter( + col => col['accessorKey'] === id || col['header'] === id + ); + return headerTexts[0]['header'] ? headerTexts[0]['header'].toString() : ''; + })} + onFilterRemove={handleColumnFilterRemove} + /> +
+
+ ) : ( + <> + )} + {table.getHeaderGroups().map(headerGroup => ( - {headerGroup.headers.map(header => ( - - {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} - - ))} + {headerGroup.headers.map(header => + hiddenColumns.columns.reduce( + (accumulator, currentValue) => accumulator || header.id.includes(currentValue), + false + ) ? ( + <> + ) : ( + + + + ) + )} ))} {table.getRowModel().rows.map(row => ( - {row.getVisibleCells().map(cell => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} + {row + .getVisibleCells() + .map(cell => + hiddenColumns.columns.reduce( + (accumulator, currentValue) => accumulator || cell.id.includes(currentValue), + false + ) ? ( + <> + ) : ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ) + )} ))} From 567a47127778de8a376e3a9553ea46e5c92fb8cc Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Wed, 28 Feb 2024 20:06:35 +0800 Subject: [PATCH 02/69] some cleanup code --- src/pages/academy/grading/Grading.tsx | 2 +- .../subcomponents/GradingSubmissionsTable.tsx | 52 +++++++++---------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index 82e24ce06b..8f4ae43087 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -141,7 +141,7 @@ const Grading: React.FC = () => { popoverProps={{ position: Position.BOTTOM }} buttonProps={{ minimal: true, rightIcon: 'caret-down' }} /> - entries per page + entries per page. = ({ return ( <> - - -
- - - {columnFilters.length > 0 - ? 'Filters: ' - : 'No filters applied. Click on any cell to filter by its value.' + - (hiddenColumns.columns.length === 0 - ? ' Click on any column header to hide it.' - : '')}{' '} - -
- -
- - } - placeholder="Search by assessment name" - value={searchQuery} - onChange={handleSearchQueryUpdate} - /> -
- {hiddenColumns.columns.length > 0 ? ( @@ -283,6 +258,31 @@ const GradingSubmissionTable: React.FC = ({ ) : ( <> )} + + + +
+ + + {columnFilters.length > 0 + ? 'Filters: ' + : 'No filters applied. Click on any cell to filter by its value.' + + (hiddenColumns.columns.length === 0 + ? ' Click on any column header to hide it.' + : '')}{' '} + +
+ +
+ + } + placeholder="Search by assessment name" + value={searchQuery} + onChange={handleSearchQueryUpdate} + /> +
@@ -299,7 +299,7 @@ const GradingSubmissionTable: React.FC = ({
{table.getHeaderGroups().map(headerGroup => ( - {headerGroup.headers.map(header => - hiddenColumns.columns.reduce( - (accumulator, currentValue) => accumulator || header.id.includes(currentValue), - false - ) ? ( - <> - ) : ( - - - - ) - )} + {headerGroup.headers.map(header => ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ))} ))} {table.getRowModel().rows.map(row => ( - {row - .getVisibleCells() - .map(cell => - hiddenColumns.columns.reduce( - (accumulator, currentValue) => accumulator || cell.id.includes(currentValue), - false - ) ? ( - <> - ) : ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ) - )} + {row.getVisibleCells().map(cell => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} ))} diff --git a/src/styles/_academy.scss b/src/styles/_academy.scss index 6058cc9477..5ac1d24b47 100644 --- a/src/styles/_academy.scss +++ b/src/styles/_academy.scss @@ -510,7 +510,7 @@ } .grading-overview-filterable-btns { - padding: 0; + padding: 0 2px; // Hides overflowed text for the buttons &, @@ -528,7 +528,7 @@ -0.875rem for icon margins -1.125rem for additional padding */ - p { + p, &:has(> span) > span { max-width: calc(20vw - 16px - 2rem); } } @@ -540,7 +540,7 @@ &:hover { // Buttons with bg - > div > span { + > div > span, &:has(> span) { // Use of contrast due to unknown background color filter: contrast(0.9); } @@ -552,6 +552,10 @@ } } +.grading-overview-filterable-btns, .grading-overview-rounded-btns { + border-radius: 9999px; +} + .grading-overview-footer-sibling { // Footer component ~ div ~ div { From 852da3cd096062dbb2c6684e334b0f3061f92da8 Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Wed, 6 Mar 2024 14:57:03 +0800 Subject: [PATCH 04/69] missing code from previous commit --- .../subcomponents/GradingSubmissionsTable.tsx | 176 +++++++++++++----- 1 file changed, 125 insertions(+), 51 deletions(-) diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 353d414758..8afa3b9a49 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -1,6 +1,6 @@ import '@tremor/react/dist/esm/tremor.css'; -import { Icon as BpIcon } from '@blueprintjs/core'; +import { Button, H6, Icon as BpIcon } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import { Column, @@ -14,9 +14,6 @@ import { useReactTable } from '@tanstack/react-table'; import { - Bold, - Button, - Flex, Footer, Table, TableBody, @@ -24,19 +21,25 @@ import { TableHead, TableHeaderCell, TableRow, - Text, TextInput } from '@tremor/react'; import { debounce } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; +import GradingFlex from 'src/commons/grading/GradingFlex'; +import GradingText from 'src/commons/grading/GradingText'; import { useTypedSelector } from 'src/commons/utils/Hooks'; -import { updateSubmissionsTableFilters } from 'src/commons/workspace/WorkspaceActions'; +import { + updateGradingColumnVisibility, + updateSubmissionsTableFilters +} from 'src/commons/workspace/WorkspaceActions'; +import { GradingColumnVisibility } from 'src/commons/workspace/WorkspaceTypes'; import { GradingOverview } from 'src/features/grading/GradingTypes'; import { convertFilterToBackendParams } from 'src/features/grading/GradingUtils'; import GradingActions from './GradingActions'; import { AssessmentTypeBadge, GradingStatusBadge, SubmissionStatusBadge } from './GradingBadges'; +import GradingColumnFilters from './GradingColumnFilters'; import GradingSubmissionFilters from './GradingSubmissionFilters'; const columnHelper = createColumnHelper(); @@ -84,13 +87,13 @@ const makeColumns = (handleClick: () => void) => [ cell: info => { const { currentXp, xpBonus, maxXp } = info.getValue(); return ( - - + + {currentXp} (+{xpBonus}) - - / - {maxXp} - + + / + {maxXp} + ); } }), @@ -119,11 +122,16 @@ const GradingSubmissionTable: React.FC = ({ }) => { const dispatch = useDispatch(); const tableFilters = useTypedSelector(state => state.workspaces.grading.submissionsTableFilters); + const columnVisibility = useTypedSelector(state => state.workspaces.grading.columnVisiblity); const [columnFilters, setColumnFilters] = useState([ ...tableFilters.columnFilters ]); + const [hiddenColumns, setHiddenColumns] = useState( + columnVisibility ? columnVisibility : { columns: [] } + ); + const [page, setPage] = useState(0); const maxPage = useMemo(() => Math.ceil(totalRows / pageSize) - 1, [totalRows, pageSize]); const resetPage = useCallback(() => setPage(0), [setPage]); @@ -185,10 +193,30 @@ const GradingSubmissionTable: React.FC = ({ resetPage(); }; + const handleColumnFilterRemove = (toRemove: string) => { + setHiddenColumns((prev: GradingColumnVisibility) => { + return { + columns: prev.columns.filter(column => column !== toRemove) + }; + }); + }; + + const handleColumnFilterAdd = (toAdd: string) => { + setHiddenColumns((prev: GradingColumnVisibility) => { + return { + columns: [...prev.columns, toAdd] + }; + }); + }; + useEffect(() => { dispatch(updateSubmissionsTableFilters({ columnFilters })); }, [columnFilters, dispatch]); + useEffect(() => { + dispatch(updateGradingColumnVisibility(hiddenColumns)); + }, [hiddenColumns, dispatch]); + useEffect(() => { resetPage(); }, [updateEntries, resetPage, searchValue]); @@ -199,18 +227,41 @@ const GradingSubmissionTable: React.FC = ({ return ( <> - - -
+ {hiddenColumns.columns.length > 0 ? ( + + + Columns Hidden: + { + const headerTexts = columns.filter( + col => col['accessorKey'] === id || col['header'] === id + ); + return headerTexts[0]['header'] ? headerTexts[0]['header'].toString() : ''; + })} + onFilterRemove={handleColumnFilterRemove} + /> + + + ) : ( + <> + )} + + + +
- + {columnFilters.length > 0 ? 'Filters: ' - : 'No filters applied. Click on any cell to filter by its value.'}{' '} - + : 'No filters applied. Click on any cell to filter by its value.' + + (hiddenColumns.columns.length === 0 + ? ' Click on any column header to hide it.' + : '')}{' '} +
- +
= ({ value={searchQuery} onChange={handleSearchQueryUpdate} /> - +
+
{table.getHeaderGroups().map(headerGroup => ( - {headerGroup.headers.map(header => ( - - {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} - - ))} + {headerGroup.headers.map(header => + hiddenColumns.columns.reduce( + (accumulator, currentValue) => accumulator || header.id.includes(currentValue), + false + ) ? ( + <> + ) : ( + + + + ) + )} ))} {table.getRowModel().rows.map(row => ( - {row.getVisibleCells().map(cell => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} + {row + .getVisibleCells() + .map(cell => + hiddenColumns.columns.reduce( + (accumulator, currentValue) => accumulator || cell.id.includes(currentValue), + false + ) ? ( + <> + ) : ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ) + )} ))}
- +
- - <> - + ); }; From c4f0ddf0e9de6d2926bd54b479738bd077f946e4 Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Wed, 13 Mar 2024 17:53:38 +0800 Subject: [PATCH 05/69] halfway done for porting tanstack/tremor to ag grid/blueprint --- .../grading/subcomponents/GradingActions.tsx | 11 +- .../subcomponents/GradingSubmissionsTable.tsx | 226 ++++++++++++++++++ src/styles/_academy.scss | 28 ++- 3 files changed, 259 insertions(+), 6 deletions(-) diff --git a/src/pages/academy/grading/subcomponents/GradingActions.tsx b/src/pages/academy/grading/subcomponents/GradingActions.tsx index 93e2400494..5a504e198b 100644 --- a/src/pages/academy/grading/subcomponents/GradingActions.tsx +++ b/src/pages/academy/grading/subcomponents/GradingActions.tsx @@ -1,20 +1,23 @@ import { Icon as BpIcon } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; -import { Flex, Icon } from '@tremor/react'; +import { Icon } from '@tremor/react'; +import React from 'react'; import { useDispatch } from 'react-redux'; import { Link } from 'react-router-dom'; import { reautogradeSubmission, unsubmitSubmission } from 'src/commons/application/actions/SessionActions'; +import GradingFlex from 'src/commons/grading/GradingFlex'; import { showSimpleConfirmDialog } from 'src/commons/utils/DialogHelper'; import { useTypedSelector } from 'src/commons/utils/Hooks'; type GradingActionsProps = { submissionId: number; + style?: React.CSSProperties; }; -const GradingActions: React.FC = ({ submissionId }) => { +const GradingActions: React.FC = ({ submissionId, style }) => { const dispatch = useDispatch(); const courseId = useTypedSelector(store => store.session.courseId); @@ -46,7 +49,7 @@ const GradingActions: React.FC = ({ submissionId }) => { }; return ( - + } variant="light" /> @@ -62,7 +65,7 @@ const GradingActions: React.FC = ({ submissionId }) => { - + ); }; diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 8afa3b9a49..13d1d7f79d 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -1,4 +1,6 @@ import '@tremor/react/dist/esm/tremor.css'; +import "ag-grid-community/styles/ag-grid.css"; +import "ag-grid-community/styles/ag-theme-quartz.css" import { Button, H6, Icon as BpIcon } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; @@ -23,6 +25,8 @@ import { TableRow, TextInput } from '@tremor/react'; +import { ColDef, ICellRendererParams } from 'ag-grid-community'; +import { AgGridReact } from 'ag-grid-react'; import { debounce } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; @@ -120,6 +124,157 @@ const GradingSubmissionTable: React.FC = ({ submissions, updateEntries }) => { + + // End of Original Code + + interface IRow { + assessmentName: string; + assessmentType: string; + studentName: string; + studentUsername: string; + groupName: string; + submissionStatus: string; + gradingStatus: string; + xp: string; + actions: string; + index: number; + } + + const defaultColumnDefs: ColDef = { + filter: false, + resizable: false, + sortable: true + }; + + const [rowData, setRowData] = useState(); + + const [colDefs, setColDefs] = useState[]>(); + + const generateCols = (resetPage: () => void) => { + const cols: ColDef[] = []; + + cols.push({ headerName: "Name", field: "assessmentName", flex: 3, cellStyle: defaultCellStyle({textAlign: "left"}), cellRendererSelector: (params: ICellRendererParams) => { + return (params.data !== undefined) + ? { + component: FilterableNew, + params: { + setColumnFilters: setColumnFilters, + id: "assessmentName", + value: params.data.assessmentName, + onClick: resetPage, + } + } + : undefined; + }, headerClass: defaultHeaderClasses("grading-left-align") }); + cols.push({ headerName: "Type", field: "assessmentType", flex: 1, cellStyle: defaultCellStyle(), cellRendererSelector: (params: ICellRendererParams) => { + return (params.data !== undefined) + ? { + component: FilterableNew, + params: { + setColumnFilters: setColumnFilters, + id: "assessmentType", + value: params.data.assessmentType, + onClick: resetPage, + children: [] + } + } + : undefined; + }, headerClass: defaultHeaderClasses() }); + cols.push({ headerName: "Student", field: "studentName", flex: 1.5, cellStyle: defaultCellStyle({textAlign: "left"}), cellRendererSelector: (params: ICellRendererParams) => { + return (params.data !== undefined) + ? { + component: FilterableNew, + params: { + setColumnFilters: setColumnFilters, + id: "studentName", + value: params.data.studentName, + onClick: resetPage, + } + } + : undefined; + }, headerClass: defaultHeaderClasses("grading-left-align") }); + cols.push({ headerName: "Username", field: "studentUsername", flex: 1, cellStyle: defaultCellStyle(), cellRendererSelector: (params: ICellRendererParams) => { + return (params.data !== undefined) + ? { + component: FilterableNew, + params: { + setColumnFilters: setColumnFilters, + id: "studentUsername", + value: params.data.studentUsername, + onClick: resetPage, + } + } + : undefined; + }, headerClass: defaultHeaderClasses() }); + cols.push({ headerName: "Group", field: "groupName", flex: 0.75, cellStyle: defaultCellStyle(), cellRendererSelector: (params: ICellRendererParams) => { + return (params.data !== undefined) + ? { + component: FilterableNew, + params: { + setColumnFilters: setColumnFilters, + id: "groupName", + value: params.data.groupName, + onClick: resetPage, + } + } + : undefined; + }, headerClass: defaultHeaderClasses() }); + cols.push({ headerName: "Progress", field: "submissionStatus", flex: 1, cellStyle: defaultCellStyle(), cellRendererSelector: (params: ICellRendererParams) => { + return (params.data !== undefined) + ? { + component: FilterableNew, + params: { + setColumnFilters: setColumnFilters, + id: "submissionStatus", + value: params.data.submissionStatus, + onClick: resetPage, + children: [] + } + } + : undefined; + }, headerClass: defaultHeaderClasses() }); + cols.push({ headerName: "Grading", field: "gradingStatus", flex: 1, cellStyle: defaultCellStyle(), cellRendererSelector: (params: ICellRendererParams) => { + return (params.data !== undefined) + ? { + component: GradingStatusBadge, + params: { + status: params.data.gradingStatus + } + } + : undefined; + }, headerClass: defaultHeaderClasses() }); + cols.push({ headerName: "Raw XP (+Bonus)", field: "xp", flex: 1, cellStyle: defaultCellStyle(), headerClass: defaultHeaderClasses() }); + cols.push({ headerName: "Actions", field: "actions", flex: 1, cellStyle: defaultCellStyle(), cellRendererSelector: (params: ICellRendererParams) => { + return (params.data !== undefined) + ? { + component: GradingActions, + params: { + submissionId: params.data.index, + style: {justifyContent: "center"} + } + } + : undefined; + }, headerClass: defaultHeaderClasses() }); + + return cols; + } + + const defaultCellStyle = (style?: React.CSSProperties) => { + return { + textAlign: "center", + display: "flex", + justifyContent: "center", + flexDirection: "column", + fontSize: "0.875rem", + ...style, + } + }; + + const defaultHeaderClasses = (extraClass?: string) => { + return ("grading-default-headers " + (extraClass !== undefined ? extraClass : "")); + }; + + // Start of Original Code const dispatch = useDispatch(); const tableFilters = useTypedSelector(state => state.workspaces.grading.submissionsTableFilters); const columnVisibility = useTypedSelector(state => state.workspaces.grading.columnVisiblity); @@ -225,6 +380,31 @@ const GradingSubmissionTable: React.FC = ({ updateEntries(page, backendFilterParams); }, [updateEntries, page, backendFilterParams]); + // End of Original Code + + useEffect(() => { + setRowData(submissions.map((submission): IRow => { + return { + assessmentName: submission.assessmentName, + assessmentType: submission.assessmentType, + studentName: submission.studentName, + studentUsername: submission.studentUsername, + groupName: submission.groupName, + submissionStatus: submission.submissionStatus, + gradingStatus: submission.gradingStatus, + xp: submission.initialXp + " (+" + submission.xpBonus + ") / " + submission.maxXp, + actions: "", + index: submission.submissionId, + }; + })); + }, [submissions]); + + useEffect(() => { + setColDefs(generateCols(resetPage)); + }, [resetPage]); + + // Start of Original Code + return ( <> {hiddenColumns.columns.length > 0 ? ( @@ -271,6 +451,23 @@ const GradingSubmissionTable: React.FC = ({ onChange={handleSearchQueryUpdate} /> + + {/* End of Original Code */} + +
+ +
+ + {/* Start of Original Code */} @@ -370,6 +567,14 @@ type FilterableProps = { onClick?: () => void; }; +type FilterablePropsNew = { + setColumnFilters: React.Dispatch>; + id: string; + value: string; + children?: React.ReactNode; + onClick?: () => void; +}; + const Filterable: React.FC = ({ column, value, children, onClick }) => { const handleFilterChange = () => { column.setFilterValue(value); @@ -383,4 +588,25 @@ const Filterable: React.FC = ({ column, value, children, onClic ); }; +const FilterableNew: React.FC = ({ setColumnFilters, id, value, children, onClick }) => { + const handleFilterChange = () => { + setColumnFilters((prev: ColumnFiltersState) => { + return [ + ...prev, + { + id: id, + value: value + } + ]; + }); + onClick?.(); + }; + + return ( + + ); +}; + export default GradingSubmissionTable; diff --git a/src/styles/_academy.scss b/src/styles/_academy.scss index 5ac1d24b47..5b225c5075 100644 --- a/src/styles/_academy.scss +++ b/src/styles/_academy.scss @@ -337,13 +337,17 @@ &:not(.ag-header-cell-sortable) .ag-header-cell-label { padding-right: 1em; } + + .ag-cell-label-container:not(:has(> span.ag-header-icon.ag-header-cell-menu-button)) > .ag-header-cell-label { + padding-right: 1em; + } } .ag-cell { font-size: 1.1em; /* Override to reduce ag-grid default padding */ - padding-left: 0.375em !important; - padding-right: 0.375em !important; + padding-left: 0.375em; + padding-right: 0.375em; text-align: center; } @@ -511,6 +515,7 @@ .grading-overview-filterable-btns { padding: 0 2px; + text-align: inherit; // Hides overflowed text for the buttons &, @@ -567,3 +572,22 @@ } } } + +.ag-header-cell.grading-default-headers { + span.ag-header-cell-text { + font-size: 0.8rem; + color: #6b7280; + font-weight: 600; + } +} + +.ag-header-cell.grading-left-align { + span.ag-header-cell-text { + margin: 0 auto 0 0; + } +} + +.ag-header-cell.hide-cols-btn { + width: 32px; + height: 32px; +} \ No newline at end of file From b22cb6f239930ab602d477d0567e5f39f1e7c2d9 Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Thu, 21 Mar 2024 00:03:49 +0800 Subject: [PATCH 06/69] more changes to grading table - animation whilst loading, filter/edit mode, bugfixes --- src/commons/application/ApplicationTypes.ts | 3 +- src/commons/workspace/WorkspaceActions.ts | 6 + src/commons/workspace/WorkspaceReducer.ts | 9 + src/commons/workspace/WorkspaceTypes.ts | 2 + src/pages/academy/grading/Grading.tsx | 13 +- .../subcomponents/GradingSubmissionsTable.tsx | 225 ++++++++++++++---- src/styles/_academy.scss | 39 ++- 7 files changed, 247 insertions(+), 50 deletions(-) diff --git a/src/commons/application/ApplicationTypes.ts b/src/commons/application/ApplicationTypes.ts index 1110304988..2068339b36 100644 --- a/src/commons/application/ApplicationTypes.ts +++ b/src/commons/application/ApplicationTypes.ts @@ -415,7 +415,8 @@ export const defaultWorkspaceManager: WorkspaceManagerState = { }, currentSubmission: undefined, currentQuestion: undefined, - hasUnsavedChanges: false + hasUnsavedChanges: false, + requestCounter: 0 }, playground: { ...createDefaultWorkspace('playground'), diff --git a/src/commons/workspace/WorkspaceActions.ts b/src/commons/workspace/WorkspaceActions.ts index 9c63f9186c..9883816f80 100644 --- a/src/commons/workspace/WorkspaceActions.ts +++ b/src/commons/workspace/WorkspaceActions.ts @@ -67,6 +67,7 @@ import { UPDATE_GRADING_COLUMN_VISIBILITY, UPDATE_HAS_UNSAVED_CHANGES, UPDATE_REPL_VALUE, + UPDATE_REQUEST_COUNTER, UPDATE_STEPSTOTAL, UPDATE_SUBLANGUAGE, UPDATE_SUBMISSIONS_TABLE_FILTERS, @@ -395,6 +396,11 @@ export const setIsEditorReadonly = createAction( }) ); +export const updateRequestCounter = createAction( + UPDATE_REQUEST_COUNTER, + (requestCount: number) => ({ payload: { requestCount } }) +); + export const updateSubmissionsTableFilters = createAction( UPDATE_SUBMISSIONS_TABLE_FILTERS, (filters: SubmissionsTableFilters) => ({ payload: { filters } }) diff --git a/src/commons/workspace/WorkspaceReducer.ts b/src/commons/workspace/WorkspaceReducer.ts index 16680eddbc..de402afa31 100644 --- a/src/commons/workspace/WorkspaceReducer.ts +++ b/src/commons/workspace/WorkspaceReducer.ts @@ -79,6 +79,7 @@ import { UPDATE_GRADING_COLUMN_VISIBILITY, UPDATE_HAS_UNSAVED_CHANGES, UPDATE_REPL_VALUE, + UPDATE_REQUEST_COUNTER, UPDATE_STEPSTOTAL, UPDATE_SUBLANGUAGE, UPDATE_SUBMISSIONS_TABLE_FILTERS, @@ -658,6 +659,14 @@ const oldWorkspaceReducer: Reducer = ( currentQuestion: action.payload.questionId } }; + case UPDATE_REQUEST_COUNTER: + return { + ...state, + grading: { + ...state.grading, + requestCounter: action.payload.requestCount + } + }; case SET_FOLDER_MODE: return { ...state, diff --git a/src/commons/workspace/WorkspaceTypes.ts b/src/commons/workspace/WorkspaceTypes.ts index 00646c728c..10842e3d36 100644 --- a/src/commons/workspace/WorkspaceTypes.ts +++ b/src/commons/workspace/WorkspaceTypes.ts @@ -40,6 +40,7 @@ export const TOGGLE_EDITOR_AUTORUN = 'TOGGLE_EDITOR_AUTORUN'; export const TOGGLE_USING_SUBST = 'TOGGLE_USING_SUBST'; export const TOGGLE_USING_CSE = 'TOGGLE_USING_CSE'; export const TOGGLE_UPDATE_CSE = 'TOGGLE_UPDATE_CSE'; +export const UPDATE_REQUEST_COUNTER = 'UPDATE_REQUEST_COUNTER'; export const UPDATE_SUBMISSIONS_TABLE_FILTERS = 'UPDATE_SUBMISSIONS_TABLE_FILTERS'; export const UPDATE_GRADING_COLUMN_VISIBILITY = 'UPDATE_GRADING_COLUMN_VISIBILITY'; export const UPDATE_CURRENT_ASSESSMENT_ID = 'UPDATE_CURRENT_ASSESSMENT_ID'; @@ -82,6 +83,7 @@ type GradingWorkspaceAttr = { readonly currentSubmission?: number; readonly currentQuestion?: number; readonly hasUnsavedChanges: boolean; + readonly requestCounter: number; }; type GradingWorkspaceState = GradingWorkspaceAttr & WorkspaceState; diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index 8f4ae43087..aa06b8f05a 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -3,13 +3,14 @@ import '@tremor/react/dist/esm/tremor.css'; import { Icon as BpIcon, NonIdealState, Position, Spinner, SpinnerSize } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import { Button, Card, Flex, Text, Title } from '@tremor/react'; -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; import { Navigate, useParams } from 'react-router'; import { fetchGradingOverviews } from 'src/commons/application/actions/SessionActions'; import { Role } from 'src/commons/application/ApplicationTypes'; import SimpleDropdown from 'src/commons/SimpleDropdown'; -import { useSession } from 'src/commons/utils/Hooks'; +import { useSession, useTypedSelector } from 'src/commons/utils/Hooks'; +import { updateRequestCounter } from 'src/commons/workspace/WorkspaceActions'; import { numberRegExp } from 'src/features/academy/AcademyTypes'; import { exportGradingCSV, @@ -50,8 +51,12 @@ const Grading: React.FC = () => { const [showAllSubmissions, setShowAllSubmissions] = useState(false); const dispatch = useDispatch(); + const requestCounter = useTypedSelector(state => state.workspaces.grading.requestCounter); + const updateGradingOverviewsCallback = useCallback( (page: number, filterParams: Object) => { + console.log("+1 parent"); + dispatch(updateRequestCounter(requestCounter + 1)); dispatch( fetchGradingOverviews( showAllGroups, @@ -64,6 +69,10 @@ const Grading: React.FC = () => { [dispatch, showAllGroups, showAllSubmissions, pageSize] ); + useEffect(() => { + dispatch(updateRequestCounter(Math.max(0, requestCounter - 1))); + }, [gradingOverviews]); + // If submissionId or questionId is defined but not numeric, redirect back to the Grading overviews page if ( (params.submissionId && !params.submissionId?.match(numberRegExp)) || diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 13d1d7f79d..09728a7e7a 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -25,16 +25,18 @@ import { TableRow, TextInput } from '@tremor/react'; -import { ColDef, ICellRendererParams } from 'ag-grid-community'; +import { CellClickedEvent, ColDef, ICellRendererParams } from 'ag-grid-community'; import { AgGridReact } from 'ag-grid-react'; import { debounce } from 'lodash'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; import GradingFlex from 'src/commons/grading/GradingFlex'; import GradingText from 'src/commons/grading/GradingText'; import { useTypedSelector } from 'src/commons/utils/Hooks'; import { updateGradingColumnVisibility, + updateRequestCounter, updateSubmissionsTableFilters } from 'src/commons/workspace/WorkspaceActions'; import { GradingColumnVisibility } from 'src/commons/workspace/WorkspaceTypes'; @@ -122,7 +124,7 @@ const GradingSubmissionTable: React.FC = ({ totalRows, pageSize, submissions, - updateEntries + updateEntries, }) => { // End of Original Code @@ -136,8 +138,22 @@ const GradingSubmissionTable: React.FC = ({ submissionStatus: string; gradingStatus: string; xp: string; - actions: string; - index: number; + actionsIndex: number; + courseID: number; + } + + interface ITableProperties { + defaultColDefs: ColDef; + pagination: boolean; + pageSize: number; + suppressPaginationPanel: boolean; + rowClass: string; + rowHeight: number; + overlayNoRowsTemplate: string; + overlayLoadingTemplate: string; + suppressRowClickSelection: boolean; + tableHeight: string; + tableMargins: string; } const defaultColumnDefs: ColDef = { @@ -146,14 +162,33 @@ const GradingSubmissionTable: React.FC = ({ sortable: true }; - const [rowData, setRowData] = useState(); + const ROW_HEIGHT: number = 60; // in px, declared here to calculate table height + + const tableProperties: ITableProperties = { + defaultColDefs: defaultColumnDefs, + pagination: true, + pageSize: pageSize, + suppressPaginationPanel: true, + rowClass: "grading-left-align grading-table-rows", + rowHeight: ROW_HEIGHT, + overlayNoRowsTemplate: "Hmm... No submissions found, did you filter them all out?", + overlayLoadingTemplate: '
', + suppressRowClickSelection: true, + tableHeight: String(ROW_HEIGHT * Math.min(pageSize, Math.max(2, submissions.length)) + 50) + "px", + tableMargins: "1rem 0 0 0", + }; + const [rowData, setRowData] = useState(); const [colDefs, setColDefs] = useState[]>(); + const [filterMode, setFilterMode] = useState(false); + + const gridRef = useRef>(null); const generateCols = (resetPage: () => void) => { + const cols: ColDef[] = []; - cols.push({ headerName: "Name", field: "assessmentName", flex: 3, cellStyle: defaultCellStyle({textAlign: "left"}), cellRendererSelector: (params: ICellRendererParams) => { + cols.push({ headerName: "Name", field: "assessmentName", flex: 3, cellStyle: defaultCellStyle({textAlign: "left", cursor: (!filterMode ? "pointer" : "")}), cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: FilterableNew, @@ -162,11 +197,15 @@ const GradingSubmissionTable: React.FC = ({ id: "assessmentName", value: params.data.assessmentName, onClick: resetPage, + submissionID: params.data.actionsIndex, + courseID: params.data.courseID, + filterMode: filterMode, } } : undefined; }, headerClass: defaultHeaderClasses("grading-left-align") }); - cols.push({ headerName: "Type", field: "assessmentType", flex: 1, cellStyle: defaultCellStyle(), cellRendererSelector: (params: ICellRendererParams) => { + + cols.push({ headerName: "Type", field: "assessmentType", flex: 1, cellStyle: defaultCellStyle({cursor: (!filterMode ? "pointer" : "")}), cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: FilterableNew, @@ -175,12 +214,16 @@ const GradingSubmissionTable: React.FC = ({ id: "assessmentType", value: params.data.assessmentType, onClick: resetPage, - children: [] + children: [], + submissionID: params.data.actionsIndex, + courseID: params.data.courseID, + filterMode: filterMode, } } : undefined; }, headerClass: defaultHeaderClasses() }); - cols.push({ headerName: "Student", field: "studentName", flex: 1.5, cellStyle: defaultCellStyle({textAlign: "left"}), cellRendererSelector: (params: ICellRendererParams) => { + + cols.push({ headerName: "Student", field: "studentName", flex: 1.5, cellStyle: defaultCellStyle({textAlign: "left", cursor: (!filterMode ? "pointer" : "")}), cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: FilterableNew, @@ -189,11 +232,15 @@ const GradingSubmissionTable: React.FC = ({ id: "studentName", value: params.data.studentName, onClick: resetPage, + submissionID: params.data.actionsIndex, + courseID: params.data.courseID, + filterMode: filterMode, } } : undefined; }, headerClass: defaultHeaderClasses("grading-left-align") }); - cols.push({ headerName: "Username", field: "studentUsername", flex: 1, cellStyle: defaultCellStyle(), cellRendererSelector: (params: ICellRendererParams) => { + + cols.push({ headerName: "Username", field: "studentUsername", flex: 1, cellStyle: defaultCellStyle({cursor: (!filterMode ? "pointer" : "")}), cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: FilterableNew, @@ -202,11 +249,15 @@ const GradingSubmissionTable: React.FC = ({ id: "studentUsername", value: params.data.studentUsername, onClick: resetPage, + submissionID: params.data.actionsIndex, + courseID: params.data.courseID, + filterMode: filterMode, } } : undefined; }, headerClass: defaultHeaderClasses() }); - cols.push({ headerName: "Group", field: "groupName", flex: 0.75, cellStyle: defaultCellStyle(), cellRendererSelector: (params: ICellRendererParams) => { + + cols.push({ headerName: "Group", field: "groupName", flex: 0.75, cellStyle: defaultCellStyle({cursor: (!filterMode ? "pointer" : "")}), cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: FilterableNew, @@ -215,11 +266,15 @@ const GradingSubmissionTable: React.FC = ({ id: "groupName", value: params.data.groupName, onClick: resetPage, + submissionID: params.data.actionsIndex, + courseID: params.data.courseID, + filterMode: filterMode, } } : undefined; }, headerClass: defaultHeaderClasses() }); - cols.push({ headerName: "Progress", field: "submissionStatus", flex: 1, cellStyle: defaultCellStyle(), cellRendererSelector: (params: ICellRendererParams) => { + + cols.push({ headerName: "Progress", field: "submissionStatus", flex: 1, cellStyle: defaultCellStyle({cursor: (!filterMode ? "pointer" : "")}), cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: FilterableNew, @@ -228,12 +283,16 @@ const GradingSubmissionTable: React.FC = ({ id: "submissionStatus", value: params.data.submissionStatus, onClick: resetPage, - children: [] + children: [], + submissionID: params.data.actionsIndex, + courseID: params.data.courseID, + filterMode: filterMode, } } : undefined; }, headerClass: defaultHeaderClasses() }); - cols.push({ headerName: "Grading", field: "gradingStatus", flex: 1, cellStyle: defaultCellStyle(), cellRendererSelector: (params: ICellRendererParams) => { + + cols.push({ headerName: "Grading", field: "gradingStatus", flex: 1, cellStyle: defaultCellStyle({cursor: (!filterMode ? "pointer" : "")}), cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: GradingStatusBadge, @@ -243,13 +302,15 @@ const GradingSubmissionTable: React.FC = ({ } : undefined; }, headerClass: defaultHeaderClasses() }); + cols.push({ headerName: "Raw XP (+Bonus)", field: "xp", flex: 1, cellStyle: defaultCellStyle(), headerClass: defaultHeaderClasses() }); - cols.push({ headerName: "Actions", field: "actions", flex: 1, cellStyle: defaultCellStyle(), cellRendererSelector: (params: ICellRendererParams) => { + + cols.push({ headerName: "Actions", field: "actionsIndex", flex: 1, cellStyle: defaultCellStyle(), cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: GradingActions, params: { - submissionId: params.data.index, + submissionId: params.data.actionsIndex, style: {justifyContent: "center"} } } @@ -266,6 +327,7 @@ const GradingSubmissionTable: React.FC = ({ justifyContent: "center", flexDirection: "column", fontSize: "0.875rem", + borderBottom: "1px solid rgba(0, 0, 0, 0.075)", ...style, } }; @@ -274,10 +336,27 @@ const GradingSubmissionTable: React.FC = ({ return ("grading-default-headers " + (extraClass !== undefined ? extraClass : "")); }; + const showLoading = useCallback(() => { + gridRef.current!.api.showLoadingOverlay(); + }, []) + + const hideLoading = useCallback(() => { + gridRef.current!.api.hideOverlay(); + }, []) + + const cellClickedEvent = (event: CellClickedEvent) => { + if (filterMode === false && event.colDef.field !== "xp" && event.colDef.field !== "actionsIndex") { + navigate(`/courses/${courseId}/grading/${event.data.actionsIndex}`); + } + }; + // Start of Original Code const dispatch = useDispatch(); + const navigate = useNavigate(); const tableFilters = useTypedSelector(state => state.workspaces.grading.submissionsTableFilters); const columnVisibility = useTypedSelector(state => state.workspaces.grading.columnVisiblity); + const requestCounter = useTypedSelector(state => state.workspaces.grading.requestCounter); + const courseId = useTypedSelector(store => store.session.courseId); const [columnFilters, setColumnFilters] = useState([ ...tableFilters.columnFilters @@ -344,6 +423,9 @@ const GradingSubmissionTable: React.FC = ({ const handleFilterRemove = ({ id, value }: ColumnFilter) => { const newFilters = columnFilters.filter(filter => filter.id !== id && filter.value !== value); + // updateIsLoading(true); + console.log("+1"); + dispatch(updateRequestCounter(requestCounter + 1)); setColumnFilters(newFilters); resetPage(); }; @@ -383,7 +465,13 @@ const GradingSubmissionTable: React.FC = ({ // End of Original Code useEffect(() => { - setRowData(submissions.map((submission): IRow => { + + let sameData: boolean = true; + + const newData: IRow[] = submissions.map((submission, index): IRow => { + if (sameData && submission.submissionId !== rowData?.[index]?.actionsIndex) { + sameData = false; + } return { assessmentName: submission.assessmentName, assessmentType: submission.assessmentType, @@ -392,17 +480,40 @@ const GradingSubmissionTable: React.FC = ({ groupName: submission.groupName, submissionStatus: submission.submissionStatus, gradingStatus: submission.gradingStatus, - xp: submission.initialXp + " (+" + submission.xpBonus + ") / " + submission.maxXp, - actions: "", - index: submission.submissionId, + xp: submission.currentXp + " (+" + submission.xpBonus + ") / " + submission.maxXp, + actionsIndex: submission.submissionId, + courseID: courseId!, }; - })); - }, [submissions]); - + }); + + if ((rowData?.length !== 0 && newData.length === 0) || !sameData) { // First 2 conditions for edge case due to multiple rerenders + setRowData(newData); + } + + }, [submissions, gridRef.current]); + useEffect(() => { - setColDefs(generateCols(resetPage)); + if (gridRef.current?.api) { + if (requestCounter <= 0) { + hideLoading(); + } else { + showLoading(); + } + } + }, [requestCounter]); + + useEffect(() => { + setColDefs(generateCols(() => { + console.log("+1"); + dispatch(updateRequestCounter(requestCounter + 1)); + resetPage(); + })); }, [resetPage]); + useEffect(() => { + setColDefs(generateCols(() => resetPage())); + }, [filterMode]); + // Start of Original Code return ( @@ -434,15 +545,21 @@ const GradingSubmissionTable: React.FC = ({ {columnFilters.length > 0 ? 'Filters: ' - : 'No filters applied. Click on any cell to filter by its value.' + - (hiddenColumns.columns.length === 0 - ? ' Click on any column header to hide it.' - : '')}{' '} + : (filterMode === true + ? 'No filters applied. Click on any cell to filter by its value.' + + (hiddenColumns.columns.length === 0 + ? ' Click on any column header to hide it.' + : '') + : Disable Edit Mode to enable click to filter)}{' '} + + } @@ -454,16 +571,24 @@ const GradingSubmissionTable: React.FC = ({ {/* End of Original Code */} -
+
@@ -573,6 +698,9 @@ type FilterablePropsNew = { value: string; children?: React.ReactNode; onClick?: () => void; + courseID: number; + submissionID: number; + filterMode: boolean; }; const Filterable: React.FC = ({ column, value, children, onClick }) => { @@ -588,21 +716,30 @@ const Filterable: React.FC = ({ column, value, children, onClic ); }; -const FilterableNew: React.FC = ({ setColumnFilters, id, value, children, onClick }) => { +const FilterableNew: React.FC = ({ setColumnFilters, id, value, children, onClick, courseID, submissionID, filterMode }) => { const handleFilterChange = () => { setColumnFilters((prev: ColumnFiltersState) => { - return [ - ...prev, - { - id: id, - value: value - } - ]; + const alreadyExists = prev.reduce((acc, curr) => acc || (curr.id === id && curr.value === value), false); + return alreadyExists + ? [...prev] + : [ + ...prev, + { + id: id, + value: value + } + ]; }); onClick?.(); }; return ( + filterMode === false + ? + + : diff --git a/src/styles/_academy.scss b/src/styles/_academy.scss index 5b225c5075..259eca518f 100644 --- a/src/styles/_academy.scss +++ b/src/styles/_academy.scss @@ -513,9 +513,15 @@ } } +.grading-overview-unfilterable-btns, .grading-overview-filterable-btns { padding: 0 2px; text-align: inherit; + outline: none; + a { + text-decoration: none; + color: black; + } // Hides overflowed text for the buttons &, @@ -533,11 +539,13 @@ -0.875rem for icon margins -1.125rem for additional padding */ - p, &:has(> span) > span { + p, &:has(> span) > span, > a { max-width: calc(20vw - 16px - 2rem); } } +} +.grading-overview-filterable-btns { // Invisible border for backgroundless buttons &:not(:has(> div)) { text-decoration: none; @@ -545,7 +553,7 @@ &:hover { // Buttons with bg - > div > span, &:has(> span) { + > div > span, &:has(> span), > a { // Use of contrast due to unknown background color filter: contrast(0.9); } @@ -557,7 +565,9 @@ } } -.grading-overview-filterable-btns, .grading-overview-rounded-btns { +.grading-overview-unfilterable-btns, +.grading-overview-filterable-btns, +.grading-overview-rounded-btns { border-radius: 9999px; } @@ -581,6 +591,12 @@ } } +.grading-table-rows { + &.ag-row-hover { + --ag-row-hover-color: rgb(245, 245, 245); + } +} + .ag-header-cell.grading-left-align { span.ag-header-cell-text { margin: 0 auto 0 0; @@ -590,4 +606,21 @@ .ag-header-cell.hide-cols-btn { width: 32px; height: 32px; +} + +.grading-filter-btn { + padding: 7.5px 15px; + border-radius: 25px; + margin: 0 15px 0 auto; + background-color: #dbeafe; + color: #3b82f6; + + &.grading-filter-btn-on { + background-color: #f5f5f5; + color: black; + } + + &:hover { + filter: contrast(0.9); + } } \ No newline at end of file From 63932c642ef72018109d8ff5bfd9642f5c9ff829 Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Sun, 24 Mar 2024 01:13:15 +0800 Subject: [PATCH 07/69] code refactoring --- src/commons/workspace/WorkspaceActions.ts | 14 +- src/commons/workspace/WorkspaceReducer.ts | 15 +- src/commons/workspace/WorkspaceTypes.ts | 3 +- src/pages/academy/grading/Grading.tsx | 12 +- .../subcomponents/GradingSubmissionsTable.tsx | 301 ++++++++++-------- src/styles/_academy.scss | 42 ++- 6 files changed, 239 insertions(+), 148 deletions(-) diff --git a/src/commons/workspace/WorkspaceActions.ts b/src/commons/workspace/WorkspaceActions.ts index 9883816f80..38d6790103 100644 --- a/src/commons/workspace/WorkspaceActions.ts +++ b/src/commons/workspace/WorkspaceActions.ts @@ -26,6 +26,7 @@ import { CLEAR_REPL_INPUT, CLEAR_REPL_OUTPUT, CLEAR_REPL_OUTPUT_LAST, + DECREMENT_REQUEST_COUNTER, DISABLE_TOKEN_COUNTER, EditorTabState, ENABLE_TOKEN_COUNTER, @@ -35,6 +36,7 @@ import { EVAL_REPL, EVAL_TESTCASE, GradingColumnVisibility, + INCREMENT_REQUEST_COUNTER, MOVE_CURSOR, NAV_DECLARATION, PLAYGROUND_EXTERNAL_SELECT, @@ -67,7 +69,6 @@ import { UPDATE_GRADING_COLUMN_VISIBILITY, UPDATE_HAS_UNSAVED_CHANGES, UPDATE_REPL_VALUE, - UPDATE_REQUEST_COUNTER, UPDATE_STEPSTOTAL, UPDATE_SUBLANGUAGE, UPDATE_SUBMISSIONS_TABLE_FILTERS, @@ -396,10 +397,13 @@ export const setIsEditorReadonly = createAction( }) ); -export const updateRequestCounter = createAction( - UPDATE_REQUEST_COUNTER, - (requestCount: number) => ({ payload: { requestCount } }) -); +export const increaseRequestCounter = createAction(INCREMENT_REQUEST_COUNTER, () => ({ + payload: {} +})); + +export const decreaseRequestCounter = createAction(DECREMENT_REQUEST_COUNTER, () => ({ + payload: {} +})); export const updateSubmissionsTableFilters = createAction( UPDATE_SUBMISSIONS_TABLE_FILTERS, diff --git a/src/commons/workspace/WorkspaceReducer.ts b/src/commons/workspace/WorkspaceReducer.ts index de402afa31..739d7be569 100644 --- a/src/commons/workspace/WorkspaceReducer.ts +++ b/src/commons/workspace/WorkspaceReducer.ts @@ -47,12 +47,14 @@ import { import { ADD_EDITOR_TAB, CHANGE_EXTERNAL_LIBRARY, + DECREMENT_REQUEST_COUNTER, DISABLE_TOKEN_COUNTER, EditorTabState, ENABLE_TOKEN_COUNTER, END_CLEAR_CONTEXT, EVAL_EDITOR, EVAL_REPL, + INCREMENT_REQUEST_COUNTER, MOVE_CURSOR, REMOVE_EDITOR_TAB, REMOVE_EDITOR_TAB_FOR_FILE, @@ -79,7 +81,6 @@ import { UPDATE_GRADING_COLUMN_VISIBILITY, UPDATE_HAS_UNSAVED_CHANGES, UPDATE_REPL_VALUE, - UPDATE_REQUEST_COUNTER, UPDATE_STEPSTOTAL, UPDATE_SUBLANGUAGE, UPDATE_SUBMISSIONS_TABLE_FILTERS, @@ -659,12 +660,20 @@ const oldWorkspaceReducer: Reducer = ( currentQuestion: action.payload.questionId } }; - case UPDATE_REQUEST_COUNTER: + case INCREMENT_REQUEST_COUNTER: return { ...state, grading: { ...state.grading, - requestCounter: action.payload.requestCount + requestCounter: state.grading.requestCounter + 1 + } + }; + case DECREMENT_REQUEST_COUNTER: + return { + ...state, + grading: { + ...state.grading, + requestCounter: Math.max(0, state.grading.requestCounter - 1) } }; case SET_FOLDER_MODE: diff --git a/src/commons/workspace/WorkspaceTypes.ts b/src/commons/workspace/WorkspaceTypes.ts index 10842e3d36..1cd35abcce 100644 --- a/src/commons/workspace/WorkspaceTypes.ts +++ b/src/commons/workspace/WorkspaceTypes.ts @@ -23,12 +23,14 @@ export const CLEAR_REPL_OUTPUT_LAST = 'CLEAR_REPL_OUTPUT_LAST'; export const END_CLEAR_CONTEXT = 'END_CLEAR_CONTEXT'; export const ENABLE_TOKEN_COUNTER = 'ENABLE_TOKEN_COUNTER'; export const DISABLE_TOKEN_COUNTER = 'DISABLE_TOKEN_COUNTER'; +export const DECREMENT_REQUEST_COUNTER = 'DECREMENT_REQUEST_COUNTER'; export const EVAL_EDITOR = 'EVAL_EDITOR'; export const EVAL_REPL = 'EVAL_REPL'; export const PROMPT_AUTOCOMPLETE = 'PROMPT_AUTOCOMPLETE'; export const EVAL_SILENT = 'EVAL_SILENT'; export const EVAL_TESTCASE = 'EVAL_TESTCASE'; export const EVAL_EDITOR_AND_TESTCASES = 'EVAL_EDITOR_AND_TESTCASES'; +export const INCREMENT_REQUEST_COUNTER = 'INCREMENT_REQUEST_COUNTER'; export const MOVE_CURSOR = 'MOVE_CURSOR'; export const NAV_DECLARATION = 'NAV_DECLARATION'; export const PLAYGROUND_EXTERNAL_SELECT = 'PLAYGROUND_EXTERNAL_SELECT '; @@ -40,7 +42,6 @@ export const TOGGLE_EDITOR_AUTORUN = 'TOGGLE_EDITOR_AUTORUN'; export const TOGGLE_USING_SUBST = 'TOGGLE_USING_SUBST'; export const TOGGLE_USING_CSE = 'TOGGLE_USING_CSE'; export const TOGGLE_UPDATE_CSE = 'TOGGLE_UPDATE_CSE'; -export const UPDATE_REQUEST_COUNTER = 'UPDATE_REQUEST_COUNTER'; export const UPDATE_SUBMISSIONS_TABLE_FILTERS = 'UPDATE_SUBMISSIONS_TABLE_FILTERS'; export const UPDATE_GRADING_COLUMN_VISIBILITY = 'UPDATE_GRADING_COLUMN_VISIBILITY'; export const UPDATE_CURRENT_ASSESSMENT_ID = 'UPDATE_CURRENT_ASSESSMENT_ID'; diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index aa06b8f05a..2f3e4943a7 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -10,7 +10,7 @@ import { fetchGradingOverviews } from 'src/commons/application/actions/SessionAc import { Role } from 'src/commons/application/ApplicationTypes'; import SimpleDropdown from 'src/commons/SimpleDropdown'; import { useSession, useTypedSelector } from 'src/commons/utils/Hooks'; -import { updateRequestCounter } from 'src/commons/workspace/WorkspaceActions'; +import { decreaseRequestCounter, increaseRequestCounter } from 'src/commons/workspace/WorkspaceActions'; import { numberRegExp } from 'src/features/academy/AcademyTypes'; import { exportGradingCSV, @@ -55,8 +55,7 @@ const Grading: React.FC = () => { const updateGradingOverviewsCallback = useCallback( (page: number, filterParams: Object) => { - console.log("+1 parent"); - dispatch(updateRequestCounter(requestCounter + 1)); + dispatch(increaseRequestCounter()); dispatch( fetchGradingOverviews( showAllGroups, @@ -70,7 +69,12 @@ const Grading: React.FC = () => { ); useEffect(() => { - dispatch(updateRequestCounter(Math.max(0, requestCounter - 1))); + console.log(requestCounter); + }, [requestCounter]); + + useEffect(() => { + console.log("minus 11"); + dispatch(decreaseRequestCounter()); }, [gradingOverviews]); // If submissionId or questionId is defined but not numeric, redirect back to the Grading overviews page diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 09728a7e7a..8505212df5 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -35,8 +35,8 @@ import GradingFlex from 'src/commons/grading/GradingFlex'; import GradingText from 'src/commons/grading/GradingText'; import { useTypedSelector } from 'src/commons/utils/Hooks'; import { + increaseRequestCounter, updateGradingColumnVisibility, - updateRequestCounter, updateSubmissionsTableFilters } from 'src/commons/workspace/WorkspaceActions'; import { GradingColumnVisibility } from 'src/commons/workspace/WorkspaceTypes'; @@ -156,12 +156,34 @@ const GradingSubmissionTable: React.FC = ({ tableMargins: string; } + enum ColumnFields { + assessmentName = "assessmentName", + assessmentType = "assessmentType", + studentName = "studentName", + studentUsername = "studentUsername", + groupName = "groupName", + submissionStatus = "submissionStatus", + gradingStatus = "gradingStatus", + xp = "xp", + actionsIndex = "actionsIndex", + } + const defaultColumnDefs: ColDef = { filter: false, resizable: false, sortable: true }; + const disabledEditModeCols: string[] = [ + ColumnFields.actionsIndex, + ] + + const disabledFilterModeCols: string[] = [ + ColumnFields.gradingStatus, + ColumnFields.xp, + ColumnFields.actionsIndex, + ] + const ROW_HEIGHT: number = 60; // in px, declared here to calculate table height const tableProperties: ITableProperties = { @@ -184,115 +206,133 @@ const GradingSubmissionTable: React.FC = ({ const gridRef = useRef>(null); - const generateCols = (resetPage: () => void) => { + const generateCols = () => { const cols: ColDef[] = []; - cols.push({ headerName: "Name", field: "assessmentName", flex: 3, cellStyle: defaultCellStyle({textAlign: "left", cursor: (!filterMode ? "pointer" : "")}), cellRendererSelector: (params: ICellRendererParams) => { + cols.push({ + headerName: "Name", + field: ColumnFields.assessmentName, + flex: 3, + cellClass: "grading-def-cell grading-def-cell-pointer grading-cell-align-left", + headerClass: "grading-default-headers grading-left-align", + cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: FilterableNew, params: { - setColumnFilters: setColumnFilters, - id: "assessmentName", value: params.data.assessmentName, - onClick: resetPage, - submissionID: params.data.actionsIndex, - courseID: params.data.courseID, filterMode: filterMode, } } : undefined; - }, headerClass: defaultHeaderClasses("grading-left-align") }); + }, + }); - cols.push({ headerName: "Type", field: "assessmentType", flex: 1, cellStyle: defaultCellStyle({cursor: (!filterMode ? "pointer" : "")}), cellRendererSelector: (params: ICellRendererParams) => { + cols.push({ + headerName: "Type", + field: ColumnFields.assessmentType, + flex: 1, + cellClass: "grading-def-cell grading-def-cell-pointer", + headerClass: "grading-default-headers", + cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: FilterableNew, params: { - setColumnFilters: setColumnFilters, - id: "assessmentType", value: params.data.assessmentType, - onClick: resetPage, children: [], - submissionID: params.data.actionsIndex, - courseID: params.data.courseID, filterMode: filterMode, } } : undefined; - }, headerClass: defaultHeaderClasses() }); + }, + }); - cols.push({ headerName: "Student", field: "studentName", flex: 1.5, cellStyle: defaultCellStyle({textAlign: "left", cursor: (!filterMode ? "pointer" : "")}), cellRendererSelector: (params: ICellRendererParams) => { + cols.push({ + headerName: "Student", + field: ColumnFields.studentName, + flex: 1.5, + cellClass: "grading-def-cell grading-def-cell-pointer grading-cell-align-left", + headerClass: "grading-default-headers grading-left-align", + cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: FilterableNew, params: { - setColumnFilters: setColumnFilters, - id: "studentName", value: params.data.studentName, - onClick: resetPage, - submissionID: params.data.actionsIndex, - courseID: params.data.courseID, filterMode: filterMode, } } : undefined; - }, headerClass: defaultHeaderClasses("grading-left-align") }); + }, + }); - cols.push({ headerName: "Username", field: "studentUsername", flex: 1, cellStyle: defaultCellStyle({cursor: (!filterMode ? "pointer" : "")}), cellRendererSelector: (params: ICellRendererParams) => { + cols.push({ + headerName: "Username", + field: ColumnFields.studentUsername, + flex: 1, + cellClass: "grading-def-cell grading-def-cell-pointer", + headerClass: "grading-default-headers", + cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: FilterableNew, params: { - setColumnFilters: setColumnFilters, - id: "studentUsername", value: params.data.studentUsername, - onClick: resetPage, - submissionID: params.data.actionsIndex, - courseID: params.data.courseID, filterMode: filterMode, } } : undefined; - }, headerClass: defaultHeaderClasses() }); + }, + }); - cols.push({ headerName: "Group", field: "groupName", flex: 0.75, cellStyle: defaultCellStyle({cursor: (!filterMode ? "pointer" : "")}), cellRendererSelector: (params: ICellRendererParams) => { + cols.push({ + headerName: "Group", + field: ColumnFields.groupName, + flex: 0.75, + cellClass: "grading-def-cell grading-def-cell-pointer", + headerClass: "grading-default-headers", + cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: FilterableNew, params: { - setColumnFilters: setColumnFilters, - id: "groupName", value: params.data.groupName, - onClick: resetPage, - submissionID: params.data.actionsIndex, - courseID: params.data.courseID, filterMode: filterMode, } } : undefined; - }, headerClass: defaultHeaderClasses() }); + }, + }); - cols.push({ headerName: "Progress", field: "submissionStatus", flex: 1, cellStyle: defaultCellStyle({cursor: (!filterMode ? "pointer" : "")}), cellRendererSelector: (params: ICellRendererParams) => { + cols.push({ + headerName: "Progress", + field: ColumnFields.submissionStatus, + flex: 1, + cellClass: "grading-def-cell grading-def-cell-pointer", + headerClass: "grading-default-headers", + cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: FilterableNew, params: { - setColumnFilters: setColumnFilters, - id: "submissionStatus", - value: params.data.submissionStatus, - onClick: resetPage, + value: params.data.submissionStatus, children: [], - submissionID: params.data.actionsIndex, - courseID: params.data.courseID, filterMode: filterMode, } } : undefined; - }, headerClass: defaultHeaderClasses() }); + }, + }); - cols.push({ headerName: "Grading", field: "gradingStatus", flex: 1, cellStyle: defaultCellStyle({cursor: (!filterMode ? "pointer" : "")}), cellRendererSelector: (params: ICellRendererParams) => { + cols.push({ + headerName: "Grading", + field: ColumnFields.gradingStatus, + flex: 1, + cellClass: "grading-def-cell" + (!filterMode ? " grading-def-cell-pointer" : ""), + headerClass: "grading-default-headers", + cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: GradingStatusBadge, @@ -301,11 +341,24 @@ const GradingSubmissionTable: React.FC = ({ } } : undefined; - }, headerClass: defaultHeaderClasses() }); + }, + }); - cols.push({ headerName: "Raw XP (+Bonus)", field: "xp", flex: 1, cellStyle: defaultCellStyle(), headerClass: defaultHeaderClasses() }); + cols.push({ + headerName: "Raw XP (+Bonus)", + field: ColumnFields.xp, + flex: 1, + cellClass: "grading-def-cell" + (!filterMode ? " grading-def-cell-pointer" : " grading-def-cell-selectable"), + headerClass: "grading-default-headers", + }); - cols.push({ headerName: "Actions", field: "actionsIndex", flex: 1, cellStyle: defaultCellStyle(), cellRendererSelector: (params: ICellRendererParams) => { + cols.push({ + headerName: "Actions", + field: ColumnFields.actionsIndex, + flex: 1, + cellClass: "grading-def-cell", + headerClass: "grading-default-headers", + cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: GradingActions, @@ -315,27 +368,12 @@ const GradingSubmissionTable: React.FC = ({ } } : undefined; - }, headerClass: defaultHeaderClasses() }); + }, + }); return cols; } - const defaultCellStyle = (style?: React.CSSProperties) => { - return { - textAlign: "center", - display: "flex", - justifyContent: "center", - flexDirection: "column", - fontSize: "0.875rem", - borderBottom: "1px solid rgba(0, 0, 0, 0.075)", - ...style, - } - }; - - const defaultHeaderClasses = (extraClass?: string) => { - return ("grading-default-headers " + (extraClass !== undefined ? extraClass : "")); - }; - const showLoading = useCallback(() => { gridRef.current!.api.showLoadingOverlay(); }, []) @@ -344,10 +382,20 @@ const GradingSubmissionTable: React.FC = ({ gridRef.current!.api.hideOverlay(); }, []) + const showNoRows = useCallback(() => { + gridRef.current!.api.showNoRowsOverlay(); + }, []) + const cellClickedEvent = (event: CellClickedEvent) => { - if (filterMode === false && event.colDef.field !== "xp" && event.colDef.field !== "actionsIndex") { + + const colClicked: string = event.colDef.field ? event.colDef.field : ""; + + if (!filterMode && !disabledEditModeCols.includes(colClicked)) { navigate(`/courses/${courseId}/grading/${event.data.actionsIndex}`); + } else if (filterMode && !disabledFilterModeCols.includes(colClicked)) { + handleFilterAdd({id: colClicked, value: event.data[colClicked]}); } + }; // Start of Original Code @@ -391,7 +439,7 @@ const GradingSubmissionTable: React.FC = ({ // Converts the columnFilters array into backend query parameters. const backendFilterParams = useMemo(() => { const filters: Array<{ [key: string]: any }> = [ - { id: 'assessmentName', value: searchValue }, + { id: ColumnFields.assessmentName, value: searchValue }, ...columnFilters ].map(convertFilterToBackendParams); @@ -421,11 +469,27 @@ const GradingSubmissionTable: React.FC = ({ getPaginationRowModel: getPaginationRowModel() }); + const handleFilterAdd = ({ id, value }: ColumnFilter) => { + dispatch(increaseRequestCounter()); + setColumnFilters((prev: ColumnFiltersState) => { + const alreadyExists = prev.reduce((acc, curr) => acc || (curr.id === id && curr.value === value), false); + return alreadyExists + ? [...prev] + : [ + ...prev, + { + id: id, + value: value + } + ]; + }); + resetPage(); + }; + + const handleFilterRemove = ({ id, value }: ColumnFilter) => { const newFilters = columnFilters.filter(filter => filter.id !== id && filter.value !== value); - // updateIsLoading(true); - console.log("+1"); - dispatch(updateRequestCounter(requestCounter + 1)); + dispatch(increaseRequestCounter()); setColumnFilters(newFilters); resetPage(); }; @@ -466,53 +530,48 @@ const GradingSubmissionTable: React.FC = ({ useEffect(() => { - let sameData: boolean = true; + if (gridRef.current?.api) { - const newData: IRow[] = submissions.map((submission, index): IRow => { - if (sameData && submission.submissionId !== rowData?.[index]?.actionsIndex) { - sameData = false; - } - return { - assessmentName: submission.assessmentName, - assessmentType: submission.assessmentType, - studentName: submission.studentName, - studentUsername: submission.studentUsername, - groupName: submission.groupName, - submissionStatus: submission.submissionStatus, - gradingStatus: submission.gradingStatus, - xp: submission.currentXp + " (+" + submission.xpBonus + ") / " + submission.maxXp, - actionsIndex: submission.submissionId, - courseID: courseId!, - }; - }); - - if ((rowData?.length !== 0 && newData.length === 0) || !sameData) { // First 2 conditions for edge case due to multiple rerenders - setRowData(newData); - } + if (requestCounter <= 0) { + let sameData: boolean = submissions.length === rowData?.length; - }, [submissions, gridRef.current]); + const newData: IRow[] = submissions.map((submission, index): IRow => { + if (sameData && submission.submissionId !== rowData?.[index]?.actionsIndex) { + sameData = false; + } + return { + assessmentName: submission.assessmentName, + assessmentType: submission.assessmentType, + studentName: submission.studentName, + studentUsername: submission.studentUsername, + groupName: submission.groupName, + submissionStatus: submission.submissionStatus, + gradingStatus: submission.gradingStatus, + xp: submission.currentXp + " (+" + submission.xpBonus + ") / " + submission.maxXp, + actionsIndex: submission.submissionId, + courseID: courseId!, + }; + }); + + if (!sameData) { + setRowData(newData); + } - useEffect(() => { - if (gridRef.current?.api) { - if (requestCounter <= 0) { hideLoading(); + if (rowData?.length === 0) { + showNoRows(); + } + } else { showLoading(); } } - }, [requestCounter]); - useEffect(() => { - setColDefs(generateCols(() => { - console.log("+1"); - dispatch(updateRequestCounter(requestCounter + 1)); - resetPage(); - })); - }, [resetPage]); + }, [requestCounter, submissions, gridRef.current]); useEffect(() => { - setColDefs(generateCols(() => resetPage())); - }, [filterMode]); + setColDefs(generateCols()); + }, [resetPage, filterMode]); // Start of Original Code @@ -698,8 +757,6 @@ type FilterablePropsNew = { value: string; children?: React.ReactNode; onClick?: () => void; - courseID: number; - submissionID: number; filterMode: boolean; }; @@ -716,31 +773,9 @@ const Filterable: React.FC = ({ column, value, children, onClic ); }; -const FilterableNew: React.FC = ({ setColumnFilters, id, value, children, onClick, courseID, submissionID, filterMode }) => { - const handleFilterChange = () => { - setColumnFilters((prev: ColumnFiltersState) => { - const alreadyExists = prev.reduce((acc, curr) => acc || (curr.id === id && curr.value === value), false); - return alreadyExists - ? [...prev] - : [ - ...prev, - { - id: id, - value: value - } - ]; - }); - onClick?.(); - }; - +const FilterableNew: React.FC = ({ value, children, filterMode }) => { return ( - filterMode === false - ? - - : - ); diff --git a/src/styles/_academy.scss b/src/styles/_academy.scss index 259eca518f..2b833cc045 100644 --- a/src/styles/_academy.scss +++ b/src/styles/_academy.scss @@ -553,7 +553,7 @@ &:hover { // Buttons with bg - > div > span, &:has(> span), > a { + &:has(> div) { // Use of contrast due to unknown background color filter: contrast(0.9); } @@ -583,7 +583,7 @@ } } -.ag-header-cell.grading-default-headers { +.grading-default-headers { span.ag-header-cell-text { font-size: 0.8rem; color: #6b7280; @@ -623,4 +623,42 @@ &:hover { filter: contrast(0.9); } +} + +.grading-def-cell { + text-align: center; + display: flex!important; + justify-content: center; + flex-direction: column; + font-size: 0.875rem; + user-select: text; + border: 0px!important; + border-bottom: 1px solid rgba(0, 0, 0, 0.075)!important; + + &:hover:not(:has(> div)):has(button.grading-overview-filterable-btns) { + button { + text-decoration: underline; + + &:has(> div) { + filter: contrast(0.9); + } + } + } + + &:active { + border-style: outset; + } + + &.grading-def-cell-pointer { + cursor: pointer; + } + + &.grading-def-cell-selectable { + cursor: text; + border-style: outset; + } + + &.grading-cell-align-left { + text-align: left!important; + } } \ No newline at end of file From de27b6d103564089a648787a1d2a81c38beafc8f Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Tue, 26 Mar 2024 23:25:14 +0800 Subject: [PATCH 08/69] filterable columns, backend sorting shell, and some partial removal of tanstack and tremor table --- src/commons/application/ApplicationTypes.ts | 4 +- src/commons/workspace/WorkspaceTypes.ts | 4 +- src/pages/academy/grading/Grading.tsx | 14 +- .../GradingColumnCustomHeaders.tsx | 36 + .../subcomponents/GradingSubmissionsTable.tsx | 646 ++++++++++-------- src/styles/_academy.scss | 77 ++- 6 files changed, 486 insertions(+), 295 deletions(-) create mode 100644 src/pages/academy/grading/subcomponents/GradingColumnCustomHeaders.tsx diff --git a/src/commons/application/ApplicationTypes.ts b/src/commons/application/ApplicationTypes.ts index 2068339b36..e5ccf8b368 100644 --- a/src/commons/application/ApplicationTypes.ts +++ b/src/commons/application/ApplicationTypes.ts @@ -410,9 +410,7 @@ export const defaultWorkspaceManager: WorkspaceManagerState = { submissionsTableFilters: { columnFilters: [] }, - columnVisiblity: { - columns: [] - }, + columnVisiblity: [], currentSubmission: undefined, currentQuestion: undefined, hasUnsavedChanges: false, diff --git a/src/commons/workspace/WorkspaceTypes.ts b/src/commons/workspace/WorkspaceTypes.ts index 1cd35abcce..1aebda0f90 100644 --- a/src/commons/workspace/WorkspaceTypes.ts +++ b/src/commons/workspace/WorkspaceTypes.ts @@ -172,6 +172,4 @@ export type SubmissionsTableFilters = { columnFilters: { id: string; value: unknown }[]; }; -export type GradingColumnVisibility = { - columns: string[]; -}; +export type GradingColumnVisibility = string[]; diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index 2f3e4943a7..85196cf724 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -9,7 +9,8 @@ import { Navigate, useParams } from 'react-router'; import { fetchGradingOverviews } from 'src/commons/application/actions/SessionActions'; import { Role } from 'src/commons/application/ApplicationTypes'; import SimpleDropdown from 'src/commons/SimpleDropdown'; -import { useSession, useTypedSelector } from 'src/commons/utils/Hooks'; +import { useSession } from 'src/commons/utils/Hooks'; +// import { useSession, useTypedSelector } from 'src/commons/utils/Hooks'; import { decreaseRequestCounter, increaseRequestCounter } from 'src/commons/workspace/WorkspaceActions'; import { numberRegExp } from 'src/features/academy/AcademyTypes'; import { @@ -51,7 +52,7 @@ const Grading: React.FC = () => { const [showAllSubmissions, setShowAllSubmissions] = useState(false); const dispatch = useDispatch(); - const requestCounter = useTypedSelector(state => state.workspaces.grading.requestCounter); + // const requestCounter = useTypedSelector(state => state.workspaces.grading.requestCounter); const updateGradingOverviewsCallback = useCallback( (page: number, filterParams: Object) => { @@ -68,14 +69,13 @@ const Grading: React.FC = () => { [dispatch, showAllGroups, showAllSubmissions, pageSize] ); - useEffect(() => { - console.log(requestCounter); - }, [requestCounter]); + // useEffect(() => { + // console.log(requestCounter); + // }, [requestCounter]); useEffect(() => { - console.log("minus 11"); dispatch(decreaseRequestCounter()); - }, [gradingOverviews]); + }, [gradingOverviews, dispatch]); // If submissionId or questionId is defined but not numeric, redirect back to the Grading overviews page if ( diff --git a/src/pages/academy/grading/subcomponents/GradingColumnCustomHeaders.tsx b/src/pages/academy/grading/subcomponents/GradingColumnCustomHeaders.tsx new file mode 100644 index 0000000000..fe6395d593 --- /dev/null +++ b/src/pages/academy/grading/subcomponents/GradingColumnCustomHeaders.tsx @@ -0,0 +1,36 @@ +import { Icon } from '@blueprintjs/core'; +import { CustomHeaderProps } from 'ag-grid-react'; +import { useState } from 'react'; + +import { getNextSortState, SortStates } from './GradingSubmissionsTable'; + +export interface GradingColumnCustomHeadersProps extends CustomHeaderProps { + hideColumn: (id: string) => void; + newSortState: (id: string, sortState: SortStates) => void; +} + +const GradingColumnCustomHeaders: React.FC = (props: GradingColumnCustomHeadersProps) => { + + // The values correspond to the available icons in the BlueprintJS library. "sort" means unsorted. + const [sortState, setSortState] = useState(SortStates.NONE); + + const nextSortState = () => { + setSortState((prev) => getNextSortState(prev)); + props.newSortState(props.column.getColId(), getNextSortState(sortState)); + } + + return ( +
+ {props.displayName} +
nextSortState()}> + +
+
props.hideColumn(props.column.getColId())}> + +
+
+ ); + +}; + +export default GradingColumnCustomHeaders; \ No newline at end of file diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 8505212df5..7bcecbdd4d 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -2,29 +2,30 @@ import '@tremor/react/dist/esm/tremor.css'; import "ag-grid-community/styles/ag-grid.css"; import "ag-grid-community/styles/ag-theme-quartz.css" -import { Button, H6, Icon as BpIcon } from '@blueprintjs/core'; +import { Button, H6, Icon as BpIcon, InputGroup } from '@blueprintjs/core'; +// import { Icon as BpIcon } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import { - Column, + // Column, ColumnFilter, ColumnFiltersState, - createColumnHelper, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - useReactTable + // createColumnHelper, + // flexRender, + // getCoreRowModel, + // getFilteredRowModel, + // getPaginationRowModel, + // useReactTable } from '@tanstack/react-table'; -import { - Footer, - Table, - TableBody, - TableCell, - TableHead, - TableHeaderCell, - TableRow, - TextInput -} from '@tremor/react'; +// import { +// Footer, +// Table, +// TableBody, +// TableCell, +// TableHead, +// TableHeaderCell, +// TableRow, +// TextInput +// } from '@tremor/react'; import { CellClickedEvent, ColDef, ICellRendererParams } from 'ag-grid-community'; import { AgGridReact } from 'ag-grid-react'; import { debounce } from 'lodash'; @@ -45,73 +46,93 @@ import { convertFilterToBackendParams } from 'src/features/grading/GradingUtils' import GradingActions from './GradingActions'; import { AssessmentTypeBadge, GradingStatusBadge, SubmissionStatusBadge } from './GradingBadges'; +import GradingColumnCustomHeaders from './GradingColumnCustomHeaders'; import GradingColumnFilters from './GradingColumnFilters'; import GradingSubmissionFilters from './GradingSubmissionFilters'; -const columnHelper = createColumnHelper(); - -const makeColumns = (handleClick: () => void) => [ - columnHelper.accessor('assessmentName', { - header: 'Name', - cell: info => - }), - columnHelper.accessor('assessmentType', { - header: 'Type', - cell: info => ( - - - - ) - }), - columnHelper.accessor('studentName', { - header: 'Student', - cell: info => - }), - columnHelper.accessor('studentUsername', { - header: 'Username', - cell: info => - }), - columnHelper.accessor('groupName', { - header: 'Group', - cell: info => - }), - columnHelper.accessor('submissionStatus', { - header: 'Progress', - cell: info => ( - - - - ) - }), - columnHelper.accessor('gradingStatus', { - header: 'Grading', - cell: info => - }), - columnHelper.accessor(({ currentXp, xpBonus, maxXp }) => ({ currentXp, xpBonus, maxXp }), { - header: 'Raw XP (+Bonus)', - enableColumnFilter: false, - cell: info => { - const { currentXp, xpBonus, maxXp } = info.getValue(); - return ( - - - {currentXp} (+{xpBonus}) - - / - {maxXp} - - ); - } - }), - columnHelper.accessor(({ submissionId }) => ({ submissionId }), { - header: 'Actions', - enableColumnFilter: false, - cell: info => { - const { submissionId } = info.getValue(); - return ; - } - }) -]; +// const columnHelper = createColumnHelper(); + +// const makeColumns = (handleClick: () => void) => [ +// columnHelper.accessor('assessmentName', { +// header: 'Name', +// cell: info => +// }), +// columnHelper.accessor('assessmentType', { +// header: 'Type', +// cell: info => ( +// +// +// +// ) +// }), +// columnHelper.accessor('studentName', { +// header: 'Student', +// cell: info => +// }), +// columnHelper.accessor('studentUsername', { +// header: 'Username', +// cell: info => +// }), +// columnHelper.accessor('groupName', { +// header: 'Group', +// cell: info => +// }), +// columnHelper.accessor('submissionStatus', { +// header: 'Progress', +// cell: info => ( +// +// +// +// ) +// }), +// columnHelper.accessor('gradingStatus', { +// header: 'Grading', +// cell: info => +// }), +// columnHelper.accessor(({ currentXp, xpBonus, maxXp }) => ({ currentXp, xpBonus, maxXp }), { +// header: 'Raw XP (+Bonus)', +// enableColumnFilter: false, +// cell: info => { +// const { currentXp, xpBonus, maxXp } = info.getValue(); +// return ( +// +// +// {currentXp} (+{xpBonus}) +// +// / +// {maxXp} +// +// ); +// } +// }), +// columnHelper.accessor(({ submissionId }) => ({ submissionId }), { +// header: 'Actions', +// enableColumnFilter: false, +// cell: info => { +// const { submissionId } = info.getValue(); +// return ; +// } +// }) +// ]; + +export enum SortStates { + ASC = 'sort-asc', + DESC = 'sort-desc', + NONE = 'sort' +} + +export const getNextSortState = (current: SortStates) => { + switch (current) { + case SortStates.NONE: + return SortStates.ASC; + case SortStates.ASC: + return SortStates.DESC; + case SortStates.DESC: + return SortStates.NONE; + default: + return SortStates.NONE; + } +} type GradingSubmissionTableProps = { totalRows: number; @@ -129,7 +150,7 @@ const GradingSubmissionTable: React.FC = ({ // End of Original Code - interface IRow { + interface IGradingTableRow { assessmentName: string; assessmentType: string; studentName: string; @@ -142,7 +163,7 @@ const GradingSubmissionTable: React.FC = ({ courseID: number; } - interface ITableProperties { + interface IGradingTableProperties { defaultColDefs: ColDef; pagination: boolean; pageSize: number; @@ -156,6 +177,18 @@ const GradingSubmissionTable: React.FC = ({ tableMargins: string; } + interface SortStateProperties { + assessmentName: SortStates; + assessmentType: SortStates; + studentName: SortStates; + studentUsername: SortStates; + groupName: SortStates; + submissionStatus: SortStates; + gradingStatus: SortStates; + xp: SortStates; + actionsIndex: SortStates; + } + enum ColumnFields { assessmentName = "assessmentName", assessmentType = "assessmentType", @@ -168,10 +201,32 @@ const GradingSubmissionTable: React.FC = ({ actionsIndex = "actionsIndex", } - const defaultColumnDefs: ColDef = { - filter: false, - resizable: false, - sortable: true + enum ColumnName { + assessmentName = "Name", + assessmentType = "Type", + studentName = "Student", + studentUsername = "Username", + groupName = "Group", + submissionStatus = "Progress", + gradingStatus = "Grading", + xp = "Raw XP (+Bonus)", + actionsIndex = "Actions", + } + + const freshSortState: SortStateProperties = { + assessmentName: SortStates.NONE, + assessmentType: SortStates.NONE, + studentName: SortStates.NONE, + studentUsername: SortStates.NONE, + groupName: SortStates.NONE, + submissionStatus: SortStates.NONE, + gradingStatus: SortStates.NONE, + xp: SortStates.NONE, + actionsIndex: SortStates.NONE, + }; + + const customComponents = { + agColumnHeader: GradingColumnCustomHeaders, }; const disabledEditModeCols: string[] = [ @@ -184,9 +239,34 @@ const GradingSubmissionTable: React.FC = ({ ColumnFields.actionsIndex, ] + const newSortState = (affectedID: ColumnFields, sortDirection: SortStates) => { + setColumnSortStates((prev) => { + const newState: SortStateProperties = {...prev}; + newState[affectedID] = sortDirection; + return newState; + }); + setColumnSortOrder((prev) => { + const newOrder: ColumnFields[] = prev.filter((item) => String(item) !== String(affectedID)); + if (sortDirection !== SortStates.NONE) { + newOrder.push(affectedID); + } + return newOrder; + }) + } + + const defaultColumnDefs: ColDef = { + filter: false, + resizable: false, + sortable: true, + headerComponentParams: { + hideColumn: (id: string) => handleColumnFilterAdd(id), + newSortState: newSortState, + }, + }; + const ROW_HEIGHT: number = 60; // in px, declared here to calculate table height - const tableProperties: ITableProperties = { + const tableProperties: IGradingTableProperties = { defaultColDefs: defaultColumnDefs, pagination: true, pageSize: pageSize, @@ -196,27 +276,37 @@ const GradingSubmissionTable: React.FC = ({ overlayNoRowsTemplate: "Hmm... No submissions found, did you filter them all out?", overlayLoadingTemplate: '
', suppressRowClickSelection: true, - tableHeight: String(ROW_HEIGHT * Math.min(pageSize, Math.max(2, submissions.length)) + 50) + "px", + tableHeight: String(ROW_HEIGHT * Math.min(pageSize, Math.max(2, submissions.length)) + 48) + "px", tableMargins: "1rem 0 0 0", }; - const [rowData, setRowData] = useState(); - const [colDefs, setColDefs] = useState[]>(); + const [rowData, setRowData] = useState(); + const [colDefs, setColDefs] = useState[]>(); const [filterMode, setFilterMode] = useState(false); + const [columnSortStates, setColumnSortStates] = useState(freshSortState); + const [columnSortOrder, setColumnSortOrder] = useState([]); - const gridRef = useRef>(null); + const gridRef = useRef>(null); - const generateCols = () => { + const generateCols = useCallback(() => { - const cols: ColDef[] = []; + const cols: ColDef[] = []; + + const generalColProperties: ColDef = { + suppressMovable: true, + cellClass: "grading-def-cell grading-def-cell-pointer", + headerClass: "grading-default-headers", + flex: 1, + } cols.push({ - headerName: "Name", + ...generalColProperties, + headerName: ColumnName.assessmentName, field: ColumnFields.assessmentName, flex: 3, - cellClass: "grading-def-cell grading-def-cell-pointer grading-cell-align-left", - headerClass: "grading-default-headers grading-left-align", - cellRendererSelector: (params: ICellRendererParams) => { + cellClass: generalColProperties.cellClass + " grading-cell-align-left", + headerClass: generalColProperties.headerClass + " grading-left-align", + cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: FilterableNew, @@ -230,12 +320,10 @@ const GradingSubmissionTable: React.FC = ({ }); cols.push({ - headerName: "Type", + ...generalColProperties, + headerName: ColumnName.assessmentType, field: ColumnFields.assessmentType, - flex: 1, - cellClass: "grading-def-cell grading-def-cell-pointer", - headerClass: "grading-default-headers", - cellRendererSelector: (params: ICellRendererParams) => { + cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: FilterableNew, @@ -250,12 +338,13 @@ const GradingSubmissionTable: React.FC = ({ }); cols.push({ - headerName: "Student", + ...generalColProperties, + headerName: ColumnName.studentName, field: ColumnFields.studentName, flex: 1.5, - cellClass: "grading-def-cell grading-def-cell-pointer grading-cell-align-left", - headerClass: "grading-default-headers grading-left-align", - cellRendererSelector: (params: ICellRendererParams) => { + cellClass: generalColProperties.cellClass + " grading-cell-align-left", + headerClass: generalColProperties.headerClass + " grading-left-align", + cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: FilterableNew, @@ -269,12 +358,10 @@ const GradingSubmissionTable: React.FC = ({ }); cols.push({ - headerName: "Username", + ...generalColProperties, + headerName: ColumnName.studentUsername, field: ColumnFields.studentUsername, - flex: 1, - cellClass: "grading-def-cell grading-def-cell-pointer", - headerClass: "grading-default-headers", - cellRendererSelector: (params: ICellRendererParams) => { + cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: FilterableNew, @@ -288,12 +375,11 @@ const GradingSubmissionTable: React.FC = ({ }); cols.push({ - headerName: "Group", + ...generalColProperties, + headerName: ColumnName.groupName, field: ColumnFields.groupName, flex: 0.75, - cellClass: "grading-def-cell grading-def-cell-pointer", - headerClass: "grading-default-headers", - cellRendererSelector: (params: ICellRendererParams) => { + cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: FilterableNew, @@ -307,12 +393,10 @@ const GradingSubmissionTable: React.FC = ({ }); cols.push({ - headerName: "Progress", + ...generalColProperties, + headerName: ColumnName.submissionStatus, field: ColumnFields.submissionStatus, - flex: 1, - cellClass: "grading-def-cell grading-def-cell-pointer", - headerClass: "grading-default-headers", - cellRendererSelector: (params: ICellRendererParams) => { + cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: FilterableNew, @@ -327,12 +411,11 @@ const GradingSubmissionTable: React.FC = ({ }); cols.push({ - headerName: "Grading", + ...generalColProperties, + headerName: ColumnName.gradingStatus, field: ColumnFields.gradingStatus, - flex: 1, - cellClass: "grading-def-cell" + (!filterMode ? " grading-def-cell-pointer" : ""), - headerClass: "grading-default-headers", - cellRendererSelector: (params: ICellRendererParams) => { + cellClass: generalColProperties.cellClass + (!filterMode ? " grading-def-cell-pointer" : ""), + cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: GradingStatusBadge, @@ -345,20 +428,17 @@ const GradingSubmissionTable: React.FC = ({ }); cols.push({ - headerName: "Raw XP (+Bonus)", + ...generalColProperties, + headerName: ColumnName.xp, field: ColumnFields.xp, - flex: 1, - cellClass: "grading-def-cell" + (!filterMode ? " grading-def-cell-pointer" : " grading-def-cell-selectable"), - headerClass: "grading-default-headers", + cellClass: generalColProperties.cellClass + (!filterMode ? " grading-def-cell-pointer" : " grading-def-cell-selectable"), }); cols.push({ - headerName: "Actions", + ...generalColProperties, + headerName: ColumnName.actionsIndex, field: ColumnFields.actionsIndex, - flex: 1, - cellClass: "grading-def-cell", - headerClass: "grading-default-headers", - cellRendererSelector: (params: ICellRendererParams) => { + cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { component: GradingActions, @@ -372,19 +452,19 @@ const GradingSubmissionTable: React.FC = ({ }); return cols; - } + }, [ColumnFields, ColumnName, filterMode]); - const showLoading = useCallback(() => { - gridRef.current!.api.showLoadingOverlay(); - }, []) + // const showLoading = useCallback(() => { + // gridRef.current!.api.showLoadingOverlay(); + // }, []) - const hideLoading = useCallback(() => { - gridRef.current!.api.hideOverlay(); - }, []) + // const hideLoading = useCallback(() => { + // gridRef.current!.api.hideOverlay(); + // }, []) - const showNoRows = useCallback(() => { - gridRef.current!.api.showNoRowsOverlay(); - }, []) + // const showNoRows = useCallback(() => { + // gridRef.current!.api.showNoRowsOverlay(); + // }, []) const cellClickedEvent = (event: CellClickedEvent) => { @@ -411,7 +491,7 @@ const GradingSubmissionTable: React.FC = ({ ]); const [hiddenColumns, setHiddenColumns] = useState( - columnVisibility ? columnVisibility : { columns: [] } + columnVisibility ? columnVisibility : [] ); const [page, setPage] = useState(0); @@ -450,24 +530,24 @@ const GradingSubmissionTable: React.FC = ({ }); }); return params; - }, [columnFilters, searchValue]); - - const columns = useMemo(() => makeColumns(resetPage), [resetPage]); - const table = useReactTable({ - data: submissions, - columns, - state: { - columnFilters, - pagination: { - pageIndex: 0, - pageSize: pageSize - } - }, - onColumnFiltersChange: setColumnFilters, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel() - }); + }, [columnFilters, searchValue, ColumnFields.assessmentName]); + + // const columns = useMemo(() => makeColumns(resetPage), [resetPage]); + // const table = useReactTable({ + // data: submissions, + // columns, + // state: { + // columnFilters, + // pagination: { + // pageIndex: 0, + // pageSize: pageSize + // } + // }, + // onColumnFiltersChange: setColumnFilters, + // getCoreRowModel: getCoreRowModel(), + // getFilteredRowModel: getFilteredRowModel(), + // getPaginationRowModel: getPaginationRowModel() + // }); const handleFilterAdd = ({ id, value }: ColumnFilter) => { dispatch(increaseRequestCounter()); @@ -495,19 +575,14 @@ const GradingSubmissionTable: React.FC = ({ }; const handleColumnFilterRemove = (toRemove: string) => { - setHiddenColumns((prev: GradingColumnVisibility) => { - return { - columns: prev.columns.filter(column => column !== toRemove) - }; - }); + if (gridRef.current?.api) { + setHiddenColumns((prev: GradingColumnVisibility) => prev.filter(column => column !== toRemove)); + gridRef.current.api.setColumnsVisible([toRemove], true); + } }; const handleColumnFilterAdd = (toAdd: string) => { - setHiddenColumns((prev: GradingColumnVisibility) => { - return { - columns: [...prev.columns, toAdd] - }; - }); + setHiddenColumns((prev: GradingColumnVisibility) => [...prev, toAdd]); }; useEffect(() => { @@ -516,6 +591,9 @@ const GradingSubmissionTable: React.FC = ({ useEffect(() => { dispatch(updateGradingColumnVisibility(hiddenColumns)); + if (gridRef.current?.api) { + gridRef.current.api.setColumnsVisible(hiddenColumns, false); + } }, [hiddenColumns, dispatch]); useEffect(() => { @@ -526,68 +604,80 @@ const GradingSubmissionTable: React.FC = ({ updateEntries(page, backendFilterParams); }, [updateEntries, page, backendFilterParams]); + useEffect(() => { + // TODO BACKEND SORT + console.log(columnSortStates); + console.log(columnSortOrder); + }, [columnSortStates, columnSortOrder]); + // End of Original Code useEffect(() => { - if (gridRef.current?.api) { - if (requestCounter <= 0) { - let sameData: boolean = submissions.length === rowData?.length; - const newData: IRow[] = submissions.map((submission, index): IRow => { - if (sameData && submission.submissionId !== rowData?.[index]?.actionsIndex) { - sameData = false; - } - return { - assessmentName: submission.assessmentName, - assessmentType: submission.assessmentType, - studentName: submission.studentName, - studentUsername: submission.studentUsername, - groupName: submission.groupName, - submissionStatus: submission.submissionStatus, - gradingStatus: submission.gradingStatus, - xp: submission.currentXp + " (+" + submission.xpBonus + ") / " + submission.maxXp, - actionsIndex: submission.submissionId, - courseID: courseId!, - }; - }); + const newData: IGradingTableRow[] = []; + + const sameData: boolean = submissions.reduce( + (accumulator, currentSubmission, index) => { + newData.push({ + assessmentName: currentSubmission.assessmentName, + assessmentType: currentSubmission.assessmentType, + studentName: currentSubmission.studentName, + studentUsername: currentSubmission.studentUsername, + groupName: currentSubmission.groupName, + submissionStatus: currentSubmission.submissionStatus, + gradingStatus: currentSubmission.gradingStatus, + xp: currentSubmission.currentXp + " (+" + currentSubmission.xpBonus + ") / " + currentSubmission.maxXp, + actionsIndex: currentSubmission.submissionId, + courseID: courseId!, + }) + return accumulator && (currentSubmission.submissionId === rowData?.[index]?.actionsIndex); + }, + submissions.length === rowData?.length + ); if (!sameData) { setRowData(newData); } - hideLoading(); - if (rowData?.length === 0) { - showNoRows(); - } + gridRef.current!.api.hideOverlay(); } else { - showLoading(); + gridRef.current!.api.showLoadingOverlay(); } } + // We ignore the dependency on rowData purposely as we setRowData above. If not, it may cause an infinite loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [requestCounter, submissions, courseId]); - }, [requestCounter, submissions, gridRef.current]); + useEffect(() => { + if (rowData?.length === 0 && requestCounter <= 0 && gridRef.current?.api) { + gridRef.current!.api.showNoRowsOverlay(); + } + }, [requestCounter, rowData]); useEffect(() => { setColDefs(generateCols()); - }, [resetPage, filterMode]); + }, [resetPage, filterMode, generateCols]); // Start of Original Code return ( <> - {hiddenColumns.columns.length > 0 ? ( + {hiddenColumns.length > 0 ? ( Columns Hidden: { - const headerTexts = columns.filter( - col => col['accessorKey'] === id || col['header'] === id - ); - return headerTexts[0]['header'] ? headerTexts[0]['header'].toString() : ''; + filters={hiddenColumns} + filtersName={hiddenColumns.map(id => { + for (const item in ColumnName) { + if (ColumnFields[item] === id) { + return ColumnName[item]; + } + } + return ''; })} onFilterRemove={handleColumnFilterRemove} /> @@ -606,26 +696,34 @@ const GradingSubmissionTable: React.FC = ({ ? 'Filters: ' : (filterMode === true ? 'No filters applied. Click on any cell to filter by its value.' + - (hiddenColumns.columns.length === 0 + (hiddenColumns.length === 0 ? ' Click on any column header to hide it.' : '') - : Disable Edit Mode to enable click to filter)}{' '} + : Disable Grading Mode to enable click to filter)}{' '}
- } placeholder="Search by assessment name" value={searchQuery} onChange={handleSearchQueryUpdate} - /> + /> */} + {/* End of Original Code */} @@ -648,17 +746,19 @@ const GradingSubmissionTable: React.FC = ({ overlayLoadingTemplate={tableProperties.overlayLoadingTemplate} suppressRowClickSelection={tableProperties.suppressRowClickSelection} onCellClicked={cellClickedEvent} + components={customComponents} + suppressMenuHide={true} /> {/* Start of Original Code */} -
+ {/*
{table.getHeaderGroups().map(headerGroup => ( {headerGroup.headers.map(header => - hiddenColumns.columns.reduce( + hiddenColumns.reduce( (accumulator, currentValue) => accumulator || header.id.includes(currentValue), false ) ? ( @@ -688,7 +788,7 @@ const GradingSubmissionTable: React.FC = ({ {row .getVisibleCells() .map(cell => - hiddenColumns.columns.reduce( + hiddenColumns.reduce( (accumulator, currentValue) => accumulator || cell.id.includes(currentValue), false ) ? ( @@ -701,55 +801,55 @@ const GradingSubmissionTable: React.FC = ({ )} ))} - -
-
- -
-
+ */} + {/*
*/} + {/*
*/} + +
*/} + {/* */} ); }; -type FilterableProps = { - column: Column; - value: string; - children?: React.ReactNode; - onClick?: () => void; -}; +// type FilterableProps = { +// column: Column; +// value: string; +// children?: React.ReactNode; +// onClick?: () => void; +// }; type FilterablePropsNew = { setColumnFilters: React.Dispatch>; @@ -760,18 +860,18 @@ type FilterablePropsNew = { filterMode: boolean; }; -const Filterable: React.FC = ({ column, value, children, onClick }) => { - const handleFilterChange = () => { - column.setFilterValue(value); - onClick?.(); - }; - - return ( - - ); -}; +// const Filterable: React.FC = ({ column, value, children, onClick }) => { +// const handleFilterChange = () => { +// column.setFilterValue(value); +// onClick?.(); +// }; + +// return ( +// +// ); +// }; const FilterableNew: React.FC = ({ value, children, filterMode }) => { return ( diff --git a/src/styles/_academy.scss b/src/styles/_academy.scss index 2b833cc045..0bb1ffd511 100644 --- a/src/styles/_academy.scss +++ b/src/styles/_academy.scss @@ -571,24 +571,59 @@ border-radius: 9999px; } -.grading-overview-footer-sibling { - // Footer component - ~ div ~ div { - pointer-events: none; - - // Footer's children - > * > * { - pointer-events: all; - } +// .grading-overview-footer-sibling { +// // Footer component +// ~ div ~ div { +// pointer-events: none; + +// // Footer's children +// > * > * { +// pointer-events: all; +// } +// } +// } + +.grading-table-col-icons { + pointer-events: none; + opacity: 0; + padding: 6px; + height: fit-content; + line-height: initial!important; + border-radius: 5px; + margin-right: 2.5px; + position: absolute; + right: 0; + + &:hover { + background-color: #00000022; } } .grading-default-headers { + justify-content: space-between; + width: 100%; + transition: 0.1s ease; + cursor: pointer; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + span.ag-header-cell-text { + margin: 0 auto; font-size: 0.8rem; color: #6b7280; font-weight: 600; } + + &:hover { + --ag-header-cell-hover-background-color: #e5e7eb; + } + + &:hover > .grading-table-col-icons { + pointer-events: all; + opacity: 1; + position: relative; + transition: 0.1s ease; + } } .grading-table-rows { @@ -661,4 +696,28 @@ &.grading-cell-align-left { text-align: left!important; } +} + +.grading-table-footer { + border-top: 1px solid rgba(0, 0, 0, 0.075)!important; + padding-top: 15px; + margin-bottom: 15px; +} + +.grading-search-input { + max-width: 24rem; + width: 100%; + margin-left: 0.75rem; + + > input { + height: 40px; + border-radius: 6px; + box-shadow: none; + border: 1px solid rgba(0, 0, 0, 0.3); + font-size: 0.875rem!important; + + &::placeholder { + color: #b3b3b3; + } + } } \ No newline at end of file From 48684a5d2114e47b0cc89251e7ea60925eaa3af3 Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Wed, 27 Mar 2024 16:55:47 +0800 Subject: [PATCH 09/69] more component migrations, refactoring and preparation for backend sort --- src/commons/application/ApplicationTypes.ts | 7 +- .../application/actions/SessionActions.ts | 21 +- src/commons/sagas/BackendSaga.ts | 9 +- src/commons/workspace/WorkspaceActions.ts | 8 +- src/commons/workspace/WorkspaceReducer.ts | 9 + src/commons/workspace/WorkspaceTypes.ts | 5 +- src/features/grading/GradingTypes.ts | 95 ++++++++ src/pages/academy/grading/Grading.tsx | 15 +- .../grading/subcomponents/GradingBadges.tsx | 2 +- .../GradingColumnCustomHeaders.tsx | 23 +- .../subcomponents/GradingSubmissionsTable.tsx | 221 +++++++----------- src/styles/_academy.scss | 2 - 12 files changed, 258 insertions(+), 159 deletions(-) diff --git a/src/commons/application/ApplicationTypes.ts b/src/commons/application/ApplicationTypes.ts index e5ccf8b368..7812b0a5d5 100644 --- a/src/commons/application/ApplicationTypes.ts +++ b/src/commons/application/ApplicationTypes.ts @@ -7,6 +7,7 @@ import { GradingQuery } from '../../features/grading/GradingTypes'; import { PlaygroundState } from '../../features/playground/PlaygroundTypes'; import { PlaybackStatus, RecordingStatus } from '../../features/sourceRecorder/SourceRecorderTypes'; import { StoriesEnvState, StoriesState } from '../../features/stories/StoriesTypes'; +import { freshSortState } from '../../pages/academy/grading/subcomponents/GradingSubmissionsTable'; import { WORKSPACE_BASE_PATHS } from '../../pages/fileSystem/createInBrowserFileSystem'; import { Assessment } from '../assessment/AssessmentTypes'; import { FileSystemState } from '../fileSystem/FileSystemTypes'; @@ -414,7 +415,11 @@ export const defaultWorkspaceManager: WorkspaceManagerState = { currentSubmission: undefined, currentQuestion: undefined, hasUnsavedChanges: false, - requestCounter: 0 + requestCounter: 0, + allColsSortStates: { + currentState: freshSortState, + sortOrder: [] + } }, playground: { ...createDefaultWorkspace('playground'), diff --git a/src/commons/application/actions/SessionActions.ts b/src/commons/application/actions/SessionActions.ts index 4fd6d51b45..dea3ed4dd5 100644 --- a/src/commons/application/actions/SessionActions.ts +++ b/src/commons/application/actions/SessionActions.ts @@ -2,9 +2,14 @@ import { paginationToBackendParams, ungradedToBackendParams } from 'src/features/grading/GradingUtils'; +import { freshSortState } from 'src/pages/academy/grading/subcomponents/GradingSubmissionsTable'; import { action } from 'typesafe-actions'; // EDITED -import { GradingOverviews, GradingQuery } from '../../../features/grading/GradingTypes'; +import { + AllColsSortStates, + GradingOverviews, + GradingQuery +} from '../../../features/grading/GradingTypes'; import { Assessment, AssessmentConfiguration, @@ -113,13 +118,23 @@ export const fetchGrading = (submissionId: number) => action(FETCH_GRADING, subm * many entries, starting from what offset, to get * @param filterParams - param that contains columnFilters converted into JSON for * processing into query parameters + * @param allColsSortStates - param that contains the sort states of all columns and + * the order it should be sorted in */ export const fetchGradingOverviews = ( filterToGroup = true, gradedFilter = ungradedToBackendParams(false), pageParams = paginationToBackendParams(0, 10), - filterParams = {} -) => action(FETCH_GRADING_OVERVIEWS, { filterToGroup, gradedFilter, pageParams, filterParams }); + filterParams = {}, + allColsSortStates: AllColsSortStates = { currentState: freshSortState, sortOrder: [] } +) => + action(FETCH_GRADING_OVERVIEWS, { + filterToGroup, + gradedFilter, + pageParams, + filterParams, + allColsSortStates + }); export const login = (providerId: string) => action(LOGIN, providerId); diff --git a/src/commons/sagas/BackendSaga.ts b/src/commons/sagas/BackendSaga.ts index 55bb55990a..f27868828b 100644 --- a/src/commons/sagas/BackendSaga.ts +++ b/src/commons/sagas/BackendSaga.ts @@ -415,7 +415,13 @@ function* BackendSaga(): SagaIterator { function* (action: ReturnType) { const tokens: Tokens = yield selectTokens(); - const { filterToGroup, gradedFilter, pageParams, filterParams } = action.payload; + const { + filterToGroup, + gradedFilter, + pageParams, + filterParams + // allColsSortStates, + } = action.payload; const gradingOverviews: GradingOverviews | null = yield call( getGradingOverviews, @@ -424,6 +430,7 @@ function* BackendSaga(): SagaIterator { gradedFilter, pageParams, filterParams + // allColsSortStates ); if (gradingOverviews) { yield put(actions.updateGradingOverviews(gradingOverviews)); diff --git a/src/commons/workspace/WorkspaceActions.ts b/src/commons/workspace/WorkspaceActions.ts index 38d6790103..029f5fc0a4 100644 --- a/src/commons/workspace/WorkspaceActions.ts +++ b/src/commons/workspace/WorkspaceActions.ts @@ -2,6 +2,7 @@ import { createAction } from '@reduxjs/toolkit'; import { Context } from 'js-slang'; import { Chapter, Variant } from 'js-slang/dist/types'; +import { AllColsSortStates, GradingColumnVisibility } from '../../features/grading/GradingTypes'; import { SET_IS_EDITOR_READONLY } from '../../features/sourceRecorder/sourcecast/SourcecastTypes'; import { SALanguage } from '../application/ApplicationTypes'; import { ExternalLibraryName } from '../application/types/ExternalTypes'; @@ -35,7 +36,6 @@ import { EVAL_EDITOR_AND_TESTCASES, EVAL_REPL, EVAL_TESTCASE, - GradingColumnVisibility, INCREMENT_REQUEST_COUNTER, MOVE_CURSOR, NAV_DECLARATION, @@ -60,6 +60,7 @@ import { TOGGLE_USING_SUBST, UPDATE_ACTIVE_EDITOR_TAB, UPDATE_ACTIVE_EDITOR_TAB_INDEX, + UPDATE_ALL_COLS_SORT_STATES, UPDATE_BREAKPOINTSTEPS, UPDATE_CURRENT_ASSESSMENT_ID, UPDATE_CURRENT_SUBMISSION_ID, @@ -405,6 +406,11 @@ export const decreaseRequestCounter = createAction(DECREMENT_REQUEST_COUNTER, () payload: {} })); +export const updateAllColsSortStates = createAction( + UPDATE_ALL_COLS_SORT_STATES, + (sortStates: AllColsSortStates) => ({ payload: { sortStates } }) +); + export const updateSubmissionsTableFilters = createAction( UPDATE_SUBMISSIONS_TABLE_FILTERS, (filters: SubmissionsTableFilters) => ({ payload: { filters } }) diff --git a/src/commons/workspace/WorkspaceReducer.ts b/src/commons/workspace/WorkspaceReducer.ts index 739d7be569..657649a78e 100644 --- a/src/commons/workspace/WorkspaceReducer.ts +++ b/src/commons/workspace/WorkspaceReducer.ts @@ -72,6 +72,7 @@ import { TOGGLE_USING_SUBST, UPDATE_ACTIVE_EDITOR_TAB, UPDATE_ACTIVE_EDITOR_TAB_INDEX, + UPDATE_ALL_COLS_SORT_STATES, UPDATE_BREAKPOINTSTEPS, UPDATE_CURRENT_ASSESSMENT_ID, UPDATE_CURRENT_SUBMISSION_ID, @@ -626,6 +627,14 @@ const oldWorkspaceReducer: Reducer = ( return state; } } + case UPDATE_ALL_COLS_SORT_STATES: + return { + ...state, + grading: { + ...state.grading, + allColsSortStates: action.payload.sortStates + } + }; case UPDATE_SUBMISSIONS_TABLE_FILTERS: return { ...state, diff --git a/src/commons/workspace/WorkspaceTypes.ts b/src/commons/workspace/WorkspaceTypes.ts index 1aebda0f90..d5fe493db7 100644 --- a/src/commons/workspace/WorkspaceTypes.ts +++ b/src/commons/workspace/WorkspaceTypes.ts @@ -1,6 +1,7 @@ import { Context } from 'js-slang'; import { GitHubAssessmentWorkspaceState } from '../../features/githubAssessment/GitHubAssessmentTypes'; +import { AllColsSortStates, GradingColumnVisibility } from '../../features/grading/GradingTypes'; import { SourcecastWorkspaceState } from '../../features/sourceRecorder/sourcecast/SourcecastTypes'; import { SourcereelWorkspaceState } from '../../features/sourceRecorder/sourcereel/SourcereelTypes'; import { InterpreterOutput } from '../application/ApplicationTypes'; @@ -50,6 +51,7 @@ export const TOGGLE_FOLDER_MODE = 'TOGGLE_FOLDER_MODE'; export const SET_FOLDER_MODE = 'SET_FOLDER_MODE'; export const UPDATE_ACTIVE_EDITOR_TAB_INDEX = 'UPDATE_ACTIVE_EDITOR_TAB_INDEX'; export const UPDATE_ACTIVE_EDITOR_TAB = 'UPDATE_ACTIVE_EDITOR_TAB'; +export const UPDATE_ALL_COLS_SORT_STATES = 'UPDATE_ALL_COLS_SORT_STATES'; export const UPDATE_EDITOR_VALUE = 'UPDATE_EDITOR_VALUE'; export const UPDATE_EDITOR_BREAKPOINTS = 'UPDATE_EDITOR_BREAKPOINTS'; export const ADD_EDITOR_TAB = 'ADD_EDITOR_TAB'; @@ -85,6 +87,7 @@ type GradingWorkspaceAttr = { readonly currentQuestion?: number; readonly hasUnsavedChanges: boolean; readonly requestCounter: number; + readonly allColsSortStates: AllColsSortStates; }; type GradingWorkspaceState = GradingWorkspaceAttr & WorkspaceState; @@ -171,5 +174,3 @@ export type DebuggerContext = { export type SubmissionsTableFilters = { columnFilters: { id: string; value: unknown }[]; }; - -export type GradingColumnVisibility = string[]; diff --git a/src/features/grading/GradingTypes.ts b/src/features/grading/GradingTypes.ts index aa99ea8bc6..8137dc98b4 100644 --- a/src/features/grading/GradingTypes.ts +++ b/src/features/grading/GradingTypes.ts @@ -1,3 +1,5 @@ +import { ColDef } from 'ag-grid-community'; + import { AssessmentType, AutogradingResult, @@ -8,6 +10,24 @@ import { } from '../../commons/assessment/AssessmentTypes'; import { Notification } from '../../commons/notificationBadge/NotificationBadgeTypes'; +export enum ColumnFields { + assessmentName = 'assessmentName', + assessmentType = 'assessmentType', + studentName = 'studentName', + studentUsername = 'studentUsername', + groupName = 'groupName', + submissionStatus = 'submissionStatus', + gradingStatus = 'gradingStatus', + xp = 'xp', + actionsIndex = 'actionsIndex' +} + +export enum SortStates { + ASC = 'sort-asc', + DESC = 'sort-desc', + NONE = 'sort' +} + /** * Information on a Grading, for a particular student submission * for a particular assessment. Used for display in the UI. @@ -49,6 +69,20 @@ export type GradingOverviewWithNotifications = { */ export type GradingAnswer = GradingQuestion[]; +export type AllColsSortStates = { + currentState: SortStateProperties; + sortOrder: string[]; +}; + +export type ColumnFiltersState = ColumnFilter[]; + +export type ColumnFilter = { + id: string; + value: unknown; +}; + +export type GradingColumnVisibility = string[]; + export type GradingAssessment = { coverPicture: string; id: number; @@ -65,6 +99,67 @@ export type GradingQuery = { assessment: GradingAssessment; }; +export type GradingSubmissionTableProps = { + totalRows: number; + pageSize: number; + submissions: GradingOverview[]; + updateEntries: (page: number, filterParams: Object) => void; +}; + +export enum ColumnName { + assessmentName = 'Name', + assessmentType = 'Type', + studentName = 'Student', + studentUsername = 'Username', + groupName = 'Group', + submissionStatus = 'Progress', + gradingStatus = 'Grading', + xp = 'Raw XP (+Bonus)', + actionsIndex = 'Actions' +} + +export type SortStateProperties = { + assessmentName: SortStates; + assessmentType: SortStates; + studentName: SortStates; + studentUsername: SortStates; + groupName: SortStates; + submissionStatus: SortStates; + gradingStatus: SortStates; + xp: SortStates; + actionsIndex: SortStates; +}; + +export type IGradingTableRow = { + assessmentName: string; + assessmentType: string; + studentName: string; + studentUsername: string; + groupName: string; + submissionStatus: string; + gradingStatus: string; + xp: string; + actionsIndex: number; // actions needs a column, but only submission ID data, so it stores submission ID + courseID: number; +}; + +export type IGradingTableProperties = { + customComponents: any; + defaultColDefs: ColDef; + headerHeight: number; + overlayLoadingTemplate: string; + overlayNoRowsTemplate: string; + pageSize: number; + pagination: boolean; + rowClass: string; + rowHeight: number; + suppressMenuHide: boolean; + suppressPaginationPanel: boolean; + suppressRowClickSelection: boolean; + tableHeight: string; + tableMargins: string; +}; + /** * Encapsulates information regarding grading a * particular question in a submission. diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index 85196cf724..ae5afe3a73 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -9,8 +9,8 @@ import { Navigate, useParams } from 'react-router'; import { fetchGradingOverviews } from 'src/commons/application/actions/SessionActions'; import { Role } from 'src/commons/application/ApplicationTypes'; import SimpleDropdown from 'src/commons/SimpleDropdown'; -import { useSession } from 'src/commons/utils/Hooks'; -// import { useSession, useTypedSelector } from 'src/commons/utils/Hooks'; +// import { useSession } from 'src/commons/utils/Hooks'; +import { useSession, useTypedSelector } from 'src/commons/utils/Hooks'; import { decreaseRequestCounter, increaseRequestCounter } from 'src/commons/workspace/WorkspaceActions'; import { numberRegExp } from 'src/features/academy/AcademyTypes'; import { @@ -52,7 +52,7 @@ const Grading: React.FC = () => { const [showAllSubmissions, setShowAllSubmissions] = useState(false); const dispatch = useDispatch(); - // const requestCounter = useTypedSelector(state => state.workspaces.grading.requestCounter); + const allColsSortStates = useTypedSelector(state => state.workspaces.grading.allColsSortStates); const updateGradingOverviewsCallback = useCallback( (page: number, filterParams: Object) => { @@ -62,16 +62,17 @@ const Grading: React.FC = () => { showAllGroups, ungradedToBackendParams(showAllSubmissions), paginationToBackendParams(page, pageSize), - filterParams + filterParams, + allColsSortStates, ) ); }, - [dispatch, showAllGroups, showAllSubmissions, pageSize] + [dispatch, showAllGroups, showAllSubmissions, pageSize, allColsSortStates] ); // useEffect(() => { - // console.log(requestCounter); - // }, [requestCounter]); + // console.log(sortStates); + // }, [sortStates]); useEffect(() => { dispatch(decreaseRequestCounter()); diff --git a/src/pages/academy/grading/subcomponents/GradingBadges.tsx b/src/pages/academy/grading/subcomponents/GradingBadges.tsx index a1d6ade271..7e72f9967f 100644 --- a/src/pages/academy/grading/subcomponents/GradingBadges.tsx +++ b/src/pages/academy/grading/subcomponents/GradingBadges.tsx @@ -1,8 +1,8 @@ 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 { ColumnFilter } from 'src/features/grading/GradingTypes'; const BADGE_COLORS = { // assessment types diff --git a/src/pages/academy/grading/subcomponents/GradingColumnCustomHeaders.tsx b/src/pages/academy/grading/subcomponents/GradingColumnCustomHeaders.tsx index fe6395d593..0c25fe6043 100644 --- a/src/pages/academy/grading/subcomponents/GradingColumnCustomHeaders.tsx +++ b/src/pages/academy/grading/subcomponents/GradingColumnCustomHeaders.tsx @@ -2,11 +2,14 @@ import { Icon } from '@blueprintjs/core'; import { CustomHeaderProps } from 'ag-grid-react'; import { useState } from 'react'; -import { getNextSortState, SortStates } from './GradingSubmissionsTable'; +import { SortStates } from 'src/features/grading/GradingTypes'; + +import { getNextSortState } from './GradingSubmissionsTable'; export interface GradingColumnCustomHeadersProps extends CustomHeaderProps { hideColumn: (id: string) => void; - newSortState: (id: string, sortState: SortStates) => void; + updateSortState: (id: string, sortState: SortStates) => void; + disabledSortCols: string[]; } const GradingColumnCustomHeaders: React.FC = (props: GradingColumnCustomHeadersProps) => { @@ -16,18 +19,26 @@ const GradingColumnCustomHeaders: React.FC = (p const nextSortState = () => { setSortState((prev) => getNextSortState(prev)); - props.newSortState(props.column.getColId(), getNextSortState(sortState)); + props.updateSortState(props.column.getColId(), getNextSortState(sortState)); } return (
+ {props.displayName} -
nextSortState()}> - -
+ + { + !props.disabledSortCols.includes(props.column.getColId()) ? +
nextSortState()}> + +
+ : <> + } +
props.hideColumn(props.column.getColId())}>
+
); diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 7bcecbdd4d..26dd5b400f 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -5,17 +5,17 @@ import "ag-grid-community/styles/ag-theme-quartz.css" import { Button, H6, Icon as BpIcon, InputGroup } from '@blueprintjs/core'; // 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 { +// Column, +// ColumnFilter, +// ColumnFiltersState, +// createColumnHelper, +// flexRender, +// getCoreRowModel, +// getFilteredRowModel, +// getPaginationRowModel, +// useReactTable +// } from '@tanstack/react-table'; // import { // Footer, // Table, @@ -37,11 +37,21 @@ import GradingText from 'src/commons/grading/GradingText'; import { useTypedSelector } from 'src/commons/utils/Hooks'; import { increaseRequestCounter, + updateAllColsSortStates, updateGradingColumnVisibility, updateSubmissionsTableFilters } from 'src/commons/workspace/WorkspaceActions'; -import { GradingColumnVisibility } from 'src/commons/workspace/WorkspaceTypes'; -import { GradingOverview } from 'src/features/grading/GradingTypes'; +import { + ColumnFields, + ColumnFilter, + ColumnFiltersState, + ColumnName, + GradingColumnVisibility, + GradingSubmissionTableProps, + IGradingTableProperties, + IGradingTableRow, + SortStateProperties, + SortStates} from 'src/features/grading/GradingTypes'; import { convertFilterToBackendParams } from 'src/features/grading/GradingUtils'; import GradingActions from './GradingActions'; @@ -115,12 +125,6 @@ import GradingSubmissionFilters from './GradingSubmissionFilters'; // }) // ]; -export enum SortStates { - ASC = 'sort-asc', - DESC = 'sort-desc', - NONE = 'sort' -} - export const getNextSortState = (current: SortStates) => { switch (current) { case SortStates.NONE: @@ -134,11 +138,16 @@ export const getNextSortState = (current: SortStates) => { } } -type GradingSubmissionTableProps = { - totalRows: number; - pageSize: number; - submissions: GradingOverview[]; - updateEntries: (page: number, filterParams: Object) => void; +export const freshSortState: SortStateProperties = { + assessmentName: SortStates.NONE, + assessmentType: SortStates.NONE, + studentName: SortStates.NONE, + studentUsername: SortStates.NONE, + groupName: SortStates.NONE, + submissionStatus: SortStates.NONE, + gradingStatus: SortStates.NONE, + xp: SortStates.NONE, + actionsIndex: SortStates.NONE, }; const GradingSubmissionTable: React.FC = ({ @@ -150,108 +159,38 @@ const GradingSubmissionTable: React.FC = ({ // End of Original Code - interface IGradingTableRow { - assessmentName: string; - assessmentType: string; - studentName: string; - studentUsername: string; - groupName: string; - submissionStatus: string; - gradingStatus: string; - xp: string; - actionsIndex: number; - courseID: number; - } - - interface IGradingTableProperties { - defaultColDefs: ColDef; - pagination: boolean; - pageSize: number; - suppressPaginationPanel: boolean; - rowClass: string; - rowHeight: number; - overlayNoRowsTemplate: string; - overlayLoadingTemplate: string; - suppressRowClickSelection: boolean; - tableHeight: string; - tableMargins: string; - } - - interface SortStateProperties { - assessmentName: SortStates; - assessmentType: SortStates; - studentName: SortStates; - studentUsername: SortStates; - groupName: SortStates; - submissionStatus: SortStates; - gradingStatus: SortStates; - xp: SortStates; - actionsIndex: SortStates; - } - - enum ColumnFields { - assessmentName = "assessmentName", - assessmentType = "assessmentType", - studentName = "studentName", - studentUsername = "studentUsername", - groupName = "groupName", - submissionStatus = "submissionStatus", - gradingStatus = "gradingStatus", - xp = "xp", - actionsIndex = "actionsIndex", - } - - enum ColumnName { - assessmentName = "Name", - assessmentType = "Type", - studentName = "Student", - studentUsername = "Username", - groupName = "Group", - submissionStatus = "Progress", - gradingStatus = "Grading", - xp = "Raw XP (+Bonus)", - actionsIndex = "Actions", - } - - const freshSortState: SortStateProperties = { - assessmentName: SortStates.NONE, - assessmentType: SortStates.NONE, - studentName: SortStates.NONE, - studentUsername: SortStates.NONE, - groupName: SortStates.NONE, - submissionStatus: SortStates.NONE, - gradingStatus: SortStates.NONE, - xp: SortStates.NONE, - actionsIndex: SortStates.NONE, - }; - - const customComponents = { - agColumnHeader: GradingColumnCustomHeaders, - }; - const disabledEditModeCols: string[] = [ ColumnFields.actionsIndex, - ] + ]; const disabledFilterModeCols: string[] = [ ColumnFields.gradingStatus, ColumnFields.xp, ColumnFields.actionsIndex, - ] + ]; - const newSortState = (affectedID: ColumnFields, sortDirection: SortStates) => { - setColumnSortStates((prev) => { - const newState: SortStateProperties = {...prev}; - newState[affectedID] = sortDirection; - return newState; - }); - setColumnSortOrder((prev) => { - const newOrder: ColumnFields[] = prev.filter((item) => String(item) !== String(affectedID)); - if (sortDirection !== SortStates.NONE) { - newOrder.push(affectedID); - } - return newOrder; - }) + const disabledSortCols: string[] = [ + ColumnFields.actionsIndex, + ]; + + const updateSortState = (affectedID: ColumnFields, sortDirection: SortStates) => { + if (!disabledSortCols.includes(affectedID)) { + + setColumnSortStates((prev) => { + const newState: SortStateProperties = {...prev}; + newState[affectedID] = sortDirection; + return newState; + }); + + setColumnSortOrder((prev) => { + const newOrder: ColumnFields[] = prev.filter((item) => String(item) !== String(affectedID)); + if (sortDirection !== SortStates.NONE) { + newOrder.push(affectedID); + } + return newOrder; + }); + + } } const defaultColumnDefs: ColDef = { @@ -260,23 +199,30 @@ const GradingSubmissionTable: React.FC = ({ sortable: true, headerComponentParams: { hideColumn: (id: string) => handleColumnFilterAdd(id), - newSortState: newSortState, + updateSortState: updateSortState, + disabledSortCols: disabledSortCols, }, }; const ROW_HEIGHT: number = 60; // in px, declared here to calculate table height + const HEADER_HEIGHT: number = 48; // in px, declared here to calculate table height const tableProperties: IGradingTableProperties = { + customComponents: { + agColumnHeader: GradingColumnCustomHeaders, + }, defaultColDefs: defaultColumnDefs, - pagination: true, + headerHeight: HEADER_HEIGHT, + overlayLoadingTemplate: '
', + overlayNoRowsTemplate: "Hmm... No submissions found, did you filter them all out?", pageSize: pageSize, - suppressPaginationPanel: true, + pagination: true, rowClass: "grading-left-align grading-table-rows", rowHeight: ROW_HEIGHT, - overlayNoRowsTemplate: "Hmm... No submissions found, did you filter them all out?", - overlayLoadingTemplate: '
', + suppressMenuHide: true, + suppressPaginationPanel: true, suppressRowClickSelection: true, - tableHeight: String(ROW_HEIGHT * Math.min(pageSize, Math.max(2, submissions.length)) + 48) + "px", + tableHeight: String(ROW_HEIGHT * Math.min(pageSize, Math.max(2, submissions.length)) + HEADER_HEIGHT) + "px", tableMargins: "1rem 0 0 0", }; @@ -296,7 +242,7 @@ const GradingSubmissionTable: React.FC = ({ suppressMovable: true, cellClass: "grading-def-cell grading-def-cell-pointer", headerClass: "grading-default-headers", - flex: 1, + flex: 1, // weight of column width } cols.push({ @@ -608,7 +554,11 @@ const GradingSubmissionTable: React.FC = ({ // TODO BACKEND SORT console.log(columnSortStates); console.log(columnSortOrder); - }, [columnSortStates, columnSortOrder]); + dispatch(updateAllColsSortStates({ + currentState: columnSortStates, + sortOrder: columnSortOrder, + })) + }, [columnSortStates, columnSortOrder, dispatch]); // End of Original Code @@ -619,7 +569,7 @@ const GradingSubmissionTable: React.FC = ({ const newData: IGradingTableRow[] = []; const sameData: boolean = submissions.reduce( - (accumulator, currentSubmission, index) => { + (sameData, currentSubmission, index) => { newData.push({ assessmentName: currentSubmission.assessmentName, assessmentType: currentSubmission.assessmentType, @@ -632,7 +582,7 @@ const GradingSubmissionTable: React.FC = ({ actionsIndex: currentSubmission.submissionId, courseID: courseId!, }) - return accumulator && (currentSubmission.submissionId === rowData?.[index]?.actionsIndex); + return sameData && (currentSubmission.submissionId === rowData?.[index]?.actionsIndex); }, submissions.length === rowData?.length ); @@ -733,21 +683,22 @@ const GradingSubmissionTable: React.FC = ({ margin: tableProperties.tableMargins }}> diff --git a/src/styles/_academy.scss b/src/styles/_academy.scss index 0bb1ffd511..c021c145e5 100644 --- a/src/styles/_academy.scss +++ b/src/styles/_academy.scss @@ -668,7 +668,6 @@ font-size: 0.875rem; user-select: text; border: 0px!important; - border-bottom: 1px solid rgba(0, 0, 0, 0.075)!important; &:hover:not(:has(> div)):has(button.grading-overview-filterable-btns) { button { @@ -699,7 +698,6 @@ } .grading-table-footer { - border-top: 1px solid rgba(0, 0, 0, 0.075)!important; padding-top: 15px; margin-bottom: 15px; } From 1e7eafcd204b557fb8868ef53dc5d994d9fdf0ba Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Sat, 30 Mar 2024 16:09:29 +0800 Subject: [PATCH 10/69] multi -> single sorting, moved over all ts/tremor components to ag/bp, removal of old code --- src/commons/application/ApplicationTypes.ts | 2 +- .../application/actions/SessionActions.ts | 4 +- src/commons/sagas/BackendSaga.ts | 28 +- src/features/grading/GradingTypes.ts | 2 +- src/features/grading/GradingUtils.ts | 2 +- src/pages/academy/grading/Grading.tsx | 41 +- .../grading/subcomponents/GradingActions.tsx | 25 +- .../grading/subcomponents/GradingBadges.tsx | 59 ++- .../GradingColumnCustomHeaders.tsx | 11 +- .../GradingSubmissionFilters.tsx | 8 +- .../subcomponents/GradingSubmissionsTable.tsx | 471 +++++------------- src/styles/_academy.scss | 159 +++++- 12 files changed, 381 insertions(+), 431 deletions(-) diff --git a/src/commons/application/ApplicationTypes.ts b/src/commons/application/ApplicationTypes.ts index 7812b0a5d5..f8c1f68c14 100644 --- a/src/commons/application/ApplicationTypes.ts +++ b/src/commons/application/ApplicationTypes.ts @@ -418,7 +418,7 @@ export const defaultWorkspaceManager: WorkspaceManagerState = { requestCounter: 0, allColsSortStates: { currentState: freshSortState, - sortOrder: [] + sortBy: '' } }, playground: { diff --git a/src/commons/application/actions/SessionActions.ts b/src/commons/application/actions/SessionActions.ts index dea3ed4dd5..724657c767 100644 --- a/src/commons/application/actions/SessionActions.ts +++ b/src/commons/application/actions/SessionActions.ts @@ -119,14 +119,14 @@ export const fetchGrading = (submissionId: number) => action(FETCH_GRADING, subm * @param filterParams - param that contains columnFilters converted into JSON for * processing into query parameters * @param allColsSortStates - param that contains the sort states of all columns and - * the order it should be sorted in + * the col it should be sorted by */ export const fetchGradingOverviews = ( filterToGroup = true, gradedFilter = ungradedToBackendParams(false), pageParams = paginationToBackendParams(0, 10), filterParams = {}, - allColsSortStates: AllColsSortStates = { currentState: freshSortState, sortOrder: [] } + allColsSortStates: AllColsSortStates = { currentState: freshSortState, sortBy: '' } ) => action(FETCH_GRADING_OVERVIEWS, { filterToGroup, diff --git a/src/commons/sagas/BackendSaga.ts b/src/commons/sagas/BackendSaga.ts index f27868828b..35cf4eddaf 100644 --- a/src/commons/sagas/BackendSaga.ts +++ b/src/commons/sagas/BackendSaga.ts @@ -415,13 +415,25 @@ function* BackendSaga(): SagaIterator { function* (action: ReturnType) { const tokens: Tokens = yield selectTokens(); - const { - filterToGroup, - gradedFilter, - pageParams, - filterParams - // allColsSortStates, - } = action.payload; + const { filterToGroup, gradedFilter, pageParams, filterParams, allColsSortStates } = + action.payload; + + const sortedBy = { + sortBy: allColsSortStates.sortBy, + sortDirection: '' + }; + + for (const key in allColsSortStates.currentState) { + if (allColsSortStates.sortBy === key) { + if (allColsSortStates.currentState[key] !== 'sort') { + sortedBy.sortDirection = allColsSortStates.currentState[key]; + } else { + sortedBy.sortBy = ''; + sortedBy.sortDirection = ''; + } + break; + } + } const gradingOverviews: GradingOverviews | null = yield call( getGradingOverviews, @@ -430,7 +442,7 @@ function* BackendSaga(): SagaIterator { gradedFilter, pageParams, filterParams - // allColsSortStates + // sortedBy ); if (gradingOverviews) { yield put(actions.updateGradingOverviews(gradingOverviews)); diff --git a/src/features/grading/GradingTypes.ts b/src/features/grading/GradingTypes.ts index 8137dc98b4..a3dfd5b26f 100644 --- a/src/features/grading/GradingTypes.ts +++ b/src/features/grading/GradingTypes.ts @@ -71,7 +71,7 @@ export type GradingAnswer = GradingQuestion[]; export type AllColsSortStates = { currentState: SortStateProperties; - sortOrder: string[]; + sortBy: string; }; export type ColumnFiltersState = ColumnFilter[]; diff --git a/src/features/grading/GradingUtils.ts b/src/features/grading/GradingUtils.ts index f7871dd425..1e153b1b41 100644 --- a/src/features/grading/GradingUtils.ts +++ b/src/features/grading/GradingUtils.ts @@ -1,5 +1,5 @@ -import { ColumnFilter } from '@tanstack/react-table'; import { GradingStatuses } from 'src/commons/assessment/AssessmentTypes'; +import { ColumnFilter } from 'src/features/grading/GradingTypes'; import { GradingOverview } from './GradingTypes'; diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index ae5afe3a73..733d78349d 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -1,13 +1,12 @@ -import '@tremor/react/dist/esm/tremor.css'; - -import { Icon as BpIcon, NonIdealState, Position, Spinner, SpinnerSize } from '@blueprintjs/core'; +import { Button, NonIdealState, Position, Spinner, SpinnerSize } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; -import { Button, Card, Flex, Text, Title } from '@tremor/react'; import React, { useCallback, useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; import { Navigate, useParams } from 'react-router'; import { fetchGradingOverviews } from 'src/commons/application/actions/SessionActions'; import { Role } from 'src/commons/application/ApplicationTypes'; +import GradingFlex from 'src/commons/grading/GradingFlex'; +import GradingText from 'src/commons/grading/GradingText'; import SimpleDropdown from 'src/commons/SimpleDropdown'; // import { useSession } from 'src/commons/utils/Hooks'; import { useSession, useTypedSelector } from 'src/commons/utils/Hooks'; @@ -116,22 +115,22 @@ const Grading: React.FC = () => { gradingOverviews?.data === undefined ? ( loadingDisplay ) : ( - - - - Submissions - - - - - Viewing +
+ + + Viewing { popoverProps={{ position: Position.BOTTOM }} buttonProps={{ minimal: true, rightIcon: 'caret-down' }} /> - submissions from + submissions from { popoverProps={{ position: Position.BOTTOM }} buttonProps={{ minimal: true, rightIcon: 'caret-down' }} /> - showing + showing { popoverProps={{ position: Position.BOTTOM }} buttonProps={{ minimal: true, rightIcon: 'caret-down' }} /> - entries per page. - + entries per page. + - + ) } fullWidth={true} diff --git a/src/pages/academy/grading/subcomponents/GradingActions.tsx b/src/pages/academy/grading/subcomponents/GradingActions.tsx index 5a504e198b..baacd4e32c 100644 --- a/src/pages/academy/grading/subcomponents/GradingActions.tsx +++ b/src/pages/academy/grading/subcomponents/GradingActions.tsx @@ -1,6 +1,5 @@ -import { Icon as BpIcon } from '@blueprintjs/core'; +import { Button, Icon } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; -import { Icon } from '@tremor/react'; import React from 'react'; import { useDispatch } from 'react-redux'; import { Link } from 'react-router-dom'; @@ -49,22 +48,20 @@ const GradingActions: React.FC = ({ submissionId, style }) }; return ( - + - } variant="light" /> + + + - + - + ); }; diff --git a/src/pages/academy/grading/subcomponents/GradingBadges.tsx b/src/pages/academy/grading/subcomponents/GradingBadges.tsx index 7e72f9967f..3b1fd53288 100644 --- a/src/pages/academy/grading/subcomponents/GradingBadges.tsx +++ b/src/pages/academy/grading/subcomponents/GradingBadges.tsx @@ -1,28 +1,65 @@ import { Icon } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; -import { Badge } from '@tremor/react'; +import { ReactNode } from 'react'; import { GradingStatus } from 'src/commons/assessment/AssessmentTypes'; import { ColumnFilter } from 'src/features/grading/GradingTypes'; +declare const sizeValues: readonly ["xs", "sm", "md", "lg", "xl"]; +declare type Size = typeof sizeValues[number]; + +interface BadgeProps { + text: string; + color?: string; + size?: Size; + icon?: () => ReactNode; +} + +const Badge: React.FC = (props: BadgeProps) => { + return ( +
+ {props.icon ? props.icon() : <>} + {props.text} +
+ ); + +}; + +// First colour is bg, second is text (text is more saturated/darker) +const AVAILABLE_COLORS = { + indigo: ['#818cf8', '#4f46e5'], + emerald: ['#6ee7b7', '#059669'], + sky: ['#7dd3fc', '#0284c7'], + green: ['#4ade80', '#15803d'], + yellow: ['#fde047', '#ca8a04'], + red: ['#f87171', '#b91c1c'], + gray: ['#9ca3af', '#374151'], +}; + const BADGE_COLORS = { // assessment types - missions: 'indigo', - quests: 'emerald', - paths: 'sky', + missions: AVAILABLE_COLORS.indigo, //indigo + quests: AVAILABLE_COLORS.emerald, //emerald + paths: AVAILABLE_COLORS.sky, //sky // submission status - submitted: 'green', - attempting: 'yellow', - attempted: 'red', + submitted: AVAILABLE_COLORS.green, //green + attempting: AVAILABLE_COLORS.yellow, //yellow + attempted: AVAILABLE_COLORS.red, //red // grading status - graded: 'green', - grading: 'yellow', - none: 'red' + graded: AVAILABLE_COLORS.green, //green + grading: AVAILABLE_COLORS.yellow, //yellow + none: AVAILABLE_COLORS.red //red }; export function getBadgeColorFromLabel(label: string) { - return BADGE_COLORS[label.toLowerCase()] || 'gray'; + return BADGE_COLORS[label.toLowerCase()] || AVAILABLE_COLORS.gray; //gray } type AssessmentTypeBadgeProps = { diff --git a/src/pages/academy/grading/subcomponents/GradingColumnCustomHeaders.tsx b/src/pages/academy/grading/subcomponents/GradingColumnCustomHeaders.tsx index 0c25fe6043..1c7ccfa3a8 100644 --- a/src/pages/academy/grading/subcomponents/GradingColumnCustomHeaders.tsx +++ b/src/pages/academy/grading/subcomponents/GradingColumnCustomHeaders.tsx @@ -1,7 +1,7 @@ import { Icon } from '@blueprintjs/core'; import { CustomHeaderProps } from 'ag-grid-react'; -import { useState } from 'react'; - +import { useEffect, useState } from 'react'; +import { useTypedSelector } from 'src/commons/utils/Hooks'; import { SortStates } from 'src/features/grading/GradingTypes'; import { getNextSortState } from './GradingSubmissionsTable'; @@ -16,12 +16,19 @@ const GradingColumnCustomHeaders: React.FC = (p // The values correspond to the available icons in the BlueprintJS library. "sort" means unsorted. const [sortState, setSortState] = useState(SortStates.NONE); + const colsSortState = useTypedSelector(state => state.workspaces.grading.allColsSortStates); const nextSortState = () => { setSortState((prev) => getNextSortState(prev)); props.updateSortState(props.column.getColId(), getNextSortState(sortState)); } + useEffect(() => { + if (colsSortState.sortBy !== props.column.getColId()) { + setSortState(SortStates.NONE); + } + }, [colsSortState, props.column]); + return (
diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx index 8f1091ff06..bda61eef2d 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx @@ -1,5 +1,5 @@ -import { ColumnFilter, ColumnFiltersState } from '@tanstack/react-table'; -import { Flex } from '@tremor/react'; +import GradingFlex from 'src/commons/grading/GradingFlex'; +import { ColumnFilter, ColumnFiltersState } from 'src/features/grading/GradingTypes'; import { FilterBadge } from './GradingBadges'; @@ -13,11 +13,11 @@ const GradingSubmissionFilters: React.FC = ({ onFilterRemove }) => { return ( - + {filters.map(filter => ( ))} - + ); }; diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 26dd5b400f..920d0b755e 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -1,31 +1,8 @@ -import '@tremor/react/dist/esm/tremor.css'; import "ag-grid-community/styles/ag-grid.css"; import "ag-grid-community/styles/ag-theme-quartz.css" -import { Button, H6, Icon as BpIcon, InputGroup } from '@blueprintjs/core'; -// import { Icon as BpIcon } from '@blueprintjs/core'; +import { Button, H6, Icon, InputGroup } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; -// import { -// Column, -// ColumnFilter, -// ColumnFiltersState, -// createColumnHelper, -// flexRender, -// getCoreRowModel, -// getFilteredRowModel, -// getPaginationRowModel, -// useReactTable -// } from '@tanstack/react-table'; -// import { -// Footer, -// Table, -// TableBody, -// TableCell, -// TableHead, -// TableHeaderCell, -// TableRow, -// TextInput -// } from '@tremor/react'; import { CellClickedEvent, ColDef, ICellRendererParams } from 'ag-grid-community'; import { AgGridReact } from 'ag-grid-react'; import { debounce } from 'lodash'; @@ -36,7 +13,6 @@ import GradingFlex from 'src/commons/grading/GradingFlex'; import GradingText from 'src/commons/grading/GradingText'; import { useTypedSelector } from 'src/commons/utils/Hooks'; import { - increaseRequestCounter, updateAllColsSortStates, updateGradingColumnVisibility, updateSubmissionsTableFilters @@ -60,71 +36,6 @@ import GradingColumnCustomHeaders from './GradingColumnCustomHeaders'; import GradingColumnFilters from './GradingColumnFilters'; import GradingSubmissionFilters from './GradingSubmissionFilters'; -// const columnHelper = createColumnHelper(); - -// const makeColumns = (handleClick: () => void) => [ -// columnHelper.accessor('assessmentName', { -// header: 'Name', -// cell: info => -// }), -// columnHelper.accessor('assessmentType', { -// header: 'Type', -// cell: info => ( -// -// -// -// ) -// }), -// columnHelper.accessor('studentName', { -// header: 'Student', -// cell: info => -// }), -// columnHelper.accessor('studentUsername', { -// header: 'Username', -// cell: info => -// }), -// columnHelper.accessor('groupName', { -// header: 'Group', -// cell: info => -// }), -// columnHelper.accessor('submissionStatus', { -// header: 'Progress', -// cell: info => ( -// -// -// -// ) -// }), -// columnHelper.accessor('gradingStatus', { -// header: 'Grading', -// cell: info => -// }), -// columnHelper.accessor(({ currentXp, xpBonus, maxXp }) => ({ currentXp, xpBonus, maxXp }), { -// header: 'Raw XP (+Bonus)', -// enableColumnFilter: false, -// cell: info => { -// const { currentXp, xpBonus, maxXp } = info.getValue(); -// return ( -// -// -// {currentXp} (+{xpBonus}) -// -// / -// {maxXp} -// -// ); -// } -// }), -// columnHelper.accessor(({ submissionId }) => ({ submissionId }), { -// header: 'Actions', -// enableColumnFilter: false, -// cell: info => { -// const { submissionId } = info.getValue(); -// return ; -// } -// }) -// ]; - export const getNextSortState = (current: SortStates) => { switch (current) { case SortStates.NONE: @@ -157,8 +68,6 @@ const GradingSubmissionTable: React.FC = ({ updateEntries, }) => { - // End of Original Code - const disabledEditModeCols: string[] = [ ColumnFields.actionsIndex, ]; @@ -173,33 +82,22 @@ const GradingSubmissionTable: React.FC = ({ ColumnFields.actionsIndex, ]; - const updateSortState = (affectedID: ColumnFields, sortDirection: SortStates) => { - if (!disabledSortCols.includes(affectedID)) { - - setColumnSortStates((prev) => { - const newState: SortStateProperties = {...prev}; - newState[affectedID] = sortDirection; - return newState; - }); - - setColumnSortOrder((prev) => { - const newOrder: ColumnFields[] = prev.filter((item) => String(item) !== String(affectedID)); - if (sortDirection !== SortStates.NONE) { - newOrder.push(affectedID); - } - return newOrder; - }); - - } - } - const defaultColumnDefs: ColDef = { filter: false, resizable: false, sortable: true, headerComponentParams: { hideColumn: (id: string) => handleColumnFilterAdd(id), - updateSortState: updateSortState, + updateSortState: (affectedID: ColumnFields, sortDirection: SortStates) => { + if (!disabledSortCols.includes(affectedID)) { + const newState: SortStateProperties = {...freshSortState}; + newState[affectedID] = sortDirection; + dispatch(updateAllColsSortStates({ + currentState: newState, + sortBy: affectedID, + })) + } + }, disabledSortCols: disabledSortCols, }, }; @@ -213,7 +111,7 @@ const GradingSubmissionTable: React.FC = ({ }, defaultColDefs: defaultColumnDefs, headerHeight: HEADER_HEIGHT, - overlayLoadingTemplate: '
', + overlayLoadingTemplate: '
', overlayNoRowsTemplate: "Hmm... No submissions found, did you filter them all out?", pageSize: pageSize, pagination: true, @@ -222,17 +120,68 @@ const GradingSubmissionTable: React.FC = ({ suppressMenuHide: true, suppressPaginationPanel: true, suppressRowClickSelection: true, - tableHeight: String(ROW_HEIGHT * Math.min(pageSize, Math.max(2, submissions.length)) + HEADER_HEIGHT) + "px", + tableHeight: String(ROW_HEIGHT * (submissions.length > 0 ? submissions.length : 2) + HEADER_HEIGHT + 4) + "px", tableMargins: "1rem 0 0 0", }; + const dispatch = useDispatch(); + const navigate = useNavigate(); + + const tableFilters = useTypedSelector(state => state.workspaces.grading.submissionsTableFilters); + const columnVisibility = useTypedSelector(state => state.workspaces.grading.columnVisiblity); + const requestCounter = useTypedSelector(state => state.workspaces.grading.requestCounter); + const courseId = useTypedSelector(store => store.session.courseId); + + const gridRef = useRef>(null); + + const [page, setPage] = useState(0); + /** The value to be shown in the search bar */ + const [searchQuery, setSearchQuery] = useState(''); + /** The actual value sent to the backend */ + const [searchValue, setSearchValue] = useState(''); + const [columnFilters, setColumnFilters] = useState([ + ...tableFilters.columnFilters + ]); + const [hiddenColumns, setHiddenColumns] = useState( + columnVisibility ? columnVisibility : [] + ); const [rowData, setRowData] = useState(); const [colDefs, setColDefs] = useState[]>(); const [filterMode, setFilterMode] = useState(false); - const [columnSortStates, setColumnSortStates] = useState(freshSortState); - const [columnSortOrder, setColumnSortOrder] = useState([]); - const gridRef = useRef>(null); + const maxPage = useMemo(() => Math.ceil(totalRows / pageSize) - 1, [totalRows, pageSize]); + const resetPage = useCallback(() => setPage(0), [setPage]); + // Placing searchValue as a dependency for triggering a page reset will result in double-querying. + const debouncedUpdateSearchValue = useMemo( + () => + debounce((newValue: string) => { + resetPage(); + setSearchValue(newValue); + }, 300), + [resetPage] + ); + + const handleSearchQueryUpdate: React.ChangeEventHandler = e => { + setSearchQuery(e.target.value); + debouncedUpdateSearchValue(e.target.value); + }; + + // Converts the columnFilters array into backend query parameters. + const backendFilterParams = useMemo(() => { + const filters: Array<{ [key: string]: any }> = [ + { id: ColumnFields.assessmentName, value: searchValue }, + ...columnFilters + ].map(convertFilterToBackendParams); + + const params: Record = {}; + filters.forEach(e => { + Object.keys(e).forEach(key => { + params[key] = e[key]; + }); + }); + return params; + }, [columnFilters, searchValue]); + const generateCols = useCallback(() => { @@ -255,7 +204,7 @@ const GradingSubmissionTable: React.FC = ({ cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { - component: FilterableNew, + component: Filterable, params: { value: params.data.assessmentName, filterMode: filterMode, @@ -272,7 +221,7 @@ const GradingSubmissionTable: React.FC = ({ cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { - component: FilterableNew, + component: Filterable, params: { value: params.data.assessmentType, children: [], @@ -293,7 +242,7 @@ const GradingSubmissionTable: React.FC = ({ cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { - component: FilterableNew, + component: Filterable, params: { value: params.data.studentName, filterMode: filterMode, @@ -310,7 +259,7 @@ const GradingSubmissionTable: React.FC = ({ cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { - component: FilterableNew, + component: Filterable, params: { value: params.data.studentUsername, filterMode: filterMode, @@ -328,7 +277,7 @@ const GradingSubmissionTable: React.FC = ({ cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { - component: FilterableNew, + component: Filterable, params: { value: params.data.groupName, filterMode: filterMode, @@ -345,7 +294,7 @@ const GradingSubmissionTable: React.FC = ({ cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { - component: FilterableNew, + component: Filterable, params: { value: params.data.submissionStatus, children: [], @@ -360,7 +309,7 @@ const GradingSubmissionTable: React.FC = ({ ...generalColProperties, headerName: ColumnName.gradingStatus, field: ColumnFields.gradingStatus, - cellClass: generalColProperties.cellClass + (!filterMode ? " grading-def-cell-pointer" : ""), + cellClass: generalColProperties.cellClass + (!filterMode ? " grading-def-cell-pointer" : " grading-def-cell-selectable"), cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { @@ -398,19 +347,7 @@ const GradingSubmissionTable: React.FC = ({ }); return cols; - }, [ColumnFields, ColumnName, filterMode]); - - // const showLoading = useCallback(() => { - // gridRef.current!.api.showLoadingOverlay(); - // }, []) - - // const hideLoading = useCallback(() => { - // gridRef.current!.api.hideOverlay(); - // }, []) - - // const showNoRows = useCallback(() => { - // gridRef.current!.api.showNoRowsOverlay(); - // }, []) + }, [filterMode]); const cellClickedEvent = (event: CellClickedEvent) => { @@ -424,79 +361,8 @@ const GradingSubmissionTable: React.FC = ({ }; - // Start of Original Code - const dispatch = useDispatch(); - const navigate = useNavigate(); - const tableFilters = useTypedSelector(state => state.workspaces.grading.submissionsTableFilters); - const columnVisibility = useTypedSelector(state => state.workspaces.grading.columnVisiblity); - const requestCounter = useTypedSelector(state => state.workspaces.grading.requestCounter); - const courseId = useTypedSelector(store => store.session.courseId); - - const [columnFilters, setColumnFilters] = useState([ - ...tableFilters.columnFilters - ]); - - const [hiddenColumns, setHiddenColumns] = useState( - columnVisibility ? columnVisibility : [] - ); - - const [page, setPage] = useState(0); - const maxPage = useMemo(() => Math.ceil(totalRows / pageSize) - 1, [totalRows, pageSize]); - const resetPage = useCallback(() => setPage(0), [setPage]); - - /** The value to be shown in the search bar */ - const [searchQuery, setSearchQuery] = useState(''); - /** The actual value sent to the backend */ - const [searchValue, setSearchValue] = useState(''); - // Placing searchValue as a dependency for triggering a page reset will result in double-querying. - const debouncedUpdateSearchValue = useMemo( - () => - debounce((newValue: string) => { - resetPage(); - setSearchValue(newValue); - }, 300), - [resetPage] - ); - const handleSearchQueryUpdate: React.ChangeEventHandler = e => { - setSearchQuery(e.target.value); - debouncedUpdateSearchValue(e.target.value); - }; - - // Converts the columnFilters array into backend query parameters. - const backendFilterParams = useMemo(() => { - const filters: Array<{ [key: string]: any }> = [ - { id: ColumnFields.assessmentName, value: searchValue }, - ...columnFilters - ].map(convertFilterToBackendParams); - - const params: Record = {}; - filters.forEach(e => { - Object.keys(e).forEach(key => { - params[key] = e[key]; - }); - }); - return params; - }, [columnFilters, searchValue, ColumnFields.assessmentName]); - - // const columns = useMemo(() => makeColumns(resetPage), [resetPage]); - // const table = useReactTable({ - // data: submissions, - // columns, - // state: { - // columnFilters, - // pagination: { - // pageIndex: 0, - // pageSize: pageSize - // } - // }, - // onColumnFiltersChange: setColumnFilters, - // getCoreRowModel: getCoreRowModel(), - // getFilteredRowModel: getFilteredRowModel(), - // getPaginationRowModel: getPaginationRowModel() - // }); - + // Filter is to filter by cell value const handleFilterAdd = ({ id, value }: ColumnFilter) => { - dispatch(increaseRequestCounter()); setColumnFilters((prev: ColumnFiltersState) => { const alreadyExists = prev.reduce((acc, curr) => acc || (curr.id === id && curr.value === value), false); return alreadyExists @@ -515,11 +381,11 @@ const GradingSubmissionTable: React.FC = ({ const handleFilterRemove = ({ id, value }: ColumnFilter) => { const newFilters = columnFilters.filter(filter => filter.id !== id && filter.value !== value); - dispatch(increaseRequestCounter()); setColumnFilters(newFilters); resetPage(); }; + // Column Filter is to hide Columns const handleColumnFilterRemove = (toRemove: string) => { if (gridRef.current?.api) { setHiddenColumns((prev: GradingColumnVisibility) => prev.filter(column => column !== toRemove)); @@ -550,18 +416,6 @@ const GradingSubmissionTable: React.FC = ({ updateEntries(page, backendFilterParams); }, [updateEntries, page, backendFilterParams]); - useEffect(() => { - // TODO BACKEND SORT - console.log(columnSortStates); - console.log(columnSortOrder); - dispatch(updateAllColsSortStates({ - currentState: columnSortStates, - sortOrder: columnSortOrder, - })) - }, [columnSortStates, columnSortOrder, dispatch]); - - // End of Original Code - useEffect(() => { if (gridRef.current?.api) { if (requestCounter <= 0) { @@ -611,8 +465,6 @@ const GradingSubmissionTable: React.FC = ({ setColDefs(generateCols()); }, [resetPage, filterMode, generateCols]); - // Start of Original Code - return ( <> {hiddenColumns.length > 0 ? ( @@ -639,8 +491,8 @@ const GradingSubmissionTable: React.FC = ({ -
- + + {columnFilters.length > 0 ? 'Filters: ' @@ -651,21 +503,18 @@ const GradingSubmissionTable: React.FC = ({ : '') : Disable Grading Mode to enable click to filter)}{' '} -
+
- + - {/* } - placeholder="Search by assessment name" - value={searchQuery} - onChange={handleSearchQueryUpdate} - /> */} = ({ onChange={handleSearchQueryUpdate} > - - {/* End of Original Code */}
= ({ />
- {/* Start of Original Code */} - - {/* - - {table.getHeaderGroups().map(headerGroup => ( - - {headerGroup.headers.map(header => - hiddenColumns.reduce( - (accumulator, currentValue) => accumulator || header.id.includes(currentValue), - false - ) ? ( - <> - ) : ( - - - - ) - )} - - ))} - - - {table.getRowModel().rows.map(row => ( - - {row - .getVisibleCells() - .map(cell => - hiddenColumns.reduce( - (accumulator, currentValue) => accumulator || cell.id.includes(currentValue), - false - ) ? ( - <> - ) : ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ) - )} - - ))} - */} - {/*
*/} - {/*
*/} - -
*/} - {/*
*/} - + + -// ); -// }; - -const FilterableNew: React.FC = ({ value, children, filterMode }) => { +const Filterable: React.FC = ({ value, children, filterMode }) => { return ( = ({ defaultColDefs: defaultColumnDefs, headerHeight: HEADER_HEIGHT, overlayLoadingTemplate: '
', - overlayNoRowsTemplate: "Hmm... No submissions found, did you filter them all out?", + overlayNoRowsTemplate: "Hmm... we didn't find any submissions, you might want to debug your filter() function.", pageSize: pageSize, pagination: true, rowClass: "grading-left-align grading-table-rows", diff --git a/src/styles/_academy.scss b/src/styles/_academy.scss index e03e8ea9fe..db8392a568 100644 --- a/src/styles/_academy.scss +++ b/src/styles/_academy.scss @@ -537,7 +537,7 @@ max-width: 100%; .contentdisplay-content { - min-width: 1500px; + min-width: 1200px; } } @@ -627,6 +627,7 @@ margin: 0 15px 0 auto; background-color: #dbeafef5!important; color: #3b82f6!important; + min-width: fit-content; &.grading-filter-btn-on { background-color: #f5f5f5!important; @@ -708,7 +709,7 @@ justify-content: center; line-height: normal!important; width: fit-content; - max-width: max(1500px * 0.2, 20vw); + max-width: max(1200px * 0.2, 20vw); margin: auto; color: rgba(0, 0, 0, 0.7); text-wrap: nowrap; @@ -742,7 +743,7 @@ .grading-badge-text { line-height: normal!important; width: fit-content; - max-width: max(calc(1500px * 0.2 - 16px - 2rem), calc(20vw - 16px - 2rem)); + max-width: max(calc(1200px * 0.2 - 16px - 2rem), calc(20vw - 16px - 2rem)); overflow: hidden; text-overflow: ellipsis; } @@ -819,4 +820,13 @@ text-decoration: underline!important; background-color: transparent!important; } +} + +.grading-refresh-loop { + svg { + transform: rotate(180deg); + transition: 0.2s ease; + } + cursor: not-allowed!important; + pointer-events: none; } \ No newline at end of file From c16e3ae846fbf68f5f519341443c6ccd75f4c0f4 Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Wed, 3 Apr 2024 13:41:53 +0800 Subject: [PATCH 13/69] fixed richard's comments (mostly), and added a fix for josh's comment and PR --- src/commons/application/ApplicationTypes.ts | 3 +- .../application/actions/SessionActions.ts | 2 +- src/commons/grading/GradingFlex.tsx | 43 ++----- src/commons/grading/GradingText.tsx | 2 +- src/commons/sagas/BackendSaga.ts | 4 +- src/commons/sagas/RequestsSaga.ts | 88 +++++++------ src/commons/workspace/WorkspaceActions.ts | 5 + src/commons/workspace/WorkspaceReducer.ts | 9 ++ src/commons/workspace/WorkspaceTypes.ts | 2 + src/features/grading/GradingTypes.ts | 5 +- src/features/grading/GradingUtils.ts | 4 +- src/pages/academy/grading/Grading.tsx | 59 +++++---- .../grading/subcomponents/GradingActions.tsx | 4 +- .../GradingSubmissionFilters.tsx | 2 +- .../subcomponents/GradingSubmissionsTable.tsx | 117 +++++++----------- src/styles/_academy.scss | 1 + 16 files changed, 164 insertions(+), 186 deletions(-) diff --git a/src/commons/application/ApplicationTypes.ts b/src/commons/application/ApplicationTypes.ts index 18374d4c4f..4673f9c524 100644 --- a/src/commons/application/ApplicationTypes.ts +++ b/src/commons/application/ApplicationTypes.ts @@ -423,7 +423,8 @@ export const defaultWorkspaceManager: WorkspaceManagerState = { allColsSortStates: { currentState: freshSortState, sortBy: '' - } + }, + hasLoadedBefore: false }, playground: { ...createDefaultWorkspace('playground'), diff --git a/src/commons/application/actions/SessionActions.ts b/src/commons/application/actions/SessionActions.ts index a10b79e1be..3e7e63e3bc 100644 --- a/src/commons/application/actions/SessionActions.ts +++ b/src/commons/application/actions/SessionActions.ts @@ -151,7 +151,7 @@ export const fetchGradingOverviews = createAction( FETCH_GRADING_OVERVIEWS, ( filterToGroup = true, - gradedFilter = ungradedToBackendParams(false), + gradedFilter = ungradedToBackendParams(false, false), pageParams = paginationToBackendParams(0, 10), filterParams = {}, allColsSortStates: AllColsSortStates = { currentState: freshSortState, sortBy: '' } diff --git a/src/commons/grading/GradingFlex.tsx b/src/commons/grading/GradingFlex.tsx index 4fb56a4e82..9a95c220c7 100644 --- a/src/commons/grading/GradingFlex.tsx +++ b/src/commons/grading/GradingFlex.tsx @@ -1,14 +1,9 @@ -declare const twJustifyContentValues: readonly ["justify-start", "justify-end", "justify-center", "justify-between", "justify-around", "justify-evenly"]; -declare type JustifyContent = typeof twJustifyContentValues[number]; -declare const twAlignItemsValues: readonly ["items-start", "items-end", "items-center", "items-baseline", "items-stretch"]; -declare type AlignItems = typeof twAlignItemsValues[number]; -declare const twFlexDirectionValues: readonly ["row", "column"]; -declare type FlexDirection = typeof twFlexDirectionValues[number]; +import { Property } from "csstype"; type GradingFlexProps = { - justifyContent?: JustifyContent; - alignItems?: AlignItems; - flexDirection?: FlexDirection; + justifyContent?: Property.JustifyContent; + alignItems?: Property.AlignItems; + flexDirection?: Property.FlexDirection; children?: React.ReactNode; style?: React.CSSProperties; className?: string; @@ -17,32 +12,8 @@ type GradingFlexProps = { const GradingFlex: React.FC = ({ justifyContent, alignItems, flexDirection, children, style, className, }: GradingFlexProps) => { const defaultStyle: React.CSSProperties = { display: "flex", - justifyContent: - (justifyContent === "justify-start" - ? "flex-start" - : justifyContent === "justify-end" - ? "flex-end" - : justifyContent === "justify-center" - ? "center" - : justifyContent === "justify-between" - ? "space-between" - : justifyContent === "justify-around" - ? "space-around" - : justifyContent === "justify-evenly" - ? "space-evenly" - : "" - ), - alignItems: - (alignItems === "items-start" - ? "start" - : alignItems === "items-end" - ? "end" - : alignItems === "items-center" - ? "center" - : alignItems === "items-baseline" - ? "baseline" - : "" - ), + justifyContent: justifyContent, + alignItems: alignItems, flexDirection: flexDirection, }; @@ -53,4 +24,4 @@ const GradingFlex: React.FC = ({ justifyContent, alignItems, f ); } -export default GradingFlex; \ No newline at end of file +export default GradingFlex; diff --git a/src/commons/grading/GradingText.tsx b/src/commons/grading/GradingText.tsx index 9cf58440da..0740220aee 100644 --- a/src/commons/grading/GradingText.tsx +++ b/src/commons/grading/GradingText.tsx @@ -23,4 +23,4 @@ const GradingText: React.FC = ({ children, style, secondaryTex ); } -export default GradingText; \ No newline at end of file +export default GradingText; diff --git a/src/commons/sagas/BackendSaga.ts b/src/commons/sagas/BackendSaga.ts index 964f9755e1..f7aa3e0f5a 100644 --- a/src/commons/sagas/BackendSaga.ts +++ b/src/commons/sagas/BackendSaga.ts @@ -483,8 +483,8 @@ function* BackendSaga(): SagaIterator { filterToGroup, gradedFilter, pageParams, - filterParams - // sortedBy + filterParams, + sortedBy ); if (gradingOverviews) { yield put(actions.updateGradingOverviews(gradingOverviews)); diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index 8654b249db..a41e4b4a47 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -640,10 +640,12 @@ export const getGradingOverviews = async ( group: boolean, graded: Record | undefined, pageParams: Record, - filterParams: Record + filterParams: Record, + sortedBy: Record ): Promise => { // gradedQuery placed behind filterQuery to override progress filter if any - const params = new URLSearchParams({ ...pageParams, ...filterParams, ...graded }); + console.log(filterParams); + const params = new URLSearchParams({ ...pageParams, ...filterParams, ...graded, ...sortedBy }); params.append('group', `${group}`); const resp = await request(`${courseId()}/admin/grading?${params.toString()}`, 'GET', { @@ -656,50 +658,44 @@ export const getGradingOverviews = async ( return { count: gradingOverviews.count, - data: gradingOverviews.data - .map((overview: any) => { - const gradingOverview: GradingOverview = { - assessmentId: overview.assessment.id, - assessmentNumber: overview.assessment.assessmentNumber, - assessmentName: overview.assessment.title, - assessmentType: overview.assessment.type, - studentId: overview.student ? overview.student.id : -1, - studentName: overview.student ? overview.student.name : undefined, - studentNames: overview.team - ? overview.team.team_members.map((member: { name: any }) => member.name) - : undefined, - studentUsername: overview.student ? overview.student.name : undefined, - studentUsernames: overview.team - ? overview.team.team_members.map((member: { username: any }) => member.username) - : undefined, - submissionId: overview.id, - submissionStatus: overview.status, - groupName: overview.student ? overview.student.groupName : '-', - groupLeaderId: overview.student ? overview.student.groupLeaderId : undefined, - // Grading Status - gradingStatus: 'none', - questionCount: overview.assessment.questionCount, - gradedCount: overview.gradedCount, - // XP - initialXp: overview.xp, - xpAdjustment: overview.xpAdjustment, - currentXp: overview.xp + overview.xpAdjustment, - maxXp: overview.assessment.maxXp, - xpBonus: overview.xpBonus - }; - gradingOverview.gradingStatus = computeGradingStatus( - overview.assessment.isManuallyGraded, - gradingOverview.submissionStatus, - gradingOverview.gradedCount, - gradingOverview.questionCount - ); - return gradingOverview; - }) - .sort((subX: GradingOverview, subY: GradingOverview) => - subX.assessmentId !== subY.assessmentId - ? subY.assessmentId - subX.assessmentId - : subY.submissionId - subX.submissionId - ) + data: gradingOverviews.data.map((overview: any) => { + const gradingOverview: GradingOverview = { + assessmentId: overview.assessment.id, + assessmentNumber: overview.assessment.assessmentNumber, + assessmentName: overview.assessment.title, + assessmentType: overview.assessment.type, + studentId: overview.student ? overview.student.id : -1, + studentName: overview.student ? overview.student.name : undefined, + studentNames: overview.team + ? overview.team.team_members.map((member: { name: any }) => member.name) + : undefined, + studentUsername: overview.student ? overview.student.name : undefined, + studentUsernames: overview.team + ? overview.team.team_members.map((member: { username: any }) => member.username) + : undefined, + submissionId: overview.id, + submissionStatus: overview.status, + groupName: overview.student ? overview.student.groupName : '-', + groupLeaderId: overview.student ? overview.student.groupLeaderId : undefined, + // Grading Status + gradingStatus: 'none', + questionCount: overview.assessment.questionCount, + gradedCount: overview.gradedCount, + // XP + initialXp: overview.xp, + xpAdjustment: overview.xpAdjustment, + currentXp: overview.xp + overview.xpAdjustment, + maxXp: overview.assessment.maxXp, + xpBonus: overview.xpBonus + }; + gradingOverview.gradingStatus = computeGradingStatus( + overview.assessment.isManuallyGraded, + gradingOverview.submissionStatus, + gradingOverview.gradedCount, + gradingOverview.questionCount + ); + return gradingOverview; + }) }; }; diff --git a/src/commons/workspace/WorkspaceActions.ts b/src/commons/workspace/WorkspaceActions.ts index fe2b562e4e..14108fd6e7 100644 --- a/src/commons/workspace/WorkspaceActions.ts +++ b/src/commons/workspace/WorkspaceActions.ts @@ -50,6 +50,7 @@ import { RESET_WORKSPACE, SEND_REPL_INPUT_TO_OUTPUT, SET_FOLDER_MODE, + SET_GRADING_HAS_LOADED_BEFORE, SET_TOKEN_COUNT, SHIFT_EDITOR_TAB, SubmissionsTableFilters, @@ -401,6 +402,10 @@ export const setIsEditorReadonly = createAction( }) ); +export const setGradingHasLoadedBefore = createAction(SET_GRADING_HAS_LOADED_BEFORE, () => ({ + payload: true +})); + export const increaseRequestCounter = createAction(INCREMENT_REQUEST_COUNTER, () => ({ payload: {} })); diff --git a/src/commons/workspace/WorkspaceReducer.ts b/src/commons/workspace/WorkspaceReducer.ts index e6eea999e6..9e27201608 100644 --- a/src/commons/workspace/WorkspaceReducer.ts +++ b/src/commons/workspace/WorkspaceReducer.ts @@ -71,6 +71,7 @@ import { RESET_TESTCASE, RESET_WORKSPACE, SET_FOLDER_MODE, + SET_GRADING_HAS_LOADED_BEFORE, SHIFT_EDITOR_TAB, TOGGLE_EDITOR_AUTORUN, TOGGLE_UPDATE_CSE, @@ -549,6 +550,14 @@ const oldWorkspaceReducer: Reducer = ( editorSessionId: action.payload.editorSessionId } }; + case SET_GRADING_HAS_LOADED_BEFORE: + return { + ...state, + grading: { + ...state.grading, + hasLoadedBefore: state.grading.hasLoadedBefore + } + }; case SET_SESSION_DETAILS: return { ...state, diff --git a/src/commons/workspace/WorkspaceTypes.ts b/src/commons/workspace/WorkspaceTypes.ts index 27a189a12c..c0b950640c 100644 --- a/src/commons/workspace/WorkspaceTypes.ts +++ b/src/commons/workspace/WorkspaceTypes.ts @@ -36,6 +36,7 @@ export const PLAYGROUND_EXTERNAL_SELECT = 'PLAYGROUND_EXTERNAL_SELECT '; export const RESET_TESTCASE = 'RESET_TESTCASE'; export const RESET_WORKSPACE = 'RESET_WORKSPACE'; export const SEND_REPL_INPUT_TO_OUTPUT = 'SEND_REPL_INPUT_TO_OUTPUT'; +export const SET_GRADING_HAS_LOADED_BEFORE = 'SET_GRADING_HAS_LOADED_BEFORE'; export const SET_TOKEN_COUNT = 'SET_TOKEN_COUNT'; export const TOGGLE_EDITOR_AUTORUN = 'TOGGLE_EDITOR_AUTORUN'; export const TOGGLE_USING_SUBST = 'TOGGLE_USING_SUBST'; @@ -89,6 +90,7 @@ type GradingWorkspaceAttr = { readonly hasUnsavedChanges: boolean; readonly requestCounter: number; readonly allColsSortStates: AllColsSortStates; + readonly hasLoadedBefore: boolean; }; type GradingWorkspaceState = GradingWorkspaceAttr & WorkspaceState; diff --git a/src/features/grading/GradingTypes.ts b/src/features/grading/GradingTypes.ts index cb3a639d92..4462b8add3 100644 --- a/src/features/grading/GradingTypes.ts +++ b/src/features/grading/GradingTypes.ts @@ -102,6 +102,7 @@ export type GradingQuery = { }; export type GradingSubmissionTableProps = { + showAllSubmissions: boolean; totalRows: number; pageSize: number; submissions: GradingOverview[]; @@ -111,8 +112,8 @@ export type GradingSubmissionTableProps = { export enum ColumnName { assessmentName = 'Name', assessmentType = 'Type', - studentName = 'Student', - studentUsername = 'Username', + studentName = 'Student(s)', + studentUsername = 'Username(s)', groupName = 'Group', submissionStatus = 'Progress', gradingStatus = 'Grading', diff --git a/src/features/grading/GradingUtils.ts b/src/features/grading/GradingUtils.ts index 1e153b1b41..53acdb32c5 100644 --- a/src/features/grading/GradingUtils.ts +++ b/src/features/grading/GradingUtils.ts @@ -98,12 +98,12 @@ export const paginationToBackendParams = (page: number, pageSize: number) => { return { offset: page * pageSize, pageSize: pageSize }; }; -export const ungradedToBackendParams = (showAll: boolean) => { +export const ungradedToBackendParams = (showAll: boolean, attempting: boolean) => { if (showAll) { return {}; } return { - status: 'submitted', + status: attempting ? 'attempting' : 'submitted', isManuallyGraded: true, notFullyGraded: true }; diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index 9516403385..8784370848 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -8,10 +8,10 @@ import { Role } from 'src/commons/application/ApplicationTypes'; import GradingFlex from 'src/commons/grading/GradingFlex'; import GradingText from 'src/commons/grading/GradingText'; import SimpleDropdown from 'src/commons/SimpleDropdown'; -// import { useSession } from 'src/commons/utils/Hooks'; import { useSession, useTypedSelector } from 'src/commons/utils/Hooks'; -import { decreaseRequestCounter, increaseRequestCounter } from 'src/commons/workspace/WorkspaceActions'; +import { decreaseRequestCounter, increaseRequestCounter, setGradingHasLoadedBefore } from 'src/commons/workspace/WorkspaceActions'; import { numberRegExp } from 'src/features/academy/AcademyTypes'; +import { GradingOverview } from 'src/features/grading/GradingTypes'; import { exportGradingCSV, paginationToBackendParams, @@ -50,21 +50,29 @@ const Grading: React.FC = () => { const [pageSize, setPageSize] = useState(10); const [showAllSubmissions, setShowAllSubmissions] = useState(false); const [refreshQuery, setRefreshQuery] = useState(false); + // const [searchAttemptingFilter, setSearchAttemptingFilter] = useState(false); + const [submissions, setSubmissions] = useState([]); const dispatch = useDispatch(); const allColsSortStates = useTypedSelector(state => state.workspaces.grading.allColsSortStates); const requestCounter = useTypedSelector(state => state.workspaces.grading.requestCounter); + const hasLoadedBefore = useTypedSelector(state => state.workspaces.grading.hasLoadedBefore); const updateGradingOverviewsCallback = useCallback( (page: number, filterParams: Object) => { - if (refreshQuery) { // Prevents es-lint missing dependency warning + + // Prevents es-lint missing dependency warning + if (refreshQuery) { return setRefreshQuery(false); } + // setSearchAttemptingFilter(filterParams["status"] === "attempting"); + + dispatch(setGradingHasLoadedBefore()); dispatch(increaseRequestCounter()); dispatch( fetchGradingOverviews( showAllGroups, - ungradedToBackendParams(showAllSubmissions), + ungradedToBackendParams(showAllSubmissions, filterParams["status"] === "attempting"), paginationToBackendParams(page, pageSize), filterParams, allColsSortStates, @@ -74,12 +82,24 @@ const Grading: React.FC = () => { [dispatch, showAllGroups, showAllSubmissions, pageSize, allColsSortStates, refreshQuery] ); - // useEffect(() => { - // console.log(sortStates); - // }, [sortStates]); - useEffect(() => { + // console.log(gradingOverviews); + // if (searchAttemptingFilter && !showAllSubmissions) { + // setSubmissions([]); + // } else { + setSubmissions(gradingOverviews?.data?.map(e => + !e.studentName + ? { + ...e, + studentName: Array.isArray(e.studentNames) ? e.studentNames.join(', ') : e.studentNames + } + : e + ) ?? []); + // } dispatch(decreaseRequestCounter()); + + // We ignore the dependency on searchAttemptingFilter purposely as we don't want a premature data update. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [gradingOverviews, dispatch]); // If submissionId or questionId is defined but not numeric, redirect back to the Grading overviews page @@ -108,26 +128,20 @@ const Grading: React.FC = () => { /> ); - const submissions = - gradingOverviews?.data?.map(e => - !e.studentName - ? { - ...e, - studentName: Array.isArray(e.studentNames) ? e.studentNames.join(', ') : e.studentNames - } - : e - ) ?? []; - return ( dispatch(fetchGradingOverviews(showAllGroups))} + loadContentDispatch={() => { + if (!hasLoadedBefore) { + dispatch(fetchGradingOverviews(showAllGroups)); + } + }} display={ gradingOverviews?.data === undefined ? ( loadingDisplay ) : ( - - + + Submissions - + Viewing { = ({ submissionId, style }) => { }; return ( - + - + diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx index 765661bf2a..c405f3b649 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx @@ -10,7 +10,7 @@ type Props = { const GradingSubmissionFilters: React.FC = ({ filters, onFilterRemove }) => { return ( - + {filters.map(filter => ( ))} diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 45a2122f0d..32c647584f 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -36,40 +36,6 @@ import GradingColumnCustomHeaders from './GradingColumnCustomHeaders'; import GradingColumnFilters from './GradingColumnFilters'; import GradingSubmissionFilters from './GradingSubmissionFilters'; -// TODO: Restore the following accessor behavior change from team assessments merge -// columnHelper.accessor('studentName', { -// header: 'Student(s)', -// cell: info => { -// const value = info.getValue(); -// const fallbackValue = info.row.original.studentNames; -// const finalValue = value || ''; -// const finalFallbackValue = fallbackValue?.join(', ') || ''; -// return ( -// -// ); -// } -// }), -// columnHelper.accessor('studentUsername', { -// header: 'Username(s)', -// cell: info => { -// const value = info.getValue(); -// const fallbackValue = info.row.original.studentUsernames; -// const finalValue = value || ''; -// const finalFallbackValue = fallbackValue?.join(', ') || ''; -// return ( -// -// ); -// } -// }), - export const getNextSortState = (current: SortStates) => { switch (current) { case SortStates.NONE: @@ -96,6 +62,7 @@ export const freshSortState: SortStateProperties = { }; const GradingSubmissionTable: React.FC = ({ + showAllSubmissions, totalRows, pageSize, submissions, @@ -136,28 +103,6 @@ const GradingSubmissionTable: React.FC = ({ }, }; - const ROW_HEIGHT: number = 60; // in px, declared here to calculate table height - const HEADER_HEIGHT: number = 48; // in px, declared here to calculate table height - - const tableProperties: IGradingTableProperties = { - customComponents: { - agColumnHeader: GradingColumnCustomHeaders, - }, - defaultColDefs: defaultColumnDefs, - headerHeight: HEADER_HEIGHT, - overlayLoadingTemplate: '
', - overlayNoRowsTemplate: "Hmm... we didn't find any submissions, you might want to debug your filter() function.", - pageSize: pageSize, - pagination: true, - rowClass: "grading-left-align grading-table-rows", - rowHeight: ROW_HEIGHT, - suppressMenuHide: true, - suppressPaginationPanel: true, - suppressRowClickSelection: true, - tableHeight: String(ROW_HEIGHT * (submissions.length > 0 ? submissions.length : 2) + HEADER_HEIGHT + 4) + "px", - tableMargins: "1rem 0 0 0", - }; - const dispatch = useDispatch(); const navigate = useNavigate(); @@ -179,12 +124,35 @@ const GradingSubmissionTable: React.FC = ({ const [hiddenColumns, setHiddenColumns] = useState( columnVisibility ? columnVisibility : [] ); - const [rowData, setRowData] = useState(); + const [rowData, setRowData] = useState([]); const [colDefs, setColDefs] = useState[]>(); const [filterMode, setFilterMode] = useState(false); const maxPage = useMemo(() => Math.ceil(totalRows / pageSize) - 1, [totalRows, pageSize]); const resetPage = useCallback(() => setPage(0), [setPage]); + + const ROW_HEIGHT: number = 60; // in px, declared here to calculate table height + const HEADER_HEIGHT: number = 48; // in px, declared here to calculate table height + + const tableProperties: IGradingTableProperties = { + customComponents: { + agColumnHeader: GradingColumnCustomHeaders, + }, + defaultColDefs: defaultColumnDefs, + headerHeight: HEADER_HEIGHT, + overlayLoadingTemplate: '
', + overlayNoRowsTemplate: "Hmm... we didn't find any submissions, you might want to debug your filter() function.", + pageSize: pageSize, + pagination: true, + rowClass: "grading-left-align grading-table-rows", + rowHeight: ROW_HEIGHT, + suppressMenuHide: true, + suppressPaginationPanel: true, + suppressRowClickSelection: true, + tableHeight: String(ROW_HEIGHT * (rowData.length > 0 ? rowData.length : 2) + HEADER_HEIGHT + 4) + "px", + tableMargins: "1rem 0 0 0", + }; + // Placing searchValue as a dependency for triggering a page reset will result in double-querying. const debouncedUpdateSearchValue = useMemo( () => @@ -458,11 +426,22 @@ const GradingSubmissionTable: React.FC = ({ const sameData: boolean = submissions.reduce( (sameData, currentSubmission, index) => { + newData.push({ assessmentName: currentSubmission.assessmentName, assessmentType: currentSubmission.assessmentType, - studentName: currentSubmission.studentName, - studentUsername: currentSubmission.studentUsername, + studentName: currentSubmission.studentName + ? currentSubmission.studentName + : (currentSubmission.studentNames + ? currentSubmission.studentNames.join(', ') + : '' + ), + studentUsername: currentSubmission.studentUsername + ? currentSubmission.studentUsername + : (currentSubmission.studentUsernames + ? currentSubmission.studentUsernames.join(', ') + : '' + ), groupName: currentSubmission.groupName, submissionStatus: currentSubmission.submissionStatus, gradingStatus: currentSubmission.gradingStatus, @@ -481,19 +460,17 @@ const GradingSubmissionTable: React.FC = ({ gridRef.current!.api.hideOverlay(); + if (newData.length === 0 && requestCounter <= 0) { + gridRef.current!.api.showNoRowsOverlay(); + } + } else { gridRef.current!.api.showLoadingOverlay(); } } // We ignore the dependency on rowData purposely as we setRowData above. If not, it may cause an infinite loop. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [requestCounter, submissions, courseId]); - - useEffect(() => { - if (rowData?.length === 0 && requestCounter <= 0 && gridRef.current?.api) { - gridRef.current!.api.showNoRowsOverlay(); - } - }, [requestCounter, rowData]); + }, [requestCounter, submissions, courseId, gridRef.current?.api]); useEffect(() => { setColDefs(generateCols()); @@ -502,7 +479,7 @@ const GradingSubmissionTable: React.FC = ({ return ( <> {hiddenColumns.length > 0 ? ( - + Columns Hidden: = ({ <> )} - - + + @@ -583,7 +560,7 @@ const GradingSubmissionTable: React.FC = ({ />
- + + ); +}; + +export default GradingFilterable; diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 32c647584f..e071975e88 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -34,6 +34,7 @@ import GradingActions from './GradingActions'; import { AssessmentTypeBadge, GradingStatusBadge, SubmissionStatusBadge } from './GradingBadges'; import GradingColumnCustomHeaders from './GradingColumnCustomHeaders'; import GradingColumnFilters from './GradingColumnFilters'; +import GradingFilterable from "./GradingFilterable"; import GradingSubmissionFilters from './GradingSubmissionFilters'; export const getNextSortState = (current: SortStates) => { @@ -61,6 +62,20 @@ export const freshSortState: SortStateProperties = { actionsIndex: SortStates.NONE, }; +const disabledEditModeCols: string[] = [ + ColumnFields.actionsIndex, +]; + +const disabledFilterModeCols: string[] = [ + ColumnFields.gradingStatus, + ColumnFields.xp, + ColumnFields.actionsIndex, +]; + +const disabledSortCols: string[] = [ + ColumnFields.actionsIndex, +]; + const GradingSubmissionTable: React.FC = ({ showAllSubmissions, totalRows, @@ -69,40 +84,6 @@ const GradingSubmissionTable: React.FC = ({ updateEntries, }) => { - const disabledEditModeCols: string[] = [ - ColumnFields.actionsIndex, - ]; - - const disabledFilterModeCols: string[] = [ - ColumnFields.gradingStatus, - ColumnFields.xp, - ColumnFields.actionsIndex, - ]; - - const disabledSortCols: string[] = [ - ColumnFields.actionsIndex, - ]; - - const defaultColumnDefs: ColDef = { - filter: false, - resizable: false, - sortable: true, - headerComponentParams: { - hideColumn: (id: string) => handleColumnFilterAdd(id), - updateSortState: (affectedID: ColumnFields, sortDirection: SortStates) => { - if (!disabledSortCols.includes(affectedID)) { - const newState: SortStateProperties = {...freshSortState}; - newState[affectedID] = sortDirection; - dispatch(updateAllColsSortStates({ - currentState: newState, - sortBy: affectedID, - })) - } - }, - disabledSortCols: disabledSortCols, - }, - }; - const dispatch = useDispatch(); const navigate = useNavigate(); @@ -126,11 +107,32 @@ const GradingSubmissionTable: React.FC = ({ ); const [rowData, setRowData] = useState([]); const [colDefs, setColDefs] = useState[]>(); + // This is what that controls Grading Mode. If future feedback says it's better to default to filter mode, change it here. const [filterMode, setFilterMode] = useState(false); const maxPage = useMemo(() => Math.ceil(totalRows / pageSize) - 1, [totalRows, pageSize]); const resetPage = useCallback(() => setPage(0), [setPage]); + const defaultColumnDefs: ColDef = { + filter: false, + resizable: false, + sortable: true, + headerComponentParams: { + hideColumn: (id: string) => handleColumnFilterAdd(id), + updateSortState: (affectedID: ColumnFields, sortDirection: SortStates) => { + if (!disabledSortCols.includes(affectedID)) { + const newState: SortStateProperties = {...freshSortState}; + newState[affectedID] = sortDirection; + dispatch(updateAllColsSortStates({ + currentState: newState, + sortBy: affectedID, + })) + } + }, + disabledSortCols: disabledSortCols, + }, + }; + const ROW_HEIGHT: number = 60; // in px, declared here to calculate table height const HEADER_HEIGHT: number = 48; // in px, declared here to calculate table height @@ -206,7 +208,7 @@ const GradingSubmissionTable: React.FC = ({ cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { - component: Filterable, + component: GradingFilterable, params: { value: params.data.assessmentName, filterMode: filterMode, @@ -223,7 +225,7 @@ const GradingSubmissionTable: React.FC = ({ cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { - component: Filterable, + component: GradingFilterable, params: { value: params.data.assessmentType, children: [], @@ -244,7 +246,7 @@ const GradingSubmissionTable: React.FC = ({ cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { - component: Filterable, + component: GradingFilterable, params: { value: params.data.studentName, filterMode: filterMode, @@ -261,7 +263,7 @@ const GradingSubmissionTable: React.FC = ({ cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { - component: Filterable, + component: GradingFilterable, params: { value: params.data.studentUsername, filterMode: filterMode, @@ -279,7 +281,7 @@ const GradingSubmissionTable: React.FC = ({ cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { - component: Filterable, + component: GradingFilterable, params: { value: params.data.groupName, filterMode: filterMode, @@ -296,7 +298,7 @@ const GradingSubmissionTable: React.FC = ({ cellRendererSelector: (params: ICellRendererParams) => { return (params.data !== undefined) ? { - component: Filterable, + component: GradingFilterable, params: { value: params.data.submissionStatus, children: [], @@ -400,8 +402,13 @@ const GradingSubmissionTable: React.FC = ({ }; useEffect(() => { + if (!showAllSubmissions && columnFilters.reduce((doesItContain, currentFilter) => doesItContain || (currentFilter.id === ColumnFields.submissionStatus && currentFilter.value !== "submitted"), false)) { + setColumnFilters((prev: ColumnFiltersState) => prev.filter(filter => (filter.id !== ColumnFields.submissionStatus))); + resetPage(); + return; + } dispatch(updateSubmissionsTableFilters({ columnFilters })); - }, [columnFilters, dispatch]); + }, [columnFilters, showAllSubmissions, dispatch, resetPage]); useEffect(() => { dispatch(updateGradingColumnVisibility(hiddenColumns)); @@ -468,7 +475,8 @@ const GradingSubmissionTable: React.FC = ({ gridRef.current!.api.showLoadingOverlay(); } } - // We ignore the dependency on rowData purposely as we setRowData above. If not, it may cause an infinite loop. + // We ignore the dependency on rowData purposely as we setRowData above. + // If not, it could cause a double execution, which is a bit expensive. // eslint-disable-next-line react-hooks/exhaustive-deps }, [requestCounter, submissions, courseId, gridRef.current?.api]); @@ -597,21 +605,4 @@ const GradingSubmissionTable: React.FC = ({ ); }; -type FilterableProps = { - setColumnFilters: React.Dispatch>; - id: string; - value: string; - children?: React.ReactNode; - onClick?: () => void; - filterMode: boolean; -}; - -const Filterable: React.FC = ({ value, children, filterMode }) => { - return ( - - ); -}; - export default GradingSubmissionTable; diff --git a/src/styles/_academy.scss b/src/styles/_academy.scss index 12cbda37c5..1661f2fb5c 100644 --- a/src/styles/_academy.scss +++ b/src/styles/_academy.scss @@ -539,14 +539,6 @@ } } -.contentdisplay-content-parent:has(div.grading-table-wrapper) { - max-width: 100%; - - .contentdisplay-content { - min-width: 1200px; - } -} - .grading-overview-filterable-btns { &:hover:not(:has(> .grading-badge)) { text-decoration: underline; @@ -563,6 +555,14 @@ border-radius: 9999px; } +.contentdisplay-content-parent:has(div.grading-table-wrapper) { + max-width: 100%; + + .contentdisplay-content { + min-width: 1200px; + } +} + .grading-table-col-icons { pointer-events: none; opacity: 0; @@ -608,7 +608,7 @@ .grading-table-rows { &.ag-row-hover { - --ag-row-hover-color: rgb(245, 245, 245); + --ag-row-hover-color: #f5f5f5; } &.ag-row.ag-row-last { From 83f00cffd1130a0376f5578afd8a54ec1d7224e7 Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Wed, 3 Apr 2024 17:32:27 +0800 Subject: [PATCH 16/69] fixed wrong username and text overflow --- src/commons/sagas/RequestsSaga.ts | 2 +- src/styles/_academy.scss | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index f4497e589d..6e359aed8f 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -668,7 +668,7 @@ export const getGradingOverviews = async ( studentNames: overview.team ? overview.team.team_members.map((member: { name: any }) => member.name) : undefined, - studentUsername: overview.student ? overview.student.name : undefined, + studentUsername: overview.student ? overview.student.username : undefined, studentUsernames: overview.team ? overview.team.team_members.map((member: { username: any }) => member.username) : undefined, diff --git a/src/styles/_academy.scss b/src/styles/_academy.scss index 1661f2fb5c..4c8f6d7e38 100644 --- a/src/styles/_academy.scss +++ b/src/styles/_academy.scss @@ -719,6 +719,8 @@ margin: auto; color: rgba(0, 0, 0, 0.7); text-wrap: nowrap; + text-overflow: ellipsis; + max-width: 90%; &.grading-badge-xs { padding: 0.25rem 0.6rem; From f1f86529214e7f9ee1178723c87e94a37c0c8cc2 Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Sat, 6 Apr 2024 16:32:24 +0800 Subject: [PATCH 17/69] eslint --- src/commons/application/actions/__tests__/SessionActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commons/application/actions/__tests__/SessionActions.ts b/src/commons/application/actions/__tests__/SessionActions.ts index 20c8b8a0d2..090c6d8794 100644 --- a/src/commons/application/actions/__tests__/SessionActions.ts +++ b/src/commons/application/actions/__tests__/SessionActions.ts @@ -4,6 +4,7 @@ import { paginationToBackendParams, ungradedToBackendParams } from 'src/features/grading/GradingUtils'; +import { freshSortState } from 'src/pages/academy/grading/subcomponents/GradingSubmissionsTable'; import { GradingOverviews, GradingQuery } from '../../../../features/grading/GradingTypes'; import { TeamFormationOverview } from '../../../../features/teamFormation/TeamFormationTypes'; @@ -103,7 +104,6 @@ import { updateTeamFormationOverviews, updateUserRole } from '../SessionActions'; -import { freshSortState } from 'src/pages/academy/grading/subcomponents/GradingSubmissionsTable'; test('acknowledgeNotifications generates correct action object', () => { const action = acknowledgeNotifications(); From 2ed0eec18086ab0f8d906551eb9d100fc98e614a Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Sat, 6 Apr 2024 16:35:09 +0800 Subject: [PATCH 18/69] prettier checks --- src/commons/grading/GradingFlex.tsx | 31 +- src/commons/grading/GradingText.tsx | 21 +- src/pages/academy/grading/Grading.tsx | 52 ++-- .../grading/subcomponents/GradingActions.tsx | 20 +- .../grading/subcomponents/GradingBadges.tsx | 15 +- .../GradingColumnCustomHeaders.tsx | 38 +-- .../subcomponents/GradingFilterable.tsx | 7 +- .../GradingSubmissionFilters.tsx | 2 +- .../subcomponents/GradingSubmissionsTable.tsx | 266 ++++++++++-------- src/styles/_academy.scss | 56 ++-- 10 files changed, 295 insertions(+), 213 deletions(-) diff --git a/src/commons/grading/GradingFlex.tsx b/src/commons/grading/GradingFlex.tsx index 9a95c220c7..4755d8b3ac 100644 --- a/src/commons/grading/GradingFlex.tsx +++ b/src/commons/grading/GradingFlex.tsx @@ -1,27 +1,34 @@ -import { Property } from "csstype"; +import { Property } from 'csstype'; type GradingFlexProps = { - justifyContent?: Property.JustifyContent; - alignItems?: Property.AlignItems; - flexDirection?: Property.FlexDirection; - children?: React.ReactNode; - style?: React.CSSProperties; - className?: string; + justifyContent?: Property.JustifyContent; + alignItems?: Property.AlignItems; + flexDirection?: Property.FlexDirection; + children?: React.ReactNode; + style?: React.CSSProperties; + className?: string; } & React.RefAttributes; -const GradingFlex: React.FC = ({ justifyContent, alignItems, flexDirection, children, style, className, }: GradingFlexProps) => { +const GradingFlex: React.FC = ({ + justifyContent, + alignItems, + flexDirection, + children, + style, + className +}: GradingFlexProps) => { const defaultStyle: React.CSSProperties = { - display: "flex", + display: 'flex', justifyContent: justifyContent, alignItems: alignItems, - flexDirection: flexDirection, + flexDirection: flexDirection }; return ( -
+
{children}
); -} +}; export default GradingFlex; diff --git a/src/commons/grading/GradingText.tsx b/src/commons/grading/GradingText.tsx index 0740220aee..c451260816 100644 --- a/src/commons/grading/GradingText.tsx +++ b/src/commons/grading/GradingText.tsx @@ -1,4 +1,4 @@ -import { Text } from "@blueprintjs/core"; +import { Text } from '@blueprintjs/core'; type GradingTextProps = { children?: React.ReactNode; @@ -7,20 +7,25 @@ type GradingTextProps = { className?: string; } & React.RefAttributes; -const GradingText: React.FC = ({ children, style, secondaryText, className = "", }: GradingTextProps) => { +const GradingText: React.FC = ({ + children, + style, + secondaryText, + className = '' +}: GradingTextProps) => { const defaultStyle: React.CSSProperties = { - width: "max-content", - margin: "auto 0" + width: 'max-content', + margin: 'auto 0' }; return ( - {children} ); -} +}; export default GradingText; diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index d9f59e45f5..d61525301e 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -9,7 +9,11 @@ import GradingFlex from 'src/commons/grading/GradingFlex'; import GradingText from 'src/commons/grading/GradingText'; import SimpleDropdown from 'src/commons/SimpleDropdown'; import { useSession, useTypedSelector } from 'src/commons/utils/Hooks'; -import { decreaseRequestCounter, increaseRequestCounter, setGradingHasLoadedBefore } from 'src/commons/workspace/WorkspaceActions'; +import { + decreaseRequestCounter, + increaseRequestCounter, + setGradingHasLoadedBefore +} from 'src/commons/workspace/WorkspaceActions'; import { numberRegExp } from 'src/features/academy/AcademyTypes'; import { GradingOverview } from 'src/features/grading/GradingTypes'; import { @@ -59,8 +63,7 @@ const Grading: React.FC = () => { const updateGradingOverviewsCallback = useCallback( (page: number, filterParams: Object) => { - - // Prevents es-lint missing dependency warning + // Prevents es-lint missing dependency warning if (refreshQuery) { return setRefreshQuery(false); } @@ -73,7 +76,7 @@ const Grading: React.FC = () => { ungradedToBackendParams(showAllSubmissions), paginationToBackendParams(page, pageSize), filterParams, - allColsSortStates, + allColsSortStates ) ); }, @@ -81,17 +84,20 @@ const Grading: React.FC = () => { ); useEffect(() => { - setSubmissions(gradingOverviews?.data?.map(e => - !e.studentName - ? { - ...e, - studentName: Array.isArray(e.studentNames) ? e.studentNames.join(', ') : e.studentNames - } - : e - ) ?? []); + setSubmissions( + gradingOverviews?.data?.map(e => + !e.studentName + ? { + ...e, + studentName: Array.isArray(e.studentNames) + ? e.studentNames.join(', ') + : e.studentNames + } + : e + ) ?? [] + ); // } dispatch(decreaseRequestCounter()); - }, [gradingOverviews, dispatch]); // If submissionId or questionId is defined but not numeric, redirect back to the Grading overviews page @@ -133,9 +139,11 @@ const Grading: React.FC = () => { ) : ( - - Submissions - - + Viewing { buttonProps={{ minimal: true, rightIcon: 'caret-down' }} /> entries per page. - diff --git a/src/pages/academy/grading/subcomponents/GradingActions.tsx b/src/pages/academy/grading/subcomponents/GradingActions.tsx index 9d06c53b8b..2a1fca9ff1 100644 --- a/src/pages/academy/grading/subcomponents/GradingActions.tsx +++ b/src/pages/academy/grading/subcomponents/GradingActions.tsx @@ -48,18 +48,32 @@ const GradingActions: React.FC = ({ submissionId, style }) => { }; return ( - + - - diff --git a/src/pages/academy/grading/subcomponents/GradingBadges.tsx b/src/pages/academy/grading/subcomponents/GradingBadges.tsx index 5fd09a4d58..c3eaaaa400 100644 --- a/src/pages/academy/grading/subcomponents/GradingBadges.tsx +++ b/src/pages/academy/grading/subcomponents/GradingBadges.tsx @@ -4,8 +4,8 @@ import { ReactNode } from 'react'; import { GradingStatus } from 'src/commons/assessment/AssessmentTypes'; import { ColumnFilter } from 'src/features/grading/GradingTypes'; -declare const sizeValues: readonly ["xs", "sm", "md", "lg", "xl"]; -declare type Size = typeof sizeValues[number]; +declare const sizeValues: readonly ['xs', 'sm', 'md', 'lg', 'xl']; +declare type Size = (typeof sizeValues)[number]; interface BadgeProps { text: string; @@ -16,18 +16,17 @@ interface BadgeProps { const Badge: React.FC = (props: BadgeProps) => { return ( -
{props.icon ? props.icon() : <>} {props.text}
); - }; // First colour is bg, second is text (text is more saturated/darker). Colours are referenced from tailwind css. @@ -38,7 +37,7 @@ const AVAILABLE_COLORS = { green: ['#4ade80', '#15803d'], yellow: ['#fde047', '#ca8a04'], red: ['#f87171', '#b91c1c'], - gray: ['#9ca3af', '#374151'], + gray: ['#9ca3af', '#374151'] }; const BADGE_COLORS = { diff --git a/src/pages/academy/grading/subcomponents/GradingColumnCustomHeaders.tsx b/src/pages/academy/grading/subcomponents/GradingColumnCustomHeaders.tsx index 1c7ccfa3a8..b19cc80ccb 100644 --- a/src/pages/academy/grading/subcomponents/GradingColumnCustomHeaders.tsx +++ b/src/pages/academy/grading/subcomponents/GradingColumnCustomHeaders.tsx @@ -12,16 +12,17 @@ export interface GradingColumnCustomHeadersProps extends CustomHeaderProps { disabledSortCols: string[]; } -const GradingColumnCustomHeaders: React.FC = (props: GradingColumnCustomHeadersProps) => { - +const GradingColumnCustomHeaders: React.FC = ( + props: GradingColumnCustomHeadersProps +) => { // The values correspond to the available icons in the BlueprintJS library. "sort" means unsorted. const [sortState, setSortState] = useState(SortStates.NONE); const colsSortState = useTypedSelector(state => state.workspaces.grading.allColsSortStates); const nextSortState = () => { - setSortState((prev) => getNextSortState(prev)); + setSortState(prev => getNextSortState(prev)); props.updateSortState(props.column.getColId(), getNextSortState(sortState)); - } + }; useEffect(() => { if (colsSortState.sortBy !== props.column.getColId()) { @@ -31,24 +32,27 @@ const GradingColumnCustomHeaders: React.FC = (p return (
- {props.displayName} - { - !props.disabledSortCols.includes(props.column.getColId()) ? -
nextSortState()}> - -
- : <> - } - -
props.hideColumn(props.column.getColId())}> + {!props.disabledSortCols.includes(props.column.getColId()) ? ( +
nextSortState()} + > + +
+ ) : ( + <> + )} + +
props.hideColumn(props.column.getColId())} + >
-
); - }; -export default GradingColumnCustomHeaders; \ No newline at end of file +export default GradingColumnCustomHeaders; diff --git a/src/pages/academy/grading/subcomponents/GradingFilterable.tsx b/src/pages/academy/grading/subcomponents/GradingFilterable.tsx index d38afe19ee..1a1642a293 100644 --- a/src/pages/academy/grading/subcomponents/GradingFilterable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingFilterable.tsx @@ -6,7 +6,12 @@ type FilterableProps = { const GradingFilterable: React.FC = ({ value, children, filterMode }) => { return ( - ); diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx index c405f3b649..26f17505a1 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionFilters.tsx @@ -10,7 +10,7 @@ type Props = { const GradingSubmissionFilters: React.FC = ({ filters, onFilterRemove }) => { return ( - + {filters.map(filter => ( ))} diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index e071975e88..114c1220d5 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -1,5 +1,5 @@ -import "ag-grid-community/styles/ag-grid.css"; -import "ag-grid-community/styles/ag-theme-quartz.css" +import 'ag-grid-community/styles/ag-grid.css'; +import 'ag-grid-community/styles/ag-theme-quartz.css'; import { Button, H6, Icon, InputGroup } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; @@ -27,14 +27,15 @@ import { IGradingTableProperties, IGradingTableRow, SortStateProperties, - SortStates} from 'src/features/grading/GradingTypes'; + SortStates +} from 'src/features/grading/GradingTypes'; import { convertFilterToBackendParams } from 'src/features/grading/GradingUtils'; import GradingActions from './GradingActions'; import { AssessmentTypeBadge, GradingStatusBadge, SubmissionStatusBadge } from './GradingBadges'; import GradingColumnCustomHeaders from './GradingColumnCustomHeaders'; import GradingColumnFilters from './GradingColumnFilters'; -import GradingFilterable from "./GradingFilterable"; +import GradingFilterable from './GradingFilterable'; import GradingSubmissionFilters from './GradingSubmissionFilters'; export const getNextSortState = (current: SortStates) => { @@ -48,7 +49,7 @@ export const getNextSortState = (current: SortStates) => { default: return SortStates.NONE; } -} +}; export const freshSortState: SortStateProperties = { assessmentName: SortStates.NONE, @@ -59,31 +60,26 @@ export const freshSortState: SortStateProperties = { submissionStatus: SortStates.NONE, gradingStatus: SortStates.NONE, xp: SortStates.NONE, - actionsIndex: SortStates.NONE, + actionsIndex: SortStates.NONE }; -const disabledEditModeCols: string[] = [ - ColumnFields.actionsIndex, -]; +const disabledEditModeCols: string[] = [ColumnFields.actionsIndex]; const disabledFilterModeCols: string[] = [ ColumnFields.gradingStatus, ColumnFields.xp, - ColumnFields.actionsIndex, + ColumnFields.actionsIndex ]; -const disabledSortCols: string[] = [ - ColumnFields.actionsIndex, -]; +const disabledSortCols: string[] = [ColumnFields.actionsIndex]; const GradingSubmissionTable: React.FC = ({ showAllSubmissions, totalRows, pageSize, submissions, - updateEntries, + updateEntries }) => { - const dispatch = useDispatch(); const navigate = useNavigate(); @@ -121,16 +117,18 @@ const GradingSubmissionTable: React.FC = ({ hideColumn: (id: string) => handleColumnFilterAdd(id), updateSortState: (affectedID: ColumnFields, sortDirection: SortStates) => { if (!disabledSortCols.includes(affectedID)) { - const newState: SortStateProperties = {...freshSortState}; + const newState: SortStateProperties = { ...freshSortState }; newState[affectedID] = sortDirection; - dispatch(updateAllColsSortStates({ - currentState: newState, - sortBy: affectedID, - })) + dispatch( + updateAllColsSortStates({ + currentState: newState, + sortBy: affectedID + }) + ); } }, - disabledSortCols: disabledSortCols, - }, + disabledSortCols: disabledSortCols + } }; const ROW_HEIGHT: number = 60; // in px, declared here to calculate table height @@ -138,21 +136,23 @@ const GradingSubmissionTable: React.FC = ({ const tableProperties: IGradingTableProperties = { customComponents: { - agColumnHeader: GradingColumnCustomHeaders, + agColumnHeader: GradingColumnCustomHeaders }, defaultColDefs: defaultColumnDefs, headerHeight: HEADER_HEIGHT, overlayLoadingTemplate: '
', - overlayNoRowsTemplate: "Hmm... we didn't find any submissions, you might want to debug your filter() function.", + overlayNoRowsTemplate: + "Hmm... we didn't find any submissions, you might want to debug your filter() function.", pageSize: pageSize, pagination: true, - rowClass: "grading-left-align grading-table-rows", + rowClass: 'grading-left-align grading-table-rows', rowHeight: ROW_HEIGHT, suppressMenuHide: true, suppressPaginationPanel: true, suppressRowClickSelection: true, - tableHeight: String(ROW_HEIGHT * (rowData.length > 0 ? rowData.length : 2) + HEADER_HEIGHT + 4) + "px", - tableMargins: "1rem 0 0 0", + tableHeight: + String(ROW_HEIGHT * (rowData.length > 0 ? rowData.length : 2) + HEADER_HEIGHT + 4) + 'px', + tableMargins: '1rem 0 0 0' }; // Placing searchValue as a dependency for triggering a page reset will result in double-querying. @@ -186,36 +186,34 @@ const GradingSubmissionTable: React.FC = ({ return params; }, [columnFilters, searchValue]); - const generateCols = useCallback(() => { - const cols: ColDef[] = []; const generalColProperties: ColDef = { suppressMovable: true, - cellClass: "grading-def-cell grading-def-cell-pointer", - headerClass: "grading-default-headers", - flex: 1, // weight of column width - } + cellClass: 'grading-def-cell grading-def-cell-pointer', + headerClass: 'grading-default-headers', + flex: 1 // weight of column width + }; cols.push({ ...generalColProperties, headerName: ColumnName.assessmentName, field: ColumnFields.assessmentName, flex: 3, - cellClass: generalColProperties.cellClass + " grading-cell-align-left", - headerClass: generalColProperties.headerClass + " grading-left-align", + cellClass: generalColProperties.cellClass + ' grading-cell-align-left', + headerClass: generalColProperties.headerClass + ' grading-left-align', cellRendererSelector: (params: ICellRendererParams) => { - return (params.data !== undefined) + return params.data !== undefined ? { component: GradingFilterable, params: { value: params.data.assessmentName, - filterMode: filterMode, + filterMode: filterMode } } : undefined; - }, + } }); cols.push({ @@ -223,17 +221,17 @@ const GradingSubmissionTable: React.FC = ({ headerName: ColumnName.assessmentType, field: ColumnFields.assessmentType, cellRendererSelector: (params: ICellRendererParams) => { - return (params.data !== undefined) + return params.data !== undefined ? { component: GradingFilterable, params: { value: params.data.assessmentType, children: [], - filterMode: filterMode, + filterMode: filterMode } } : undefined; - }, + } }); cols.push({ @@ -241,19 +239,19 @@ const GradingSubmissionTable: React.FC = ({ headerName: ColumnName.studentName, field: ColumnFields.studentName, flex: 1.5, - cellClass: generalColProperties.cellClass + " grading-cell-align-left", - headerClass: generalColProperties.headerClass + " grading-left-align", + cellClass: generalColProperties.cellClass + ' grading-cell-align-left', + headerClass: generalColProperties.headerClass + ' grading-left-align', cellRendererSelector: (params: ICellRendererParams) => { - return (params.data !== undefined) + return params.data !== undefined ? { component: GradingFilterable, params: { value: params.data.studentName, - filterMode: filterMode, + filterMode: filterMode } } : undefined; - }, + } }); cols.push({ @@ -261,16 +259,16 @@ const GradingSubmissionTable: React.FC = ({ headerName: ColumnName.studentUsername, field: ColumnFields.studentUsername, cellRendererSelector: (params: ICellRendererParams) => { - return (params.data !== undefined) + return params.data !== undefined ? { component: GradingFilterable, params: { value: params.data.studentUsername, - filterMode: filterMode, + filterMode: filterMode } } : undefined; - }, + } }); cols.push({ @@ -279,16 +277,16 @@ const GradingSubmissionTable: React.FC = ({ field: ColumnFields.groupName, flex: 0.75, cellRendererSelector: (params: ICellRendererParams) => { - return (params.data !== undefined) + return params.data !== undefined ? { component: GradingFilterable, params: { value: params.data.groupName, - filterMode: filterMode, + filterMode: filterMode } } : undefined; - }, + } }); cols.push({ @@ -296,26 +294,28 @@ const GradingSubmissionTable: React.FC = ({ headerName: ColumnName.submissionStatus, field: ColumnFields.submissionStatus, cellRendererSelector: (params: ICellRendererParams) => { - return (params.data !== undefined) + return params.data !== undefined ? { component: GradingFilterable, params: { value: params.data.submissionStatus, children: [], - filterMode: filterMode, + filterMode: filterMode } } : undefined; - }, + } }); cols.push({ ...generalColProperties, headerName: ColumnName.gradingStatus, field: ColumnFields.gradingStatus, - cellClass: generalColProperties.cellClass + (!filterMode ? " grading-def-cell-pointer" : " grading-def-cell-selectable"), + cellClass: + generalColProperties.cellClass + + (!filterMode ? ' grading-def-cell-pointer' : ' grading-def-cell-selectable'), cellRendererSelector: (params: ICellRendererParams) => { - return (params.data !== undefined) + return params.data !== undefined ? { component: GradingStatusBadge, params: { @@ -323,14 +323,16 @@ const GradingSubmissionTable: React.FC = ({ } } : undefined; - }, + } }); cols.push({ ...generalColProperties, headerName: ColumnName.xp, field: ColumnFields.xp, - cellClass: generalColProperties.cellClass + (!filterMode ? " grading-def-cell-pointer" : " grading-def-cell-selectable"), + cellClass: + generalColProperties.cellClass + + (!filterMode ? ' grading-def-cell-pointer' : ' grading-def-cell-selectable') }); cols.push({ @@ -338,51 +340,51 @@ const GradingSubmissionTable: React.FC = ({ headerName: ColumnName.actionsIndex, field: ColumnFields.actionsIndex, cellRendererSelector: (params: ICellRendererParams) => { - return (params.data !== undefined) + return params.data !== undefined ? { component: GradingActions, params: { submissionId: params.data.actionsIndex, - style: {justifyContent: "center"} + style: { justifyContent: 'center' } } } : undefined; - }, + } }); return cols; }, [filterMode]); const cellClickedEvent = (event: CellClickedEvent) => { - - const colClicked: string = event.colDef.field ? event.colDef.field : ""; + const colClicked: string = event.colDef.field ? event.colDef.field : ''; if (!filterMode && !disabledEditModeCols.includes(colClicked)) { navigate(`/courses/${courseId}/grading/${event.data.actionsIndex}`); } else if (filterMode && !disabledFilterModeCols.includes(colClicked)) { - handleFilterAdd({id: colClicked, value: event.data[colClicked]}); + handleFilterAdd({ id: colClicked, value: event.data[colClicked] }); } - }; // Filter is to filter by cell value const handleFilterAdd = ({ id, value }: ColumnFilter) => { setColumnFilters((prev: ColumnFiltersState) => { - const alreadyExists = prev.reduce((acc, curr) => acc || (curr.id === id && curr.value === value), false); + const alreadyExists = prev.reduce( + (acc, curr) => acc || (curr.id === id && curr.value === value), + false + ); return alreadyExists ? [...prev] : [ - ...prev, - { - id: id, - value: value - } - ]; + ...prev, + { + id: id, + value: value + } + ]; }); resetPage(); }; - const handleFilterRemove = ({ id, value }: ColumnFilter) => { const newFilters = columnFilters.filter(filter => filter.id !== id && filter.value !== value); setColumnFilters(newFilters); @@ -392,7 +394,9 @@ const GradingSubmissionTable: React.FC = ({ // Column Filter is to hide Columns const handleColumnFilterRemove = (toRemove: string) => { if (gridRef.current?.api) { - setHiddenColumns((prev: GradingColumnVisibility) => prev.filter(column => column !== toRemove)); + setHiddenColumns((prev: GradingColumnVisibility) => + prev.filter(column => column !== toRemove) + ); gridRef.current.api.setColumnsVisible([toRemove], true); } }; @@ -402,8 +406,19 @@ const GradingSubmissionTable: React.FC = ({ }; useEffect(() => { - if (!showAllSubmissions && columnFilters.reduce((doesItContain, currentFilter) => doesItContain || (currentFilter.id === ColumnFields.submissionStatus && currentFilter.value !== "submitted"), false)) { - setColumnFilters((prev: ColumnFiltersState) => prev.filter(filter => (filter.id !== ColumnFields.submissionStatus))); + if ( + !showAllSubmissions && + columnFilters.reduce( + (doesItContain, currentFilter) => + doesItContain || + (currentFilter.id === ColumnFields.submissionStatus && + currentFilter.value !== 'submitted'), + false + ) + ) { + setColumnFilters((prev: ColumnFiltersState) => + prev.filter(filter => filter.id !== ColumnFields.submissionStatus) + ); resetPage(); return; } @@ -428,35 +443,36 @@ const GradingSubmissionTable: React.FC = ({ useEffect(() => { if (gridRef.current?.api) { if (requestCounter <= 0) { - const newData: IGradingTableRow[] = []; const sameData: boolean = submissions.reduce( (sameData, currentSubmission, index) => { - newData.push({ assessmentName: currentSubmission.assessmentName, assessmentType: currentSubmission.assessmentType, - studentName: currentSubmission.studentName - ? currentSubmission.studentName - : (currentSubmission.studentNames - ? currentSubmission.studentNames.join(', ') - : '' - ), - studentUsername: currentSubmission.studentUsername - ? currentSubmission.studentUsername - : (currentSubmission.studentUsernames - ? currentSubmission.studentUsernames.join(', ') - : '' - ), + studentName: currentSubmission.studentName + ? currentSubmission.studentName + : currentSubmission.studentNames + ? currentSubmission.studentNames.join(', ') + : '', + studentUsername: currentSubmission.studentUsername + ? currentSubmission.studentUsername + : currentSubmission.studentUsernames + ? currentSubmission.studentUsernames.join(', ') + : '', groupName: currentSubmission.groupName, submissionStatus: currentSubmission.submissionStatus, gradingStatus: currentSubmission.gradingStatus, - xp: currentSubmission.currentXp + " (+" + currentSubmission.xpBonus + ") / " + currentSubmission.maxXp, + xp: + currentSubmission.currentXp + + ' (+' + + currentSubmission.xpBonus + + ') / ' + + currentSubmission.maxXp, actionsIndex: currentSubmission.submissionId, - courseID: courseId!, - }) - return sameData && (currentSubmission.submissionId === rowData?.[index]?.actionsIndex); + courseID: courseId! + }); + return sameData && currentSubmission.submissionId === rowData?.[index]?.actionsIndex; }, submissions.length === rowData?.length ); @@ -470,12 +486,11 @@ const GradingSubmissionTable: React.FC = ({ if (newData.length === 0 && requestCounter <= 0) { gridRef.current!.api.showNoRowsOverlay(); } - } else { gridRef.current!.api.showLoadingOverlay(); } } - // We ignore the dependency on rowData purposely as we setRowData above. + // We ignore the dependency on rowData purposely as we setRowData above. // If not, it could cause a double execution, which is a bit expensive. // eslint-disable-next-line react-hooks/exhaustive-deps }, [requestCounter, submissions, courseId, gridRef.current?.api]); @@ -487,7 +502,11 @@ const GradingSubmissionTable: React.FC = ({ return ( <> {hiddenColumns.length > 0 ? ( - + Columns Hidden: = ({ <> )} - + - + - {columnFilters.length > 0 - ? 'Filters: ' - : (filterMode === true - ? 'No filters applied. Click on any cell to filter by its value.' + - (hiddenColumns.length === 0 - ? ' Click on any column header to hide it.' - : '') - : Disable Grading Mode to enable click to filter)}{' '} + {columnFilters.length > 0 ? ( + 'Filters: ' + ) : filterMode === true ? ( + 'No filters applied. Click on any cell to filter by its value.' + + (hiddenColumns.length === 0 ? ' Click on any column header to hide it.' : '') + ) : ( + Disable Grading Mode to enable click to filter + )}{' '} @@ -528,10 +553,10 @@ const GradingSubmissionTable: React.FC = ({ = ({ > -
+
= ({ />
- + diff --git a/src/pages/academy/grading/subcomponents/GradingBadges.tsx b/src/pages/academy/grading/subcomponents/GradingBadges.tsx index c3eaaaa400..c33f7e7a17 100644 --- a/src/pages/academy/grading/subcomponents/GradingBadges.tsx +++ b/src/pages/academy/grading/subcomponents/GradingBadges.tsx @@ -121,6 +121,7 @@ const FilterBadge: React.FC = ({ filter, onRemove }) => { type="button" className="grading-overview-filterable-btns" onClick={() => onRemove(filter)} + style={{ marginLeft: '5px' }} > = ({ filter, onRemove, type="button" className="grading-overview-filterable-btns" onClick={() => onRemove(filter)} + style={{ marginLeft: '5px' }} > = ({ filters, onFilterRemove }) => { return ( - + {filters.map(filter => ( ))} diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 02ab7336f1..977b1428ee 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -333,6 +333,7 @@ const GradingSubmissionTable: React.FC = ({ field: ColumnFields.xp, cellClass: generalColProperties.cellClass + + ' grading-xp-cell' + (!filterMode ? ' grading-def-cell-pointer' : ' grading-def-cell-selectable') }); @@ -340,6 +341,7 @@ const GradingSubmissionTable: React.FC = ({ ...generalColProperties, headerName: ColumnName.actionsIndex, field: ColumnFields.actionsIndex, + flex: 1.25, cellRendererSelector: (params: ICellRendererParams) => { return params.data !== undefined ? { @@ -536,12 +538,14 @@ const GradingSubmissionTable: React.FC = ({ - + {columnFilters.length > 0 ? ( 'Filters: ' ) : filterMode === true ? ( 'No filters applied. Click on any cell to filter by its value.' + - (hiddenColumns.length === 0 ? ' Click on any column header to hide it.' : '') + (hiddenColumns.length === 0 + ? " Click on any column header's eye icon to hide it." + : '') ) : ( Disable Grading Mode to enable click to filter )}{' '} @@ -615,7 +619,7 @@ const GradingSubmissionTable: React.FC = ({ disabled={page <= 0} />
- Page {page + 1} of {maxPage + 1} + Page {maxPage + 1 === 0 ? 0 : page + 1} of {maxPage + 1}
); - return ; }; type FilterBadgeProps = { @@ -132,33 +130,50 @@ const FilterBadge: React.FC = ({ filter, onRemove }) => { ); }; -type ColumnFilterBadgeProps = { - filter: string; - onRemove: (toRemove: string) => void; - filtersName: string; +type ProgressStatusBadgeProps = { + progress: ProgressStatus; }; -const ColumnFilterBadge: React.FC = ({ filter, onRemove, filtersName }) => { - return ( - +const ProgressStatusBadge: React.FC = ({ progress }) => { + const statusText = progress.charAt(0).toUpperCase() + progress.slice(1); + const badgeIcon = () => ( + ); + return ; +}; + + // TO BE REMOVED +type SubmissionStatusBadgeProps = { + status: string; +}; + + // TO BE REMOVED +const SubmissionStatusBadge: React.FC = ({ status }) => { + const statusText = status.charAt(0).toUpperCase() + status.slice(1); + return ; }; export { AssessmentTypeBadge, ColumnFilterBadge, FilterBadge, - GradingStatusBadge, + ProgressStatusBadge, SubmissionStatusBadge }; diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 977b1428ee..168b9a781f 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -32,7 +32,7 @@ import { import { convertFilterToBackendParams } from 'src/features/grading/GradingUtils'; import GradingActions from './GradingActions'; -import { AssessmentTypeBadge, GradingStatusBadge, SubmissionStatusBadge } from './GradingBadges'; +import { AssessmentTypeBadge, ProgressStatusBadge, SubmissionStatusBadge } from './GradingBadges'; import GradingColumnCustomHeaders from './GradingColumnCustomHeaders'; import GradingColumnFilters from './GradingColumnFilters'; import GradingFilterable from './GradingFilterable'; @@ -289,7 +289,8 @@ const GradingSubmissionTable: React.FC = ({ : undefined; } }); - + + // TODO TO REMOVE this or gradingStatus cols.push({ ...generalColProperties, headerName: ColumnName.submissionStatus, @@ -318,9 +319,9 @@ const GradingSubmissionTable: React.FC = ({ cellRendererSelector: (params: ICellRendererParams) => { return params.data !== undefined ? { - component: GradingStatusBadge, + component: ProgressStatusBadge, params: { - status: params.data.gradingStatus + progress: params.data.gradingStatus } } : undefined; @@ -348,6 +349,7 @@ const GradingSubmissionTable: React.FC = ({ component: GradingActions, params: { submissionId: params.data.actionsIndex, + // progress: params.data.progress, // TODO add this style: { justifyContent: 'center' } } } @@ -473,6 +475,7 @@ const GradingSubmissionTable: React.FC = ({ ') / ' + currentSubmission.maxXp, actionsIndex: currentSubmission.submissionId, + //progress: currentSubmission.progress, // TODO to add this courseID: courseId! }); return sameData && currentSubmission.submissionId === rowData?.[index]?.actionsIndex; From 651e4575b83a03d14ff0fd1612b42be002dda33a Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Wed, 10 Apr 2024 23:23:23 +0800 Subject: [PATCH 23/69] eslint prettier --- src/features/grading/GradingTypes.ts | 2 +- src/pages/academy/grading/subcomponents/GradingBadges.tsx | 6 +++--- .../grading/subcomponents/GradingSubmissionsTable.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/features/grading/GradingTypes.ts b/src/features/grading/GradingTypes.ts index fb96fff749..cec5467e4f 100644 --- a/src/features/grading/GradingTypes.ts +++ b/src/features/grading/GradingTypes.ts @@ -3,8 +3,8 @@ import { ColDef } from 'ag-grid-community'; import { AssessmentType, AutogradingResult, - ProgressStatus, MCQChoice, + ProgressStatus, Question, Testcase } from '../../commons/assessment/AssessmentTypes'; diff --git a/src/pages/academy/grading/subcomponents/GradingBadges.tsx b/src/pages/academy/grading/subcomponents/GradingBadges.tsx index 9a952e7c75..c8c406940b 100644 --- a/src/pages/academy/grading/subcomponents/GradingBadges.tsx +++ b/src/pages/academy/grading/subcomponents/GradingBadges.tsx @@ -39,7 +39,7 @@ const AVAILABLE_COLORS = { red: ['#f87171', '#b91c1c'], gray: ['#9ca3af', '#374151'], purple: ['#c084fc', '#7e22ce'], - blue: ['#93c5fd', '#2563eb'], + blue: ['#93c5fd', '#2563eb'] }; const BADGE_COLORS = { @@ -159,12 +159,12 @@ const ProgressStatusBadge: React.FC = ({ progress }) = return ; }; - // TO BE REMOVED +// TO BE REMOVED type SubmissionStatusBadgeProps = { status: string; }; - // TO BE REMOVED +// TO BE REMOVED const SubmissionStatusBadge: React.FC = ({ status }) => { const statusText = status.charAt(0).toUpperCase() + status.slice(1); return ; diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 168b9a781f..bd9ff798ae 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -289,7 +289,7 @@ const GradingSubmissionTable: React.FC = ({ : undefined; } }); - + // TODO TO REMOVE this or gradingStatus cols.push({ ...generalColProperties, From 2dc8125da45e6ea50acee9aa70aabbd98826b0d6 Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Wed, 10 Apr 2024 23:37:11 +0800 Subject: [PATCH 24/69] compile erros --- src/commons/XMLParser/XMLParserHelper.ts | 4 ++-- src/commons/assessment/Assessment.tsx | 8 ++++---- src/commons/mocks/AssessmentMocks.ts | 22 +++++++++++----------- src/commons/profile/Profile.tsx | 6 +++--- src/commons/sagas/RequestsSaga.ts | 4 ++-- src/features/grading/GradingUtils.ts | 4 ++-- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/commons/XMLParser/XMLParserHelper.ts b/src/commons/XMLParser/XMLParserHelper.ts index 0d67017cac..a63154c9ad 100644 --- a/src/commons/XMLParser/XMLParserHelper.ts +++ b/src/commons/XMLParser/XMLParserHelper.ts @@ -9,11 +9,11 @@ import { AssessmentType, BaseQuestion, emptyLibrary, - GradingStatuses, IMCQQuestion, IProgrammingQuestion, Library, MCQChoice, + ProgressStatuses, Question, Testcase, TestcaseTypes @@ -85,7 +85,7 @@ const makeAssessmentOverview = (result: any, maxXpVal: number): AssessmentOvervi status: AssessmentStatuses.attempting, story: rawOverview.story, xp: 0, - gradingStatus: 'none' as GradingStatuses, + gradingStatus: 'none' as ProgressStatuses, maxTeamSize: 1, hasVotingFeatures: false }; diff --git a/src/commons/assessment/Assessment.tsx b/src/commons/assessment/Assessment.tsx index 87ec4ba7c3..811be01cb0 100644 --- a/src/commons/assessment/Assessment.tsx +++ b/src/commons/assessment/Assessment.tsx @@ -51,7 +51,7 @@ import { AssessmentOverview, AssessmentStatuses, AssessmentWorkspaceParams, - GradingStatuses + ProgressStatuses } from './AssessmentTypes'; export type AssessmentProps = { @@ -427,17 +427,17 @@ const makeGradingStatus = (gradingStatus: string) => { let tooltip: string; switch (gradingStatus) { - case GradingStatuses.graded: + case ProgressStatuses.graded: iconName = IconNames.TICK; intent = Intent.SUCCESS; tooltip = 'Fully graded'; break; - case GradingStatuses.grading: + case ProgressStatuses.grading: iconName = IconNames.TIME; intent = Intent.WARNING; tooltip = 'Grading in progress'; break; - case GradingStatuses.none: + case ProgressStatuses.none: iconName = IconNames.CROSS; intent = Intent.DANGER; tooltip = 'Not graded yet'; diff --git a/src/commons/mocks/AssessmentMocks.ts b/src/commons/mocks/AssessmentMocks.ts index 35e80a0d6d..2655dc84d7 100644 --- a/src/commons/mocks/AssessmentMocks.ts +++ b/src/commons/mocks/AssessmentMocks.ts @@ -6,11 +6,11 @@ import { AssessmentConfiguration, AssessmentOverview, AssessmentStatuses, - GradingStatuses, IContestVotingQuestion, IMCQQuestion, IProgrammingQuestion, Library, + ProgressStatuses, TestcaseTypes } from '../assessment/AssessmentTypes'; @@ -117,7 +117,7 @@ const mockUnopenedAssessmentsOverviews: AssessmentOverview[] = [ status: AssessmentStatuses.not_attempted, story: 'mission-1', xp: 0, - gradingStatus: GradingStatuses.none, + gradingStatus: ProgressStatuses.none, maxTeamSize: 1, hasVotingFeatures: false } @@ -151,7 +151,7 @@ const mockOpenedAssessmentsOverviews: AssessmentOverview[] = [ status: AssessmentStatuses.attempted, story: 'mission-1', xp: 1, - gradingStatus: GradingStatuses.none, + gradingStatus: ProgressStatuses.none, maxTeamSize: 4, hasVotingFeatures: false }, @@ -170,7 +170,7 @@ const mockOpenedAssessmentsOverviews: AssessmentOverview[] = [ status: AssessmentStatuses.attempting, story: 'mission-2', xp: 2, - gradingStatus: GradingStatuses.none, + gradingStatus: ProgressStatuses.none, maxTeamSize: 1, hasVotingFeatures: false }, @@ -189,7 +189,7 @@ const mockOpenedAssessmentsOverviews: AssessmentOverview[] = [ status: AssessmentStatuses.not_attempted, story: 'sidequest-2.1', xp: 3, - gradingStatus: GradingStatuses.none, + gradingStatus: ProgressStatuses.none, maxTeamSize: 2, hasVotingFeatures: false }, @@ -208,7 +208,7 @@ const mockOpenedAssessmentsOverviews: AssessmentOverview[] = [ status: AssessmentStatuses.not_attempted, story: null, xp: 0, - gradingStatus: GradingStatuses.excluded, + gradingStatus: ProgressStatuses.excluded, maxTeamSize: 2, hasVotingFeatures: false }, @@ -227,7 +227,7 @@ const mockOpenedAssessmentsOverviews: AssessmentOverview[] = [ status: AssessmentStatuses.not_attempted, story: 'sidequest-2.1', xp: 3, - gradingStatus: GradingStatuses.none, + gradingStatus: ProgressStatuses.none, private: true, maxTeamSize: 1, hasVotingFeatures: false @@ -250,7 +250,7 @@ const mockClosedAssessmentOverviews: AssessmentOverview[] = [ status: AssessmentStatuses.submitted, story: 'mission-3', xp: 800, - gradingStatus: GradingStatuses.grading, + gradingStatus: ProgressStatuses.grading, maxTeamSize: 1, hasVotingFeatures: false }, @@ -269,7 +269,7 @@ const mockClosedAssessmentOverviews: AssessmentOverview[] = [ status: AssessmentStatuses.submitted, story: null, xp: 500, - gradingStatus: GradingStatuses.none, + gradingStatus: ProgressStatuses.none, maxTeamSize: 1, hasVotingFeatures: false }, @@ -288,7 +288,7 @@ const mockClosedAssessmentOverviews: AssessmentOverview[] = [ status: AssessmentStatuses.submitted, story: null, xp: 150, - gradingStatus: GradingStatuses.graded, + gradingStatus: ProgressStatuses.graded, maxTeamSize: 1, hasVotingFeatures: false }, @@ -307,7 +307,7 @@ const mockClosedAssessmentOverviews: AssessmentOverview[] = [ status: AssessmentStatuses.submitted, story: null, xp: 100, - gradingStatus: GradingStatuses.excluded, + gradingStatus: ProgressStatuses.excluded, maxTeamSize: 1, hasVotingFeatures: false } diff --git a/src/commons/profile/Profile.tsx b/src/commons/profile/Profile.tsx index 6fd1dda073..f6846251b3 100644 --- a/src/commons/profile/Profile.tsx +++ b/src/commons/profile/Profile.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; import { fetchAssessmentOverviews, fetchTotalXp } from '../application/actions/SessionActions'; -import { AssessmentStatuses, AssessmentType, GradingStatuses } from '../assessment/AssessmentTypes'; +import { AssessmentStatuses, AssessmentType, ProgressStatuses } from '../assessment/AssessmentTypes'; import Constants from '../utils/Constants'; import { useSession } from '../utils/Hooks'; import ProfileCard from './ProfileCard'; @@ -129,8 +129,8 @@ const Profile: React.FC = props => { .filter( item => item.status === AssessmentStatuses.submitted && - (item.gradingStatus === GradingStatuses.graded || - item.gradingStatus === GradingStatuses.excluded) + (item.gradingStatus === ProgressStatuses.graded || + item.gradingStatus === ProgressStatuses.excluded) ) .map((assessment, index) => { return ( diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index de4b35b2e3..de4798d2e8 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -48,9 +48,9 @@ import { AssessmentConfiguration, AssessmentOverview, ContestEntry, - GradingStatus, IContestVotingQuestion, IProgrammingQuestion, + ProgressStatus, QuestionType, QuestionTypes } from '../assessment/AssessmentTypes'; @@ -1673,7 +1673,7 @@ const computeGradingStatus = ( submissionStatus: any, numGraded: number, numQuestions: number -): GradingStatus => +): ProgressStatus => // isGraded refers to whether the assessment type is graded or not, as specified in // the respective assessment configuration isManuallyGraded && submissionStatus === 'submitted' diff --git a/src/features/grading/GradingUtils.ts b/src/features/grading/GradingUtils.ts index 1e153b1b41..14118c3c6a 100644 --- a/src/features/grading/GradingUtils.ts +++ b/src/features/grading/GradingUtils.ts @@ -1,4 +1,4 @@ -import { GradingStatuses } from 'src/commons/assessment/AssessmentTypes'; +import { ProgressStatuses } from 'src/commons/assessment/AssessmentTypes'; import { ColumnFilter } from 'src/features/grading/GradingTypes'; import { GradingOverview } from './GradingTypes'; @@ -7,7 +7,7 @@ import { GradingOverview } from './GradingTypes'; export const isSubmissionUngraded = (s: GradingOverview): boolean => { const isSubmitted = s.submissionStatus === 'submitted'; const isNotGraded = - s.gradingStatus !== GradingStatuses.graded && s.gradingStatus !== GradingStatuses.excluded; + s.gradingStatus !== ProgressStatuses.graded && s.gradingStatus !== ProgressStatuses.excluded; return isSubmitted && isNotGraded; }; From 480bbfccdbcf882d7b868231ed22ae6879deebaf Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Thu, 11 Apr 2024 00:14:39 +0800 Subject: [PATCH 25/69] compile error and eslint --- .../application/reducers/__tests__/SessionReducer.ts | 6 +++--- src/commons/profile/Profile.tsx | 6 +++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/commons/application/reducers/__tests__/SessionReducer.ts b/src/commons/application/reducers/__tests__/SessionReducer.ts index 80c9e86cfd..d73ff739b5 100644 --- a/src/commons/application/reducers/__tests__/SessionReducer.ts +++ b/src/commons/application/reducers/__tests__/SessionReducer.ts @@ -5,7 +5,7 @@ import { Assessment, AssessmentOverview, AssessmentStatuses, - GradingStatuses + ProgressStatuses } from '../../../assessment/AssessmentTypes'; import { Notification } from '../../../notificationBadge/NotificationBadgeTypes'; import { defaultSession, GameState, Role, Story } from '../../ApplicationTypes'; @@ -342,7 +342,7 @@ const assessmentOverviewsTest1: AssessmentOverview[] = [ status: AssessmentStatuses.not_attempted, story: null, xp: 0, - gradingStatus: GradingStatuses.none, + gradingStatus: ProgressStatuses.none, maxTeamSize: 5, hasVotingFeatures: false } @@ -364,7 +364,7 @@ const assessmentOverviewsTest2: AssessmentOverview[] = [ status: AssessmentStatuses.attempted, story: null, xp: 1, - gradingStatus: GradingStatuses.grading, + gradingStatus: ProgressStatuses.grading, maxTeamSize: 1, hasVotingFeatures: false } diff --git a/src/commons/profile/Profile.tsx b/src/commons/profile/Profile.tsx index f6846251b3..578f96f9e1 100644 --- a/src/commons/profile/Profile.tsx +++ b/src/commons/profile/Profile.tsx @@ -4,7 +4,11 @@ import React, { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; import { fetchAssessmentOverviews, fetchTotalXp } from '../application/actions/SessionActions'; -import { AssessmentStatuses, AssessmentType, ProgressStatuses } from '../assessment/AssessmentTypes'; +import { + AssessmentStatuses, + AssessmentType, + ProgressStatuses +} from '../assessment/AssessmentTypes'; import Constants from '../utils/Constants'; import { useSession } from '../utils/Hooks'; import ProfileCard from './ProfileCard'; From f608c8772b05f79b0fd8dc00310abfb442e8fa85 Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Sun, 14 Apr 2024 12:49:10 +0800 Subject: [PATCH 26/69] prettier checks --- src/features/grading/GradingTypes.ts | 8 +- .../grading/subcomponents/GradingActions.tsx | 108 ++++++++---------- .../subcomponents/GradingSubmissionsTable.tsx | 16 ++- 3 files changed, 58 insertions(+), 74 deletions(-) diff --git a/src/features/grading/GradingTypes.ts b/src/features/grading/GradingTypes.ts index fb1562986a..21cb43621f 100644 --- a/src/features/grading/GradingTypes.ts +++ b/src/features/grading/GradingTypes.ts @@ -17,7 +17,7 @@ export enum ColumnFields { studentName = 'studentName', studentUsername = 'studentUsername', groupName = 'groupName', - submissionStatus = 'submissionStatus', + progressStatus = 'progressStatus', gradingStatus = 'gradingStatus', xp = 'xp', actionsIndex = 'actionsIndex' @@ -117,7 +117,7 @@ export enum ColumnName { studentName = 'Student(s)', studentUsername = 'Username(s)', groupName = 'Group', - submissionStatus = 'Progress', + progressStatus = 'Progress', xp = 'Raw XP (+Bonus)', actionsIndex = 'Actions' } @@ -128,7 +128,7 @@ export type SortStateProperties = { studentName: SortStates; studentUsername: SortStates; groupName: SortStates; - submissionStatus: SortStates; + progressStatus: SortStates; xp: SortStates; actionsIndex: SortStates; }; @@ -139,7 +139,7 @@ export type IGradingTableRow = { studentName: string; studentUsername: string; groupName: string; - submissionStatus: string; + progressStatus: string; xp: string; actionsIndex: number; // actions needs a column, but only submission ID data, so it stores submission ID progress: ProgressStatus; diff --git a/src/pages/academy/grading/subcomponents/GradingActions.tsx b/src/pages/academy/grading/subcomponents/GradingActions.tsx index 44932ffa71..8614bdca0b 100644 --- a/src/pages/academy/grading/subcomponents/GradingActions.tsx +++ b/src/pages/academy/grading/subcomponents/GradingActions.tsx @@ -1,6 +1,5 @@ import { Button, Icon } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; -import React, { useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { Link } from 'react-router-dom'; import { @@ -74,76 +73,63 @@ const GradingActions: React.FC = ({ submissionId, style, progress, filter } }; - useEffect(() => { - console.log(progress); - }, []) - return ( - { - (filterMode) - ? - - - - - : <> - } - - { - !(progress !== ProgressStatuses.graded && progress !== ProgressStatuses.submitted) - ? - : <> - } + {filterMode ? ( + + + + + + ) : ( + <> + )} - { - !(progress !== ProgressStatuses.graded && progress !== ProgressStatuses.submitted) - ? - : <> - } + {!(progress !== ProgressStatuses.graded && progress !== ProgressStatuses.submitted) ? ( + + ) : ( + <> + )} - { - !(progress !== ProgressStatuses.graded) - ? - : <> - } + {!(progress !== ProgressStatuses.graded && progress !== ProgressStatuses.submitted) ? ( + + ) : ( + <> + )} - { - !(progress !== ProgressStatuses.published) - ? - : <> - } + {!(progress !== ProgressStatuses.graded) ? ( + + ) : ( + <> + )} + {!(progress !== ProgressStatuses.published) ? ( + + ) : ( + <> + )} ); }; diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 266bb8c3e4..a2f28cbd52 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -57,7 +57,7 @@ export const freshSortState: SortStateProperties = { studentName: SortStates.NONE, studentUsername: SortStates.NONE, groupName: SortStates.NONE, - submissionStatus: SortStates.NONE, + progressStatus: SortStates.NONE, xp: SortStates.NONE, actionsIndex: SortStates.NONE }; @@ -289,17 +289,16 @@ const GradingSubmissionTable: React.FC = ({ } }); - // TODO TO REMOVE this or gradingStatus cols.push({ ...generalColProperties, - headerName: ColumnName.submissionStatus, - field: ColumnFields.submissionStatus, + headerName: ColumnName.progressStatus, + field: ColumnFields.progressStatus, cellRendererSelector: (params: ICellRendererParams) => { return params.data !== undefined ? { component: GradingFilterable, params: { - value: params.data.submissionStatus, + value: params.data.progressStatus, children: [], filterMode: filterMode } @@ -397,13 +396,12 @@ const GradingSubmissionTable: React.FC = ({ columnFilters.reduce( (doesItContain, currentFilter) => doesItContain || - (currentFilter.id === ColumnFields.submissionStatus && - currentFilter.value !== 'submitted'), + (currentFilter.id === ColumnFields.progressStatus && currentFilter.value !== 'submitted'), false ) ) { setColumnFilters((prev: ColumnFiltersState) => - prev.filter(filter => filter.id !== ColumnFields.submissionStatus) + prev.filter(filter => filter.id !== ColumnFields.progressStatus) ); resetPage(); return; @@ -447,7 +445,7 @@ const GradingSubmissionTable: React.FC = ({ ? currentSubmission.studentUsernames.join(', ') : '', groupName: currentSubmission.groupName, - submissionStatus: currentSubmission.submissionStatus, + progressStatus: currentSubmission.submissionStatus, xp: currentSubmission.currentXp + ' (+' + From e849778d2d4551f1f75fd9d6832f87f47e7e9633 Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Sun, 14 Apr 2024 13:09:56 +0800 Subject: [PATCH 27/69] minor ui adjustments --- .../academy/grading/subcomponents/GradingSubmissionsTable.tsx | 2 +- src/styles/_academy.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index a2f28cbd52..cff98f8cab 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -322,13 +322,13 @@ const GradingSubmissionTable: React.FC = ({ headerName: ColumnName.actionsIndex, field: ColumnFields.actionsIndex, flex: 1.25, + headerClass: generalColProperties.headerClass + ' grading-left-align', cellRendererSelector: (params: ICellRendererParams) => { return params.data !== undefined ? { component: GradingActions, params: { submissionId: params.data.actionsIndex, - style: { justifyContent: 'center' }, progress: params.data.progress, filterMode: filterMode } diff --git a/src/styles/_academy.scss b/src/styles/_academy.scss index 9b9a984015..dd5e4efca4 100644 --- a/src/styles/_academy.scss +++ b/src/styles/_academy.scss @@ -688,7 +688,7 @@ } &.grading-xp-cell { - text-wrap: pretty; + text-wrap: wrap; line-height: 15px; } } From 3c4c93950ef0bd30c93f94c38ef4d0e5f5912c56 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Sun, 14 Apr 2024 13:53:23 +0800 Subject: [PATCH 28/69] Revert change back to raw strings --- .../grading/subcomponents/GradingBadges.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pages/academy/grading/subcomponents/GradingBadges.tsx b/src/pages/academy/grading/subcomponents/GradingBadges.tsx index 50278d6b06..6fe9165d20 100644 --- a/src/pages/academy/grading/subcomponents/GradingBadges.tsx +++ b/src/pages/academy/grading/subcomponents/GradingBadges.tsx @@ -49,20 +49,20 @@ const BADGE_COLORS = Object.freeze({ paths: AVAILABLE_COLORS.sky, // submission status - autograded: AVAILABLE_COLORS.purple, - not_attempted: AVAILABLE_COLORS.gray, - attempting: AVAILABLE_COLORS.red, - attempted: AVAILABLE_COLORS.red, + [ProgressStatuses.autograded]: AVAILABLE_COLORS.purple, + [ProgressStatuses.not_attempted]: AVAILABLE_COLORS.gray, + [ProgressStatuses.attempting]: AVAILABLE_COLORS.red, + [ProgressStatuses.attempted]: AVAILABLE_COLORS.red, // grading status - submitted: AVAILABLE_COLORS.yellow, - graded: AVAILABLE_COLORS.green, - published: AVAILABLE_COLORS.blue + [ProgressStatuses.submitted]: AVAILABLE_COLORS.yellow, + [ProgressStatuses.graded]: AVAILABLE_COLORS.green, + [ProgressStatuses.published]: AVAILABLE_COLORS.blue }); export function getBadgeColorFromLabel(label: string) { const maybeKey = label.toLowerCase() as keyof typeof BADGE_COLORS; - return BADGE_COLORS[maybeKey] || AVAILABLE_COLORS.gray; //gray + return BADGE_COLORS[maybeKey] || AVAILABLE_COLORS.gray; } type AssessmentTypeBadgeProps = { From d57f0d81a3078ce6c336a5ed641fdb55d23aa1cc Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Sun, 14 Apr 2024 19:31:15 +0800 Subject: [PATCH 29/69] typescript v5 fixes and richard's comments --- src/commons/grading/GradingText.tsx | 9 +++--- src/commons/mocks/BackendMocks.ts | 14 +++++---- src/commons/sagas/BackendSaga.ts | 12 +++---- src/features/grading/GradingTypes.ts | 8 +++-- src/features/grading/GradingUtils.ts | 14 ++++----- .../grading/subcomponents/GradingBadges.tsx | 31 +++++++++++++++++-- .../subcomponents/GradingSubmissionsTable.tsx | 20 +++++------- .../subcomponents/TeamFormationBadges.tsx | 4 +-- 8 files changed, 71 insertions(+), 41 deletions(-) diff --git a/src/commons/grading/GradingText.tsx b/src/commons/grading/GradingText.tsx index c451260816..163d6254d9 100644 --- a/src/commons/grading/GradingText.tsx +++ b/src/commons/grading/GradingText.tsx @@ -1,18 +1,19 @@ import { Text } from '@blueprintjs/core'; +import { Classes } from '@blueprintjs/core'; type GradingTextProps = { children?: React.ReactNode; style?: React.CSSProperties; - secondaryText?: boolean; + isSecondaryText?: boolean; className?: string; } & React.RefAttributes; const GradingText: React.FC = ({ children, style, - secondaryText, + isSecondaryText, className = '' -}: GradingTextProps) => { +}) => { const defaultStyle: React.CSSProperties = { width: 'max-content', margin: 'auto 0' @@ -20,7 +21,7 @@ const GradingText: React.FC = ({ return ( {children} diff --git a/src/commons/mocks/BackendMocks.ts b/src/commons/mocks/BackendMocks.ts index db6ffbd65b..1e349362db 100644 --- a/src/commons/mocks/BackendMocks.ts +++ b/src/commons/mocks/BackendMocks.ts @@ -5,7 +5,8 @@ import { FETCH_GROUP_GRADING_SUMMARY } from '../../features/dashboard/DashboardT import { GradingOverviews, GradingQuery, - GradingQuestion + GradingQuestion, + SortStates } from '../../features/grading/GradingTypes'; import { OverallState, @@ -186,17 +187,18 @@ export function* mockBackendSaga(): SagaIterator { sortBy: allColsSortStates.sortBy, sortDirection: '' }; - for (const key in allColsSortStates.currentState) { - if (allColsSortStates.sortBy === key) { - if (allColsSortStates.currentState[key] !== 'sort') { + + Object.keys(allColsSortStates.currentState).forEach(key => { + if (allColsSortStates.sortBy === key && key != '') { + if (allColsSortStates.currentState[key] !== SortStates.NONE) { sortedBy.sortDirection = allColsSortStates.currentState[key]; } else { sortedBy.sortBy = ''; sortedBy.sortDirection = ''; } - break; } - } + }); + const gradingOverviews = yield call(() => mockFetchGradingOverview(accessToken, filterToGroup, pageParams, filterParams, sortedBy) ); diff --git a/src/commons/sagas/BackendSaga.ts b/src/commons/sagas/BackendSaga.ts index 407dba2abb..e1335fce9b 100644 --- a/src/commons/sagas/BackendSaga.ts +++ b/src/commons/sagas/BackendSaga.ts @@ -13,7 +13,8 @@ import { GradingOverview, GradingOverviews, GradingQuery, - GradingQuestion + GradingQuestion, + SortStates } from '../../features/grading/GradingTypes'; import { ASSIGN_ENTRIES_FOR_VOTING, @@ -475,17 +476,16 @@ function* BackendSaga(): SagaIterator { sortDirection: '' }; - for (const key in allColsSortStates.currentState) { - if (allColsSortStates.sortBy === key) { - if (allColsSortStates.currentState[key] !== 'sort') { + Object.keys(allColsSortStates.currentState).forEach(key => { + if (allColsSortStates.sortBy === key && key != '') { + if (allColsSortStates.currentState[key] !== SortStates.NONE) { sortedBy.sortDirection = allColsSortStates.currentState[key]; } else { sortedBy.sortBy = ''; sortedBy.sortDirection = ''; } - break; } - } + }); const gradingOverviews: GradingOverviews | null = yield call( getGradingOverviews, diff --git a/src/features/grading/GradingTypes.ts b/src/features/grading/GradingTypes.ts index 21cb43621f..1979bf779f 100644 --- a/src/features/grading/GradingTypes.ts +++ b/src/features/grading/GradingTypes.ts @@ -75,7 +75,7 @@ export type GradingAnswer = GradingQuestion[]; export type AllColsSortStates = { currentState: SortStateProperties; - sortBy: string; + sortBy: ColumnFieldsKeys | ''; }; export type ColumnFiltersState = ColumnFilter[]; @@ -85,7 +85,7 @@ export type ColumnFilter = { value: unknown; }; -export type GradingColumnVisibility = string[]; +export type GradingColumnVisibility = ColumnFieldsKeys[]; export type GradingAssessment = { coverPicture: string; @@ -122,6 +122,8 @@ export enum ColumnName { actionsIndex = 'Actions' } +export type ColumnFieldsKeys = keyof typeof ColumnName; + export type SortStateProperties = { assessmentName: SortStates; assessmentType: SortStates; @@ -133,6 +135,8 @@ export type SortStateProperties = { actionsIndex: SortStates; }; +export type SortStatePropertiesTypes = keyof SortStateProperties; + export type IGradingTableRow = { assessmentName: string; assessmentType: string; diff --git a/src/features/grading/GradingUtils.ts b/src/features/grading/GradingUtils.ts index 37b66eb077..d8c5488576 100644 --- a/src/features/grading/GradingUtils.ts +++ b/src/features/grading/GradingUtils.ts @@ -6,7 +6,7 @@ import { ProgressStatuses } from 'src/commons/assessment/AssessmentTypes'; -import { GradingOverview } from './GradingTypes'; +import { ColumnFields, GradingOverview } from './GradingTypes'; export const exportGradingCSV = (gradingOverviews: GradingOverview[] | undefined) => { if (!gradingOverviews) return; @@ -71,17 +71,17 @@ export const exportGradingCSV = (gradingOverviews: GradingOverview[] | undefined // TODO: Two-way conversion function for frontend-backend parameter conversion export const convertFilterToBackendParams = (column: ColumnFilter) => { switch (column.id) { - case 'assessmentName': + case ColumnFields.assessmentName: return { title: column.value }; - case 'assessmentType': + case ColumnFields.assessmentType: return { type: column.value }; - case 'studentName': + case ColumnFields.studentName: return { name: column.value }; - case 'studentUsername': + case ColumnFields.studentUsername: return { username: column.value }; - case 'progress': + case ColumnFields.progressStatus: return progressStatusToBackendParams(column.value as ProgressStatus); - case 'groupName': + case ColumnFields.groupName: return { groupName: column.value }; default: return {}; diff --git a/src/pages/academy/grading/subcomponents/GradingBadges.tsx b/src/pages/academy/grading/subcomponents/GradingBadges.tsx index 6fe9165d20..aeee0d7089 100644 --- a/src/pages/academy/grading/subcomponents/GradingBadges.tsx +++ b/src/pages/academy/grading/subcomponents/GradingBadges.tsx @@ -9,12 +9,12 @@ declare type Size = (typeof sizeValues)[number]; interface BadgeProps { text: string; - color?: string; + color?: string[]; size?: Size; icon?: () => ReactNode; } -const Badge: React.FC = (props: BadgeProps) => { +export const Badge: React.FC = (props: BadgeProps) => { return (
= ({ resizable: false, sortable: true, headerComponentParams: { - hideColumn: (id: string) => handleColumnFilterAdd(id), - updateSortState: (affectedID: ColumnFields, sortDirection: SortStates) => { + hideColumn: (id: ColumnFieldsKeys) => handleColumnFilterAdd(id), + updateSortState: (affectedID: ColumnFieldsKeys, sortDirection: SortStates) => { if (!disabledSortCols.includes(affectedID)) { const newState: SortStateProperties = { ...freshSortState }; newState[affectedID] = sortDirection; @@ -386,7 +387,7 @@ const GradingSubmissionTable: React.FC = ({ } }; - const handleColumnFilterAdd = (toAdd: string) => { + const handleColumnFilterAdd = (toAdd: ColumnFieldsKeys) => { setHiddenColumns((prev: GradingColumnVisibility) => [...prev, toAdd]); }; @@ -492,16 +493,11 @@ const GradingSubmissionTable: React.FC = ({ style={{ marginTop: '0.5rem' }} > - Columns Hidden: + Columns Hidden: { - for (const item in ColumnName) { - if (ColumnFields[item] === id) { - return ColumnName[item]; - } - } - return ''; + filtersName={hiddenColumns.map((id: ColumnFieldsKeys) => { + return ColumnName[id]; })} onFilterRemove={handleColumnFilterRemove} /> @@ -519,7 +515,7 @@ const GradingSubmissionTable: React.FC = ({ - + {columnFilters.length > 0 ? ( 'Filters: ' ) : filterMode === true ? ( diff --git a/src/pages/academy/teamFormation/subcomponents/TeamFormationBadges.tsx b/src/pages/academy/teamFormation/subcomponents/TeamFormationBadges.tsx index 27a1b304e0..3fe2af04f5 100644 --- a/src/pages/academy/teamFormation/subcomponents/TeamFormationBadges.tsx +++ b/src/pages/academy/teamFormation/subcomponents/TeamFormationBadges.tsx @@ -3,7 +3,7 @@ import { IconNames } from '@blueprintjs/icons'; import { ColumnFilter } from '@tanstack/react-table'; import { Badge } from '@tremor/react'; -import { getBadgeColorFromLabel } from '../../grading/subcomponents/GradingBadges'; +import { getBadgeColorFromLabelLegacy } from '../../grading/subcomponents/GradingBadges'; type FilterBadgeProps = { filter: ColumnFilter; @@ -18,7 +18,7 @@ const FilterBadge: React.FC = ({ filter, onRemove }) => { } - color={getBadgeColorFromLabel(filterValue)} + color={getBadgeColorFromLabelLegacy(filterValue)} /> ); From 3c5edf10cbc04c248c6dc405385388d22d2e4e01 Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Sun, 14 Apr 2024 19:52:01 +0800 Subject: [PATCH 30/69] more typescript v5 fixes --- .../application/actions/__tests__/SessionActions.ts | 8 ++++++-- src/features/grading/GradingTypes.ts | 5 +++-- .../grading/subcomponents/GradingSubmissionsTable.tsx | 8 ++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/commons/application/actions/__tests__/SessionActions.ts b/src/commons/application/actions/__tests__/SessionActions.ts index 33ca255ea5..52f4f341c9 100644 --- a/src/commons/application/actions/__tests__/SessionActions.ts +++ b/src/commons/application/actions/__tests__/SessionActions.ts @@ -6,7 +6,11 @@ import { } from 'src/features/grading/GradingUtils'; import { freshSortState } from 'src/pages/academy/grading/subcomponents/GradingSubmissionsTable'; -import { GradingOverviews, GradingQuery } from '../../../../features/grading/GradingTypes'; +import { + ColumnFields, + GradingOverviews, + GradingQuery +} from '../../../../features/grading/GradingTypes'; import { TeamFormationOverview } from '../../../../features/teamFormation/TeamFormationTypes'; import { Assessment, @@ -192,7 +196,7 @@ test('fetchGradingOverviews generates correct action object', () => { const publishedFilter = unpublishedToBackendParams(true); const pageParams = { offset: 123, pageSize: 456 }; const filterParams = { abc: 'xxx', def: 'yyy' }; - const allColsSortStates = { currentState: freshSortState, sortBy: '' }; + const allColsSortStates = { currentState: freshSortState, sortBy: ColumnFields.assessmentName }; const action = fetchGradingOverviews( filterToGroup, publishedFilter, diff --git a/src/features/grading/GradingTypes.ts b/src/features/grading/GradingTypes.ts index 1979bf779f..d84c98982c 100644 --- a/src/features/grading/GradingTypes.ts +++ b/src/features/grading/GradingTypes.ts @@ -18,7 +18,6 @@ export enum ColumnFields { studentUsername = 'studentUsername', groupName = 'groupName', progressStatus = 'progressStatus', - gradingStatus = 'gradingStatus', xp = 'xp', actionsIndex = 'actionsIndex' } @@ -122,7 +121,9 @@ export enum ColumnName { actionsIndex = 'Actions' } -export type ColumnFieldsKeys = keyof typeof ColumnName; +export type ColumnNameKeys = keyof typeof ColumnName; + +export type ColumnFieldsKeys = keyof typeof ColumnFields; export type SortStateProperties = { assessmentName: SortStates; diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index da6adab33b..d381de33a0 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -23,6 +23,7 @@ import { ColumnFilter, ColumnFiltersState, ColumnName, + ColumnNameKeys, GradingColumnVisibility, GradingSubmissionTableProps, IGradingTableProperties, @@ -66,7 +67,6 @@ export const freshSortState: SortStateProperties = { const disabledEditModeCols: string[] = [ColumnFields.actionsIndex]; const disabledFilterModeCols: string[] = [ - ColumnFields.gradingStatus, ColumnFields.xp, ColumnFields.actionsIndex ]; @@ -114,8 +114,8 @@ const GradingSubmissionTable: React.FC = ({ resizable: false, sortable: true, headerComponentParams: { - hideColumn: (id: ColumnFieldsKeys) => handleColumnFilterAdd(id), - updateSortState: (affectedID: ColumnFieldsKeys, sortDirection: SortStates) => { + hideColumn: (id: ColumnNameKeys) => handleColumnFilterAdd(id), + updateSortState: (affectedID: ColumnNameKeys, sortDirection: SortStates) => { if (!disabledSortCols.includes(affectedID)) { const newState: SortStateProperties = { ...freshSortState }; newState[affectedID] = sortDirection; @@ -387,7 +387,7 @@ const GradingSubmissionTable: React.FC = ({ } }; - const handleColumnFilterAdd = (toAdd: ColumnFieldsKeys) => { + const handleColumnFilterAdd = (toAdd: ColumnNameKeys) => { setHiddenColumns((prev: GradingColumnVisibility) => [...prev, toAdd]); }; From fbba60acec9dd9b320d2a1c98f1c269f3258db66 Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Sun, 14 Apr 2024 19:57:14 +0800 Subject: [PATCH 31/69] prettier --- .../grading/subcomponents/GradingSubmissionsTable.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index d381de33a0..f48daf5596 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -66,10 +66,7 @@ export const freshSortState: SortStateProperties = { const disabledEditModeCols: string[] = [ColumnFields.actionsIndex]; -const disabledFilterModeCols: string[] = [ - ColumnFields.xp, - ColumnFields.actionsIndex -]; +const disabledFilterModeCols: string[] = [ColumnFields.xp, ColumnFields.actionsIndex]; const disabledSortCols: string[] = [ColumnFields.actionsIndex]; From b03cfe9d214531c27cb262274e908e91cdf13896 Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Sun, 14 Apr 2024 23:23:44 +0800 Subject: [PATCH 32/69] some refactoring and bug fixing --- src/features/grading/GradingTypes.ts | 7 ++-- src/pages/academy/grading/Grading.tsx | 33 +++++++++++++++---- .../grading/subcomponents/GradingBadges.tsx | 19 +---------- .../subcomponents/GradingSubmissionsTable.tsx | 25 +++++++++----- 4 files changed, 48 insertions(+), 36 deletions(-) diff --git a/src/features/grading/GradingTypes.ts b/src/features/grading/GradingTypes.ts index d84c98982c..67a92cfe82 100644 --- a/src/features/grading/GradingTypes.ts +++ b/src/features/grading/GradingTypes.ts @@ -22,6 +22,8 @@ export enum ColumnFields { actionsIndex = 'actionsIndex' } +export type ColumnFieldsKeys = keyof typeof ColumnFields; + export enum SortStates { ASC = 'sort-asc', DESC = 'sort-desc', @@ -123,8 +125,6 @@ export enum ColumnName { export type ColumnNameKeys = keyof typeof ColumnName; -export type ColumnFieldsKeys = keyof typeof ColumnFields; - export type SortStateProperties = { assessmentName: SortStates; assessmentType: SortStates; @@ -144,10 +144,9 @@ export type IGradingTableRow = { studentName: string; studentUsername: string; groupName: string; - progressStatus: string; + progressStatus: ProgressStatus; xp: string; actionsIndex: number; // actions needs a column, but only submission ID data, so it stores submission ID - progress: ProgressStatus; courseID: number; }; diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index ac8057d66e..f29b0db31b 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -53,6 +53,7 @@ const Grading: React.FC = () => { const [pageSize, setPageSize] = useState(10); const [showAllSubmissions, setShowAllSubmissions] = useState(false); + const [refreshQueryData, setRefreshQueryData] = useState({ page: 1, filterParams: {} }); const [refreshQueried, setRefreshQueried] = useState(false); // for callback (immediately becomes false) const [animateRefresh, setAnimateRefresh] = useState(false); // for animation (becomes false on animation end) const [submissions, setSubmissions] = useState([]); @@ -63,11 +64,7 @@ const Grading: React.FC = () => { const updateGradingOverviewsCallback = useCallback( (page: number, filterParams: Object) => { - // Prevents es-lint missing dependency warning - if (refreshQueried) { - return setRefreshQueried(false); - } - + setRefreshQueryData({ page, filterParams }); dispatch(setGradingHasLoadedBefore()); dispatch(increaseRequestCounter()); dispatch( @@ -80,9 +77,33 @@ const Grading: React.FC = () => { ) ); }, - [dispatch, showAllGroups, showAllSubmissions, pageSize, allColsSortStates, refreshQueried] + [dispatch, showAllGroups, showAllSubmissions, pageSize, allColsSortStates] ); + useEffect(() => { + if (refreshQueried) { + dispatch(increaseRequestCounter()); + dispatch( + fetchGradingOverviews( + showAllGroups, + unpublishedToBackendParams(showAllSubmissions), + paginationToBackendParams(refreshQueryData.page, pageSize), + refreshQueryData.filterParams, + allColsSortStates + ) + ); + setRefreshQueried(false); + } + }, [ + dispatch, + showAllGroups, + showAllSubmissions, + pageSize, + allColsSortStates, + refreshQueried, + refreshQueryData + ]); + useEffect(() => { setSubmissions( gradingOverviews?.data?.map(e => diff --git a/src/pages/academy/grading/subcomponents/GradingBadges.tsx b/src/pages/academy/grading/subcomponents/GradingBadges.tsx index aeee0d7089..1d0d5b0197 100644 --- a/src/pages/academy/grading/subcomponents/GradingBadges.tsx +++ b/src/pages/academy/grading/subcomponents/GradingBadges.tsx @@ -179,21 +179,4 @@ const ProgressStatusBadge: React.FC = ({ progress }) = return ; }; -// TO BE REMOVED -type SubmissionStatusBadgeProps = { - status: string; -}; - -// TO BE REMOVED -const SubmissionStatusBadge: React.FC = ({ status }) => { - const statusText = status.charAt(0).toUpperCase() + status.slice(1); - return ; -}; - -export { - AssessmentTypeBadge, - ColumnFilterBadge, - FilterBadge, - ProgressStatusBadge, - SubmissionStatusBadge -}; +export { AssessmentTypeBadge, ColumnFilterBadge, FilterBadge, ProgressStatusBadge }; diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index f48daf5596..74a7e17a4d 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -9,6 +9,7 @@ import { debounce } from 'lodash'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useNavigate } from 'react-router-dom'; +import { ProgressStatuses } from 'src/commons/assessment/AssessmentTypes'; import GradingFlex from 'src/commons/grading/GradingFlex'; import GradingText from 'src/commons/grading/GradingText'; import { useTypedSelector } from 'src/commons/utils/Hooks'; @@ -297,7 +298,7 @@ const GradingSubmissionTable: React.FC = ({ component: GradingFilterable, params: { value: params.data.progressStatus, - children: [], + children: [], filterMode: filterMode } } @@ -327,7 +328,7 @@ const GradingSubmissionTable: React.FC = ({ component: GradingActions, params: { submissionId: params.data.actionsIndex, - progress: params.data.progress, + progress: params.data.progressStatus, filterMode: filterMode } } @@ -394,7 +395,8 @@ const GradingSubmissionTable: React.FC = ({ columnFilters.reduce( (doesItContain, currentFilter) => doesItContain || - (currentFilter.id === ColumnFields.progressStatus && currentFilter.value !== 'submitted'), + (currentFilter.id === ColumnFields.progressStatus && + String(currentFilter.value).toLowerCase() !== ProgressStatuses.graded), false ) ) { @@ -429,7 +431,7 @@ const GradingSubmissionTable: React.FC = ({ const sameData: boolean = submissions.reduce( (sameData, currentSubmission, index) => { - newData.push({ + const newRow: IGradingTableRow = { assessmentName: currentSubmission.assessmentName, assessmentType: currentSubmission.assessmentType, studentName: currentSubmission.studentName @@ -443,7 +445,7 @@ const GradingSubmissionTable: React.FC = ({ ? currentSubmission.studentUsernames.join(', ') : '', groupName: currentSubmission.groupName, - progressStatus: currentSubmission.submissionStatus, + progressStatus: currentSubmission.progress, xp: currentSubmission.currentXp + ' (+' + @@ -451,10 +453,17 @@ const GradingSubmissionTable: React.FC = ({ ') / ' + currentSubmission.maxXp, actionsIndex: currentSubmission.submissionId, - progress: currentSubmission.progress, courseID: courseId! - }); - return sameData && currentSubmission.submissionId === rowData?.[index]?.actionsIndex; + }; + newData.push(newRow); + return ( + sameData && + newRow.actionsIndex === rowData?.[index]?.actionsIndex && + newRow.studentUsername === rowData?.[index]?.studentUsername && + newRow.groupName === rowData?.[index]?.groupName && + newRow.progressStatus === rowData?.[index]?.progressStatus && + newRow.xp === rowData?.[index]?.xp + ); }, submissions.length === rowData?.length ); From 505de5fe8fea32ba8f9c756f02858162dd651eb2 Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Tue, 16 Apr 2024 23:14:45 +0800 Subject: [PATCH 33/69] null value error in empty cell & wider actions col --- .../grading/subcomponents/GradingSubmissionsTable.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 74a7e17a4d..5d5f440145 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -320,7 +320,7 @@ const GradingSubmissionTable: React.FC = ({ ...generalColProperties, headerName: ColumnName.actionsIndex, field: ColumnFields.actionsIndex, - flex: 1.25, + flex: 1.4, headerClass: generalColProperties.headerClass + ' grading-left-align', cellRendererSelector: (params: ICellRendererParams) => { return params.data !== undefined @@ -345,6 +345,9 @@ const GradingSubmissionTable: React.FC = ({ if (!filterMode && !disabledEditModeCols.includes(colClicked)) { navigate(`/courses/${courseId}/grading/${event.data.actionsIndex}`); } else if (filterMode && !disabledFilterModeCols.includes(colClicked)) { + if (event.data[colClicked] === null || event.data[colClicked] === '') { + return; + } handleFilterAdd({ id: colClicked, value: event.data[colClicked] }); } }; From d161760597556f5ab1a59c77cd03ed77b664e9fa Mon Sep 17 00:00:00 2001 From: Infinity <2poh.junkang@gmail.com> Date: Tue, 16 Apr 2024 23:23:50 +0800 Subject: [PATCH 34/69] added submitted to unpublished allowed filters --- .../academy/grading/subcomponents/GradingSubmissionsTable.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 5d5f440145..cff0078564 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -399,7 +399,8 @@ const GradingSubmissionTable: React.FC = ({ (doesItContain, currentFilter) => doesItContain || (currentFilter.id === ColumnFields.progressStatus && - String(currentFilter.value).toLowerCase() !== ProgressStatuses.graded), + String(currentFilter.value).toLowerCase() !== ProgressStatuses.graded && + String(currentFilter.value).toLowerCase() !== ProgressStatuses.submitted), false ) ) { From c4134c0eb2739a413db789cd124f6f2ea6b4275a Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Sun, 12 May 2024 15:49:03 +0800 Subject: [PATCH 35/69] Fix format --- src/commons/workspace/WorkspaceTypes.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commons/workspace/WorkspaceTypes.ts b/src/commons/workspace/WorkspaceTypes.ts index 4c364b368f..12e7e93467 100644 --- a/src/commons/workspace/WorkspaceTypes.ts +++ b/src/commons/workspace/WorkspaceTypes.ts @@ -13,7 +13,8 @@ export const EVAL_SILENT = 'EVAL_SILENT'; export const INCREMENT_REQUEST_COUNTER = 'INCREMENT_REQUEST_COUNTER'; export const SET_GRADING_HAS_LOADED_BEFORE = 'SET_GRADING_HAS_LOADED_BEFORE'; export const UPDATE_GRADING_COLUMN_VISIBILITY = 'UPDATE_GRADING_COLUMN_VISIBILITY'; -export const UPDATE_ALL_COLS_SORT_STATES = 'UPDATE_ALL_COLS_SORT_STATES';export const UPDATE_LAST_DEBUGGER_RESULT = 'UPDATE_LAST_DEBUGGER_RESULT'; +export const UPDATE_ALL_COLS_SORT_STATES = 'UPDATE_ALL_COLS_SORT_STATES'; +export const UPDATE_LAST_DEBUGGER_RESULT = 'UPDATE_LAST_DEBUGGER_RESULT'; export const UPDATE_LAST_NON_DET_RESULT = 'UPDATE_LAST_NON_DET_RESULT'; export type WorkspaceLocation = keyof WorkspaceManagerState; From 7449edf3ed130d8153d8eb9f9944386a7d23a80a Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Sun, 12 May 2024 15:50:50 +0800 Subject: [PATCH 36/69] Fix compile error post-merge --- 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 94e4b4d633..efb55499ce 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -84,7 +84,7 @@ const Grading: React.FC = () => { if (refreshQueried) { dispatch(increaseRequestCounter()); dispatch( - fetchGradingOverviews( + SessionActions.fetchGradingOverviews( showAllGroups, unpublishedToBackendParams(showAllSubmissions), paginationToBackendParams(refreshQueryData.page, pageSize), From 5428c577e9856e9d9c20928edfab96edcf5737d5 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Sun, 12 May 2024 15:54:56 +0800 Subject: [PATCH 37/69] Refactor GradingFlex * Add React import * Remove unnecessary type annotations * Extract default styles outside component * Remove unused props --- src/commons/grading/GradingFlex.tsx | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/commons/grading/GradingFlex.tsx b/src/commons/grading/GradingFlex.tsx index 4755d8b3ac..9a7f0f4dfc 100644 --- a/src/commons/grading/GradingFlex.tsx +++ b/src/commons/grading/GradingFlex.tsx @@ -1,31 +1,30 @@ import { Property } from 'csstype'; +import React from 'react'; -type GradingFlexProps = { +const defaultStyles: React.CSSProperties = { + display: 'flex' +}; + +type Props = { justifyContent?: Property.JustifyContent; alignItems?: Property.AlignItems; flexDirection?: Property.FlexDirection; children?: React.ReactNode; style?: React.CSSProperties; className?: string; -} & React.RefAttributes; +}; -const GradingFlex: React.FC = ({ +const GradingFlex: React.FC = ({ justifyContent, alignItems, flexDirection, children, style, className -}: GradingFlexProps) => { - const defaultStyle: React.CSSProperties = { - display: 'flex', - justifyContent: justifyContent, - alignItems: alignItems, - flexDirection: flexDirection - }; - +}) => { + const styles: React.CSSProperties = { ...style, justifyContent, alignItems, flexDirection }; return ( -
+
{children}
); From ceebfb9126794be56bf9c673164f0a1117765a16 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Sun, 12 May 2024 15:59:15 +0800 Subject: [PATCH 38/69] Refactor GradingText * Add React import * Use `classnames` utility * Use `Classes` object instead of raw string CSS API * Move constant default styles out of component * Remove unused props --- src/commons/grading/GradingText.tsx | 30 +++++++++++++---------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/commons/grading/GradingText.tsx b/src/commons/grading/GradingText.tsx index 163d6254d9..a96d7bb75a 100644 --- a/src/commons/grading/GradingText.tsx +++ b/src/commons/grading/GradingText.tsx @@ -1,28 +1,24 @@ -import { Text } from '@blueprintjs/core'; -import { Classes } from '@blueprintjs/core'; +import { Classes, Text } from '@blueprintjs/core'; +import classNames from 'classnames'; +import React from 'react'; -type GradingTextProps = { +const defaultStyles: React.CSSProperties = { + width: 'max-content', + margin: 'auto 0' +}; + +type Props = { children?: React.ReactNode; style?: React.CSSProperties; isSecondaryText?: boolean; className?: string; -} & React.RefAttributes; - -const GradingText: React.FC = ({ - children, - style, - isSecondaryText, - className = '' -}) => { - const defaultStyle: React.CSSProperties = { - width: 'max-content', - margin: 'auto 0' - }; +}; +const GradingText: React.FC = ({ children, style, isSecondaryText, className }) => { return ( {children} From 916680c67eaef9ea408674726b0cf4fe11e45877 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Sun, 12 May 2024 16:03:24 +0800 Subject: [PATCH 39/69] Update BackendSaga.ts * Use strict inequality * Use `Object.entries` to iterate over both key and value --- src/commons/sagas/BackendSaga.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commons/sagas/BackendSaga.ts b/src/commons/sagas/BackendSaga.ts index f6a493a883..0b9a05da87 100644 --- a/src/commons/sagas/BackendSaga.ts +++ b/src/commons/sagas/BackendSaga.ts @@ -413,10 +413,10 @@ const newBackendSagaOne = combineSagaHandlers(SessionActions, { sortDirection: '' }; - Object.keys(allColsSortStates.currentState).forEach(key => { - if (allColsSortStates.sortBy === key && key != '') { - if (allColsSortStates.currentState[key] !== SortStates.NONE) { - sortedBy.sortDirection = allColsSortStates.currentState[key]; + Object.entries(allColsSortStates.currentState).forEach(([key, value]) => { + if (allColsSortStates.sortBy === key && key !== '') { + if (value !== SortStates.NONE) { + sortedBy.sortDirection = value; } else { sortedBy.sortBy = ''; sortedBy.sortDirection = ''; From 8a1cad8d4dd8a625f89804ed8ac3082f2e125051 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Sun, 12 May 2024 16:07:03 +0800 Subject: [PATCH 40/69] Refactor GradingFilterable.tsx --- .../academy/grading/subcomponents/GradingFilterable.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/academy/grading/subcomponents/GradingFilterable.tsx b/src/pages/academy/grading/subcomponents/GradingFilterable.tsx index 1a1642a293..d3e2d7e941 100644 --- a/src/pages/academy/grading/subcomponents/GradingFilterable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingFilterable.tsx @@ -1,10 +1,12 @@ -type FilterableProps = { +import React from 'react'; + +type Props = { value: string; children?: React.ReactNode; filterMode: boolean; }; -const GradingFilterable: React.FC = ({ value, children, filterMode }) => { +const GradingFilterable: React.FC = ({ value, children, filterMode }) => { return ( - ) : ( - <> )} - {!(progress !== ProgressStatuses.graded && progress !== ProgressStatuses.submitted) ? ( + {!(progress !== ProgressStatuses.graded && progress !== ProgressStatuses.submitted) && ( - ) : ( - <> )} - {!(progress !== ProgressStatuses.graded) ? ( - - ) : ( - <> )} - {!(progress !== ProgressStatuses.published) ? ( - - ) : ( - <> )} ); From 1f4108f6d184c8bc270dc543f2e7f9e0db248f51 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Sun, 12 May 2024 16:21:16 +0800 Subject: [PATCH 42/69] Simplify conditions in conditionals For better readability. --- .../academy/grading/subcomponents/GradingActions.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/academy/grading/subcomponents/GradingActions.tsx b/src/pages/academy/grading/subcomponents/GradingActions.tsx index 1906dde3c3..6bf505845c 100644 --- a/src/pages/academy/grading/subcomponents/GradingActions.tsx +++ b/src/pages/academy/grading/subcomponents/GradingActions.tsx @@ -85,7 +85,7 @@ const GradingActions: React.FC = ({ submissionId, style, progress, filter )} - {!(progress !== ProgressStatuses.graded && progress !== ProgressStatuses.submitted) && ( + {(progress === ProgressStatuses.graded || progress === ProgressStatuses.submitted) && ( )} - {!(progress !== ProgressStatuses.graded && progress !== ProgressStatuses.submitted) && ( + {(progress === ProgressStatuses.graded || progress === ProgressStatuses.submitted) && ( )} - {!(progress !== ProgressStatuses.graded) && ( + {progress === ProgressStatuses.graded && ( )} - {!(progress !== ProgressStatuses.published) && ( + {progress === ProgressStatuses.published && ( )} - {(progress === ProgressStatuses.graded || progress === ProgressStatuses.submitted) && ( + {(isGraded || isSubmitted) && ( )} - {progress === ProgressStatuses.graded && ( + {isGraded && ( )} - {progress === ProgressStatuses.published && ( + {isPublished && ( From 5f88cd2e70f33f86e4892cfb428b19ef202d878d Mon Sep 17 00:00:00 2001 From: unknown <2poh.junkang@gmail.com> Date: Tue, 6 Aug 2024 14:01:25 +0800 Subject: [PATCH 50/69] fixed randomly broken grading table headers and merge conflicts --- .../GradingColumnCustomHeaders.tsx | 7 ++- src/styles/_academy.scss | 63 ++++--------------- 2 files changed, 17 insertions(+), 53 deletions(-) diff --git a/src/pages/academy/grading/subcomponents/GradingColumnCustomHeaders.tsx b/src/pages/academy/grading/subcomponents/GradingColumnCustomHeaders.tsx index bcb0771e90..0cf6e0ced1 100644 --- a/src/pages/academy/grading/subcomponents/GradingColumnCustomHeaders.tsx +++ b/src/pages/academy/grading/subcomponents/GradingColumnCustomHeaders.tsx @@ -2,6 +2,7 @@ import { Icon } from '@blueprintjs/core'; import { CustomHeaderProps } from 'ag-grid-react'; import classNames from 'classnames'; import React, { useEffect, useState } from 'react'; +import GradingFlex from 'src/commons/grading/GradingFlex'; import { useTypedSelector } from 'src/commons/utils/Hooks'; import { SortStates } from 'src/features/grading/GradingTypes'; @@ -30,7 +31,9 @@ const GradingColumnCustomHeaders: React.FC = props => { }, [colsSortState, props.column]); return ( -
+ {props.displayName} {!props.disabledSortCols.includes(props.column.getColId()) && ( @@ -48,7 +51,7 @@ const GradingColumnCustomHeaders: React.FC = props => { >
-
+
); }; diff --git a/src/styles/_academy.scss b/src/styles/_academy.scss index dd5e4efca4..34a6a541f3 100644 --- a/src/styles/_academy.scss +++ b/src/styles/_academy.scss @@ -307,55 +307,6 @@ } } -.ag-grid-controls { - .pagination-details.pagination-details.pagination-details { - /* Chain selectors for increased specificity: set default cursor */ - cursor: default; - font-weight: bold; - - .#{$ns}-button-text { - display: inline-flex; - flex-direction: column; - align-items: center; - } - } -} - -.ag-header-cell.ag-header-cell.ag-header-cell { - /* Chain selectors for increased specificity: reduce internal padding of header cells */ - padding-left: 0.375em; - padding-right: 0.375em; - - .ag-header-cell-label { - /* Increase font size of headers; pad left to centre with filter icon */ - font-size: 1.1em; - font-weight: bold; - justify-content: center; - padding-left: 1em; - } - - &:not(.ag-header-cell-sortable) .ag-header-cell-label { - padding-right: 1em; - } - - .ag-cell-label-container:not(:has(> span.ag-header-icon.ag-header-cell-menu-button)) - > .ag-header-cell-label { - padding-right: 1em; - } -} - -.ag-cell { - font-size: 1.1em; - /* Override to reduce ag-grid default padding */ - padding-left: 0.375em; - padding-right: 0.375em; - text-align: center; -} - -#filterBar { - width: 400px; -} - .md th, .md td { text-align: left; @@ -596,13 +547,15 @@ font-size: 0.8rem; color: #6b7280; font-weight: 600; + margin: auto; + padding: 0 5px; } &:hover { --ag-header-cell-hover-background-color: #e5e7eb; } - &:hover > .grading-table-col-icons { + &:hover .grading-table-col-icons { pointer-events: all; opacity: 1; position: relative; @@ -610,6 +563,10 @@ } } +.grading-table-header-individual { + width: 100%; +} + .grading-table-rows { &.ag-row-hover { --ag-row-hover-color: #f5f5f5; @@ -622,7 +579,7 @@ .ag-header-cell.grading-left-align { span.ag-header-cell-text { - margin: 0 auto 0 0; + margin: auto auto auto 0px; } } @@ -809,6 +766,10 @@ .bp5-icon { margin: 6px; } + + .bp5-popover-target { + max-height: 32px + } } .grading-table-wrapper { From a858c180b4bfec7ea57cb073eb2f4d0257f159f1 Mon Sep 17 00:00:00 2001 From: unknown <2poh.junkang@gmail.com> Date: Tue, 6 Aug 2024 14:08:30 +0800 Subject: [PATCH 51/69] prettier --- src/styles/_academy.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/_academy.scss b/src/styles/_academy.scss index 34a6a541f3..b3a378c1dc 100644 --- a/src/styles/_academy.scss +++ b/src/styles/_academy.scss @@ -768,7 +768,7 @@ } .bp5-popover-target { - max-height: 32px + max-height: 32px; } } From 6d21dca60d890274ad35313ef53cd131eb7c92f0 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Fri, 16 Aug 2024 14:19:31 +0800 Subject: [PATCH 52/69] Reformat post-lint updates --- src/features/grading/GradingTypes.ts | 2 +- .../subcomponents/GradingSubmissionsTable.tsx | 65 +++++++++---------- 2 files changed, 32 insertions(+), 35 deletions(-) diff --git a/src/features/grading/GradingTypes.ts b/src/features/grading/GradingTypes.ts index 67a92cfe82..bb9f4c667c 100644 --- a/src/features/grading/GradingTypes.ts +++ b/src/features/grading/GradingTypes.ts @@ -109,7 +109,7 @@ export type GradingSubmissionTableProps = { totalRows: number; pageSize: number; submissions: GradingOverview[]; - updateEntries: (page: number, filterParams: Object) => void; + updateEntries: (page: number, filterParams: object) => void; }; export enum ColumnName { diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 9c678fa9ab..e1d618f1c0 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -429,44 +429,41 @@ const GradingSubmissionTable: React.FC = ({ if (requestCounter <= 0) { const newData: IGradingTableRow[] = []; - const sameData: boolean = submissions.reduce( - (sameData, currentSubmission, index) => { - const newRow: IGradingTableRow = { - assessmentName: currentSubmission.assessmentName, - assessmentType: currentSubmission.assessmentType, - studentName: currentSubmission.studentName - ? currentSubmission.studentName - : currentSubmission.studentNames + const sameData: boolean = submissions.reduce((sameData, currentSubmission, index) => { + const newRow: IGradingTableRow = { + assessmentName: currentSubmission.assessmentName, + assessmentType: currentSubmission.assessmentType, + studentName: currentSubmission.studentName + ? currentSubmission.studentName + : currentSubmission.studentNames ? currentSubmission.studentNames.join(', ') : '', - studentUsername: currentSubmission.studentUsername - ? currentSubmission.studentUsername - : currentSubmission.studentUsernames + studentUsername: currentSubmission.studentUsername + ? currentSubmission.studentUsername + : currentSubmission.studentUsernames ? currentSubmission.studentUsernames.join(', ') : '', - groupName: currentSubmission.groupName, - progressStatus: currentSubmission.progress, - xp: - currentSubmission.currentXp + - ' (+' + - currentSubmission.xpBonus + - ') / ' + - currentSubmission.maxXp, - actionsIndex: currentSubmission.submissionId, - courseID: courseId! - }; - newData.push(newRow); - return ( - sameData && - newRow.actionsIndex === rowData?.[index]?.actionsIndex && - newRow.studentUsername === rowData?.[index]?.studentUsername && - newRow.groupName === rowData?.[index]?.groupName && - newRow.progressStatus === rowData?.[index]?.progressStatus && - newRow.xp === rowData?.[index]?.xp - ); - }, - submissions.length === rowData?.length - ); + groupName: currentSubmission.groupName, + progressStatus: currentSubmission.progress, + xp: + currentSubmission.currentXp + + ' (+' + + currentSubmission.xpBonus + + ') / ' + + currentSubmission.maxXp, + actionsIndex: currentSubmission.submissionId, + courseID: courseId! + }; + newData.push(newRow); + return ( + sameData && + newRow.actionsIndex === rowData?.[index]?.actionsIndex && + newRow.studentUsername === rowData?.[index]?.studentUsername && + newRow.groupName === rowData?.[index]?.groupName && + newRow.progressStatus === rowData?.[index]?.progressStatus && + newRow.xp === rowData?.[index]?.xp + ); + }, submissions.length === rowData?.length); if (!sameData) { setRowData(newData); From d2db6d23b13739de8299999c0c2517ebd50d2f31 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Mon, 19 Aug 2024 10:22:56 +0800 Subject: [PATCH 53/69] Add TODO --- src/commons/application/ApplicationTypes.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/commons/application/ApplicationTypes.ts b/src/commons/application/ApplicationTypes.ts index c3de49b34b..e0a54c99ac 100644 --- a/src/commons/application/ApplicationTypes.ts +++ b/src/commons/application/ApplicationTypes.ts @@ -440,10 +440,12 @@ export const defaultWorkspaceManager: WorkspaceManagerState = { submissionsTableFilters: { columnFilters: [] }, - columnVisiblity: [], currentSubmission: undefined, currentQuestion: undefined, hasUnsavedChanges: false, + // TODO: The below should be a separate state + // instead of using the grading workspace state + columnVisiblity: [], requestCounter: 0, allColsSortStates: { currentState: freshSortState, From 057dd5a76e4bdbca7cf415002a89baa099192cc1 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Mon, 19 Aug 2024 10:31:52 +0800 Subject: [PATCH 54/69] Remove unused CSS class --- src/styles/_academy.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/styles/_academy.scss b/src/styles/_academy.scss index 2e09fc1e14..89f045a130 100644 --- a/src/styles/_academy.scss +++ b/src/styles/_academy.scss @@ -496,8 +496,7 @@ } .grading-overview-unfilterable-btns, -.grading-overview-filterable-btns, -.grading-overview-rounded-btns { +.grading-overview-filterable-btns { border-radius: 9999px; } From 74aa4e79d5322afca306f2f07db69c971ba63092 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Mon, 19 Aug 2024 10:47:29 +0800 Subject: [PATCH 55/69] Scope most styles to CSS modules --- .../grading/subcomponents/GradingBadges.tsx | 5 +- .../GradingColumnCustomHeaders.tsx | 5 +- .../subcomponents/GradingFilterable.tsx | 5 +- .../subcomponents/GradingSubmissionsTable.tsx | 16 ++-- src/styles/Grading.module.scss | 90 +++++++++++++++++++ src/styles/_academy.scss | 89 ------------------ 6 files changed, 109 insertions(+), 101 deletions(-) create mode 100644 src/styles/Grading.module.scss diff --git a/src/pages/academy/grading/subcomponents/GradingBadges.tsx b/src/pages/academy/grading/subcomponents/GradingBadges.tsx index 90379f5621..1b778797db 100644 --- a/src/pages/academy/grading/subcomponents/GradingBadges.tsx +++ b/src/pages/academy/grading/subcomponents/GradingBadges.tsx @@ -3,6 +3,7 @@ import { IconNames } from '@blueprintjs/icons'; import React from 'react'; import { ProgressStatus, ProgressStatuses } from 'src/commons/assessment/AssessmentTypes'; import { ColumnFilter } from 'src/features/grading/GradingTypes'; +import classes from 'src/styles/Grading.module.scss'; declare const sizeValues: readonly ['xs', 'sm', 'md', 'lg', 'xl']; declare type Size = (typeof sizeValues)[number]; @@ -118,7 +119,7 @@ const ColumnFilterBadge: React.FC = ({ filter, onRemove, return ( @@ -154,7 +154,7 @@ const FilterBadge: React.FC = ({ filter, onRemove }) => { > } + icon={} color={getBadgeColorFromLabel(filterValue)} /> @@ -167,7 +167,7 @@ type ProgressStatusBadgeProps = { const ProgressStatusBadge: React.FC = ({ progress }) => { const statusText = progress.charAt(0).toUpperCase() + progress.slice(1); - const badgeIcon = () => ( + const badgeIcon = (