From 7612c87456bb0788c28bd3d81a1576da010b685e Mon Sep 17 00:00:00 2001 From: hello Date: Fri, 26 Jan 2024 11:02:39 +0800 Subject: [PATCH 001/102] hello world. --- blablabla.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 blablabla.txt diff --git a/blablabla.txt b/blablabla.txt new file mode 100644 index 0000000000..d0c61deac3 --- /dev/null +++ b/blablabla.txt @@ -0,0 +1 @@ +rbwiebrfuiebiufwebiufbiuy \ No newline at end of file From bb5bf9fc197c509797c8558b8dd7586bda22f6cf Mon Sep 17 00:00:00 2001 From: hello Date: Fri, 26 Jan 2024 11:08:44 +0800 Subject: [PATCH 002/102] Y this so hard --- xxxx.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 xxxx.txt diff --git a/xxxx.txt b/xxxx.txt new file mode 100644 index 0000000000..d0c61deac3 --- /dev/null +++ b/xxxx.txt @@ -0,0 +1 @@ +rbwiebrfuiebiufwebiufbiuy \ No newline at end of file From b067e6f8f4e5410b298ec2b9c988a5fd9556a6bc Mon Sep 17 00:00:00 2001 From: hello Date: Sat, 27 Jan 2024 11:13:59 +0800 Subject: [PATCH 003/102] commit test: remove junk text files --- blablabla.txt | 1 - xxxx.txt | 1 - 2 files changed, 2 deletions(-) delete mode 100644 blablabla.txt delete mode 100644 xxxx.txt diff --git a/blablabla.txt b/blablabla.txt deleted file mode 100644 index d0c61deac3..0000000000 --- a/blablabla.txt +++ /dev/null @@ -1 +0,0 @@ -rbwiebrfuiebiufwebiufbiuy \ No newline at end of file diff --git a/xxxx.txt b/xxxx.txt deleted file mode 100644 index d0c61deac3..0000000000 --- a/xxxx.txt +++ /dev/null @@ -1 +0,0 @@ -rbwiebrfuiebiufwebiufbiuy \ No newline at end of file From 385be3890b8a2d90db14d169f0438b258c0cd3cf Mon Sep 17 00:00:00 2001 From: hello Date: Sat, 27 Jan 2024 11:59:37 +0800 Subject: [PATCH 004/102] changed pagination index from +1 to +12345 to explore consequences --- .../academy/grading/subcomponents/GradingSubmissionsTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 13e874e0d6..0acc9ab432 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -206,7 +206,7 @@ const GradingSubmissionTable: React.FC = ({ submiss disabled={!table.getCanPreviousPage()} /> - Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()} + Page {table.getState().pagination.pageIndex + 12345} of {table.getPageCount()} ); }; From 49aae24b3ea8a7046d09a124a764811f7e115f72 Mon Sep 17 00:00:00 2001 From: Josh Date: Tue, 13 Feb 2024 18:23:36 +0800 Subject: [PATCH 022/102] Filtering for name and type (Proof of concept) --- .../application/actions/SessionActions.ts | 5 +- src/commons/sagas/BackendSaga.ts | 2 + src/commons/sagas/RequestsSaga.ts | 7 ++- src/pages/academy/grading/Grading.tsx | 9 +-- .../subcomponents/GradingSubmissionsTable.tsx | 55 +++++++++++++++---- 5 files changed, 56 insertions(+), 22 deletions(-) diff --git a/src/commons/application/actions/SessionActions.ts b/src/commons/application/actions/SessionActions.ts index 48057bcd3d..f9e2cc4fa5 100644 --- a/src/commons/application/actions/SessionActions.ts +++ b/src/commons/application/actions/SessionActions.ts @@ -112,10 +112,11 @@ export const fetchGrading = (submissionId: number) => action(FETCH_GRADING, subm * of the grader */ // TEMPORARY IMPLEMENTATION. Refactor into a filters type once proof of feature is complete. -export const fetchGradingOverviews = (filterToGroup = true, pageParams: any) => +export const fetchGradingOverviews = (filterToGroup = true, pageParams: any, filterParams: any) => action(FETCH_GRADING_OVERVIEWS, { filterToGroup, - pageParams + pageParams, + filterParams, }); export const login = (providerId: string) => action(LOGIN, providerId); diff --git a/src/commons/sagas/BackendSaga.ts b/src/commons/sagas/BackendSaga.ts index e95eca6f36..aca821a5fd 100644 --- a/src/commons/sagas/BackendSaga.ts +++ b/src/commons/sagas/BackendSaga.ts @@ -412,12 +412,14 @@ function* BackendSaga(): SagaIterator { const filterToGroup = action.payload.filterToGroup; const pageParams = action.payload.pageParams; + const filterParams = action.payload.filterParams; const gradingOverviews: GradingOverview[] | null = yield call( getGradingOverviews, tokens, filterToGroup, pageParams, + filterParams, ); if (gradingOverviews) { yield put(actions.updateGradingOverviews(gradingOverviews)); diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index 9f97ed0ae5..d590dcf141 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -611,15 +611,18 @@ export const postAssessment = async (id: number, tokens: Tokens): Promise => { // this converts the payload into a useable query string without a leading '?' via implicit toString(). const pageQuery = new URLSearchParams(pageParams); - console.log(pageQuery); - const resp = await request(`${courseId()}/admin/grading?group=${group}&${pageQuery}`, 'GET', { + const filterQuery = new URLSearchParams(filterParams); + const resp = await request(`${courseId()}/admin/grading?group=${group}&${pageQuery}&${filterQuery}`, 'GET', { ...tokens }); if (!resp) { diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index a02b6b121a..2e873e4820 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -43,8 +43,8 @@ const Grading: React.FC = () => { const dispatch = useDispatch(); /**Passed as a prop to submissions table sub-component for sub-component to feed pagination and filter logic.*/ // TEMPORARY IMPLEMENTATION. TODO: Refactor into a filters type once proof of feature is complete. - const updateGradingOverviewsCallback = (group: boolean, pageParams: any) => { - dispatch(fetchGradingOverviews(false, pageParams)); + const updateGradingOverviewsCallback = (group: boolean, pageParams: any, filterParams: any) => { + dispatch(fetchGradingOverviews(false, pageParams, filterParams)); } /**Initializes grading submissions table with default values. @@ -52,10 +52,7 @@ const Grading: React.FC = () => { */ // TEMPORARY IMPLEMENTATION. TODO: Refactor into a filters type once proof of feature is complete. useEffect(() => { - dispatch(fetchGradingOverviews(false, { - offset: 0, - pageSize: 10 - })); + dispatch(fetchGradingOverviews(false, {offset: 0, pageSize: 10}, {})); }, [dispatch]); diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 197accd504..20290cb87b 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -159,18 +159,40 @@ const GradingSubmissionTable: React.FC = ({ submiss ); }, [columnFilters, globalFilter, dispatch]); - /**Wraps prop argument from Gradings table with component pagination logic and conversion.*/ - // USE ANY for now to avoid types. CHANGE THIS LATER. - const changePage = (page: number, pageSize: number) => { - setPage(page); - setPageSize(pageSize); - const entryOffset = page * pageSize; - updateEntries(false, { - offset: entryOffset, + // TEMPORARY IMPLEMENTATION. TODO: Refactor into a standardized filters type + const pageParamsBuilder = (): any => { + return { + offset: page * pageSize, pageSize: pageSize, - }); + } + } + + // TEMPORARY IMPLEMENTATION. TODO: Refactor into a standardized filters type + const filterParamsBuilder = (): any => { + // translates filter columns to backend query name + const shabbyenum = (val: string): string => { + switch (val) { + case "assessmentName": + return "title"; + case "assessmentType": + return "type"; + default: + return val; + } + } + + // This restricts each column to have only 1 accepted filter. Could be improved? + const queryParams = {} + columnFilters.map(column => {queryParams[shabbyenum(column.id)] = column.value;}); + return queryParams; } + // handles re-rendering of component after update of filters or parameters. + useEffect(() => { + updateEntries(false, pageParamsBuilder(), filterParamsBuilder()); + }, [columnFilters, page, pageSize]) + + return ( <> @@ -226,14 +248,20 @@ const GradingSubmissionTable: React.FC = ({ submiss size="xs" icon={() => } variant="light" - onClick={() => changePage(0, pageSize)} + onClick={() => { + setPage(0); + //updateSubmissionsTableView(); + }} disabled={page <= 0} /> @@ -136,6 +136,7 @@ const Grading: React.FC = () => { /> showAllSubmissions || isSubmissionUngraded(s) )} diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 20290cb87b..d0bd669671 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -109,12 +109,14 @@ const columns = [ ]; type GradingSubmissionTableProps = { + totalRows: number; submissions: GradingOverview[]; - // TEMPORARY IMPLEMENTATION. TODO: Refactor into a filters type once proof of feature is complete. - updateEntries: (group: boolean, pageParams: any) => void; + // TEMPORARY IMPLEMENTATION. TODO: Refactor into a unified type once proof of feature is complete. + updateEntries: (group: boolean, pageParams: any, filterParams: any) => void; }; -const GradingSubmissionTable: React.FC = ({ submissions, updateEntries }) => { + +const GradingSubmissionTable: React.FC = ({ totalRows, submissions, updateEntries }) => { const tableFilters = useTypedSelector(state => state.workspaces.grading.submissionsTableFilters); const [columnFilters, setColumnFilters] = useState([ @@ -246,42 +248,34 @@ const GradingSubmissionTable: React.FC = ({ submiss ); }; From c0cb4e65e38c9758f1437033c2955cc5181dcad3 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 14 Feb 2024 12:14:24 +0800 Subject: [PATCH 024/102] table columns and Filterable now included within the GradingSubmissionsTable component to allow for page reset upon application of filter --- .../subcomponents/GradingSubmissionsTable.tsx | 178 +++++++++--------- 1 file changed, 90 insertions(+), 88 deletions(-) diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index d0bd669671..8aa7c8bc78 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -39,75 +39,6 @@ import GradingSubmissionFilters from './GradingSubmissionFilters'; import { FilterStatus } from 'src/features/achievement/AchievementTypes'; -const columnHelper = createColumnHelper(); - -const columns = [ - columnHelper.accessor('assessmentName', { - header: 'Name', - cell: info => - }), - columnHelper.accessor('assessmentType', { - header: 'Type', - cell: info => ( - - - - ) - }), - columnHelper.accessor('studentName', { - header: 'Student', - cell: info => - }), - columnHelper.accessor('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 ; - } - }) -]; - type GradingSubmissionTableProps = { totalRows: number; submissions: GradingOverview[]; @@ -115,8 +46,97 @@ type GradingSubmissionTableProps = { updateEntries: (group: boolean, pageParams: any, filterParams: any) => void; }; - const GradingSubmissionTable: React.FC = ({ totalRows, submissions, updateEntries }) => { + const columnHelper = createColumnHelper(); + + const columns = [ + columnHelper.accessor('assessmentName', { + header: 'Name', + cell: info => setPage(0)}/> + }), + columnHelper.accessor('assessmentType', { + header: 'Type', + cell: info => ( + setPage(0)}> + + + ) + }), + columnHelper.accessor('studentName', { + header: 'Student', + cell: info => setPage(0)} /> + }), + columnHelper.accessor('studentUsername', { + header: 'Username', + cell: info => setPage(0)}/> + }), + columnHelper.accessor('groupName', { + header: 'Group', + cell: info => setPage(0)}/> + }), + columnHelper.accessor('submissionStatus', { + header: 'Progress', + cell: info => ( + setPage(0)}> + + + ) + }), + columnHelper.accessor('gradingStatus', { + header: 'Grading', + cell: info => ( + setPage(0)}> + + + ) + }), + 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 ; + } + }) + ]; + + type FilterableProps = { + column: Column; + value: string; + children?: React.ReactNode; + resetpage: () => void; + }; + + const Filterable: React.FC = ({ column, value, children, resetpage }) => { + const handleFilterChange = () => { + column.setFilterValue(value); + resetpage(); + }; + + return ( + + ); + }; + + const tableFilters = useTypedSelector(state => state.workspaces.grading.submissionsTableFilters); const [columnFilters, setColumnFilters] = useState([ @@ -284,22 +304,4 @@ const GradingSubmissionTable: React.FC = ({ totalRo ); }; -type FilterableProps = { - column: Column; - value: string; - children?: React.ReactNode; -}; - -const Filterable: React.FC = ({ column, value, children }) => { - const handleFilterChange = () => { - column.setFilterValue(value); - }; - - return ( - - ); -}; - export default GradingSubmissionTable; From a76d0aa932c1c376588d6b905ba7ae08f1893296 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 14 Feb 2024 12:31:54 +0800 Subject: [PATCH 025/102] simple list selection for pageSize. --- .../subcomponents/GradingSubmissionsTable.tsx | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 8aa7c8bc78..f072de862a 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 { Icon as BpIcon, Position } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import { Column, @@ -25,7 +25,8 @@ import { TableHeaderCell, TableRow, Text, - TextInput + TextInput, + Dropdown } from '@tremor/react'; import { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; @@ -37,6 +38,7 @@ import GradingActions from './GradingActions'; import { AssessmentTypeBadge, GradingStatusBadge, SubmissionStatusBadge } from './GradingBadges'; import GradingSubmissionFilters from './GradingSubmissionFilters'; import { FilterStatus } from 'src/features/achievement/AchievementTypes'; +import SimpleDropdown from 'src/commons/SimpleDropdown'; type GradingSubmissionTableProps = { @@ -215,6 +217,16 @@ const GradingSubmissionTable: React.FC = ({ totalRo }, [columnFilters, page, pageSize]) + // dropdown to select pageSize + const pageSizeOptions = [ + { value: 1, label: '1' }, + { value: 5, label: '5' }, + { value: 10, label: '10' }, + { value: 20, label: '20' }, + { value: 50, label: '50' } + ]; + + return ( <> @@ -297,6 +309,13 @@ const GradingSubmissionTable: React.FC = ({ totalRo onClick={() => setPage(Math.ceil(totalRows / pageSize) - 1)} disabled={page >= (Math.ceil(totalRows / pageSize) - 1)} /> + From 572c9d19f993021cf6a229f4699994d2b398dbac Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 14 Feb 2024 18:15:55 +0800 Subject: [PATCH 026/102] Filtering for student name and student username transferred to backend --- src/commons/sagas/RequestsSaga.ts | 2 ++ .../subcomponents/GradingSubmissionsTable.tsx | 15 ++++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index f590101a37..0ee734c85f 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -51,6 +51,7 @@ import { castLibrary } from '../utils/CastBackend'; import Constants from '../utils/Constants'; import { showWarningMessage } from '../utils/notifications/NotificationsHelper'; import { request } from '../utils/RequestHelper'; +import GradingSubmissionFilters from 'src/pages/academy/grading/subcomponents/GradingSubmissionFilters'; /** * GET / @@ -630,6 +631,7 @@ export const getGradingOverviews = async ( } const gradingOverviews = await resp.json(); + console.log(gradingOverviews.data); return { count: gradingOverviews.count, data: gradingOverviews.data diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index f072de862a..25ca197854 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -6,6 +6,7 @@ import { Column, ColumnFilter, ColumnFiltersState, + Pagination, createColumnHelper, flexRender, getCoreRowModel, @@ -154,10 +155,10 @@ const GradingSubmissionTable: React.FC = ({ totalRo columns, state: { columnFilters, - globalFilter - }, - initialState: { + globalFilter, pagination: { + // pagination is handled by server to fit exactly the pageSize. Thus, hardcode frontend pageIndex to 0. + pageIndex: 0, pageSize: pageSize } }, @@ -196,10 +197,10 @@ const GradingSubmissionTable: React.FC = ({ totalRo // translates filter columns to backend query name const shabbyenum = (val: string): string => { switch (val) { - case "assessmentName": - return "title"; - case "assessmentType": - return "type"; + case "assessmentName": return "title"; + case "assessmentType": return "type"; + case "studentName": return "name"; + case "studentUsername": return "username"; default: return val; } From 9fc1c828507525e43204e281fffc54792763fab7 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 16 Feb 2024 14:15:01 +0800 Subject: [PATCH 027/102] Unused tanstack pagination has been removed, as backend filters ensure only 1 page's worth of entries. --- src/commons/sagas/RequestsSaga.ts | 1 - .../subcomponents/GradingSubmissionsTable.tsx | 16 +++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index 0ee734c85f..8edf56332e 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -631,7 +631,6 @@ export const getGradingOverviews = async ( } const gradingOverviews = await resp.json(); - console.log(gradingOverviews.data); return { count: gradingOverviews.count, data: gradingOverviews.data diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 25ca197854..a639134ebe 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -6,7 +6,6 @@ import { Column, ColumnFilter, ColumnFiltersState, - Pagination, createColumnHelper, flexRender, getCoreRowModel, @@ -27,7 +26,6 @@ import { TableRow, Text, TextInput, - Dropdown } from '@tremor/react'; import { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; @@ -184,16 +182,18 @@ const GradingSubmissionTable: React.FC = ({ totalRo ); }, [columnFilters, globalFilter, dispatch]); - // TEMPORARY IMPLEMENTATION. TODO: Refactor into a standardized filters type - const pageParamsBuilder = (): any => { + // Adapts frontend page and pageSize state into useable offset for backend usage. + const pageParams = (): any => { return { offset: page * pageSize, pageSize: pageSize, } } - // TEMPORARY IMPLEMENTATION. TODO: Refactor into a standardized filters type - const filterParamsBuilder = (): any => { + // Converts the columnFilters array into backend query parameters. + // TEMP IMPLEMENTATION. Values currently hardcoded. + // TODO: implement reversible backend-frontend name conversion for use in RequestsSaga and here. + const backendFilterParams = (columnFilters: ColumnFilter[]): any => { // translates filter columns to backend query name const shabbyenum = (val: string): string => { switch (val) { @@ -206,15 +206,17 @@ const GradingSubmissionTable: React.FC = ({ totalRo } } + // This restricts each column to have only 1 accepted filter. Could be improved? const queryParams = {} + console.log(columnFilters.entries()); columnFilters.map(column => {queryParams[shabbyenum(column.id)] = column.value;}); return queryParams; } // handles re-rendering of component after update of filters or parameters. useEffect(() => { - updateEntries(false, pageParamsBuilder(), filterParamsBuilder()); + updateEntries(false, pageParams(), backendFilterParams(columnFilters)); }, [columnFilters, page, pageSize]) From 3979b69ce03a7690eaac09be076f70347cb0e82c Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 16 Feb 2024 20:16:46 +0800 Subject: [PATCH 028/102] Separation of concerns test commit --- src/pages/academy/grading/Grading.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index bd3534350b..7b3a753286 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -40,6 +40,15 @@ const Grading: React.FC = () => { { value: true, label: 'all groups' } ]; + const [pageSize, setPageSize] = useState(10); + const pageSizeOptions = [ + { value: 1, label: '1' }, + { value: 5, label: '5' }, + { value: 10, label: '10' }, + { value: 20, label: '20' }, + { value: 50, label: '50' } + ]; + const dispatch = useDispatch(); /**Passed as a prop to submissions table sub-component for sub-component to feed pagination and filter logic.*/ // TEMPORARY IMPLEMENTATION. TODO: Refactor into a filters type once proof of feature is complete. @@ -126,7 +135,7 @@ const Grading: React.FC = () => { popoverProps={{ position: Position.BOTTOM }} buttonProps={{ minimal: true, rightIcon: 'caret-down' }} /> - submissions from + Submissions from { popoverProps={{ position: Position.BOTTOM }} buttonProps={{ minimal: true, rightIcon: 'caret-down' }} /> + Entries per page + Date: Fri, 16 Feb 2024 20:20:19 +0800 Subject: [PATCH 029/102] SubmissionStatus now runs on backend filtering. Remaining: GroupName (OK), GradingStatus (tougher) --- .../grading/subcomponents/GradingSubmissionsTable.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index a639134ebe..02c2b83dcb 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -195,12 +195,13 @@ const GradingSubmissionTable: React.FC = ({ totalRo // TODO: implement reversible backend-frontend name conversion for use in RequestsSaga and here. const backendFilterParams = (columnFilters: ColumnFilter[]): any => { // translates filter columns to backend query name - const shabbyenum = (val: string): string => { + const backendNameOf = (val: string): string => { switch (val) { case "assessmentName": return "title"; case "assessmentType": return "type"; case "studentName": return "name"; case "studentUsername": return "username"; + case "submissionStatus": return "status"; default: return val; } @@ -210,7 +211,7 @@ const GradingSubmissionTable: React.FC = ({ totalRo // This restricts each column to have only 1 accepted filter. Could be improved? const queryParams = {} console.log(columnFilters.entries()); - columnFilters.map(column => {queryParams[shabbyenum(column.id)] = column.value;}); + columnFilters.map(column => {queryParams[backendNameOf(column.id)] = column.value;}); return queryParams; } From 57467d83197b789f4b116d93bbe40bb16b0be3a8 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 17 Feb 2024 13:42:31 +0800 Subject: [PATCH 030/102] Refactored filter parameters from Gradings page into Grading Submission Table to use as filtering parameters. --- .../application/actions/SessionActions.ts | 8 +- .../actions/__tests__/SessionActions.ts | 23 ++- src/commons/mocks/BackendMocks.ts | 15 +- src/commons/mocks/GradingMocks.ts | 6 +- src/commons/sagas/RequestsSaga.ts | 1 - src/pages/academy/grading/Grading.tsx | 80 +--------- .../subcomponents/GradingSubmissionsTable.tsx | 138 ++++++++++++------ 7 files changed, 144 insertions(+), 127 deletions(-) diff --git a/src/commons/application/actions/SessionActions.ts b/src/commons/application/actions/SessionActions.ts index b154356f34..b18be03144 100644 --- a/src/commons/application/actions/SessionActions.ts +++ b/src/commons/application/actions/SessionActions.ts @@ -110,9 +110,13 @@ export const fetchGrading = (submissionId: number) => action(FETCH_GRADING, subm /** * @param filterToGroup - param when set to true, only shows submissions under the group * of the grader + * @param pageParams - param that contains offset and pageSize, informing backend about how + * many entries, starting from what offset, to get + * @param filterParams - param that contains columnFilters converted into JSON for + * processing into query parameters */ -// TEMPORARY IMPLEMENTATION. Refactor into a filters type once proof of feature is complete. -export const fetchGradingOverviews = (filterToGroup = true, pageParams: any, filterParams: any) => +// TEMPORARY IMPLEMENTATION. TODO: Refactor into a filters type once proof of feature is complete. +export const fetchGradingOverviews = (filterToGroup = true, pageParams = { offset: 0, pageSize: 10 }, filterParams = {}) => action(FETCH_GRADING_OVERVIEWS, { filterToGroup, pageParams, diff --git a/src/commons/application/actions/__tests__/SessionActions.ts b/src/commons/application/actions/__tests__/SessionActions.ts index 4b336e82fc..879bb2d45c 100644 --- a/src/commons/application/actions/__tests__/SessionActions.ts +++ b/src/commons/application/actions/__tests__/SessionActions.ts @@ -150,16 +150,26 @@ test('fetchGradingOverviews generates correct default action object', () => { const action = fetchGradingOverviews(); expect(action).toEqual({ type: FETCH_GRADING_OVERVIEWS, - payload: true + payload: { + filterToGroup: true, + pageParams: {}, + filterParams: {} + } }); }); test('fetchGradingOverviews generates correct action object', () => { const filterToGroup = false; - const action = fetchGradingOverviews(filterToGroup); + const pageParams = { offset: 12, pageSize: 3 }; + const filterParams = { abc: "xxx", def: "yyy" }; + const action = fetchGradingOverviews(filterToGroup, pageParams, filterParams); expect(action).toEqual({ type: FETCH_GRADING_OVERVIEWS, - payload: filterToGroup + payload: { + filterToGroup: filterToGroup, + pageParams: pageParams, + filterParams: filterParams + } }); }); @@ -506,7 +516,9 @@ test('updateAssessment generates correct action object', () => { }); test('updateGradingOverviews generates correct action object', () => { - const overviews: GradingOverview[] = [ + const overviews: { count: number; data: GradingOverview[]} = { + count: 1, + data: [ { assessmentId: 1, assessmentNumber: 'M1A', @@ -527,7 +539,8 @@ test('updateGradingOverviews generates correct action object', () => { questionCount: 6, gradedCount: 0 } - ]; + ] + }; const action = updateGradingOverviews(overviews); expect(action).toEqual({ diff --git a/src/commons/mocks/BackendMocks.ts b/src/commons/mocks/BackendMocks.ts index 3b1e2736cb..5eb0d44097 100644 --- a/src/commons/mocks/BackendMocks.ts +++ b/src/commons/mocks/BackendMocks.ts @@ -163,11 +163,13 @@ export function* mockBackendSaga(): SagaIterator { function* (action: ReturnType): any { const accessToken = yield select((state: OverallState) => state.session.accessToken); const filterToGroup = action.payload.filterToGroup; + const pageParams = action.payload.pageParams; + const backendParams = action.payload.filterParams; const gradingOverviews = yield call(() => - mockFetchGradingOverview(accessToken, filterToGroup) + mockFetchGradingOverview(accessToken, filterToGroup, pageParams, backendParams) ); if (gradingOverviews !== null) { - yield put(actions.updateGradingOverviews([...gradingOverviews])); + yield put(actions.updateGradingOverviews(gradingOverviews)); } } ); @@ -196,14 +198,17 @@ export function* mockBackendSaga(): SagaIterator { yield call(showWarningMessage, '400: Bad Request'); return; } - const newOverviews = (overviews as GradingOverview[]).map(overview => { + const newEntries = { + count: Object.keys(overviews).length, + data: (overviews as GradingOverview[]).map(overview => { if (overview.submissionId === submissionId) { return { ...overview, submissionStatus: 'attempted' }; } return overview; - }); + }) + }; yield call(showSuccessMessage, 'Unsubmit successful!', 1000); - yield put(actions.updateGradingOverviews(newOverviews)); + yield put(actions.updateGradingOverviews(newEntries)); } ); diff --git a/src/commons/mocks/GradingMocks.ts b/src/commons/mocks/GradingMocks.ts index a634679582..6d06a085f6 100644 --- a/src/commons/mocks/GradingMocks.ts +++ b/src/commons/mocks/GradingMocks.ts @@ -74,10 +74,14 @@ export const mockGradingOverviews: GradingOverview[] = [ * * @param accessToken a valid access token for the cadet backend. * @param group a boolean if true, only fetches submissions from the grader's group + * @param pageParams contains pagination details on offset and page index. + * @param backendParams contains filters to set conditions in SQL query. */ export const mockFetchGradingOverview = ( accessToken: string, - group: boolean + group: boolean, + pageParams: {offset: number, pageSize: number}, + backendParams: Object ): GradingOverview[] | null => { // mocks backend role fetching const permittedRoles: Role[] = [Role.Admin, Role.Staff]; diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index 8edf56332e..f590101a37 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -51,7 +51,6 @@ import { castLibrary } from '../utils/CastBackend'; import Constants from '../utils/Constants'; import { showWarningMessage } from '../utils/notifications/NotificationsHelper'; import { request } from '../utils/RequestHelper'; -import GradingSubmissionFilters from 'src/pages/academy/grading/subcomponents/GradingSubmissionFilters'; /** * GET / diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index 7b3a753286..66fa4e1476 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -7,9 +7,6 @@ import React, { 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 { GradingStatuses } from 'src/commons/assessment/AssessmentTypes'; -import SimpleDropdown from 'src/commons/SimpleDropdown'; import { useSession } from 'src/commons/utils/Hooks'; import { numberRegExp } from 'src/features/academy/AcademyTypes'; import { exportGradingCSV, isSubmissionUngraded } from 'src/features/grading/GradingUtils'; @@ -24,8 +21,6 @@ const Grading: React.FC = () => { const { courseId, gradingOverviews, - role, - group, assessmentOverviews: assessments = [] } = useSession(); const params = useParams<{ @@ -33,43 +28,16 @@ const Grading: React.FC = () => { questionId: string; }>(); - const isAdmin = role === Role.Admin; - const [showAllGroups, setShowAllGroups] = useState(isAdmin || group === null); - const groupOptions = [ - { value: false, label: 'my groups' }, - { value: true, label: 'all groups' } - ]; - - const [pageSize, setPageSize] = useState(10); - const pageSizeOptions = [ - { value: 1, label: '1' }, - { value: 5, label: '5' }, - { value: 10, label: '10' }, - { value: 20, label: '20' }, - { value: 50, label: '50' } - ]; const dispatch = useDispatch(); - /**Passed as a prop to submissions table sub-component for sub-component to feed pagination and filter logic.*/ - // TEMPORARY IMPLEMENTATION. TODO: Refactor into a filters type once proof of feature is complete. - const updateGradingOverviewsCallback = (group: boolean, pageParams: any, filterParams: any) => { - dispatch(fetchGradingOverviews(false, pageParams, filterParams)); - } + const updateGradingOverviewsCallback = (group: boolean, pageParams: {offset: number, pageSize: number}, filterParams: Object) => { + dispatch(fetchGradingOverviews(group, pageParams, filterParams)); + }; - /**Initializes grading submissions table with default values. - * useEffect ensures that initialization is run only once component is mounted. - */ - // TEMPORARY IMPLEMENTATION. TODO: Refactor into a filters type once proof of feature is complete. + // Default value initializer useEffect(() => { - dispatch(fetchGradingOverviews(false, {offset: 0, pageSize: 10}, {})); - }, [dispatch]); - - - const [showAllSubmissions, setShowAllSubmissions] = useState(true); - const showOptions = [ - { value: false, label: 'ungraded' }, - { value: true, label: 'all' } - ]; + dispatch(fetchGradingOverviews()); + }, []); // If submissionId or questionId is defined but not numeric, redirect back to the Grading overviews page if ( @@ -126,37 +94,9 @@ const Grading: React.FC = () => { - - Viewing - - Submissions from - - Entries per page - - showAllSubmissions || isSubmissionUngraded(s) - )} + submissions={submissions} updateEntries={updateGradingOverviewsCallback} /> @@ -165,11 +105,7 @@ const Grading: React.FC = () => { - groupName === group && gradingStatus !== GradingStatuses.excluded - )} + submissions={submissions} assessments={assessments} /> diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 02c2b83dcb..17af925221 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -29,25 +29,28 @@ import { } from '@tremor/react'; import { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; -import { useTypedSelector } from 'src/commons/utils/Hooks'; +import SimpleDropdown from 'src/commons/SimpleDropdown'; +import { useSession, useTypedSelector } from 'src/commons/utils/Hooks'; import { updateSubmissionsTableFilters } from 'src/commons/workspace/WorkspaceActions'; import { GradingOverview } from 'src/features/grading/GradingTypes'; import GradingActions from './GradingActions'; import { AssessmentTypeBadge, GradingStatusBadge, SubmissionStatusBadge } from './GradingBadges'; import GradingSubmissionFilters from './GradingSubmissionFilters'; -import { FilterStatus } from 'src/features/achievement/AchievementTypes'; -import SimpleDropdown from 'src/commons/SimpleDropdown'; +import { GradingStatuses } from 'src/commons/assessment/AssessmentTypes'; type GradingSubmissionTableProps = { totalRows: number; submissions: GradingOverview[]; // TEMPORARY IMPLEMENTATION. TODO: Refactor into a unified type once proof of feature is complete. - updateEntries: (group: boolean, pageParams: any, filterParams: any) => void; + updateEntries: (group: boolean, pageParams: {offset: number, pageSize: number}, filterParams: any) => void; }; const GradingSubmissionTable: React.FC = ({ totalRows, submissions, updateEntries }) => { + const { + group + } = useSession(); const columnHelper = createColumnHelper(); const columns = [ @@ -191,37 +194,52 @@ const GradingSubmissionTable: React.FC = ({ totalRo } // Converts the columnFilters array into backend query parameters. - // TEMP IMPLEMENTATION. Values currently hardcoded. - // TODO: implement reversible backend-frontend name conversion for use in RequestsSaga and here. - const backendFilterParams = (columnFilters: ColumnFilter[]): any => { - // translates filter columns to backend query name - const backendNameOf = (val: string): string => { - switch (val) { - case "assessmentName": return "title"; - case "assessmentType": return "type"; - case "studentName": return "name"; - case "studentUsername": return "username"; - case "submissionStatus": return "status"; - default: - return val; - } - } - - - // This restricts each column to have only 1 accepted filter. Could be improved? - const queryParams = {} - console.log(columnFilters.entries()); - columnFilters.map(column => {queryParams[backendNameOf(column.id)] = column.value;}); - return queryParams; + // TEMP IMPLEMENTATION. Values currently hardcoded with knowledge of what a ColumnFilter is. + // TODO: remove hardcoding conversion of all submissions to column filter. it is a hacky workaround. + // TODO: implement reversible backend-frontend name conversion for use in RequestsSaga and here, remove hardcode. + const backendFilterParams = (columnFilters: ColumnFilter[], showAllSubmissions: boolean): any => { + return columnFilters + .concat([{ id: (showAllSubmissions ? "paramIgnoredByBackend" : "gradingStatus"), value: GradingStatuses.none}]) + .map((column: ColumnFilter) => { + // TODO: change all references to column properties in backend saga to backend name to reduce + // un-needed hardcode conversion, ensuring that places that reference it are updated. + switch (column.id) { + case "assessmentName": + return {"title": column.value}; + case "assessmentType": + return {"type": column.value}; + case "studentName": + return {"name": column.value}; + case "studentUsername": + return {"username": column.value}; + case "submissionStatus": + return {"status": column.value}; + case "gradingStatus": + if (column.value === GradingStatuses.none) { + return { + "isManuallyGraded": true, + "status": "submitted", + "numGraded": 0, + }; + } else if (column.value === GradingStatuses.graded) { + // TODO: coordinate with backend on subquerying to implement the third query + // currently ignored by backend as of 16 Feb 24 commit + return { + "isManuallyGraded": true, + "status": "submitted", + "numGradedEqualToTotal": true, + }; + } else { + // case: excluded or grading. Not implemented yet. + return {}; + } + default: + return column; + } + }) + .reduce(Object.assign, {}); } - - // handles re-rendering of component after update of filters or parameters. - useEffect(() => { - updateEntries(false, pageParams(), backendFilterParams(columnFilters)); - }, [columnFilters, page, pageSize]) - - // dropdown to select pageSize const pageSizeOptions = [ { value: 1, label: '1' }, { value: 5, label: '5' }, @@ -229,10 +247,54 @@ const GradingSubmissionTable: React.FC = ({ totalRo { value: 20, label: '20' }, { value: 50, label: '50' } ]; - + + // TODO: implement isAdmin functionality + const [showAllGroups, setShowAllGroups] = useState(false); + const groupOptions = [ + { value: false, label: 'my groups' }, + { value: true, label: 'all groups' } + ]; + + const [showAllSubmissions, setShowAllSubmissions] = useState(false); + const showSubmissionOptions = [ + { value: false, label: 'ungraded' }, + { value: true, label: 'all' } + ]; + + // tells page to ask for new entries from main page when its state changes. + useEffect(() => { + updateEntries(showAllGroups, pageParams(), backendFilterParams(columnFilters, showAllSubmissions)); + }, [showAllGroups, page, pageSize, columnFilters, showAllSubmissions]); + return ( <> + + Viewing + + Submissions from + + Entries per page + +
@@ -313,13 +375,7 @@ const GradingSubmissionTable: React.FC = ({ totalRo onClick={() => setPage(Math.ceil(totalRows / pageSize) - 1)} disabled={page >= (Math.ceil(totalRows / pageSize) - 1)} /> - + From d86de79816e980cc487b845407addddc327d57d7 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 17 Feb 2024 13:52:36 +0800 Subject: [PATCH 031/102] Frontend filtering for graded status removed. (BUG: False positives) --- src/pages/academy/grading/Grading.tsx | 8 ++++---- .../grading/subcomponents/GradingSubmissionsTable.tsx | 9 ++++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index 66fa4e1476..fc7070a9ee 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -1,15 +1,15 @@ import '@tremor/react/dist/esm/tremor.css'; -import { Icon as BpIcon, NonIdealState, Position, Spinner, SpinnerSize } from '@blueprintjs/core'; +import { Icon as BpIcon, NonIdealState, Spinner, SpinnerSize } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; -import { Button, Card, Col, ColGrid, Flex, Text, Title } from '@tremor/react'; -import React, { useEffect, useState } from 'react'; +import { Button, Card, Col, ColGrid, Flex, Title } from '@tremor/react'; +import React, { useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { Navigate, useParams} from 'react-router'; import { fetchGradingOverviews } from 'src/commons/application/actions/SessionActions'; import { useSession } from 'src/commons/utils/Hooks'; import { numberRegExp } from 'src/features/academy/AcademyTypes'; -import { exportGradingCSV, isSubmissionUngraded } from 'src/features/grading/GradingUtils'; +import { exportGradingCSV } from 'src/features/grading/GradingUtils'; import ContentDisplay from '../../../commons/ContentDisplay'; import { convertParamToInt } from '../../../commons/utils/ParamParseHelper'; diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 17af925221..06391c515f 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -29,28 +29,30 @@ import { } from '@tremor/react'; import { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; +import { GradingStatuses } from 'src/commons/assessment/AssessmentTypes'; import SimpleDropdown from 'src/commons/SimpleDropdown'; -import { useSession, useTypedSelector } from 'src/commons/utils/Hooks'; +import { useTypedSelector } from 'src/commons/utils/Hooks'; import { updateSubmissionsTableFilters } from 'src/commons/workspace/WorkspaceActions'; import { GradingOverview } from 'src/features/grading/GradingTypes'; import GradingActions from './GradingActions'; import { AssessmentTypeBadge, GradingStatusBadge, SubmissionStatusBadge } from './GradingBadges'; import GradingSubmissionFilters from './GradingSubmissionFilters'; -import { GradingStatuses } from 'src/commons/assessment/AssessmentTypes'; type GradingSubmissionTableProps = { totalRows: number; submissions: GradingOverview[]; - // TEMPORARY IMPLEMENTATION. TODO: Refactor into a unified type once proof of feature is complete. + // TODO: Abstract pageParams object into a useable type. updateEntries: (group: boolean, pageParams: {offset: number, pageSize: number}, filterParams: any) => void; }; const GradingSubmissionTable: React.FC = ({ totalRows, submissions, updateEntries }) => { + /* TODO: implement functionality for submission filtering by groups using the following state. const { group } = useSession(); + */ const columnHelper = createColumnHelper(); const columns = [ @@ -197,6 +199,7 @@ const GradingSubmissionTable: React.FC = ({ totalRo // TEMP IMPLEMENTATION. Values currently hardcoded with knowledge of what a ColumnFilter is. // TODO: remove hardcoding conversion of all submissions to column filter. it is a hacky workaround. // TODO: implement reversible backend-frontend name conversion for use in RequestsSaga and here, remove hardcode. + // TODO: make a controller component, like in the achievements page, to handle conversion of page state into JSON. const backendFilterParams = (columnFilters: ColumnFilter[], showAllSubmissions: boolean): any => { return columnFilters .concat([{ id: (showAllSubmissions ? "paramIgnoredByBackend" : "gradingStatus"), value: GradingStatuses.none}]) From 032068624821c142f09a77fcbdd5cf48bef35f3e Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 18 Feb 2024 00:34:27 +0800 Subject: [PATCH 032/102] Removees prop passing for Filterable to take advantage of its nested state within GradingSubmissionTable. Filterable is nested so that it can update the pagination state and share a common callback function. --- .../subcomponents/GradingSubmissionsTable.tsx | 80 ++++++++++--------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 06391c515f..85a183f5b5 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -53,37 +53,72 @@ const GradingSubmissionTable: React.FC = ({ totalRo group } = useSession(); */ + + // Nesting of Filterable here allows for mutual re-rendering with the dropdown options tab if either of them update. + // This is needed because a filter change is accompanied with a page reset. + type FilterableProps = { + column: Column; + value: string; + children?: React.ReactNode; + }; + + const tableFilters = useTypedSelector(state => state.workspaces.grading.submissionsTableFilters); + + const [columnFilters, setColumnFilters] = useState([ + ...tableFilters.columnFilters + ]); + const [globalFilter, setGlobalFilter] = useState(tableFilters.globalFilter); + + const [page, setPage] = useState(0); + const [pageSize, setPageSize] = useState(10); + + const columnHelper = createColumnHelper(); + + const Filterable: React.FC = ({ column, value, children }) => { + const handleFilterChange = () => { + column.setFilterValue(value); + setPage(0); + }; + + return ( + + ); + }; + + const columns = [ columnHelper.accessor('assessmentName', { header: 'Name', - cell: info => setPage(0)}/> + cell: info => }), columnHelper.accessor('assessmentType', { header: 'Type', cell: info => ( - setPage(0)}> + ) }), columnHelper.accessor('studentName', { header: 'Student', - cell: info => setPage(0)} /> + cell: info => }), columnHelper.accessor('studentUsername', { header: 'Username', - cell: info => setPage(0)}/> + cell: info => }), columnHelper.accessor('groupName', { header: 'Group', - cell: info => setPage(0)}/> + cell: info => }), columnHelper.accessor('submissionStatus', { header: 'Progress', cell: info => ( - setPage(0)}> + ) @@ -91,7 +126,7 @@ const GradingSubmissionTable: React.FC = ({ totalRo columnHelper.accessor('gradingStatus', { header: 'Grading', cell: info => ( - setPage(0)}> + ) @@ -122,37 +157,6 @@ const GradingSubmissionTable: React.FC = ({ totalRo }) ]; - type FilterableProps = { - column: Column; - value: string; - children?: React.ReactNode; - resetpage: () => void; - }; - - const Filterable: React.FC = ({ column, value, children, resetpage }) => { - const handleFilterChange = () => { - column.setFilterValue(value); - resetpage(); - }; - - return ( - - ); - }; - - - const tableFilters = useTypedSelector(state => state.workspaces.grading.submissionsTableFilters); - - const [columnFilters, setColumnFilters] = useState([ - ...tableFilters.columnFilters - ]); - const [globalFilter, setGlobalFilter] = useState(tableFilters.globalFilter); - - const [page, setPage] = useState(0); - const [pageSize, setPageSize] = useState(10); - const table = useReactTable({ data: submissions, columns, From 4968e12ccef66b2813e34afefbe8bf63184ea8ff Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 18 Feb 2024 00:48:47 +0800 Subject: [PATCH 033/102] Swapped underlying boolean values for group filtering to filter only by group when set to true, matching original definition --- .../subcomponents/GradingSubmissionsTable.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 85a183f5b5..73cd559f78 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -72,7 +72,6 @@ const GradingSubmissionTable: React.FC = ({ totalRo const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState(10); - const columnHelper = createColumnHelper(); @@ -247,6 +246,8 @@ const GradingSubmissionTable: React.FC = ({ totalRo .reduce(Object.assign, {}); } + // Dropdown tab options, which contains some external state. + // This can be a candidate for its own component once backend feature implementation is complete. const pageSizeOptions = [ { value: 1, label: '1' }, { value: 5, label: '5' }, @@ -256,10 +257,10 @@ const GradingSubmissionTable: React.FC = ({ totalRo ]; // TODO: implement isAdmin functionality - const [showAllGroups, setShowAllGroups] = useState(false); + const [limitGroup, setLimitGroup] = useState(true); const groupOptions = [ - { value: false, label: 'my groups' }, - { value: true, label: 'all groups' } + { value: true, label: 'my groups' }, + { value: false, label: 'all groups' }, ]; const [showAllSubmissions, setShowAllSubmissions] = useState(false); @@ -270,8 +271,8 @@ const GradingSubmissionTable: React.FC = ({ totalRo // tells page to ask for new entries from main page when its state changes. useEffect(() => { - updateEntries(showAllGroups, pageParams(), backendFilterParams(columnFilters, showAllSubmissions)); - }, [showAllGroups, page, pageSize, columnFilters, showAllSubmissions]); + updateEntries(limitGroup, pageParams(), backendFilterParams(columnFilters, showAllSubmissions)); + }, [limitGroup, page, pageSize, columnFilters, showAllSubmissions]); return ( @@ -288,8 +289,8 @@ const GradingSubmissionTable: React.FC = ({ totalRo Submissions from From f1762e9fa76be77fd2dbbaf2dae31b7c8eaf7a61 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Sun, 18 Feb 2024 12:40:22 +0800 Subject: [PATCH 034/102] Fix format --- .../application/actions/SessionActions.ts | 12 +- .../actions/__tests__/SessionActions.ts | 44 +++--- src/commons/application/types/SessionTypes.ts | 2 +- src/commons/assessment/AssessmentTypes.ts | 4 +- src/commons/mocks/BackendMocks.ts | 8 +- src/commons/mocks/GradingMocks.ts | 2 +- src/commons/sagas/BackendSaga.ts | 12 +- src/commons/sagas/RequestsSaga.ts | 18 ++- src/pages/academy/grading/Grading.tsx | 20 ++- .../subcomponents/GradingSubmissionsTable.tsx | 136 +++++++++--------- 10 files changed, 137 insertions(+), 121 deletions(-) diff --git a/src/commons/application/actions/SessionActions.ts b/src/commons/application/actions/SessionActions.ts index 4d92baada0..53299f178a 100644 --- a/src/commons/application/actions/SessionActions.ts +++ b/src/commons/application/actions/SessionActions.ts @@ -108,15 +108,19 @@ export const fetchGrading = (submissionId: number) => action(FETCH_GRADING, subm * of the grader * @param pageParams - param that contains offset and pageSize, informing backend about how * many entries, starting from what offset, to get - * @param filterParams - param that contains columnFilters converted into JSON for + * @param filterParams - param that contains columnFilters converted into JSON for * processing into query parameters */ // TEMPORARY IMPLEMENTATION. TODO: Refactor into a filters type once proof of feature is complete. -export const fetchGradingOverviews = (filterToGroup = true, pageParams = { offset: 0, pageSize: 10 }, filterParams = {}) => +export const fetchGradingOverviews = ( + filterToGroup = true, + pageParams = { offset: 0, pageSize: 10 }, + filterParams = {} +) => action(FETCH_GRADING_OVERVIEWS, { filterToGroup, pageParams, - filterParams, + filterParams }); export const login = (providerId: string) => action(LOGIN, providerId); @@ -216,7 +220,7 @@ export const updateTotalXp = (totalXp: number) => action(UPDATE_TOTAL_XP, totalX export const updateAssessment = (assessment: Assessment) => action(UPDATE_ASSESSMENT, assessment); -export const updateGradingOverviews = (overviews: {count: number, data: GradingOverview[]}) => +export const updateGradingOverviews = (overviews: { count: number; data: GradingOverview[] }) => action(UPDATE_GRADING_OVERVIEWS, overviews); /** diff --git a/src/commons/application/actions/__tests__/SessionActions.ts b/src/commons/application/actions/__tests__/SessionActions.ts index b84be81912..0ff918f453 100644 --- a/src/commons/application/actions/__tests__/SessionActions.ts +++ b/src/commons/application/actions/__tests__/SessionActions.ts @@ -161,7 +161,7 @@ test('fetchGradingOverviews generates correct default action object', () => { test('fetchGradingOverviews generates correct action object', () => { const filterToGroup = false; const pageParams = { offset: 12, pageSize: 3 }; - const filterParams = { abc: "xxx", def: "yyy" }; + const filterParams = { abc: 'xxx', def: 'yyy' }; const action = fetchGradingOverviews(filterToGroup, pageParams, filterParams); expect(action).toEqual({ type: FETCH_GRADING_OVERVIEWS, @@ -520,29 +520,29 @@ test('updateAssessment generates correct action object', () => { }); test('updateGradingOverviews generates correct action object', () => { - const overviews: { count: number; data: GradingOverview[]} = { + const overviews: { count: number; data: GradingOverview[] } = { count: 1, data: [ - { - assessmentId: 1, - assessmentNumber: 'M1A', - assessmentName: 'test assessment', - assessmentType: 'Contests', - initialXp: 0, - xpBonus: 100, - xpAdjustment: 50, - currentXp: 50, - maxXp: 500, - studentId: 100, - studentName: 'test student', - studentUsername: 'E0123456', - submissionId: 1, - submissionStatus: 'attempting', - groupName: 'group', - gradingStatus: 'excluded', - questionCount: 6, - gradedCount: 0 - } + { + assessmentId: 1, + assessmentNumber: 'M1A', + assessmentName: 'test assessment', + assessmentType: 'Contests', + initialXp: 0, + xpBonus: 100, + xpAdjustment: 50, + currentXp: 50, + maxXp: 500, + studentId: 100, + studentName: 'test student', + studentUsername: 'E0123456', + submissionId: 1, + submissionStatus: 'attempting', + groupName: 'group', + gradingStatus: 'excluded', + questionCount: 6, + gradedCount: 0 + } ] }; diff --git a/src/commons/application/types/SessionTypes.ts b/src/commons/application/types/SessionTypes.ts index e2b5437f5d..e12fa4b67b 100644 --- a/src/commons/application/types/SessionTypes.ts +++ b/src/commons/application/types/SessionTypes.ts @@ -118,7 +118,7 @@ export type SessionState = { readonly assessmentOverviews?: AssessmentOverview[]; readonly assessments: Map; - readonly gradingOverviews?: { count: number, data: GradingOverview[] }; + readonly gradingOverviews?: { count: number; data: GradingOverview[] }; readonly gradings: Map; readonly notifications: Notification[]; readonly googleUser?: string; diff --git a/src/commons/assessment/AssessmentTypes.ts b/src/commons/assessment/AssessmentTypes.ts index 9fe9a7fecc..febf1c0c88 100644 --- a/src/commons/assessment/AssessmentTypes.ts +++ b/src/commons/assessment/AssessmentTypes.ts @@ -46,8 +46,8 @@ export enum QuestionTypes { export type QuestionType = keyof typeof QuestionTypes; /* -* Used to display information regarding an assessment in the UI. -* + * Used to display information regarding an assessment in the UI. + * * @property closeAt an ISO 8601 compliant date string specifiying when * the assessment closes * @property openAt an ISO 8601 compliant date string specifiying when diff --git a/src/commons/mocks/BackendMocks.ts b/src/commons/mocks/BackendMocks.ts index 5eb0d44097..b602143c8e 100644 --- a/src/commons/mocks/BackendMocks.ts +++ b/src/commons/mocks/BackendMocks.ts @@ -201,10 +201,10 @@ export function* mockBackendSaga(): SagaIterator { const newEntries = { count: Object.keys(overviews).length, data: (overviews as GradingOverview[]).map(overview => { - if (overview.submissionId === submissionId) { - return { ...overview, submissionStatus: 'attempted' }; - } - return overview; + if (overview.submissionId === submissionId) { + return { ...overview, submissionStatus: 'attempted' }; + } + return overview; }) }; yield call(showSuccessMessage, 'Unsubmit successful!', 1000); diff --git a/src/commons/mocks/GradingMocks.ts b/src/commons/mocks/GradingMocks.ts index 6d06a085f6..a689e46574 100644 --- a/src/commons/mocks/GradingMocks.ts +++ b/src/commons/mocks/GradingMocks.ts @@ -80,7 +80,7 @@ export const mockGradingOverviews: GradingOverview[] = [ export const mockFetchGradingOverview = ( accessToken: string, group: boolean, - pageParams: {offset: number, pageSize: number}, + pageParams: { offset: number; pageSize: number }, backendParams: Object ): GradingOverview[] | null => { // mocks backend role fetching diff --git a/src/commons/sagas/BackendSaga.ts b/src/commons/sagas/BackendSaga.ts index 12a5f01233..9f0adc8938 100644 --- a/src/commons/sagas/BackendSaga.ts +++ b/src/commons/sagas/BackendSaga.ts @@ -415,12 +415,12 @@ function* BackendSaga(): SagaIterator { const pageParams = action.payload.pageParams; const filterParams = action.payload.filterParams; - const gradingOverviews: {count: number, data: GradingOverview[]} | null = yield call( + const gradingOverviews: { count: number; data: GradingOverview[] } | null = yield call( getGradingOverviews, tokens, filterToGroup, pageParams, - filterParams, + filterParams ); if (gradingOverviews) { @@ -463,10 +463,14 @@ function* BackendSaga(): SagaIterator { return overview; }); - const totalPossibleEntries = yield select((state: OverallState) => state.session.gradingOverviews?.count); + const totalPossibleEntries = yield select( + (state: OverallState) => state.session.gradingOverviews?.count + ); yield call(showSuccessMessage, 'Unsubmit successful', 1000); - yield put(actions.updateGradingOverviews({ count: totalPossibleEntries, data: newOverviews })); + yield put( + actions.updateGradingOverviews({ count: totalPossibleEntries, data: newOverviews }) + ); } ); diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index 834cdbf170..c6a77b79f2 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -603,14 +603,18 @@ export const getGradingOverviews = async ( tokens: Tokens, group: boolean, pageParams: any, - filterParams: any, -): Promise<{count: number, data: GradingOverview[]} | null> => { + filterParams: any +): Promise<{ count: number; data: GradingOverview[] } | null> => { // this converts the payload into a useable query string without a leading '?' via implicit toString(). const pageQuery = new URLSearchParams(pageParams); const filterQuery = new URLSearchParams(filterParams); - const resp = await request(`${courseId()}/admin/grading?group=${group}&${pageQuery}&${filterQuery}`, 'GET', { - ...tokens - }); + const resp = await request( + `${courseId()}/admin/grading?group=${group}&${pageQuery}&${filterQuery}`, + 'GET', + { + ...tokens + } + ); if (!resp) { return null; // invalid accessToken _and_ refreshToken } @@ -655,8 +659,8 @@ export const getGradingOverviews = async ( subX.assessmentId !== subY.assessmentId ? subY.assessmentId - subX.assessmentId : subY.submissionId - subX.submissionId - ), - } + ) + }; }; /** diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index fc7070a9ee..714940fe1f 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -5,7 +5,7 @@ import { IconNames } from '@blueprintjs/icons'; import { Button, Card, Col, ColGrid, Flex, Title } from '@tremor/react'; import React, { useEffect } from 'react'; import { useDispatch } from 'react-redux'; -import { Navigate, useParams} from 'react-router'; +import { Navigate, useParams } from 'react-router'; import { fetchGradingOverviews } from 'src/commons/application/actions/SessionActions'; import { useSession } from 'src/commons/utils/Hooks'; import { numberRegExp } from 'src/features/academy/AcademyTypes'; @@ -18,19 +18,18 @@ import GradingSummary from './subcomponents/GradingSummary'; import GradingWorkspace from './subcomponents/GradingWorkspace'; const Grading: React.FC = () => { - const { - courseId, - gradingOverviews, - assessmentOverviews: assessments = [] - } = useSession(); + const { courseId, gradingOverviews, assessmentOverviews: assessments = [] } = useSession(); const params = useParams<{ submissionId: string; questionId: string; }>(); - const dispatch = useDispatch(); - const updateGradingOverviewsCallback = (group: boolean, pageParams: {offset: number, pageSize: number}, filterParams: Object) => { + const updateGradingOverviewsCallback = ( + group: boolean, + pageParams: { offset: number; pageSize: number }, + filterParams: Object + ) => { dispatch(fetchGradingOverviews(group, pageParams, filterParams)); }; @@ -104,10 +103,7 @@ const Grading: React.FC = () => { - + diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 73cd559f78..12f391a0e5 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -11,7 +11,7 @@ import { getCoreRowModel, getFilteredRowModel, getPaginationRowModel, - useReactTable, + useReactTable } from '@tanstack/react-table'; import { Bold, @@ -25,7 +25,7 @@ import { TableHeaderCell, TableRow, Text, - TextInput, + TextInput } from '@tremor/react'; import { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; @@ -39,15 +39,22 @@ import GradingActions from './GradingActions'; import { AssessmentTypeBadge, GradingStatusBadge, SubmissionStatusBadge } from './GradingBadges'; import GradingSubmissionFilters from './GradingSubmissionFilters'; - type GradingSubmissionTableProps = { totalRows: number; submissions: GradingOverview[]; // TODO: Abstract pageParams object into a useable type. - updateEntries: (group: boolean, pageParams: {offset: number, pageSize: number}, filterParams: any) => void; + updateEntries: ( + group: boolean, + pageParams: { offset: number; pageSize: number }, + filterParams: any + ) => void; }; -const GradingSubmissionTable: React.FC = ({ totalRows, submissions, updateEntries }) => { +const GradingSubmissionTable: React.FC = ({ + totalRows, + submissions, + updateEntries +}) => { /* TODO: implement functionality for submission filtering by groups using the following state. const { group @@ -74,13 +81,12 @@ const GradingSubmissionTable: React.FC = ({ totalRo const columnHelper = createColumnHelper(); - const Filterable: React.FC = ({ column, value, children }) => { const handleFilterChange = () => { column.setFilterValue(value); setPage(0); }; - + return ( - ); - }; + const columnHelper = createColumnHelper(); const columns = [ columnHelper.accessor('assessmentName', { header: 'Name', - cell: info => + cell: info => }), columnHelper.accessor('assessmentType', { header: 'Type', cell: info => ( - + ) }), columnHelper.accessor('studentName', { header: 'Student', - cell: info => + cell: info => }), columnHelper.accessor('studentUsername', { header: 'Username', - cell: info => + cell: info => }), columnHelper.accessor('groupName', { header: 'Group', - cell: info => + cell: info => }), columnHelper.accessor('submissionStatus', { header: 'Progress', cell: info => ( - + ) @@ -130,7 +112,7 @@ const GradingSubmissionTable: React.FC = ({ columnHelper.accessor('gradingStatus', { header: 'Grading', cell: info => ( - + ) @@ -400,3 +382,22 @@ const GradingSubmissionTable: React.FC = ({ }; export default GradingSubmissionTable; + +type FilterableProps = { + column: Column; + value: string; + children?: React.ReactNode; + onClick?: () => void; +}; +const Filterable: React.FC = ({ column, value, children, onClick }) => { + const handleFilterChange = () => { + column.setFilterValue(value); + onClick?.(); + }; + + return ( + + ); +}; From 15792fb033e874175691e4711584ef784c320c5b Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Sun, 18 Feb 2024 12:57:03 +0800 Subject: [PATCH 037/102] Revert unintended changes * Fix incorrect merge resolution * Reorder lines to optimize diff readability * Revert unintended ESLint changes --- public/externalLibs/sound/soundToneMatrix.js | 6 +++--- .../grading/subcomponents/GradingSubmissionsTable.tsx | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/public/externalLibs/sound/soundToneMatrix.js b/public/externalLibs/sound/soundToneMatrix.js index d0246757bf..8638a90378 100644 --- a/public/externalLibs/sound/soundToneMatrix.js +++ b/public/externalLibs/sound/soundToneMatrix.js @@ -36,7 +36,7 @@ var timeout_matrix; // for coloring the matrix accordingly while it's being played var timeout_color; -var timeout_objects = []; +var timeout_objects = new Array(); // vector_to_list returns a list that contains the elements of the argument vector // in the given order. @@ -54,7 +54,7 @@ function vector_to_list(vector) { function x_y_to_row_column(x, y) { var row = Math.floor((y - margin_length) / (square_side_length + distance_between_squares)); var column = Math.floor((x - margin_length) / (square_side_length + distance_between_squares)); - return [row, column]; + return Array(row, column); } // given the row number of a square, return the leftmost coordinate @@ -365,5 +365,5 @@ function clear_all_timeout() { clearTimeout(timeout_objects[i]); } - timeout_objects = []; + timeout_objects = new Array(); } diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index a7d2f9d52e..04775583f0 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -381,8 +381,6 @@ const GradingSubmissionTable: React.FC = ({ ); }; -export default GradingSubmissionTable; - type FilterableProps = { column: Column; value: string; @@ -396,8 +394,9 @@ const Filterable: React.FC = ({ column, value, children, onClic }; return ( - ); }; +export default GradingSubmissionTable; From 199c8e11e29725d1406f785f5f1582d17dfe415e Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Sun, 18 Feb 2024 13:02:02 +0800 Subject: [PATCH 038/102] Use destructuring for simplicity --- src/commons/mocks/BackendMocks.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/commons/mocks/BackendMocks.ts b/src/commons/mocks/BackendMocks.ts index b602143c8e..7fb565c417 100644 --- a/src/commons/mocks/BackendMocks.ts +++ b/src/commons/mocks/BackendMocks.ts @@ -162,11 +162,9 @@ export function* mockBackendSaga(): SagaIterator { FETCH_GRADING_OVERVIEWS, function* (action: ReturnType): any { const accessToken = yield select((state: OverallState) => state.session.accessToken); - const filterToGroup = action.payload.filterToGroup; - const pageParams = action.payload.pageParams; - const backendParams = action.payload.filterParams; + const { filterToGroup, pageParams, filterParams } = action.payload; const gradingOverviews = yield call(() => - mockFetchGradingOverview(accessToken, filterToGroup, pageParams, backendParams) + mockFetchGradingOverview(accessToken, filterToGroup, pageParams, filterParams) ); if (gradingOverviews !== null) { yield put(actions.updateGradingOverviews(gradingOverviews)); From 355bee9196eddeef899735a5bf27a28e3b3c521f Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Sun, 18 Feb 2024 13:05:11 +0800 Subject: [PATCH 039/102] Use type alias for GradingOverviews To support further refactoring. --- src/commons/application/actions/SessionActions.ts | 4 ++-- src/commons/application/types/SessionTypes.ts | 4 ++-- src/features/grading/GradingTypes.ts | 5 +++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/commons/application/actions/SessionActions.ts b/src/commons/application/actions/SessionActions.ts index 53299f178a..2856112f96 100644 --- a/src/commons/application/actions/SessionActions.ts +++ b/src/commons/application/actions/SessionActions.ts @@ -1,6 +1,6 @@ import { action } from 'typesafe-actions'; // EDITED -import { Grading, GradingOverview } from '../../../features/grading/GradingTypes'; +import { Grading, GradingOverviews } from '../../../features/grading/GradingTypes'; import { Assessment, AssessmentConfiguration, @@ -220,7 +220,7 @@ export const updateTotalXp = (totalXp: number) => action(UPDATE_TOTAL_XP, totalX export const updateAssessment = (assessment: Assessment) => action(UPDATE_ASSESSMENT, assessment); -export const updateGradingOverviews = (overviews: { count: number; data: GradingOverview[] }) => +export const updateGradingOverviews = (overviews: GradingOverviews) => action(UPDATE_GRADING_OVERVIEWS, overviews); /** diff --git a/src/commons/application/types/SessionTypes.ts b/src/commons/application/types/SessionTypes.ts index e12fa4b67b..4b2a916943 100644 --- a/src/commons/application/types/SessionTypes.ts +++ b/src/commons/application/types/SessionTypes.ts @@ -1,7 +1,7 @@ import { Octokit } from '@octokit/rest'; import { Chapter, Variant } from 'js-slang/dist/types'; -import { Grading, GradingOverview } from '../../../features/grading/GradingTypes'; +import { Grading, GradingOverviews } from '../../../features/grading/GradingTypes'; import { Device, DeviceSession } from '../../../features/remoteExecution/RemoteExecutionTypes'; import { Assessment, @@ -118,7 +118,7 @@ export type SessionState = { readonly assessmentOverviews?: AssessmentOverview[]; readonly assessments: Map; - readonly gradingOverviews?: { count: number; data: GradingOverview[] }; + readonly gradingOverviews?: GradingOverviews; readonly gradings: Map; readonly notifications: Notification[]; readonly googleUser?: string; diff --git a/src/features/grading/GradingTypes.ts b/src/features/grading/GradingTypes.ts index 80e5938516..30e7863f27 100644 --- a/src/features/grading/GradingTypes.ts +++ b/src/features/grading/GradingTypes.ts @@ -34,6 +34,11 @@ export type GradingOverview = { gradedCount: number; }; +export type GradingOverviews = { + count: number; // To support server-side pagination + data: GradingOverview[]; +}; + export type GradingOverviewWithNotifications = { notifications: Notification[]; } & GradingOverview; From 263919fd8703e4041474947159f6d97b7f12d3ec Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Sun, 18 Feb 2024 13:08:34 +0800 Subject: [PATCH 040/102] Fix incorrect typing --- src/commons/mocks/BackendMocks.ts | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/commons/mocks/BackendMocks.ts b/src/commons/mocks/BackendMocks.ts index 7fb565c417..18f88f3756 100644 --- a/src/commons/mocks/BackendMocks.ts +++ b/src/commons/mocks/BackendMocks.ts @@ -2,7 +2,7 @@ import { SagaIterator } from 'redux-saga'; import { call, put, select, takeEvery } from 'redux-saga/effects'; import { FETCH_GROUP_GRADING_SUMMARY } from '../../features/dashboard/DashboardTypes'; -import { Grading, GradingOverview, GradingQuestion } from '../../features/grading/GradingTypes'; +import { Grading, GradingOverviews, GradingQuestion } from '../../features/grading/GradingTypes'; import { OverallState, Role, @@ -185,10 +185,14 @@ export function* mockBackendSaga(): SagaIterator { UNSUBMIT_SUBMISSION, function* (action: ReturnType) { const { submissionId } = action.payload; - const overviews: GradingOverview[] = yield select( - (state: OverallState) => state.session.gradingOverviews || [] + const overviews: GradingOverviews = yield select( + (state: OverallState) => + state.session.gradingOverviews || { + count: 0, + data: [] + } ); - const index = overviews.findIndex( + const index = overviews.data.findIndex( overview => overview.submissionId === submissionId && overview.submissionStatus === 'submitted' ); @@ -196,17 +200,14 @@ export function* mockBackendSaga(): SagaIterator { yield call(showWarningMessage, '400: Bad Request'); return; } - const newEntries = { - count: Object.keys(overviews).length, - data: (overviews as GradingOverview[]).map(overview => { - if (overview.submissionId === submissionId) { - return { ...overview, submissionStatus: 'attempted' }; - } - return overview; - }) - }; + const newOverviews = overviews.data.map(overview => { + if (overview.submissionId === submissionId) { + return { ...overview, submissionStatus: 'attempted' }; + } + return overview; + }); yield call(showSuccessMessage, 'Unsubmit successful!', 1000); - yield put(actions.updateGradingOverviews(newEntries)); + yield put(actions.updateGradingOverviews({ ...overviews, data: newOverviews })); } ); From a410e25ae87191fc5121e70e6180839848385410 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Sun, 18 Feb 2024 13:09:20 +0800 Subject: [PATCH 041/102] Remove extra whitespace --- src/commons/sagas/RequestsSaga.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index c6a77b79f2..6521ee6327 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -611,9 +611,7 @@ export const getGradingOverviews = async ( const resp = await request( `${courseId()}/admin/grading?group=${group}&${pageQuery}&${filterQuery}`, 'GET', - { - ...tokens - } + { ...tokens } ); if (!resp) { return null; // invalid accessToken _and_ refreshToken From 5aeec7569d6b5f232af3d9b081a68378029a1c50 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Sun, 18 Feb 2024 13:11:26 +0800 Subject: [PATCH 042/102] Fix tests --- src/commons/application/reducers/__tests__/SessionReducer.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/commons/application/reducers/__tests__/SessionReducer.ts b/src/commons/application/reducers/__tests__/SessionReducer.ts index 8d622a5a15..705d994fed 100644 --- a/src/commons/application/reducers/__tests__/SessionReducer.ts +++ b/src/commons/application/reducers/__tests__/SessionReducer.ts @@ -532,7 +532,10 @@ test('UPDATE_GRADING_OVERVIEWS works correctly in inserting grading overviews', test('UPDATE_GRADING_OVERVIEWS works correctly in updating grading overviews', () => { const newDefaultSession = { ...defaultSession, - gradingOverviews: gradingOverviewTest1 + gradingOverviews: { + count: gradingOverviewTest1.length, + data: gradingOverviewTest1 + } }; const gradingOverviewsPayload = [...gradingOverviewTest2, ...gradingOverviewTest1]; const action = { From 8584e9b2116389657d8599a37e50604e22531ccc Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Sun, 18 Feb 2024 13:19:09 +0800 Subject: [PATCH 043/102] Use new type alias in test file This was missed during the initial refactor. --- src/commons/application/actions/__tests__/SessionActions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commons/application/actions/__tests__/SessionActions.ts b/src/commons/application/actions/__tests__/SessionActions.ts index 0ff918f453..17d36f710e 100644 --- a/src/commons/application/actions/__tests__/SessionActions.ts +++ b/src/commons/application/actions/__tests__/SessionActions.ts @@ -1,6 +1,6 @@ import { Chapter, Variant } from 'js-slang/dist/types'; -import { Grading, GradingOverview } from '../../../../features/grading/GradingTypes'; +import { Grading, GradingOverviews } from '../../../../features/grading/GradingTypes'; import { Assessment, AssessmentOverview } from '../../../assessment/AssessmentTypes'; import { Notification } from '../../../notificationBadge/NotificationBadgeTypes'; import { GameState, Role, Story } from '../../ApplicationTypes'; @@ -520,7 +520,7 @@ test('updateAssessment generates correct action object', () => { }); test('updateGradingOverviews generates correct action object', () => { - const overviews: { count: number; data: GradingOverview[] } = { + const overviews: GradingOverviews = { count: 1, data: [ { From 4b628c5cb966f91a1a583704991d896842852eb3 Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 19 Feb 2024 12:48:36 +0800 Subject: [PATCH 044/102] Now includes backend filtering by group name. All backend filters implemented except grading status. --- .../academy/grading/subcomponents/GradingSubmissionsTable.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 04775583f0..c6191c3182 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -212,6 +212,8 @@ const GradingSubmissionTable: React.FC = ({ return { username: column.value }; case 'submissionStatus': return { status: column.value }; + case 'groupName': + return { groupName: column.value }; case 'gradingStatus': if (column.value === GradingStatuses.none) { return { From 0e8152e697bb283dafb116555954d0c630675d97 Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 19 Feb 2024 15:03:50 +0800 Subject: [PATCH 045/102] Fixed bug where filtering did not reset page. --- src/commons/sagas/RequestsSaga.ts | 1 - .../subcomponents/GradingSubmissionsTable.tsx | 14 +++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index 6521ee6327..13d4a4cf78 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -704,7 +704,6 @@ export const getGrading = async (submissionId: number, tokens: Tokens): Promise< result.grade.grader = gradingQuestion.grade.grader; result.grade.gradedAt = gradingQuestion.grade.gradedAt; } - return result; }); diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index c6191c3182..c3e82570db 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -263,6 +263,14 @@ const GradingSubmissionTable: React.FC = ({ { value: true, label: 'all' } ]; + // modifies state setters to reset page as well. + function setStateAndReset(stateChanger: React.Dispatch): React.Dispatch{ + return (value: T) => { + stateChanger(value); + resetPage(); + } + } + // tells page to ask for new entries from main page when its state changes. useEffect(() => { updateEntries(limitGroup, pageParams(), backendFilterParams(columnFilters, showAllSubmissions)); @@ -275,7 +283,7 @@ const GradingSubmissionTable: React.FC = ({ (setShowAllSubmissions)} popoverProps={{ position: Position.BOTTOM }} buttonProps={{ minimal: true, rightIcon: 'caret-down' }} /> @@ -283,7 +291,7 @@ const GradingSubmissionTable: React.FC = ({ (setLimitGroup)} popoverProps={{ position: Position.BOTTOM }} buttonProps={{ minimal: true, rightIcon: 'caret-down' }} /> @@ -349,7 +357,7 @@ const GradingSubmissionTable: React.FC = ({ size="xs" icon={() => } variant="light" - onClick={() => setPage(0)} + onClick={() => resetPage()} disabled={page <= 0} /> - - - - - - - - - - - - + + + + Submissions + + + + + ) } fullWidth={true} diff --git a/src/pages/academy/grading/subcomponents/GradingSummary.tsx b/src/pages/academy/grading/subcomponents/GradingSummary.tsx deleted file mode 100644 index 2e302f543b..0000000000 --- a/src/pages/academy/grading/subcomponents/GradingSummary.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { - Badge, - Block, - Bold, - Flex, - List, - ListItem, - Metric, - ProgressBar, - Text, - Title -} from '@tremor/react'; -import { AssessmentOverview } from 'src/commons/assessment/AssessmentTypes'; -import { GradingOverview } from 'src/features/grading/GradingTypes'; -import { isSubmissionUngraded } from 'src/features/grading/GradingUtils'; - -import { AssessmentTypeBadge } from './GradingBadges'; - -type GradingSummaryProps = { - submissions: GradingOverview[]; - assessments: AssessmentOverview[]; -}; - -type AssessmentSummary = { - id: number; - type: string; - title: string; -}; - -const GradingSummary: React.FC = ({ submissions, assessments }) => { - const ungraded = submissions.filter(isSubmissionUngraded); - const ungradedAssessments = [...new Set(ungraded.map(({ assessmentId }) => assessmentId))].reduce( - (acc: AssessmentSummary[], assessmentId) => { - const assessment = assessments.find(assessment => assessment.id === assessmentId); - if (!assessment) return acc; - return [ - ...acc, - { - id: assessmentId, - type: assessment.type, - title: assessment.title - } - ]; - }, - [] - ); - - const numSubmissions = submissions.length; - const numGraded = numSubmissions - ungraded.length; - const percentGraded = Math.round((numGraded / numSubmissions) * 100); - - const numUngradedByAssessment = (assessmentId: number) => { - return ungraded.filter(({ assessmentId: id }) => id === assessmentId).length; - }; - - return ( - <> - My gradings - - {numGraded} - / {numSubmissions} graded - - - - - - Graded - - {numGraded} ({percentGraded}%) - - - - Ungraded - - {numSubmissions - numGraded} ({100 - percentGraded}%) - - - - - - Ungraded assessments - - {ungradedAssessments.length === 0 ? ( - 🎉 Good job! You've graded everything! 🎉 - ) : ( - - {ungradedAssessments.map(({ id, title, type }) => ( - - - - {title} - - - - ))} - - )} - - ); -}; - -export default GradingSummary; From 49824b5f7f99f7b02e209fa3bce28f3daa4e70a0 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 21 Feb 2024 12:48:55 +0800 Subject: [PATCH 064/102] Move expensive util function out of component --- src/features/grading/GradingUtils.ts | 2 +- .../subcomponents/GradingSubmissionsTable.tsx | 84 ++++++++++--------- 2 files changed, 45 insertions(+), 41 deletions(-) diff --git a/src/features/grading/GradingUtils.ts b/src/features/grading/GradingUtils.ts index e3cfb9c8a1..f03e34fe94 100644 --- a/src/features/grading/GradingUtils.ts +++ b/src/features/grading/GradingUtils.ts @@ -68,4 +68,4 @@ export const exportGradingCSV = (gradingOverviews: GradingOverview[] | undefined win.setTimeout(() => { win.URL.revokeObjectURL(url); }, 0); -}; +}; \ 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 3edc5a6d8e..9b631edbec 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -190,46 +190,8 @@ const GradingSubmissionTable: React.FC = ({ } ] ) - .map((column: ColumnFilter) => { - // TODO: change all references to column properties in backend saga to backend name to reduce - // un-needed hardcode conversion, ensuring that places that reference it are updated. - switch (column.id) { - case 'assessmentName': - return { title: column.value }; - case 'assessmentType': - return { type: column.value }; - case 'studentName': - return { name: column.value }; - case 'studentUsername': - return { username: column.value }; - case 'submissionStatus': - return { status: column.value }; - case 'groupName': - return { groupName: column.value }; - case 'gradingStatus': - if (column.value === GradingStatuses.none) { - return { - isManuallyGraded: true, - status: 'submitted', - numGraded: 0 - }; - } else if (column.value === GradingStatuses.graded) { - // TODO: coordinate with backend on subquerying to implement the third query - // currently ignored by backend as of 16 Feb 24 commit - return { - isManuallyGraded: true, - status: 'submitted', - numGradedEqualToTotal: true - }; - } else { - // case: excluded or grading. Not implemented yet. - return {}; - } - default: - return column; - } - }) - .reduce(Object.assign, {}); + .map(convertFilterToBackendParams) + .reduce(Object.assign, {}) }, [columnFilters, showAllSubmissions]); // Adapts frontend page and pageSize state into useable offset for backend usage. @@ -395,6 +357,48 @@ const GradingSubmissionTable: React.FC = ({ ); }; +// Cleanup work: change all references to column properties in backend saga to backend name to reduce +// un-needed hardcode conversion, ensuring that places that reference it are updated. A two-way conversion +// function would be good to implement in GradingUtils. +const convertFilterToBackendParams = (column: ColumnFilter) => { + switch (column.id) { + case 'assessmentName': + return { title: column.value }; + case 'assessmentType': + return { type: column.value }; + case 'studentName': + return { name: column.value }; + case 'studentUsername': + return { username: column.value }; + case 'submissionStatus': + return { status: column.value }; + case 'groupName': + return { groupName: column.value }; + case 'gradingStatus': + if (column.value === GradingStatuses.none) { + return { + isManuallyGraded: true, + status: 'submitted', + numGraded: 0 + }; + } else if (column.value === GradingStatuses.graded) { + // TODO: coordinate with backend on subquerying to implement the third query + // currently ignored by backend as of 16 Feb 24 commit + return { + isManuallyGraded: true, + status: 'submitted', + numGradedEqualToTotal: true + }; + } else { + // case: excluded or grading. Not implemented yet. + return {}; + } + default: + return column; + } +}; + + type FilterableProps = { column: Column; value: string; From 6e754c21988f4537c117503db1da3268dd212995 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 21 Feb 2024 13:24:52 +0800 Subject: [PATCH 065/102] PR change: util function + shift dropdown back to grading page --- src/features/grading/GradingUtils.ts | 2 +- src/pages/academy/grading/Grading.tsx | 62 +++++++++++-- .../subcomponents/GradingSubmissionsTable.tsx | 90 +++---------------- 3 files changed, 69 insertions(+), 85 deletions(-) diff --git a/src/features/grading/GradingUtils.ts b/src/features/grading/GradingUtils.ts index f03e34fe94..e3cfb9c8a1 100644 --- a/src/features/grading/GradingUtils.ts +++ b/src/features/grading/GradingUtils.ts @@ -68,4 +68,4 @@ export const exportGradingCSV = (gradingOverviews: GradingOverview[] | undefined win.setTimeout(() => { win.URL.revokeObjectURL(url); }, 0); -}; \ No newline at end of file +}; diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index 158db21586..97f3ae1f82 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -1,12 +1,13 @@ import '@tremor/react/dist/esm/tremor.css'; -import { Icon as BpIcon, NonIdealState, Spinner, SpinnerSize } from '@blueprintjs/core'; +import { Icon as BpIcon, NonIdealState, Position, Spinner, SpinnerSize } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; -import { Button, Card, Flex, Title } from '@tremor/react'; -import React, { useCallback, useEffect } from 'react'; +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 SimpleDropdown from 'src/commons/SimpleDropdown'; import { useSession } from 'src/commons/utils/Hooks'; import { numberRegExp } from 'src/features/academy/AcademyTypes'; import { exportGradingCSV } from 'src/features/grading/GradingUtils'; @@ -23,12 +24,34 @@ const Grading: React.FC = () => { questionId: string; }>(); + const [showAllSubmissions, setShowAllSubmissions] = useState(false); + const showSubmissionOptions = [ + { value: false, label: 'ungraded' }, + { value: true, label: 'all' } + ]; + + // TODO: implement isAdmin functionality + const [limitGroup, setLimitGroup] = useState(true); + const groupOptions = [ + { value: true, label: 'my groups' }, + { value: false, label: 'all groups' } + ]; + + // Dropdown tab options, which contains some external state. + // This can be a candidate for its own component once backend feature implementation is complete. + const [pageSize, setPageSize] = useState(10); + const pageSizeOptions = [ + { value: 10, label: '10' }, + { value: 20, label: '20' }, + { value: 50, label: '50' } + ]; + const dispatch = useDispatch(); const updateGradingOverviewsCallback = useCallback( - (group: boolean, pageParams: { offset: number; pageSize: number }, filterParams: Object) => { - dispatch(fetchGradingOverviews(group, pageParams, filterParams)); + (pageParams: { offset: number; pageSize: number }, filterParams: Object) => { + dispatch(fetchGradingOverviews(limitGroup, pageParams, filterParams)); }, - [dispatch] + [dispatch, limitGroup] ); // Default value initializer @@ -87,8 +110,35 @@ const Grading: React.FC = () => { + + Viewing + + submissions from + + Entries per page + + diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 9b631edbec..17de04351b 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, Position } from '@blueprintjs/core'; +import { Icon as BpIcon } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import { Column, @@ -30,7 +30,6 @@ import { import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; import { GradingStatuses } from 'src/commons/assessment/AssessmentTypes'; -import SimpleDropdown from 'src/commons/SimpleDropdown'; import { useTypedSelector } from 'src/commons/utils/Hooks'; import { updateSubmissionsTableFilters } from 'src/commons/workspace/WorkspaceActions'; import { GradingOverview } from 'src/features/grading/GradingTypes'; @@ -110,10 +109,11 @@ const makeColumns = (handleClick: () => void) => [ type GradingSubmissionTableProps = { totalRows: number; + pageSize: number; submissions: GradingOverview[]; // TODO: Abstract pageParams object into a useable type. updateEntries: ( - group: boolean, + pageParams: { offset: number; pageSize: number }, filterParams: any ) => void; @@ -121,6 +121,7 @@ type GradingSubmissionTableProps = { const GradingSubmissionTable: React.FC = ({ totalRows, + pageSize, submissions, updateEntries }) => { @@ -134,44 +135,12 @@ const GradingSubmissionTable: React.FC = ({ const [globalFilter, setGlobalFilter] = useState(tableFilters.globalFilter); const [page, setPage] = useState(0); - const [pageSize, setPageSize] = useState(10); - // This is needed because a filter change is accompanied with a page reset. + // This is needed because a filter change, or a change in pageSize prop, is accompanied with a page reset. const resetPage = useCallback(() => setPage(0), [setPage]); - const [showAllSubmissions, setShowAllSubmissions] = useState(false); - const showSubmissionOptions = [ - { value: false, label: 'ungraded' }, - { value: true, label: 'all' } - ]; - - // TODO: implement isAdmin functionality - const [limitGroup, setLimitGroup] = useState(true); - const groupOptions = [ - { value: true, label: 'my groups' }, - { value: false, label: 'all groups' } - ]; - - // Dropdown tab options, which contains some external state. - // This can be a candidate for its own component once backend feature implementation is complete. - const pageSizeOptions = [ - { value: 1, label: '1' }, - { value: 5, label: '5' }, - { value: 10, label: '10' }, - { value: 20, label: '20' }, - { value: 50, label: '50' } - ]; - const maxPage = useMemo(() => Math.ceil(totalRows / pageSize) - 1, [totalRows, pageSize]); - // modifies state setters to reset page as well. - function setStateAndReset(stateChanger: React.Dispatch): React.Dispatch { - return (value: T) => { - stateChanger(value); - resetPage(); - }; - } - // Converts the columnFilters array into backend query parameters. // Memoized as derived data to prevent infinite re-rendering. // TEMP IMPLEMENTATION. Values currently hardcoded with knowledge of what a ColumnFilter is. @@ -180,19 +149,9 @@ const GradingSubmissionTable: React.FC = ({ // TODO: make a controller component, like in the achievements page, to handle conversion of page state into JSON. const backendFilterParams = useMemo(() => { return columnFilters - .concat( - showAllSubmissions - ? [] - : [ - { - id: 'gradingStatus', - value: GradingStatuses.none - } - ] - ) .map(convertFilterToBackendParams) - .reduce(Object.assign, {}) - }, [columnFilters, showAllSubmissions]); + .reduce(Object.assign, {}); + }, [columnFilters]); // Adapts frontend page and pageSize state into useable offset for backend usage. const pageParams = useMemo(() => { @@ -203,6 +162,9 @@ const GradingSubmissionTable: React.FC = ({ }, [page, pageSize]); const columns = useMemo(() => makeColumns(resetPage), [resetPage]); + + useEffect(() => resetPage(), [pageSize]); + const table = useReactTable({ data: submissions, columns, @@ -239,37 +201,11 @@ const GradingSubmissionTable: React.FC = ({ // tells page to ask for new entries from main page when its state changes. useEffect(() => { - updateEntries(limitGroup, pageParams, backendFilterParams); - }, [updateEntries, limitGroup, pageParams, backendFilterParams]); + updateEntries(pageParams, backendFilterParams); + }, [updateEntries, pageParams, backendFilterParams]); return ( <> - - Viewing - - Submissions from - - Entries per page - -
@@ -282,7 +218,6 @@ const GradingSubmissionTable: React.FC = ({
- } @@ -398,7 +333,6 @@ const convertFilterToBackendParams = (column: ColumnFilter) => { } }; - type FilterableProps = { column: Column; value: string; From 87e072b9da60106c30f316f36cf2aceebd7ff108 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 21 Feb 2024 13:25:50 +0800 Subject: [PATCH 066/102] linting --- .../grading/subcomponents/GradingSubmissionsTable.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 17de04351b..91c2497ab8 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -112,11 +112,7 @@ type GradingSubmissionTableProps = { pageSize: number; submissions: GradingOverview[]; // TODO: Abstract pageParams object into a useable type. - updateEntries: ( - - pageParams: { offset: number; pageSize: number }, - filterParams: any - ) => void; + updateEntries: (pageParams: { offset: number; pageSize: number }, filterParams: any) => void; }; const GradingSubmissionTable: React.FC = ({ @@ -148,9 +144,7 @@ const GradingSubmissionTable: React.FC = ({ // TODO: implement reversible backend-frontend name conversion for use in RequestsSaga and here, remove hardcode. // TODO: make a controller component, like in the achievements page, to handle conversion of page state into JSON. const backendFilterParams = useMemo(() => { - return columnFilters - .map(convertFilterToBackendParams) - .reduce(Object.assign, {}); + return columnFilters.map(convertFilterToBackendParams).reduce(Object.assign, {}); }, [columnFilters]); // Adapts frontend page and pageSize state into useable offset for backend usage. From a1d8bfda0cdb1106f9dc595dd713bddeba926103 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 21 Feb 2024 16:33:50 +0800 Subject: [PATCH 067/102] Actually pushes grading util migration --- src/features/grading/GradingUtils.ts | 42 ++++++++++++++++++ .../subcomponents/GradingSubmissionsTable.tsx | 43 +------------------ 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/src/features/grading/GradingUtils.ts b/src/features/grading/GradingUtils.ts index e3cfb9c8a1..7179fe2bf8 100644 --- a/src/features/grading/GradingUtils.ts +++ b/src/features/grading/GradingUtils.ts @@ -1,3 +1,4 @@ +import { ColumnFilter } from '@tanstack/react-table'; import { GradingStatuses } from 'src/commons/assessment/AssessmentTypes'; import { GradingOverview } from './GradingTypes'; @@ -69,3 +70,44 @@ export const exportGradingCSV = (gradingOverviews: GradingOverview[] | undefined win.URL.revokeObjectURL(url); }, 0); }; + +// Cleanup work: change all references to column properties in backend saga to backend name to reduce +// un-needed hardcode conversion, ensuring that places that reference it are updated. A two-way conversion +// function would be good to implement in GradingUtils. +export const convertFilterToBackendParams = (column: ColumnFilter) => { + switch (column.id) { + case 'assessmentName': + return { title: column.value }; + case 'assessmentType': + return { type: column.value }; + case 'studentName': + return { name: column.value }; + case 'studentUsername': + return { username: column.value }; + case 'submissionStatus': + return { status: column.value }; + case 'groupName': + return { groupName: column.value }; + case 'gradingStatus': + if (column.value === GradingStatuses.none) { + return { + isManuallyGraded: true, + status: 'submitted', + numGraded: 0 + }; + } else if (column.value === GradingStatuses.graded) { + // TODO: coordinate with backend on subquerying to implement the third query + // currently ignored by backend as of 16 Feb 24 commit + return { + isManuallyGraded: true, + status: 'submitted', + numGradedEqualToTotal: true + }; + } else { + // case: excluded or grading. Not implemented yet. + return {}; + } + default: + return column; + } +}; diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 91c2497ab8..ba26a32473 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -29,10 +29,10 @@ import { } from '@tremor/react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; -import { GradingStatuses } from 'src/commons/assessment/AssessmentTypes'; import { useTypedSelector } from 'src/commons/utils/Hooks'; import { updateSubmissionsTableFilters } from 'src/commons/workspace/WorkspaceActions'; import { GradingOverview } from 'src/features/grading/GradingTypes'; +import { convertFilterToBackendParams } from 'src/features/grading/GradingUtils'; import GradingActions from './GradingActions'; import { AssessmentTypeBadge, GradingStatusBadge, SubmissionStatusBadge } from './GradingBadges'; @@ -286,47 +286,6 @@ const GradingSubmissionTable: React.FC = ({ ); }; -// Cleanup work: change all references to column properties in backend saga to backend name to reduce -// un-needed hardcode conversion, ensuring that places that reference it are updated. A two-way conversion -// function would be good to implement in GradingUtils. -const convertFilterToBackendParams = (column: ColumnFilter) => { - switch (column.id) { - case 'assessmentName': - return { title: column.value }; - case 'assessmentType': - return { type: column.value }; - case 'studentName': - return { name: column.value }; - case 'studentUsername': - return { username: column.value }; - case 'submissionStatus': - return { status: column.value }; - case 'groupName': - return { groupName: column.value }; - case 'gradingStatus': - if (column.value === GradingStatuses.none) { - return { - isManuallyGraded: true, - status: 'submitted', - numGraded: 0 - }; - } else if (column.value === GradingStatuses.graded) { - // TODO: coordinate with backend on subquerying to implement the third query - // currently ignored by backend as of 16 Feb 24 commit - return { - isManuallyGraded: true, - status: 'submitted', - numGradedEqualToTotal: true - }; - } else { - // case: excluded or grading. Not implemented yet. - return {}; - } - default: - return column; - } -}; - type FilterableProps = { column: Column; value: string; From 44f0a14b27a3a818bbe1360c87c4340175cd802f Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 21 Feb 2024 17:20:46 +0800 Subject: [PATCH 068/102] useRef flag to disable re-initialization of callback prop in submissions table component --- src/pages/academy/grading/Grading.tsx | 3 ++- .../subcomponents/GradingSubmissionsTable.tsx | 14 +++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index 97f3ae1f82..a8c63cbc2f 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -42,7 +42,8 @@ const Grading: React.FC = () => { const [pageSize, setPageSize] = useState(10); const pageSizeOptions = [ { value: 10, label: '10' }, - { value: 20, label: '20' }, + { value: 15, label: '15' }, + { value: 25, label: '25' }, { value: 50, label: '50' } ]; diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index ba26a32473..7ecd657a2a 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -27,7 +27,7 @@ import { Text, TextInput } from '@tremor/react'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useTypedSelector } from 'src/commons/utils/Hooks'; import { updateSubmissionsTableFilters } from 'src/commons/workspace/WorkspaceActions'; @@ -121,8 +121,6 @@ const GradingSubmissionTable: React.FC = ({ submissions, updateEntries }) => { - // TODO: implement functionality for submission filtering by groups using the group state from useSession. - const tableFilters = useTypedSelector(state => state.workspaces.grading.submissionsTableFilters); const [columnFilters, setColumnFilters] = useState([ @@ -157,7 +155,7 @@ const GradingSubmissionTable: React.FC = ({ const columns = useMemo(() => makeColumns(resetPage), [resetPage]); - useEffect(() => resetPage(), [pageSize]); + useEffect(() => resetPage(), [resetPage,pageSize]); const table = useReactTable({ data: submissions, @@ -193,9 +191,15 @@ const GradingSubmissionTable: React.FC = ({ ); }, [columnFilters, globalFilter, dispatch]); + // initialization is done by the main page. + const isFirstRender = useRef(true); // tells page to ask for new entries from main page when its state changes. useEffect(() => { - updateEntries(pageParams, backendFilterParams); + if (isFirstRender.current === true) { + isFirstRender.current = false; + } else { + updateEntries(pageParams, backendFilterParams); + } }, [updateEntries, pageParams, backendFilterParams]); return ( From da82dc0daf32fa93282895c8f250c497103fc0ad Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 21 Feb 2024 17:29:55 +0800 Subject: [PATCH 069/102] Revert: Submissions from groups now default to all / my groups based on admin role and valid group name --- src/pages/academy/grading/Grading.tsx | 6 +++--- .../grading/subcomponents/GradingSubmissionsTable.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index a8c63cbc2f..f73cd8c3c3 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -7,6 +7,7 @@ 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 { numberRegExp } from 'src/features/academy/AcademyTypes'; @@ -18,7 +19,7 @@ import GradingSubmissionsTable from './subcomponents/GradingSubmissionsTable'; import GradingWorkspace from './subcomponents/GradingWorkspace'; const Grading: React.FC = () => { - const { courseId, gradingOverviews } = useSession(); + const { courseId, gradingOverviews, role, group } = useSession(); const params = useParams<{ submissionId: string; questionId: string; @@ -30,8 +31,7 @@ const Grading: React.FC = () => { { value: true, label: 'all' } ]; - // TODO: implement isAdmin functionality - const [limitGroup, setLimitGroup] = useState(true); + const [limitGroup, setLimitGroup] = useState(role !== Role.Admin && group !== null); const groupOptions = [ { value: true, label: 'my groups' }, { value: false, label: 'all groups' } diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 7ecd657a2a..e45f9ceb8b 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -155,7 +155,7 @@ const GradingSubmissionTable: React.FC = ({ const columns = useMemo(() => makeColumns(resetPage), [resetPage]); - useEffect(() => resetPage(), [resetPage,pageSize]); + useEffect(() => resetPage(), [resetPage, pageSize]); const table = useReactTable({ data: submissions, From 5b997a35be6017a09d8e8d3c012a218151099abd Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 21 Feb 2024 17:33:59 +0800 Subject: [PATCH 070/102] Move dropdown options out of component --- src/pages/academy/grading/Grading.tsx | 34 ++++++++++++++------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index f73cd8c3c3..640e1c0f30 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -18,6 +18,23 @@ import { convertParamToInt } from '../../../commons/utils/ParamParseHelper'; import GradingSubmissionsTable from './subcomponents/GradingSubmissionsTable'; import GradingWorkspace from './subcomponents/GradingWorkspace'; +const showSubmissionOptions = [ + { value: false, label: 'ungraded' }, + { value: true, label: 'all' } +]; + +const groupOptions = [ + { value: true, label: 'my groups' }, + { value: false, label: 'all groups' } +]; + +const pageSizeOptions = [ + { value: 10, label: '10' }, + { value: 15, label: '15' }, + { value: 25, label: '25' }, + { value: 50, label: '50' } +]; + const Grading: React.FC = () => { const { courseId, gradingOverviews, role, group } = useSession(); const params = useParams<{ @@ -26,26 +43,11 @@ const Grading: React.FC = () => { }>(); const [showAllSubmissions, setShowAllSubmissions] = useState(false); - const showSubmissionOptions = [ - { value: false, label: 'ungraded' }, - { value: true, label: 'all' } - ]; const [limitGroup, setLimitGroup] = useState(role !== Role.Admin && group !== null); - const groupOptions = [ - { value: true, label: 'my groups' }, - { value: false, label: 'all groups' } - ]; - // Dropdown tab options, which contains some external state. - // This can be a candidate for its own component once backend feature implementation is complete. const [pageSize, setPageSize] = useState(10); - const pageSizeOptions = [ - { value: 10, label: '10' }, - { value: 15, label: '15' }, - { value: 25, label: '25' }, - { value: 50, label: '50' } - ]; + const dispatch = useDispatch(); const updateGradingOverviewsCallback = useCallback( From be95a8872495bb58e79c416c43f882be04f122f6 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Wed, 21 Feb 2024 19:05:15 +0800 Subject: [PATCH 071/102] Update whitespace Minimizes unnecessary changes to the diff. --- .../grading/subcomponents/GradingSubmissionsTable.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index e45f9ceb8b..1f51bc7975 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -129,12 +129,10 @@ const GradingSubmissionTable: React.FC = ({ const [globalFilter, setGlobalFilter] = useState(tableFilters.globalFilter); const [page, setPage] = useState(0); - + const maxPage = useMemo(() => Math.ceil(totalRows / pageSize) - 1, [totalRows, pageSize]); // This is needed because a filter change, or a change in pageSize prop, is accompanied with a page reset. const resetPage = useCallback(() => setPage(0), [setPage]); - const maxPage = useMemo(() => Math.ceil(totalRows / pageSize) - 1, [totalRows, pageSize]); - // Converts the columnFilters array into backend query parameters. // Memoized as derived data to prevent infinite re-rendering. // TEMP IMPLEMENTATION. Values currently hardcoded with knowledge of what a ColumnFilter is. @@ -216,6 +214,7 @@ const GradingSubmissionTable: React.FC = ({
+ } @@ -296,6 +295,7 @@ type FilterableProps = { children?: React.ReactNode; onClick?: () => void; }; + const Filterable: React.FC = ({ column, value, children, onClick }) => { const handleFilterChange = () => { column.setFilterValue(value); @@ -308,4 +308,5 @@ const Filterable: React.FC = ({ column, value, children, onClic ); }; + export default GradingSubmissionTable; From a905b6fe967acb3456b828a0f7cd41d95ae340d7 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Wed, 21 Feb 2024 19:15:13 +0800 Subject: [PATCH 072/102] Invert `limitGroup` back to `showAllGroups` --- src/pages/academy/grading/Grading.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index 640e1c0f30..4d226ca510 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -24,8 +24,8 @@ const showSubmissionOptions = [ ]; const groupOptions = [ - { value: true, label: 'my groups' }, - { value: false, label: 'all groups' } + { value: false, label: 'my groups' }, + { value: true, label: 'all groups' } ]; const pageSizeOptions = [ @@ -44,17 +44,17 @@ const Grading: React.FC = () => { const [showAllSubmissions, setShowAllSubmissions] = useState(false); - const [limitGroup, setLimitGroup] = useState(role !== Role.Admin && group !== null); + const isAdmin = role === Role.Admin; + const [showAllGroups, setShowAllGroups] = useState(isAdmin || group === null); const [pageSize, setPageSize] = useState(10); - const dispatch = useDispatch(); const updateGradingOverviewsCallback = useCallback( (pageParams: { offset: number; pageSize: number }, filterParams: Object) => { - dispatch(fetchGradingOverviews(limitGroup, pageParams, filterParams)); + dispatch(fetchGradingOverviews(!showAllGroups, pageParams, filterParams)); }, - [dispatch, limitGroup] + [dispatch, showAllGroups] ); // Default value initializer @@ -125,8 +125,8 @@ const Grading: React.FC = () => { submissions from From da2773882ae565b914108da418db785e504438a5 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Wed, 21 Feb 2024 19:17:35 +0800 Subject: [PATCH 073/102] Rename `showSubmissionOptions` back to `showOptions` Facilitates merging the upstream later. --- src/pages/academy/grading/Grading.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index 4d226ca510..c0ae23c4ba 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -18,16 +18,16 @@ import { convertParamToInt } from '../../../commons/utils/ParamParseHelper'; import GradingSubmissionsTable from './subcomponents/GradingSubmissionsTable'; import GradingWorkspace from './subcomponents/GradingWorkspace'; -const showSubmissionOptions = [ - { value: false, label: 'ungraded' }, - { value: true, label: 'all' } -]; - const groupOptions = [ { value: false, label: 'my groups' }, { value: true, label: 'all groups' } ]; +const showOptions = [ + { value: false, label: 'ungraded' }, + { value: true, label: 'all' } +]; + const pageSizeOptions = [ { value: 10, label: '10' }, { value: 15, label: '15' }, @@ -116,7 +116,7 @@ const Grading: React.FC = () => { Viewing Date: Wed, 21 Feb 2024 19:20:26 +0800 Subject: [PATCH 074/102] Reorder lines To facilitate merging later, as well as minimizing unnecessary changes to the diff. --- src/pages/academy/grading/Grading.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index c0ae23c4ba..0606edb790 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -42,8 +42,6 @@ const Grading: React.FC = () => { questionId: string; }>(); - const [showAllSubmissions, setShowAllSubmissions] = useState(false); - const isAdmin = role === Role.Admin; const [showAllGroups, setShowAllGroups] = useState(isAdmin || group === null); @@ -62,6 +60,8 @@ const Grading: React.FC = () => { dispatch(fetchGradingOverviews()); }, [dispatch]); + const [showAllSubmissions, setShowAllSubmissions] = useState(false); + // If submissionId or questionId is defined but not numeric, redirect back to the Grading overviews page if ( (params.submissionId && !params.submissionId?.match(numberRegExp)) || From 6ba9a5abc6842b12ecd40df1c0db09fddf264b74 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Wed, 21 Feb 2024 19:33:15 +0800 Subject: [PATCH 075/102] Improve code readability * Remove extra whitespace * Use destructuring --- src/commons/application/actions/SessionActions.ts | 7 +------ src/commons/sagas/BackendSaga.ts | 5 +---- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/commons/application/actions/SessionActions.ts b/src/commons/application/actions/SessionActions.ts index 8a8b786cfe..bfd9474dad 100644 --- a/src/commons/application/actions/SessionActions.ts +++ b/src/commons/application/actions/SessionActions.ts @@ -114,12 +114,7 @@ export const fetchGradingOverviews = ( filterToGroup = true, pageParams = { offset: 0, pageSize: 10 }, filterParams = {} -) => - action(FETCH_GRADING_OVERVIEWS, { - filterToGroup, - pageParams, - filterParams - }); +) => action(FETCH_GRADING_OVERVIEWS, { filterToGroup, pageParams, filterParams }); export const login = (providerId: string) => action(LOGIN, providerId); diff --git a/src/commons/sagas/BackendSaga.ts b/src/commons/sagas/BackendSaga.ts index 301b217949..7032c50c0d 100644 --- a/src/commons/sagas/BackendSaga.ts +++ b/src/commons/sagas/BackendSaga.ts @@ -414,9 +414,7 @@ function* BackendSaga(): SagaIterator { function* (action: ReturnType) { const tokens: Tokens = yield selectTokens(); - const filterToGroup = action.payload.filterToGroup; - const pageParams = action.payload.pageParams; - const filterParams = action.payload.filterParams; + const { filterToGroup, pageParams, filterParams } = action.payload; const gradingOverviews: { count: number; data: GradingOverview[] } | null = yield call( getGradingOverviews, @@ -425,7 +423,6 @@ function* BackendSaga(): SagaIterator { pageParams, filterParams ); - if (gradingOverviews) { yield put(actions.updateGradingOverviews(gradingOverviews)); } From 61f06affc25e718cddb2b41950282e9fe24ef99b Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 21 Feb 2024 22:13:02 +0800 Subject: [PATCH 076/102] globalFilter removed from tanstack, search now mapped to mission title for backend querying --- src/commons/application/ApplicationTypes.ts | 3 +- src/commons/workspace/WorkspaceTypes.ts | 1 - .../subcomponents/GradingSubmissionsTable.tsx | 34 +++++++++---------- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/commons/application/ApplicationTypes.ts b/src/commons/application/ApplicationTypes.ts index 35a39af4aa..92f7182593 100644 --- a/src/commons/application/ApplicationTypes.ts +++ b/src/commons/application/ApplicationTypes.ts @@ -402,8 +402,7 @@ export const defaultWorkspaceManager: WorkspaceManagerState = { grading: { ...createDefaultWorkspace('grading'), submissionsTableFilters: { - columnFilters: [], - globalFilter: null + columnFilters: [] }, currentSubmission: undefined, currentQuestion: undefined, diff --git a/src/commons/workspace/WorkspaceTypes.ts b/src/commons/workspace/WorkspaceTypes.ts index 81998af787..1295bf0608 100644 --- a/src/commons/workspace/WorkspaceTypes.ts +++ b/src/commons/workspace/WorkspaceTypes.ts @@ -165,5 +165,4 @@ export type DebuggerContext = { export type SubmissionsTableFilters = { columnFilters: { id: string; value: unknown }[]; - globalFilter: string | null; }; diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 1f51bc7975..9c77b804ee 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -111,7 +111,6 @@ type GradingSubmissionTableProps = { totalRows: number; pageSize: number; submissions: GradingOverview[]; - // TODO: Abstract pageParams object into a useable type. updateEntries: (pageParams: { offset: number; pageSize: number }, filterParams: any) => void; }; @@ -126,22 +125,26 @@ const GradingSubmissionTable: React.FC = ({ const [columnFilters, setColumnFilters] = useState([ ...tableFilters.columnFilters ]); - const [globalFilter, setGlobalFilter] = useState(tableFilters.globalFilter); + + // Polish: debounce this search, or have a onClick event listener search instead. + // Polish: if search value does not change content of submissions, do not reset page. + // not as easy as i thought it was with setTimeout. + const [searchValue, setSearchValue] = useState(''); const [page, setPage] = useState(0); + const maxPage = useMemo(() => Math.ceil(totalRows / pageSize) - 1, [totalRows, pageSize]); // This is needed because a filter change, or a change in pageSize prop, is accompanied with a page reset. const resetPage = useCallback(() => setPage(0), [setPage]); // Converts the columnFilters array into backend query parameters. - // Memoized as derived data to prevent infinite re-rendering. - // TEMP IMPLEMENTATION. Values currently hardcoded with knowledge of what a ColumnFilter is. - // TODO: remove hardcoding conversion of all submissions to column filter. it is a hacky workaround. - // TODO: implement reversible backend-frontend name conversion for use in RequestsSaga and here, remove hardcode. - // TODO: make a controller component, like in the achievements page, to handle conversion of page state into JSON. + // Concat search params override filter. const backendFilterParams = useMemo(() => { - return columnFilters.map(convertFilterToBackendParams).reduce(Object.assign, {}); - }, [columnFilters]); + return columnFilters + .map(convertFilterToBackendParams) + .concat([{ title: searchValue }]) + .reduce(Object.assign, {}); + }, [columnFilters, searchValue]); // Adapts frontend page and pageSize state into useable offset for backend usage. const pageParams = useMemo(() => { @@ -153,14 +156,13 @@ const GradingSubmissionTable: React.FC = ({ const columns = useMemo(() => makeColumns(resetPage), [resetPage]); - useEffect(() => resetPage(), [resetPage, pageSize]); + useEffect(() => resetPage(), [resetPage, pageSize, searchValue]); const table = useReactTable({ data: submissions, columns, state: { columnFilters, - globalFilter, pagination: { // pagination is handled by server to fit exactly the pageSize. Thus, hardcode frontend pageIndex to 0. pageIndex: 0, @@ -168,7 +170,6 @@ const GradingSubmissionTable: React.FC = ({ } }, onColumnFiltersChange: setColumnFilters, - onGlobalFilterChange: setGlobalFilter, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), getPaginationRowModel: getPaginationRowModel() @@ -183,11 +184,10 @@ const GradingSubmissionTable: React.FC = ({ useEffect(() => { dispatch( updateSubmissionsTableFilters({ - columnFilters, - globalFilter + columnFilters }) ); - }, [columnFilters, globalFilter, dispatch]); + }, [columnFilters, dispatch]); // initialization is done by the main page. const isFirstRender = useRef(true); @@ -219,8 +219,8 @@ const GradingSubmissionTable: React.FC = ({ maxWidth="max-w-sm" icon={() => } placeholder="Search for any value here..." - value={globalFilter ?? ''} - onChange={e => setGlobalFilter(e.target.value)} + value={searchValue} + onChange={e => setSearchValue(e.target.value)} /> From 7ef4d847f2208c80f0a6d4909750028204202c39 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 21 Feb 2024 23:22:55 +0800 Subject: [PATCH 077/102] update test types --- src/commons/workspace/__tests__/WorkspaceActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commons/workspace/__tests__/WorkspaceActions.ts b/src/commons/workspace/__tests__/WorkspaceActions.ts index 1e10a0345a..915b74a8ac 100644 --- a/src/commons/workspace/__tests__/WorkspaceActions.ts +++ b/src/commons/workspace/__tests__/WorkspaceActions.ts @@ -559,7 +559,7 @@ test('updateSubmissionsTableFilters generates correct action object', () => { } ]; const globalFilter = 'runes'; - const action = updateSubmissionsTableFilters({ columnFilters, globalFilter }); + const action = updateSubmissionsTableFilters({ columnFilters }); expect(action).toEqual({ type: UPDATE_SUBMISSIONS_TABLE_FILTERS, payload: { From 4b076a7034dbe89b0cd3a8c12f2a482ac4577a7f Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 21 Feb 2024 23:30:54 +0800 Subject: [PATCH 078/102] removed globalFilter in test --- src/commons/workspace/__tests__/WorkspaceActions.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/commons/workspace/__tests__/WorkspaceActions.ts b/src/commons/workspace/__tests__/WorkspaceActions.ts index 915b74a8ac..9135f1b218 100644 --- a/src/commons/workspace/__tests__/WorkspaceActions.ts +++ b/src/commons/workspace/__tests__/WorkspaceActions.ts @@ -558,14 +558,12 @@ test('updateSubmissionsTableFilters generates correct action object', () => { value: 'Missions' } ]; - const globalFilter = 'runes'; const action = updateSubmissionsTableFilters({ columnFilters }); expect(action).toEqual({ type: UPDATE_SUBMISSIONS_TABLE_FILTERS, payload: { filters: { columnFilters, - globalFilter } } }); From 212ad3561a7c5e27cab907c1e9b71ad70cffa4c8 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 21 Feb 2024 23:56:09 +0800 Subject: [PATCH 079/102] lint --- src/commons/workspace/__tests__/WorkspaceActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commons/workspace/__tests__/WorkspaceActions.ts b/src/commons/workspace/__tests__/WorkspaceActions.ts index 9135f1b218..7e76bbf03a 100644 --- a/src/commons/workspace/__tests__/WorkspaceActions.ts +++ b/src/commons/workspace/__tests__/WorkspaceActions.ts @@ -563,7 +563,7 @@ test('updateSubmissionsTableFilters generates correct action object', () => { type: UPDATE_SUBMISSIONS_TABLE_FILTERS, payload: { filters: { - columnFilters, + columnFilters } } }); From 987123c5c9c61a12126ad74aae2dc159ed6ce813 Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 22 Feb 2024 14:35:12 +0800 Subject: [PATCH 080/102] Removed filtering support for individual grading stauses, filtering by grading status will be handled by dropdown only --- src/features/grading/GradingUtils.ts | 21 +------------------ .../subcomponents/GradingSubmissionsTable.tsx | 4 ++-- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/src/features/grading/GradingUtils.ts b/src/features/grading/GradingUtils.ts index 7179fe2bf8..7e48743f99 100644 --- a/src/features/grading/GradingUtils.ts +++ b/src/features/grading/GradingUtils.ts @@ -88,26 +88,7 @@ export const convertFilterToBackendParams = (column: ColumnFilter) => { return { status: column.value }; case 'groupName': return { groupName: column.value }; - case 'gradingStatus': - if (column.value === GradingStatuses.none) { - return { - isManuallyGraded: true, - status: 'submitted', - numGraded: 0 - }; - } else if (column.value === GradingStatuses.graded) { - // TODO: coordinate with backend on subquerying to implement the third query - // currently ignored by backend as of 16 Feb 24 commit - return { - isManuallyGraded: true, - status: 'submitted', - numGradedEqualToTotal: true - }; - } else { - // case: excluded or grading. Not implemented yet. - return {}; - } default: - return column; + return {}; } }; diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 9c77b804ee..b29b914e28 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -76,9 +76,9 @@ const makeColumns = (handleClick: () => void) => [ columnHelper.accessor('gradingStatus', { header: 'Grading', cell: info => ( - + - + ) }), columnHelper.accessor(({ currentXp, xpBonus, maxXp }) => ({ currentXp, xpBonus, maxXp }), { From daee1398917382c2fef2fed84f2826584a120e21 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 23 Feb 2024 04:52:58 +0800 Subject: [PATCH 081/102] All frontend filtering removed. --- .../application/actions/SessionActions.ts | 13 ++-- .../actions/__tests__/SessionActions.ts | 13 +++- src/commons/sagas/BackendSaga.ts | 3 +- src/commons/sagas/RequestsSaga.ts | 10 +-- src/features/grading/GradingUtils.ts | 13 ++++ src/pages/academy/grading/Grading.tsx | 31 +++++--- .../subcomponents/GradingSubmissionsTable.tsx | 72 +++++++++---------- 7 files changed, 99 insertions(+), 56 deletions(-) diff --git a/src/commons/application/actions/SessionActions.ts b/src/commons/application/actions/SessionActions.ts index bfd9474dad..4fd6d51b45 100644 --- a/src/commons/application/actions/SessionActions.ts +++ b/src/commons/application/actions/SessionActions.ts @@ -1,3 +1,7 @@ +import { + paginationToBackendParams, + ungradedToBackendParams +} from 'src/features/grading/GradingUtils'; import { action } from 'typesafe-actions'; // EDITED import { GradingOverviews, GradingQuery } from '../../../features/grading/GradingTypes'; @@ -102,19 +106,20 @@ export const fetchTotalXpAdmin = (courseRegId: number) => action(FETCH_TOTAL_XP_ export const fetchGrading = (submissionId: number) => action(FETCH_GRADING, submissionId); /** - * @param filterToGroup - param when set to true, only shows submissions under the group + * @param filterToGroup - param that when set to true, only shows submissions under the group * of the grader + * @param gradedFilter - backend params to filter to ungraded * @param pageParams - param that contains offset and pageSize, informing backend about how * many entries, starting from what offset, to get * @param filterParams - param that contains columnFilters converted into JSON for * processing into query parameters */ -// TEMPORARY IMPLEMENTATION. TODO: Refactor into a filters type once proof of feature is complete. export const fetchGradingOverviews = ( filterToGroup = true, - pageParams = { offset: 0, pageSize: 10 }, + gradedFilter = ungradedToBackendParams(false), + pageParams = paginationToBackendParams(0, 10), filterParams = {} -) => action(FETCH_GRADING_OVERVIEWS, { filterToGroup, pageParams, filterParams }); +) => action(FETCH_GRADING_OVERVIEWS, { filterToGroup, gradedFilter, pageParams, filterParams }); export const login = (providerId: string) => action(LOGIN, providerId); diff --git a/src/commons/application/actions/__tests__/SessionActions.ts b/src/commons/application/actions/__tests__/SessionActions.ts index 65d38be6a4..ad878862f5 100644 --- a/src/commons/application/actions/__tests__/SessionActions.ts +++ b/src/commons/application/actions/__tests__/SessionActions.ts @@ -1,4 +1,8 @@ import { Chapter, Variant } from 'js-slang/dist/types'; +import { + paginationToBackendParams, + ungradedToBackendParams +} from 'src/features/grading/GradingUtils'; import { GradingOverviews, GradingQuery } from '../../../../features/grading/GradingTypes'; import { Assessment, AssessmentOverview } from '../../../assessment/AssessmentTypes'; @@ -152,7 +156,8 @@ test('fetchGradingOverviews generates correct default action object', () => { type: FETCH_GRADING_OVERVIEWS, payload: { filterToGroup: true, - pageParams: { offset: 0, pageSize: 10 }, + gradedFilter: ungradedToBackendParams(false), + pageParams: paginationToBackendParams(0, 10), filterParams: {} } }); @@ -160,13 +165,15 @@ test('fetchGradingOverviews generates correct default action object', () => { test('fetchGradingOverviews generates correct action object', () => { const filterToGroup = false; - const pageParams = { offset: 12, pageSize: 3 }; + const gradedFilter = ungradedToBackendParams(true); + const pageParams = { offset: 123, pageSize: 456 }; const filterParams = { abc: 'xxx', def: 'yyy' }; - const action = fetchGradingOverviews(filterToGroup, pageParams, filterParams); + const action = fetchGradingOverviews(filterToGroup, gradedFilter, pageParams, filterParams); expect(action).toEqual({ type: FETCH_GRADING_OVERVIEWS, payload: { filterToGroup: filterToGroup, + gradedFilter: gradedFilter, pageParams: pageParams, filterParams: filterParams } diff --git a/src/commons/sagas/BackendSaga.ts b/src/commons/sagas/BackendSaga.ts index 7032c50c0d..29fd7a0a9b 100644 --- a/src/commons/sagas/BackendSaga.ts +++ b/src/commons/sagas/BackendSaga.ts @@ -414,12 +414,13 @@ function* BackendSaga(): SagaIterator { function* (action: ReturnType) { const tokens: Tokens = yield selectTokens(); - const { filterToGroup, pageParams, filterParams } = action.payload; + const { filterToGroup, gradedFilter, pageParams, filterParams } = action.payload; const gradingOverviews: { count: number; data: GradingOverview[] } | null = yield call( getGradingOverviews, tokens, filterToGroup, + gradedFilter, pageParams, filterParams ); diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index 33903fa9b9..61600ec47f 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -602,19 +602,21 @@ export const postAssessment = async (id: number, tokens: Tokens): Promise => { - // this converts the payload into a useable query string without a leading '?' via implicit toString(). + // this converts the payload into a useable query string without a leading '?' + const gradedQuery = new URLSearchParams(graded); const pageQuery = new URLSearchParams(pageParams); const filterQuery = new URLSearchParams(filterParams); + + // gradedQuery placed behind filterQuery to override progress filter if any const resp = await request( - `${courseId()}/admin/grading?group=${group}&${pageQuery}&${filterQuery}`, + `${courseId()}/admin/grading?group=${group}&${pageQuery}&${filterQuery}&${gradedQuery}`, 'GET', { ...tokens } ); diff --git a/src/features/grading/GradingUtils.ts b/src/features/grading/GradingUtils.ts index 7e48743f99..18dfff8330 100644 --- a/src/features/grading/GradingUtils.ts +++ b/src/features/grading/GradingUtils.ts @@ -92,3 +92,16 @@ export const convertFilterToBackendParams = (column: ColumnFilter) => { return {}; } }; + +export const paginationToBackendParams = (page: number, pageSize: number) => { + return { offset: page * pageSize, pageSize: pageSize }; +}; + +export const ungradedToBackendParams = (showAll: boolean) => { + return showAll + ? {} + : { + status: 'submitted', + isManuallyGraded: true + }; +}; diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index 70c542ab3f..522761b581 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -11,7 +11,11 @@ import { Role } from 'src/commons/application/ApplicationTypes'; import SimpleDropdown from 'src/commons/SimpleDropdown'; import { useSession } from 'src/commons/utils/Hooks'; import { numberRegExp } from 'src/features/academy/AcademyTypes'; -import { exportGradingCSV } from 'src/features/grading/GradingUtils'; +import { + exportGradingCSV, + paginationToBackendParams, + ungradedToBackendParams +} from 'src/features/grading/GradingUtils'; import ContentDisplay from '../../../commons/ContentDisplay'; import { convertParamToInt } from '../../../commons/utils/ParamParseHelper'; @@ -42,23 +46,33 @@ const Grading: React.FC = () => { const isAdmin = role === Role.Admin; const [showAllGroups, setShowAllGroups] = useState(isAdmin || group === null); + const [showAllSubmissions, setShowAllSubmissions] = useState(false); + + const [page, setPage] = useState(0); + const [pageSize, setPageSize] = useState(10); const dispatch = useDispatch(); const updateGradingOverviewsCallback = useCallback( - (pageParams: { offset: number; pageSize: number }, filterParams: Object) => { - dispatch(fetchGradingOverviews(!showAllGroups, pageParams, filterParams)); + (filterParams: Object, changeTo = 0) => { + setPage(p => changeTo); + dispatch( + fetchGradingOverviews( + showAllGroups, + ungradedToBackendParams(showAllSubmissions), + paginationToBackendParams(changeTo, pageSize), + filterParams + ) + ); }, - [dispatch, showAllGroups] + [dispatch, showAllGroups, showAllSubmissions, pageSize] ); - // Default value initializer + // Default value initializer, runs once only useEffect(() => { - dispatch(fetchGradingOverviews()); + dispatch(fetchGradingOverviews(showAllGroups)); }, [dispatch]); - const [showAllSubmissions, setShowAllSubmissions] = useState(false); - // If submissionId or questionId is defined but not numeric, redirect back to the Grading overviews page if ( (params.submissionId && !params.submissionId?.match(numberRegExp)) || @@ -138,6 +152,7 @@ const Grading: React.FC = () => { (); -const makeColumns = (handleClick: () => void) => [ +const columns = [ columnHelper.accessor('assessmentName', { header: 'Name', - cell: info => + cell: info => }), columnHelper.accessor('assessmentType', { header: 'Type', cell: info => ( - + ) }), columnHelper.accessor('studentName', { header: 'Student', - cell: info => + cell: info => }), columnHelper.accessor('studentUsername', { header: 'Username', - cell: info => + cell: info => }), columnHelper.accessor('groupName', { header: 'Group', - cell: info => + cell: info => }), columnHelper.accessor('submissionStatus', { header: 'Progress', cell: info => ( - + ) @@ -109,13 +109,15 @@ const makeColumns = (handleClick: () => void) => [ type GradingSubmissionTableProps = { totalRows: number; + page: number; pageSize: number; submissions: GradingOverview[]; - updateEntries: (pageParams: { offset: number; pageSize: number }, filterParams: any) => void; + updateEntries: (filterParams: Object, pageChange?: any) => void; }; const GradingSubmissionTable: React.FC = ({ totalRows, + page, pageSize, submissions, updateEntries @@ -131,32 +133,22 @@ const GradingSubmissionTable: React.FC = ({ // not as easy as i thought it was with setTimeout. const [searchValue, setSearchValue] = useState(''); - const [page, setPage] = useState(0); + // masquerade search value as a column filter. + const searchFilter: ColumnFilter[] = useMemo( + () => [{ id: 'assessmentName', value: searchValue }], + [searchValue] + ); const maxPage = useMemo(() => Math.ceil(totalRows / pageSize) - 1, [totalRows, pageSize]); - // This is needed because a filter change, or a change in pageSize prop, is accompanied with a page reset. - const resetPage = useCallback(() => setPage(0), [setPage]); // Converts the columnFilters array into backend query parameters. - // Concat search params override filter. const backendFilterParams = useMemo(() => { - return columnFilters + // allow column filters to override search bar filter. + return searchFilter + .concat(columnFilters) .map(convertFilterToBackendParams) - .concat([{ title: searchValue }]) .reduce(Object.assign, {}); - }, [columnFilters, searchValue]); - - // Adapts frontend page and pageSize state into useable offset for backend usage. - const pageParams = useMemo(() => { - return { - offset: page * pageSize, - pageSize: pageSize - }; - }, [page, pageSize]); - - const columns = useMemo(() => makeColumns(resetPage), [resetPage]); - - useEffect(() => resetPage(), [resetPage, pageSize, searchValue]); + }, [searchFilter, columnFilters]); const table = useReactTable({ data: submissions, @@ -189,16 +181,24 @@ const GradingSubmissionTable: React.FC = ({ ); }, [columnFilters, dispatch]); - // initialization is done by the main page. + // for operations that do not need a page reset, i.e. page change. + const changePage = useCallback( + (change: any) => { + updateEntries(backendFilterParams, change); + }, + [updateEntries, backendFilterParams] + ); + + // for operations that require a page reset (indirectly). + // also initializes the table when dropdown filters change, affecting updateEntries const isFirstRender = useRef(true); - // tells page to ask for new entries from main page when its state changes. useEffect(() => { if (isFirstRender.current === true) { isFirstRender.current = false; } else { - updateEntries(pageParams, backendFilterParams); + updateEntries(backendFilterParams); } - }, [updateEntries, pageParams, backendFilterParams]); + }, [updateEntries, backendFilterParams]); return ( <> @@ -218,7 +218,7 @@ const GradingSubmissionTable: React.FC = ({ } - placeholder="Search for any value here..." + placeholder="assessment name search" value={searchValue} onChange={e => setSearchValue(e.target.value)} /> @@ -255,14 +255,14 @@ const GradingSubmissionTable: React.FC = ({ size="xs" icon={() => } variant="light" - onClick={() => setPage(0)} + onClick={() => changePage(0)} disabled={page <= 0} />
From 6358e22d8ef2ae802b9a6c2ec300bf7432053c31 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Fri, 23 Feb 2024 12:57:52 +0800 Subject: [PATCH 084/102] Revert "Debouncing added to search bar." This reverts commit d990a37fc9aed2fba8482138d56a93e3828aef66. --- .../subcomponents/GradingSubmissionsTable.tsx | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index f9c292fae6..8eb4094912 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -128,22 +128,15 @@ const GradingSubmissionTable: React.FC = ({ ...tableFilters.columnFilters ]); - const [visibleSearchValue, setVisibleSearchValue] = useState(''); - const [debouncedSearchValue, setDebouncedSearchValue] = useState(''); - - // timeout delay could be a placed within a config. - useEffect(() => { - const changeSearch = setTimeout(() => { - setDebouncedSearchValue(visibleSearchValue); - }, 300); - - return () => clearTimeout(changeSearch); - }, [visibleSearchValue]); + // Polish: debounce this search, or have a onClick event listener search instead. + // Polish: if search value does not change content of submissions, do not reset page. + // not as easy as i thought it was with setTimeout. + const [searchValue, setSearchValue] = useState(''); // masquerade search value as a column filter. const searchFilter: ColumnFilter[] = useMemo( - () => [{ id: 'assessmentName', value: debouncedSearchValue }], - [debouncedSearchValue] + () => [{ id: 'assessmentName', value: searchValue }], + [searchValue] ); const maxPage = useMemo(() => Math.ceil(totalRows / pageSize) - 1, [totalRows, pageSize]); @@ -226,8 +219,8 @@ const GradingSubmissionTable: React.FC = ({ maxWidth="max-w-sm" icon={() => } placeholder="assessment name search" - value={visibleSearchValue} - onChange={e => setVisibleSearchValue(e.target.value)} + value={searchValue} + onChange={e => setSearchValue(e.target.value)} />
From 844c38b274ff1100e75b5cb3125e26bf9c39d487 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Fri, 23 Feb 2024 13:00:04 +0800 Subject: [PATCH 085/102] Use lodash for debouncing over setTimeout --- .../grading/subcomponents/GradingSubmissionsTable.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 8eb4094912..c9fcbbb75f 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -27,6 +27,7 @@ import { Text, TextInput } from '@tremor/react'; +import { debounce } from 'lodash'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useTypedSelector } from 'src/commons/utils/Hooks'; @@ -128,10 +129,8 @@ const GradingSubmissionTable: React.FC = ({ ...tableFilters.columnFilters ]); - // Polish: debounce this search, or have a onClick event listener search instead. - // Polish: if search value does not change content of submissions, do not reset page. - // not as easy as i thought it was with setTimeout. const [searchValue, setSearchValue] = useState(''); + const debouncedSetSearchValue = debounce(setSearchValue, 300); // masquerade search value as a column filter. const searchFilter: ColumnFilter[] = useMemo( @@ -220,7 +219,7 @@ const GradingSubmissionTable: React.FC = ({ icon={() => } placeholder="assessment name search" value={searchValue} - onChange={e => setSearchValue(e.target.value)} + onChange={e => debouncedSetSearchValue(e.target.value)} />
From 8157a3f0b07eed77932397e7ad6e4708917b3276 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Fri, 23 Feb 2024 13:06:16 +0800 Subject: [PATCH 086/102] Fix controlled search field regression --- .../grading/subcomponents/GradingSubmissionsTable.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index c9fcbbb75f..999e67b296 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -129,8 +129,15 @@ const GradingSubmissionTable: React.FC = ({ ...tableFilters.columnFilters ]); + /** 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 debouncedSetSearchValue = debounce(setSearchValue, 300); + const handleSearchQueryUpdate: React.ChangeEventHandler = e => { + setSearchQuery(e.target.value); + debouncedSetSearchValue(e.target.value); + }; // masquerade search value as a column filter. const searchFilter: ColumnFilter[] = useMemo( @@ -218,8 +225,8 @@ const GradingSubmissionTable: React.FC = ({ maxWidth="max-w-sm" icon={() => } placeholder="assessment name search" - value={searchValue} - onChange={e => debouncedSetSearchValue(e.target.value)} + value={searchQuery} + onChange={handleSearchQueryUpdate} />
From 52f8fbfa8fb78512ffdc23533c5f9402283db85b Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Fri, 23 Feb 2024 21:15:55 +0800 Subject: [PATCH 087/102] Move pagination logic back inside table component This reverts part of daee1398917382c2fef2fed84f2826584a120e21. --- src/pages/academy/grading/Grading.tsx | 10 ++--- .../subcomponents/GradingSubmissionsTable.tsx | 41 ++++++++----------- 2 files changed, 20 insertions(+), 31 deletions(-) diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index 522761b581..16a13f6b6c 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -48,19 +48,16 @@ const Grading: React.FC = () => { const [showAllSubmissions, setShowAllSubmissions] = useState(false); - const [page, setPage] = useState(0); - const [pageSize, setPageSize] = useState(10); const dispatch = useDispatch(); const updateGradingOverviewsCallback = useCallback( - (filterParams: Object, changeTo = 0) => { - setPage(p => changeTo); + (page: number, filterParams: Object) => { dispatch( fetchGradingOverviews( showAllGroups, ungradedToBackendParams(showAllSubmissions), - paginationToBackendParams(changeTo, pageSize), + paginationToBackendParams(page, pageSize), filterParams ) ); @@ -71,7 +68,7 @@ const Grading: React.FC = () => { // Default value initializer, runs once only useEffect(() => { dispatch(fetchGradingOverviews(showAllGroups)); - }, [dispatch]); + }, [dispatch, showAllGroups]); // If submissionId or questionId is defined but not numeric, redirect back to the Grading overviews page if ( @@ -152,7 +149,6 @@ const Grading: React.FC = () => { (); -const columns = [ +const makeColumns = (handleClick: () => void) => [ columnHelper.accessor('assessmentName', { header: 'Name', - cell: info => + cell: info => }), columnHelper.accessor('assessmentType', { header: 'Type', cell: info => ( - + ) }), columnHelper.accessor('studentName', { header: 'Student', - cell: info => + cell: info => }), columnHelper.accessor('studentUsername', { header: 'Username', - cell: info => + cell: info => }), columnHelper.accessor('groupName', { header: 'Group', - cell: info => + cell: info => }), columnHelper.accessor('submissionStatus', { header: 'Progress', cell: info => ( - + ) @@ -110,15 +110,13 @@ const columns = [ type GradingSubmissionTableProps = { totalRows: number; - page: number; pageSize: number; submissions: GradingOverview[]; - updateEntries: (filterParams: Object, pageChange?: any) => void; + updateEntries: (page: number, filterParams: Object) => void; }; const GradingSubmissionTable: React.FC = ({ totalRows, - page, pageSize, submissions, updateEntries @@ -145,7 +143,9 @@ const GradingSubmissionTable: React.FC = ({ [searchValue] ); + const [page, setPage] = useState(0); const maxPage = useMemo(() => Math.ceil(totalRows / pageSize) - 1, [totalRows, pageSize]); + const resetPage = useCallback(() => setPage(0), [setPage]); // Converts the columnFilters array into backend query parameters. const backendFilterParams = useMemo(() => { @@ -156,6 +156,7 @@ const GradingSubmissionTable: React.FC = ({ .reduce(Object.assign, {}); }, [searchFilter, columnFilters]); + const columns = useMemo(() => makeColumns(resetPage), [resetPage]); const table = useReactTable({ data: submissions, columns, @@ -187,14 +188,6 @@ const GradingSubmissionTable: React.FC = ({ ); }, [columnFilters, dispatch]); - // for operations that do not need a page reset, i.e. page change. - const changePage = useCallback( - (change: any) => { - updateEntries(backendFilterParams, change); - }, - [updateEntries, backendFilterParams] - ); - // for operations that require a page reset (indirectly). // also initializes the table when dropdown filters change, affecting updateEntries const isFirstRender = useRef(true); @@ -202,9 +195,9 @@ const GradingSubmissionTable: React.FC = ({ if (isFirstRender.current === true) { isFirstRender.current = false; } else { - updateEntries(backendFilterParams); + updateEntries(page, backendFilterParams); } - }, [updateEntries, backendFilterParams]); + }, [updateEntries, page, backendFilterParams]); return ( <> @@ -261,14 +254,14 @@ const GradingSubmissionTable: React.FC = ({ size="xs" icon={() => } variant="light" - onClick={() => changePage(0)} + onClick={() => setPage(0)} disabled={page <= 0} />