From d8cb5d593bac097abb5dd51f67a977743cfd1721 Mon Sep 17 00:00:00 2001 From: Alex Lewin Date: Thu, 27 Feb 2025 14:41:25 +0000 Subject: [PATCH 1/7] Move manage tests filters to sidebar (phy) --- .../elements/layout/SidebarLayout.tsx | 109 ++++++- .../components/pages/quizzes/SetQuizzes.tsx | 303 +++++++++--------- 2 files changed, 265 insertions(+), 147 deletions(-) diff --git a/src/app/components/elements/layout/SidebarLayout.tsx b/src/app/components/elements/layout/SidebarLayout.tsx index 4426bc9aa5..3fe7ff2ae8 100644 --- a/src/app/components/elements/layout/SidebarLayout.tsx +++ b/src/app/components/elements/layout/SidebarLayout.tsx @@ -1,5 +1,5 @@ import React, { ChangeEvent, RefObject, useEffect, useRef, useState } from "react"; -import { Col, ColProps, RowProps, Input, Offcanvas, OffcanvasBody, OffcanvasHeader, Row } from "reactstrap"; +import { Col, ColProps, RowProps, Input, Offcanvas, OffcanvasBody, OffcanvasHeader, Row, DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from "reactstrap"; import partition from "lodash/partition"; import classNames from "classnames"; import { AssignmentDTO, ContentSummaryDTO, IsaacConceptPageDTO, QuestionDTO, RegisteredUserDTO } from "../../../../IsaacApiTypes"; @@ -15,6 +15,7 @@ import { ShowLoadingQuery } from "../../handlers/ShowLoadingQuery"; import { Spacer } from "../Spacer"; import { StyledTabPicker } from "../inputs/StyledTabPicker"; import { GroupSelector } from "../../pages/Groups"; +import { formatISODateOnly } from "../DateString"; export const SidebarLayout = (props: RowProps) => { const { className, ...rest } = props; @@ -556,3 +557,109 @@ export const SignupSidebar = ({activeTab} : {activeTab: number}) => { ; }; + +interface SetQuizzesSidebarProps extends SidebarProps { + titleFilter?: string; + setTitleFilter: React.Dispatch>; +}; + +export const SetQuizzesSidebar = (props: SetQuizzesSidebarProps) => { + const { titleFilter, setTitleFilter } = props; + return +
+
Search & Filter
+ Title + setTitleFilter(event.target.value)} + placeholder="Search by title" aria-label="Search by title" + /> + ; +}; + +interface ManageQuizzesSidebarProps extends SidebarProps { + manageQuizzesTitleFilter: string; + setManageQuizzesTitleFilter: React.Dispatch>; + quizStartDate: Date | undefined; + setQuizStartDate: React.Dispatch>; + quizSetDateFilterType: string; + setQuizSetDateFilterType: React.Dispatch>; + quizDueDate: Date | undefined; + setQuizDueDate: React.Dispatch>; + quizDueDateFilterType: string; + setQuizDueDateFilterType: React.Dispatch>; + manageQuizzesGroupNameFilter: string; + setManageQuizzesGroupNameFilter: React.Dispatch>; +}; + +export const ManageQuizzesSidebar = (props: ManageQuizzesSidebarProps) => { + const { manageQuizzesTitleFilter, setManageQuizzesTitleFilter, quizStartDate, setQuizStartDate, + quizSetDateFilterType, setQuizSetDateFilterType, quizDueDate, setQuizDueDate, quizDueDateFilterType, + setQuizDueDateFilterType, manageQuizzesGroupNameFilter, setManageQuizzesGroupNameFilter} = props; + + const dateFilterTypeSelector = (dateFilterType: string, setDateFilterType: React.Dispatch>) => + {dateFilterType} + + setDateFilterType('after')}> + after + + setDateFilterType('before')}> + before + + setDateFilterType('on')}> + on + + + ; + + const titleFilterInput =
+ Title + setManageQuizzesTitleFilter(event.target.value)} + placeholder="Search by title" aria-label="Search by title" + /> +
; + + const groupFilterInput =
+ Group + setManageQuizzesGroupNameFilter(event.target.value)} + placeholder="Search by group" aria-label="Search by group" + /> +
; + + const setDateFilterInput =
+
+ Starting + {dateFilterTypeSelector(quizSetDateFilterType, setQuizSetDateFilterType)} +
+ setQuizStartDate(new Date(event.target.value))} + placeholder="Filter by set date" aria-label="Filter by set date" + /> +
; + + const dueDateFilterInput =
+
+ Due + {dateFilterTypeSelector(quizDueDateFilterType, setQuizDueDateFilterType)} +
+ setQuizDueDate(new Date(event.target.value))} + placeholder="Filter by due date" aria-label="Filter by due date" + /> +
; + + return +
+
Search & Filter
+ {titleFilterInput} + {groupFilterInput} + {setDateFilterInput} + {dueDateFilterInput} + ; +}; \ No newline at end of file diff --git a/src/app/components/pages/quizzes/SetQuizzes.tsx b/src/app/components/pages/quizzes/SetQuizzes.tsx index febce27ab5..9162520afb 100644 --- a/src/app/components/pages/quizzes/SetQuizzes.tsx +++ b/src/app/components/pages/quizzes/SetQuizzes.tsx @@ -37,8 +37,9 @@ import {RenderNothing} from "../../elements/RenderNothing"; import { useHistoryState } from "../../../state/actions/history"; import classNames from "classnames"; import { ExtendDueDateModal } from "../../elements/modals/ExtendDueDateModal"; -import { UncontrolledTooltip, Button, Table, UncontrolledButtonDropdown, DropdownToggle, DropdownMenu, DropdownItem, Row, Input, UncontrolledDropdown, Container, ListGroup, ListGroupItem, Col, Alert } from "reactstrap"; +import { UncontrolledTooltip, Button, Table, UncontrolledButtonDropdown, DropdownToggle, DropdownMenu, DropdownItem, Row, Container, ListGroup, ListGroupItem, Col, Alert, Input, UncontrolledDropdown } from "reactstrap"; import { ListView } from "../../elements/list-groups/ListView"; +import { MainContent, ManageQuizzesSidebar, SetQuizzesSidebar, SidebarLayout } from "../../elements/layout/SidebarLayout"; interface SetQuizzesPageProps extends RouteComponentProps { user: RegisteredUserDTO; @@ -88,7 +89,7 @@ const _compareDates = (a: Date | number | undefined, b: Date | number | undefine return b.valueOf() - a.valueOf(); }; -function QuizAssignment({user, assignedGroups, index}: QuizAssignmentProps) { +function QuizAssignment({assignedGroups, index}: QuizAssignmentProps) { const compareGroupNames = (a: AssignedGroup, b: AssignedGroup) => _compareStrings(a?.group, b?.group); const compareCreationDates = (a: AssignedGroup, b: AssignedGroup) => _compareDates(a?.assignment?.creationDate, b?.assignment?.creationDate); @@ -365,154 +366,164 @@ const SetQuizzesPageComponent = ({user}: SetQuizzesPageProps) => { return - - {{ - [siteSpecific("Set Tests", "Available tests")]: - - {undeprecatedQuizzes && <> -

The following tests are available to set to your groups.

- setTitleFilter(event.target.value)} - placeholder="Search by title" aria-label="Search by title" - /> - {undeprecatedQuizzes.length === 0 &&

There are no tests you can set which match your search term.

} - - {siteSpecific( - , - - - {undeprecatedQuizzes.map(quiz => - - -
- {quiz.title} - {roleVisibilitySummary(quiz)} -
- - - - - - - Preview - - - - - - Actions - - - dispatch(showQuizSettingModal(quiz))} style={{zIndex: '1'}}> + + {activeTab === MANAGE_QUIZ_TAB.set + ? + : } + + + {{ + [siteSpecific("Set Tests", "Available tests")]: + + {undeprecatedQuizzes && <> +

The following tests are available to set to your groups.

+ + {undeprecatedQuizzes.length === 0 &&

There are no tests you can set which match your search term.

} + + {siteSpecific( + , + + {undeprecatedQuizzes.map(quiz => + + +
+ {quiz.title} + {roleVisibilitySummary(quiz)} +
+ + + + + + + Preview -
-
- -
-
)} -
)} - } -
, - - [siteSpecific("Manage Tests", "Previously set tests")]: - <> -
- -
- {showFilters && (rowFiltersView - ? - - {titleFilterInput} - {setDateFilterInput} - - - {groupFilterInput} - {dueDateFilterInput} - - - : - {titleFilterInput} - {groupFilterInput} - {setDateFilterInput} - {dueDateFilterInput} - ) - } - Tests you have assigned have failed to load, please try refreshing the page.} - thenRender={quizAssignments => { - let quizAssignmentsWithGroupNames: AppQuizAssignment[] = quizAssignments.map(assignment => { - const groupName = persistence.load(KEY.ANONYMISE_GROUPS) === "YES" - ? `Demo Group ${assignment.groupId}` - : groupIdToName[assignment.groupId as number] ?? "Unknown Group"; - return {...assignment, groupName}; - }).reverse(); - if (showFilters) { - const filters = []; - if (manageQuizzesTitleFilter !== "") { - filters.push((assignment : AppQuizAssignment) => assignment.quizSummary?.title?.toLowerCase().includes(manageQuizzesTitleFilter.toLowerCase())); - } - if (manageQuizzesGroupNameFilter !== "") { - filters.push((assignment : AppQuizAssignment) => assignment.groupName?.toLowerCase().includes(manageQuizzesGroupNameFilter.toLowerCase())); - } - if (quizStartDate && !isNaN(quizStartDate.valueOf())) { - filters.push((assignment : AppQuizAssignment) => { - return filterByDate(quizSetDateFilterType, assignment.scheduledStartDate ?? assignment.creationDate, quizStartDate); - }); - } - if (quizDueDate && !isNaN(quizDueDate.valueOf())) { - filters.push((assignment : AppQuizAssignment) => { - return filterByDate(quizDueDateFilterType, assignment.dueDate, quizDueDate); - }); - } - quizAssignmentsWithGroupNames = quizAssignmentsWithGroupNames.filter(filters.reduce((acc, filter) => (assignment) => acc(assignment) && filter(assignment), () => true)); + + + + + Actions + + + dispatch(showQuizSettingModal(quiz))} style={{zIndex: '1'}}> + {siteSpecific("Set Test", "Set test")} + + + + + Preview + + + + + + + )} + )} + } + , + + [siteSpecific("Manage Tests", "Previously set tests")]: + <> + {isAda &&
+ +
} + + {/* Ada filters */} + {showFilters && (rowFiltersView + ? + + {titleFilterInput} + {setDateFilterInput} + + + {groupFilterInput} + {dueDateFilterInput} + + + : + {titleFilterInput} + {groupFilterInput} + {setDateFilterInput} + {dueDateFilterInput} + ) } - // an array of objects, each representing one test and the groups it is assigned to - const quizAssignment: QuizAssignmentProps[] = quizAssignmentsWithGroupNames.reduce((acc, assignment) => { - const existing = acc.find(q => q.assignedGroups.map(a => a.assignment.quizId).includes(assignment.quizId)); - if (existing) { - existing.assignedGroups.push({group: assignment.groupName, assignment: assignment}); - } else { - acc.push({user: user, assignedGroups: [{group: assignment.groupName, assignment: assignment}], index: 0}); - } - return acc; - }, [] as QuizAssignmentProps[]); - - // sort the outermost table by quiz title - quizAssignment.sort((a, b) => a.assignedGroups[0].assignment.quizSummary?.title?.localeCompare(b.assignedGroups[0].assignment.quizSummary?.title ?? "") ?? 0); - - return <> - {quizAssignments.length === 0 &&

You have not set any tests to your groups yet.

} - {quizAssignments.length > 0 && - - - - {below["xs"](deviceSize) ? <> : below["lg"](deviceSize) ? : } - - - - {quizAssignment.map((g, i) => )} - -
} - ; - }} - /> - - }} -
+ Tests you have assigned have failed to load, please try refreshing the page.} + thenRender={quizAssignments => { + let quizAssignmentsWithGroupNames: AppQuizAssignment[] = quizAssignments.map(assignment => { + const groupName = persistence.load(KEY.ANONYMISE_GROUPS) === "YES" + ? `Demo Group ${assignment.groupId}` + : groupIdToName[assignment.groupId as number] ?? "Unknown Group"; + return {...assignment, groupName}; + }).reverse(); + + if (showFilters || isPhy) { + const filters = []; + if (manageQuizzesTitleFilter !== "") { + filters.push((assignment : AppQuizAssignment) => assignment.quizSummary?.title?.toLowerCase().includes(manageQuizzesTitleFilter)); + } + if (manageQuizzesGroupNameFilter !== "") { + filters.push((assignment : AppQuizAssignment) => assignment.groupName?.toLowerCase().includes(manageQuizzesGroupNameFilter.toLowerCase())); + } + if (quizStartDate && !isNaN(quizStartDate.valueOf())) { + filters.push((assignment : AppQuizAssignment) => { + return filterByDate(quizSetDateFilterType, assignment.scheduledStartDate ?? assignment.creationDate, quizStartDate); + }); + } + if (quizDueDate && !isNaN(quizDueDate.valueOf())) { + filters.push((assignment : AppQuizAssignment) => { + return filterByDate(quizDueDateFilterType, assignment.dueDate, quizDueDate); + }); + } + quizAssignmentsWithGroupNames = quizAssignmentsWithGroupNames.filter(filters.reduce((acc, filter) => (assignment) => acc(assignment) && filter(assignment), () => true)); + } + + // an array of objects, each representing one test and the groups it is assigned to + const quizAssignment: QuizAssignmentProps[] = quizAssignmentsWithGroupNames.reduce((acc, assignment) => { + const existing = acc.find(q => q.assignedGroups.map(a => a.assignment.quizId).includes(assignment.quizId)); + if (existing) { + existing.assignedGroups.push({group: assignment.groupName, assignment: assignment}); + } else { + acc.push({user: user, assignedGroups: [{group: assignment.groupName, assignment: assignment}], index: 0}); + } + return acc; + }, [] as QuizAssignmentProps[]); + + // sort the outermost table by quiz title + quizAssignment.sort((a, b) => a.assignedGroups[0].assignment.quizSummary?.title?.localeCompare(b.assignedGroups[0].assignment.quizSummary?.title ?? "") ?? 0); + + return <> + {quizAssignments.length === 0 &&

You have not set any tests to your groups yet.

} + {quizAssignments.length > 0 && + + + + {below["xs"](deviceSize) ? <> : below["lg"](deviceSize) ? : } + + + + {quizAssignment.map((g, i) => )} + +
} + ; + }} + /> + + }} + + +
; }; From f4486f56804253500c81de3cbf2b67367c65005f Mon Sep 17 00:00:00 2001 From: Alex Lewin Date: Fri, 28 Feb 2025 15:53:05 +0000 Subject: [PATCH 2/7] Restyle quizzes on Manage Tests tab --- public/assets/phy/icons/redesign/quiz-bg.svg | 5 + public/assets/phy/icons/redesign/quiz-fg.svg | 5 + .../components/pages/quizzes/SetQuizzes.tsx | 106 +++++++++++------- src/scss/phy/icons.scss | 8 ++ 4 files changed, 85 insertions(+), 39 deletions(-) create mode 100644 public/assets/phy/icons/redesign/quiz-bg.svg create mode 100644 public/assets/phy/icons/redesign/quiz-fg.svg diff --git a/public/assets/phy/icons/redesign/quiz-bg.svg b/public/assets/phy/icons/redesign/quiz-bg.svg new file mode 100644 index 0000000000..30a77a1dbc --- /dev/null +++ b/public/assets/phy/icons/redesign/quiz-bg.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/assets/phy/icons/redesign/quiz-fg.svg b/public/assets/phy/icons/redesign/quiz-fg.svg new file mode 100644 index 0000000000..069bbe68de --- /dev/null +++ b/public/assets/phy/icons/redesign/quiz-fg.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/components/pages/quizzes/SetQuizzes.tsx b/src/app/components/pages/quizzes/SetQuizzes.tsx index 9162520afb..0946cd0772 100644 --- a/src/app/components/pages/quizzes/SetQuizzes.tsx +++ b/src/app/components/pages/quizzes/SetQuizzes.tsx @@ -16,6 +16,8 @@ import {AppQuizAssignment} from "../../../../IsaacAppTypes"; import { above, below, confirmThen, + generateGameboardSubjectHexagons, + HUMAN_SUBJECTS, ifKeyIsEnter, isAda, isDefined, @@ -24,6 +26,7 @@ import { MANAGE_QUIZ_TAB, nthHourOf, persistence, siteSpecific, + Subject, tags, TODAY, useDeviceSize, @@ -37,9 +40,11 @@ import {RenderNothing} from "../../elements/RenderNothing"; import { useHistoryState } from "../../../state/actions/history"; import classNames from "classnames"; import { ExtendDueDateModal } from "../../elements/modals/ExtendDueDateModal"; -import { UncontrolledTooltip, Button, Table, UncontrolledButtonDropdown, DropdownToggle, DropdownMenu, DropdownItem, Row, Container, ListGroup, ListGroupItem, Col, Alert, Input, UncontrolledDropdown } from "reactstrap"; +import { UncontrolledTooltip, Button, Table, UncontrolledButtonDropdown, DropdownToggle, DropdownMenu, DropdownItem, Row, Container, ListGroup, ListGroupItem, Col, Alert, Input, UncontrolledDropdown, Label } from "reactstrap"; import { ListView } from "../../elements/list-groups/ListView"; import { MainContent, ManageQuizzesSidebar, SetQuizzesSidebar, SidebarLayout } from "../../elements/layout/SidebarLayout"; +import { PhyHexIcon } from "../../elements/svg/PhyHexIcon"; +import { AffixButton } from "../../elements/AffixButton"; interface SetQuizzesPageProps extends RouteComponentProps { user: RegisteredUserDTO; @@ -137,10 +142,10 @@ function QuizAssignment({assignedGroups, index}: QuizAssignmentProps) { ); const determineQuizSubjects = (quizSummary?: QuizSummaryDTO) => { - return quizSummary?.tags?.filter(tag => tags.allSubjectTags.map(t => t.id.valueOf()).includes(tag.toLowerCase())).reduce((acc, tag) => acc + `subject-${tag.toLowerCase()} `, ""); + return quizSummary?.tags?.filter(tag => tags.allSubjectTags.map(t => t.id.valueOf()).includes(tag.toLowerCase())).reduce((acc, tag) => acc + `${tag.toLowerCase()}`, ""); }; - const subjects = determineQuizSubjects(assignment.quizSummary) || "subject-physics"; + const subjects = determineQuizSubjects(assignment.quizSummary) || "physics"; const innerTableHeaders : InnerTableHeader[] = [ {title: "Group name", sort: compareGroupNames}, @@ -159,42 +164,65 @@ function QuizAssignment({assignedGroups, index}: QuizAssignmentProps) { setIsExpanded(e => !e)} onKeyDown={ifKeyIsEnter(() => setIsExpanded(e => !e))} > - {isPhy && -
-
-
- - {assignedGroups.length} - group{(!assignedGroups || assignedGroups.length != 1) && "s"} - {assignedGroups.length === 0 ? - "No groups have been assigned." - : (`Test assigned to: ` + assignedGroups.map(g => g.group).join(", "))} - - + {siteSpecific( + <> + +
+
+ {generateGameboardSubjectHexagons([subjects as Subject])} +
+ {/* Quizzes only have one subject */} +
-
-
- } - {isAda && - {assignedGroups.length} 
- group{(!assignedGroups || assignedGroups.length != 1) && "s"} - {assignedGroups.length === 0 ? - "No groups have been assigned." - : (`Test assigned to: ` + assignedGroups.map(g => g.group).join(", "))} - - } - {quizTitle} - - - - + + +
+

{quizTitle}

+ {HUMAN_SUBJECTS[subjects]} +
+ { { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + assignment.quizSummary && dispatch(showQuizSettingModal(assignment.quizSummary)); + e.stopPropagation();}}> + Set Test + } + + + + + + , + + <> + + {assignedGroups.length} 
+ group{(!assignedGroups || assignedGroups.length != 1) && "s"} + {assignedGroups.length === 0 ? + "No groups have been assigned." + : (`Test assigned to: ` + assignedGroups.map(g => g.group).join(", "))} + + + {quizTitle} + + + + + + )} + {isExpanded && @@ -364,7 +392,7 @@ const SetQuizzesPageComponent = ({user}: SetQuizzesPageProps) => {
; return - + {activeTab === MANAGE_QUIZ_TAB.set diff --git a/src/scss/phy/icons.scss b/src/scss/phy/icons.scss index 9e22248394..4c72825f9b 100644 --- a/src/scss/phy/icons.scss +++ b/src/scss/phy/icons.scss @@ -164,6 +164,14 @@ ); } +.page-icon-quiz { + @include svg-icon-layered( + '/assets/phy/icons/redesign/quiz-fg.svg', + '/assets/phy/icons/redesign/quiz-bg.svg', + 75px, 75px, 44px + ); +} + .page-icon-finder { @include svg-icon-layered( '/assets/phy/icons/redesign/page-finder-fg.svg', From d5fd0e1c0a4beee6186fce4e95add35eda3035b1 Mon Sep 17 00:00:00 2001 From: Alex Lewin Date: Mon, 3 Mar 2025 11:45:14 +0000 Subject: [PATCH 3/7] Restyle Manage Tests to better match Set Tests --- .../elements/layout/SidebarLayout.tsx | 2 +- .../elements/list-groups/ListView.tsx | 2 +- .../elements/modals/QuizSettingModal.tsx | 1 + .../components/pages/quizzes/SetQuizzes.tsx | 25 ++++++------------- src/scss/phy/boards.scss | 6 +++++ src/scss/phy/icons.scss | 8 ++++++ src/scss/phy/quiz.scss | 23 +++++++++++++---- 7 files changed, 43 insertions(+), 24 deletions(-) diff --git a/src/app/components/elements/layout/SidebarLayout.tsx b/src/app/components/elements/layout/SidebarLayout.tsx index 3fe7ff2ae8..c12ef45ae1 100644 --- a/src/app/components/elements/layout/SidebarLayout.tsx +++ b/src/app/components/elements/layout/SidebarLayout.tsx @@ -597,7 +597,7 @@ export const ManageQuizzesSidebar = (props: ManageQuizzesSidebarProps) => { quizSetDateFilterType, setQuizSetDateFilterType, quizDueDate, setQuizDueDate, quizDueDateFilterType, setQuizDueDateFilterType, manageQuizzesGroupNameFilter, setManageQuizzesGroupNameFilter} = props; - const dateFilterTypeSelector = (dateFilterType: string, setDateFilterType: React.Dispatch>) => + const dateFilterTypeSelector = (dateFilterType: string, setDateFilterType: React.Dispatch>) => {dateFilterType} setDateFilterType('after')}> diff --git a/src/app/components/elements/list-groups/ListView.tsx b/src/app/components/elements/list-groups/ListView.tsx index 058f184fa1..343d52f110 100644 --- a/src/app/components/elements/list-groups/ListView.tsx +++ b/src/app/components/elements/list-groups/ListView.tsx @@ -90,7 +90,7 @@ export const QuizListViewItem = ({item, isQuizSetter, ...rest}: {item: QuizSumma ; return ) => setScheduledStartDate(e.target.valueAsDate)} /> + {/* TODO update tooltip icon here once we have a consistent style for them */} You can schedule a test to appear in the future by setting a start date. The test will be visible to students from this date onwards.
diff --git a/src/app/components/pages/quizzes/SetQuizzes.tsx b/src/app/components/pages/quizzes/SetQuizzes.tsx index 0946cd0772..9834d2a0ae 100644 --- a/src/app/components/pages/quizzes/SetQuizzes.tsx +++ b/src/app/components/pages/quizzes/SetQuizzes.tsx @@ -166,21 +166,12 @@ function QuizAssignment({assignedGroups, index}: QuizAssignmentProps) { > {siteSpecific( <> - -
-
- {generateGameboardSubjectHexagons([subjects as Subject])} -
- {/* Quizzes only have one subject */} - -
+ + - -
-

{quizTitle}

- {HUMAN_SUBJECTS[subjects]} -
- { + {quizTitle} + { { // eslint-disable-next-line @typescript-eslint/no-unused-expressions assignment.quizSummary && dispatch(showQuizSettingModal(assignment.quizSummary)); @@ -188,10 +179,10 @@ function QuizAssignment({assignedGroups, index}: QuizAssignmentProps) { Set Test } - -