From b5e23e9e382c80c5c42ea8ab8965b1710eb39bb0 Mon Sep 17 00:00:00 2001 From: Jaycie Brown Date: Fri, 12 Jun 2026 17:00:10 +0100 Subject: [PATCH 01/11] Overhaul test pages logic for consistency and cleanliness --- .../elements/quiz/QuizAttemptFooter.tsx | 6 +- .../elements/quiz/QuizContentsComponent.tsx | 162 ++++++++++-------- .../elements/quiz/useSectionViewLogging.tsx | 8 +- .../elements/sidebar/QuizSidebar.tsx | 39 ++--- .../pages/quizzes/PracticeQuizzes.tsx | 9 +- .../pages/quizzes/QuizAttemptFeedback.tsx | 29 +++- .../pages/quizzes/QuizDoAssignment.tsx | 20 ++- .../pages/quizzes/QuizDoFreeAttempt.tsx | 20 ++- .../components/pages/quizzes/QuizPreview.tsx | 40 +++-- src/app/components/pages/quizzes/QuizView.tsx | 29 ++-- 10 files changed, 212 insertions(+), 150 deletions(-) diff --git a/src/app/components/elements/quiz/QuizAttemptFooter.tsx b/src/app/components/elements/quiz/QuizAttemptFooter.tsx index 7ff139a1b0..2c038d0c54 100644 --- a/src/app/components/elements/quiz/QuizAttemptFooter.tsx +++ b/src/app/components/elements/quiz/QuizAttemptFooter.tsx @@ -1,4 +1,4 @@ -import {QuizAttemptProps, QuizPagination} from "./QuizContentsComponent"; +import {FullQuizInfo, QuizPagination, QuizProps} from "./QuizContentsComponent"; import { mutationSucceeded, showSuccessToast, @@ -18,8 +18,8 @@ function extractSectionIdFromQuizQuestionId(questionId: string) { return ids[0] + "|" + ids[1]; } -export function QuizAttemptFooter(props: QuizAttemptProps & {feedbackLink: string}) { - const {attempt, page, sections, questions, pageLink} = props; +export function QuizAttemptFooter(props: QuizProps & FullQuizInfo & {feedbackLink: string}) { + const {attempt, page, quizContents: {sections, questions, pageLink}} = props; const dispatch = useAppDispatch(); const navigate = useNavigate(); diff --git a/src/app/components/elements/quiz/QuizContentsComponent.tsx b/src/app/components/elements/quiz/QuizContentsComponent.tsx index 9c8487f3c8..69537eb332 100644 --- a/src/app/components/elements/quiz/QuizContentsComponent.tsx +++ b/src/app/components/elements/quiz/QuizContentsComponent.tsx @@ -1,5 +1,7 @@ import { + ContentDTO, DetailedQuizSummaryDTO, + IsaacQuizDTO, IsaacQuizSectionDTO, QuestionDTO, QuizAttemptDTO, @@ -36,41 +38,43 @@ import {Markup} from "../markup"; import classNames from "classnames"; import { MainContent, SidebarLayout } from "../layout/SidebarLayout"; import { SetQuizzesModal } from "../modals/SetQuizzesModal"; -import { QuizSidebar, QuizSidebarAttemptProps, QuizSidebarViewProps } from "../sidebar/QuizSidebar"; +import { QuizSidebar, QuizSidebarProps } from "../sidebar/QuizSidebar"; -type PageLinkCreator = (page?: number) => string; export type QuizView = { quiz?: DetailedQuizSummaryDTO & { subjectId?: SUBJECTS | TAG_ID }, quizId: string | undefined }; -interface QuizProps { +type QuizContents = { + questions: QuestionDTO[]; + sections: { [id: string]: IsaacQuizSectionDTO }; + pageLink: (page?: number) => string; +}; + +export type FullQuizInfo = { + quiz: IsaacQuizDTO; + attempt: QuizAttemptDTO; + quizContents: QuizContents; +}; + +export type QuizSummaryInfo = { + quiz: DetailedQuizSummaryDTO; +} + +export type QuizProps = { user: RegisteredUserDTO; pageHelp: React.ReactElement; studentUser?: UserSummaryDTO; quizAssignmentId?: string; -} -export interface QuizAttemptProps extends QuizProps { - attempt: QuizAttemptDTO - view?: undefined; preview?: boolean; - page: number | null; - pageLink: PageLinkCreator; - questions: QuestionDTO[]; - sections: { [id: string]: IsaacQuizSectionDTO }; -} -interface QuizViewProps extends QuizProps { - attempt?: undefined; - view: QuizView; - preview?: undefined; - page?: undefined; - pageLink?: undefined; - questions?: undefined; - sections?: undefined; -} + page?: number; +} & (FullQuizInfo | QuizSummaryInfo); + +const isFullQuiz = (quiz: IsaacQuizDTO | DetailedQuizSummaryDTO): quiz is IsaacQuizDTO => isDefined((quiz as IsaacQuizDTO).defaultFeedbackMode); + function inSection(section: IsaacQuizSectionDTO, questions: QuestionDTO[]) { return questions.filter(q => q.id?.startsWith(section.id as string + "|")); } -function QuizDetails({attempt, sections, questions, pageLink}: QuizAttemptProps) { +function QuizDetails({quizContents: {sections, questions, pageLink}, attempt}: FullQuizInfo) { if (isDefined(attempt.completedDate)) { return attempt.feedbackMode === "NONE" ?

No feedback available

@@ -123,18 +127,17 @@ function QuizDetails({attempt, sections, questions, pageLink}: QuizAttemptProps) } } -function QuizHeader({attempt, preview, view, user}: QuizAttemptProps | QuizViewProps) { +function QuizHeader(quizProps: QuizProps & FullQuizInfo) { + const {quiz, attempt, preview, user} = quizProps; const dispatch = useAppDispatch(); - if (view) { - return isTeacherOrAbove(user) && ; - } - else if (preview) { + + if (preview) { return <> - +

You are previewing this test.

- {isTeacherOrAbove(user) && } + {isTeacherOrAbove(user) && }
; } else if (isDefined(attempt.quizAssignment)) { @@ -165,41 +168,39 @@ function QuizHeader({attempt, preview, view, user}: QuizAttemptProps | QuizViewP } } -function QuizRubric({attempt, view}: Pick) { - const rubric = attempt ? attempt.quiz?.rubric : view?.quiz?.rubric; +function QuizRubric({rubric}: {rubric?: ContentDTO}) { const renderRubric = (rubric?.children || []).length > 0; - return
- {rubric && renderRubric &&
- - {rubric.children} - -
} + return rubric && renderRubric &&
+ + {rubric.children} +
; } -export function QuizRubricButton({attempt}: {attempt: QuizAttemptDTO}) { +export function QuizRubricButton({rubric}: {rubric?: ContentDTO}) { const dispatch = useAppDispatch(); - const rubric = attempt.quiz?.rubric; - const renderRubric = (rubric?.children || []).length > 0 && (isPhy || !isDefined(attempt.completedDate)); - const openQuestionModal = (attempt: QuizAttemptDTO) => { + const openQuestionModal = () => { dispatch(openActiveModal({ closeAction: () => {dispatch(closeActiveModal());}, size: "lg", - title: "Test Instructions", body: + title: "Test Instructions", body: })); }; - if (rubric && renderRubric) { + if (rubric) { return ; } } -function QuizSection({attempt, page, studentUser, user, quizAssignmentId}: QuizAttemptProps & {page: number}) { +function QuizSection(quizProps: QuizProps & FullQuizInfo) { + const {attempt, page, studentUser, user, quizAssignmentId} = quizProps; const deviceSize = useDeviceSize(); const sections = attempt.quiz?.children; - const section = sections && sections[page - 1]; + const section = !!(page && sections) && sections[page - 1]; const attribution = attempt.quiz?.attribution; const viewingAsSomeoneElse = isDefined(studentUser) && studentUser?.id !== user?.id; @@ -212,7 +213,7 @@ function QuizSection({attempt, page, studentUser, user, quizAssignmentId}: QuizA {(isAda || above["lg"](deviceSize)) &&
- +
}
@@ -236,34 +237,45 @@ function QuizSection({attempt, page, studentUser, user, quizAssignmentId}: QuizA export const myQuizzesCrumbs = [{title: siteSpecific("My tests", "Tests"), to: `/tests`}]; export const teacherQuizzesCrumbs = [{title: siteSpecific("Set / manage tests", "Tests"), to: `/set_tests`}]; export const rubricCrumbs = [{title: "Practice tests", to: "/practice_tests"}]; -const getCrumbs = (preview: boolean | undefined, view: boolean | undefined, user: RegisteredUserDTO) => { +export const viewQuizzesCrumbs = [{title: "View tests", to: "/view_tests"}]; +const getCrumbs = (preview: boolean | undefined, user: RegisteredUserDTO) => { if (preview && isTeacherOrAbove(user)) { return teacherQuizzesCrumbs; - } if (view) { - return rubricCrumbs; } + return viewQuizzesCrumbs; + // TODO return myQuizzesCrumbs; }; -const QuizTitle = ({attempt, view, page, pageLink, pageHelp, preview, studentUser, user}: QuizAttemptProps | QuizViewProps) => { - const quiz = attempt ? attempt.quiz : view.quiz; +const generateQuizTitle = (quiz: IsaacQuizDTO | DetailedQuizSummaryDTO | undefined, preview: boolean | undefined, attempt: QuizAttemptDTO | undefined, studentUser: RegisteredUserDTO | undefined) => { let quizTitle = quiz?.title || quiz?.id || "Test"; + if (preview) { + return `${quizTitle} Preview`; + } + if (isDefined(attempt?.completedDate)) { quizTitle += " Feedback"; } if (isDefined(studentUser)) { quizTitle += ` for ${studentUser.givenName} ${studentUser.familyName}`; } - if (preview) { - quizTitle += " Preview"; - } - const crumbs = getCrumbs(preview, !!view, user); - if (page === null || page === undefined) { + return quizTitle; +} + +const QuizTitle = (quizProps: QuizProps) => { + const {page, pageHelp, preview, studentUser, user, quiz} = quizProps; + + const crumbs = getCrumbs(preview, user); + if (page === null || page === undefined || !isFullQuiz(quiz)) { + const quizTitle = generateQuizTitle(quiz, preview, undefined, studentUser); return ; } else { + const {attempt, quizContents: {pageLink}} = quizProps as QuizProps & FullQuizInfo; + const quizTitle = generateQuizTitle(quiz, preview, attempt, studentUser); + const sections = attempt.quiz?.children; const section = sections && sections[page - 1] as IsaacQuizSectionDTO; const sectionTitle = section?.title ?? "Section " + page; @@ -275,12 +287,13 @@ const QuizTitle = ({attempt, view, page, pageLink, pageHelp, preview, studentUse }; interface QuizPaginationProps { - page: number; finalLabel: string; } -export function QuizPagination({page, sections, pageLink, finalLabel}: QuizAttemptProps & QuizPaginationProps) { +export function QuizPagination({page, quizContents: {sections, pageLink}, finalLabel}: QuizProps & FullQuizInfo & QuizPaginationProps) { const deviceSize = useDeviceSize(); + if (!page) return; + const sectionCount = Object.keys(sections).length; const backLink = pageLink(page > 1 ? page - 1 : undefined); const finalSection = page === sectionCount; @@ -299,33 +312,33 @@ export enum SectionProgress { COMPLETED = "Completed" } -function QuizOverview(props: (QuizAttemptProps | QuizViewProps) & { viewingAsSomeoneElse: boolean }) { - const {attempt, studentUser, quizAssignmentId, viewingAsSomeoneElse} = props; +function QuizOverview(props: QuizProps & { viewingAsSomeoneElse: boolean }) { + const {studentUser, quizAssignmentId, viewingAsSomeoneElse, quiz} = props; return
- {!isDefined(studentUser?.id) && } + {isFullQuiz(quiz) && !isDefined(studentUser?.id) && } {viewingAsSomeoneElse &&
You are viewing this test as {studentUser?.givenName} {studentUser?.familyName}.{quizAssignmentId && <> Click here to return to the teacher test feedback page.}
} - - {attempt && } + + {isFullQuiz(quiz) && }
; } -function QuizQuestions(props: Omit & {page: number}) { +function QuizQuestions(props: QuizProps & FullQuizInfo) { // Assumes that ids of questions are defined - I don't know why this is not enforced in the editor/backend, because // we do unchecked casts of "possibly undefined" content ids to strings almost everywhere - const questionNumbers = Object.assign({}, ...props.questions.map((q, i) => ({[q.id as string]: i + 1}))); + const questionNumbers = Object.assign({}, ...props.quizContents.questions.map((q, i) => ({[q.id as string]: i + 1}))); return ; } -export function QuizContentsComponent(props: QuizAttemptProps | QuizViewProps) { - const {attempt, view, studentUser, user} = props; +export function QuizContentsComponent(props: QuizProps) { + const {quiz, studentUser, user} = props; - const questions = attempt ? props.questions : []; - const sections = attempt ? props.sections : {}; + const questions = isFullQuiz(quiz) ? (props as QuizProps & FullQuizInfo).quizContents.questions : []; + const sections = isFullQuiz(quiz) ? (props as QuizProps & FullQuizInfo).quizContents.sections : {}; const sectionState = (section: IsaacQuizSectionDTO) => { const sectionQs = section ? inSection(section, questions) : undefined; @@ -336,20 +349,23 @@ export function QuizContentsComponent(props: QuizAttemptProps | QuizViewProps) { const viewingAsSomeoneElse = isDefined(studentUser) && studentUser?.id !== user?.id; - const sidebarProps: QuizSidebarAttemptProps | QuizSidebarViewProps = Object.assign({ + const sidebarProps: QuizSidebarProps = { + quiz, viewingAsSomeoneElse, totalSections: Object.keys(sections).length, currentSection: props.page ? props.page : undefined, sectionStates: Object.values(sections).map(section => sectionState(section)), sectionTitles: Object.keys(sections).map(k => sections[k].title || "Section " + k), - }, attempt ? {attempt} : {view}); + }; return <> - {props.page === null || props.page == undefined ? QuizOverview({...{viewingAsSomeoneElse, ...props}}): } + {props.page === null || props.page == undefined + ? + : } ; diff --git a/src/app/components/elements/quiz/useSectionViewLogging.tsx b/src/app/components/elements/quiz/useSectionViewLogging.tsx index c3b26f5232..818805df93 100644 --- a/src/app/components/elements/quiz/useSectionViewLogging.tsx +++ b/src/app/components/elements/quiz/useSectionViewLogging.tsx @@ -3,12 +3,12 @@ import {QuizAttemptDTO} from "../../../../IsaacApiTypes"; import {isDefined} from "../../../services"; import {useLogQuizSectionViewMutation} from "../../../state"; -export function useSectionViewLogging(attempt: QuizAttemptDTO | undefined, pageNumber: number | null) { +export function useSectionViewLogging(attempt: QuizAttemptDTO | undefined, pageNumber: number | undefined) { const [logQuizSectionView] = useLogQuizSectionViewMutation(); const attemptId = attempt?.id; useEffect(() => { - if (isDefined(attemptId) && pageNumber !== null) { - logQuizSectionView({quizAttemptId: attemptId, page: pageNumber}); + if (isDefined(attemptId) && isDefined(pageNumber)) { + void logQuizSectionView({quizAttemptId: attemptId, page: pageNumber}); } - }, [attemptId, pageNumber]); + }, [attemptId, logQuizSectionView, pageNumber]); } diff --git a/src/app/components/elements/sidebar/QuizSidebar.tsx b/src/app/components/elements/sidebar/QuizSidebar.tsx index 78fda897e8..a60c10c4f4 100644 --- a/src/app/components/elements/sidebar/QuizSidebar.tsx +++ b/src/app/components/elements/sidebar/QuizSidebar.tsx @@ -1,15 +1,16 @@ import React from "react"; import { useLocation, useNavigate } from "react-router"; import { Row, Col } from "reactstrap"; -import { QuizAttemptDTO } from "../../../../IsaacApiTypes"; +import { DetailedQuizSummaryDTO, IsaacQuizDTO } from "../../../../IsaacApiTypes"; import { useDeviceSize, TAG_ID, isDefined, below, isPhy } from "../../../services"; import { StyledTabPicker } from "../inputs/StyledTabPicker"; import { ContentSidebar } from "../layout/SidebarLayout"; -import { SectionProgress, QuizView, QuizRubricButton } from "../quiz/QuizContentsComponent"; +import { SectionProgress, QuizRubricButton } from "../quiz/QuizContentsComponent"; import { tags as tagsService } from "../../../services"; import { Pill, KeyItem } from "./SidebarElements"; -interface QuizSidebarProps { +export interface QuizSidebarProps { + quiz: IsaacQuizDTO | DetailedQuizSummaryDTO; viewingAsSomeoneElse: boolean; totalSections: number; currentSection?: number; @@ -17,30 +18,22 @@ interface QuizSidebarProps { sectionTitles: string[]; } -export interface QuizSidebarAttemptProps extends QuizSidebarProps { - attempt: QuizAttemptDTO; - view?: undefined; -} - -export interface QuizSidebarViewProps extends QuizSidebarProps { - attempt?: undefined; - view: QuizView; -} - -export const QuizSidebar = (props: QuizSidebarAttemptProps | QuizSidebarViewProps) => { - const { attempt, view, viewingAsSomeoneElse, totalSections, currentSection, sectionStates, sectionTitles} = props; +export const QuizSidebar = (props: QuizSidebarProps) => { + const { quiz, viewingAsSomeoneElse, totalSections, currentSection, sectionStates, sectionTitles} = props; const deviceSize = useDeviceSize(); const navigate = useNavigate(); const location = useLocation(); + + const isFullQuiz = (quiz: QuizSidebarProps['quiz']): quiz is IsaacQuizDTO => isDefined((quiz as IsaacQuizDTO).defaultFeedbackMode); + const rubricPath = viewingAsSomeoneElse ? location.pathname.split("/").slice(0, 6).join("/") : - attempt && attempt.feedbackMode ? location.pathname.split("/").slice(0, 5).join("/") : + isFullQuiz(quiz) ? location.pathname.split("/").slice(0, 5).join("/") : location.pathname.split("/page")[0]; const hasSections = totalSections > 0; - const tags = attempt ? attempt.quiz?.tags : view.quiz?.tags; - const subjects = tagsService.getSubjectTags(tags as TAG_ID[]); - const topics = tagsService.getTopicTags(tags as TAG_ID[]); - const fields = tagsService.getFieldTags(tags as TAG_ID[]); + const subjects = tagsService.getSubjectTags(quiz.tags as TAG_ID[]); + const topics = tagsService.getTopicTags(quiz.tags as TAG_ID[]); + const fields = tagsService.getFieldTags(quiz.tags as TAG_ID[]); const topicsAndFields = (topics.length + fields.length) > 0 ? [...topics, ...fields] : [{id: 'na', title: "N/A", alias: undefined}]; const progressIcon = (section: number) => { @@ -50,7 +43,7 @@ export const QuizSidebar = (props: QuizSidebarAttemptProps | QuizSidebarViewProp }; const switchToPage = (page: string) => { - if (viewingAsSomeoneElse || attempt && attempt.feedbackMode) { + if (viewingAsSomeoneElse || isFullQuiz(quiz)) { void navigate(rubricPath.concat("/", page)); } else { @@ -101,13 +94,13 @@ export const QuizSidebar = (props: QuizSidebarAttemptProps | QuizSidebarViewProp }; return <> - {below["md"](deviceSize) && attempt && isPhy && currentSection ? + {below["md"](deviceSize) && isPhy && currentSection ? - + : diff --git a/src/app/components/pages/quizzes/PracticeQuizzes.tsx b/src/app/components/pages/quizzes/PracticeQuizzes.tsx index db167c050d..46e4732d44 100644 --- a/src/app/components/pages/quizzes/PracticeQuizzes.tsx +++ b/src/app/components/pages/quizzes/PracticeQuizzes.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import { Input, Col } from "reactstrap"; import { generateSubjectLandingPageCrumbFromContext, TitleAndBreadcrumb } from "../../elements/TitleAndBreadcrumb"; -import { getFilteredStageOptions, isAda, isDefined, isLoggedIn, isPhy, LearningStage, siteSpecific, sortByStringValue, STAGE_TO_LEARNING_STAGE, Subjects, TAG_ID, tags } from "../../../services"; +import { getFilteredStageOptions, isAda, isDefined, isLoggedIn, isPhy, isTeacherOrAbove, LearningStage, siteSpecific, sortByStringValue, STAGE_TO_LEARNING_STAGE, Subjects, TAG_ID, tags } from "../../../services"; import { AudienceContext, QuizSummaryDTO, Stage } from "../../../../IsaacApiTypes"; import { Tag} from "../../../../IsaacAppTypes"; import { ShowLoading } from "../../handlers/ShowLoading"; @@ -113,16 +113,17 @@ export const PracticeQuizzes = () => { {isAda && setFilterText(e.target.value)} />} +
+ {isTeacherOrAbove(user) && } + + +
} ; +}; const pageHelp = Preview the questions on this test. @@ -33,7 +40,7 @@ export const QuizPreview = ({user}: {user: RegisteredUserDTO}) => { const quizPreviewQuery = useGetQuizPreviewQuery(isDefined(quizId) ? quizId : skipToken); const {data: quiz} = quizPreviewQuery; - const pageNumber = isDefined(page) ? parseInt(page, 10) : null; + const pageNumber = isDefined(page) ? parseInt(page, 10) : undefined; const attempt = useMemo(() => quiz @@ -51,7 +58,18 @@ export const QuizPreview = ({user}: {user: RegisteredUserDTO}) => { `/test/preview/${quizId}` + (isDefined(page) ? `/page/${page}` : "") , [quizId]); - const subProps: QuizAttemptProps = {attempt: attempt as QuizAttemptDTO, page: pageNumber, questions, sections, pageLink, pageHelp, user}; + const subProps: QuizProps = { + user, + pageHelp, + page: pageNumber, + quiz: attempt?.quiz as IsaacQuizDTO, + quizContents: { + questions, + sections, + pageLink, + }, + attempt: attempt as QuizAttemptDTO, + }; return diff --git a/src/app/components/pages/quizzes/QuizView.tsx b/src/app/components/pages/quizzes/QuizView.tsx index 11e8642db5..1cfc76c08b 100644 --- a/src/app/components/pages/quizzes/QuizView.tsx +++ b/src/app/components/pages/quizzes/QuizView.tsx @@ -1,5 +1,5 @@ import React from "react"; -import {useGetQuizRubricQuery} from "../../../state"; +import {openActiveModal, useAppDispatch, useGetQuizRubricQuery} from "../../../state"; import {Link, useParams} from "react-router-dom"; import {getThemeFromTags, isDefined, isTeacherOrAbove, tags} from "../../../services"; import {QuizContentsComponent, rubricCrumbs} from "../../elements/quiz/QuizContentsComponent"; @@ -17,20 +17,17 @@ const pageHelp = const Error = buildErrorComponent("Unknown Test", "There was an error loading that test.", rubricCrumbs); -const FooterButton = ({link, label}: {link: string, label: string}) => - -; - -const QuizFooter = ({quizId, user}: {quizId: string, user: RegisteredUserDTO}) => - +const QuizFooter = ({quizId, user}: {quizId: string, user: RegisteredUserDTO}) => { + // this is intended to be a student-only page, so no options to set are considered (preview is, temporarily, while teachers can still access this page) + return - - {isTeacherOrAbove(user) && } - - +
+ + {isTeacherOrAbove(user) && } + +
; +}; export const QuizView = ({user}: {user: RegisteredUserDTO}) => { const {quizId} = useParams(); @@ -41,9 +38,9 @@ export const QuizView = ({user}: {user: RegisteredUserDTO}) => { }; return - - + <> + - + )} /> ; }; From c504e1db360a7e76d0c5f44bb6f80edff95cea93 Mon Sep 17 00:00:00 2001 From: Jaycie Brown Date: Fri, 12 Jun 2026 17:31:46 +0100 Subject: [PATCH 02/11] Tidy up imports --- src/app/components/elements/quiz/QuizContentsComponent.tsx | 4 ---- src/app/components/pages/quizzes/QuizView.tsx | 6 +++--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/app/components/elements/quiz/QuizContentsComponent.tsx b/src/app/components/elements/quiz/QuizContentsComponent.tsx index 69537eb332..f2cedacc30 100644 --- a/src/app/components/elements/quiz/QuizContentsComponent.tsx +++ b/src/app/components/elements/quiz/QuizContentsComponent.tsx @@ -19,8 +19,6 @@ import { isTeacherOrAbove, QUIZ_VIEW_STUDENT_ANSWERS_RELEASE_TIMESTAMP, siteSpecific, - SUBJECTS, - TAG_ID, useDeviceSize } from "../../../services"; import {Spacer} from "../Spacer"; @@ -40,8 +38,6 @@ import { MainContent, SidebarLayout } from "../layout/SidebarLayout"; import { SetQuizzesModal } from "../modals/SetQuizzesModal"; import { QuizSidebar, QuizSidebarProps } from "../sidebar/QuizSidebar"; -export type QuizView = { quiz?: DetailedQuizSummaryDTO & { subjectId?: SUBJECTS | TAG_ID }, quizId: string | undefined }; - type QuizContents = { questions: QuestionDTO[]; sections: { [id: string]: IsaacQuizSectionDTO }; diff --git a/src/app/components/pages/quizzes/QuizView.tsx b/src/app/components/pages/quizzes/QuizView.tsx index 1cfc76c08b..af64820896 100644 --- a/src/app/components/pages/quizzes/QuizView.tsx +++ b/src/app/components/pages/quizzes/QuizView.tsx @@ -1,9 +1,9 @@ import React from "react"; -import {openActiveModal, useAppDispatch, useGetQuizRubricQuery} from "../../../state"; -import {Link, useParams} from "react-router-dom"; +import {useGetQuizRubricQuery} from "../../../state"; +import {useParams} from "react-router-dom"; import {getThemeFromTags, isDefined, isTeacherOrAbove, tags} from "../../../services"; import {QuizContentsComponent, rubricCrumbs} from "../../elements/quiz/QuizContentsComponent"; -import {Button, Col, Container, Row} from "reactstrap"; +import {Button, Container} from "reactstrap"; import {ShowLoadingQuery} from "../../handlers/ShowLoadingQuery"; import type { RegisteredUserDTO } from "../../../../IsaacApiTypes"; import { buildErrorComponent } from "../../elements/quiz/buildErrorComponent"; From d99d22d3c309e7b9b9e0c4d0e33f6283f5b976f1 Mon Sep 17 00:00:00 2001 From: Jaycie Brown Date: Mon, 15 Jun 2026 09:57:56 +0100 Subject: [PATCH 03/11] Fix ESLint --- src/app/components/elements/quiz/QuizContentsComponent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/elements/quiz/QuizContentsComponent.tsx b/src/app/components/elements/quiz/QuizContentsComponent.tsx index f2cedacc30..44a1539f1c 100644 --- a/src/app/components/elements/quiz/QuizContentsComponent.tsx +++ b/src/app/components/elements/quiz/QuizContentsComponent.tsx @@ -257,7 +257,7 @@ const generateQuizTitle = (quiz: IsaacQuizDTO | DetailedQuizSummaryDTO | undefin } return quizTitle; -} +}; const QuizTitle = (quizProps: QuizProps) => { const {page, pageHelp, preview, studentUser, user, quiz} = quizProps; From 250577c62b9129110326ceb6ea7de42e162f64e7 Mon Sep 17 00:00:00 2001 From: Jaycie Brown Date: Mon, 15 Jun 2026 13:49:15 +0100 Subject: [PATCH 04/11] Fix invalid quiz redux state after clearing attempt --- src/IsaacAppTypes.tsx | 1 + src/app/services/constants.ts | 1 + src/app/state/actions/quizzes.tsx | 2 +- src/app/state/reducers/quizState.ts | 2 ++ 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/IsaacAppTypes.tsx b/src/IsaacAppTypes.tsx index a4f9be7184..c1da2595a7 100644 --- a/src/IsaacAppTypes.tsx +++ b/src/IsaacAppTypes.tsx @@ -151,6 +151,7 @@ export type Action = | {type: ACTION_TYPE.QUIZ_START_FREE_ATTEMPT_REQUEST; quizId: string} | {type: ACTION_TYPE.QUIZ_LOAD_ATTEMPT_RESPONSE_SUCCESS; attempt: ApiTypes.QuizAttemptDTO} | {type: ACTION_TYPE.QUIZ_LOAD_ATTEMPT_RESPONSE_FAILURE; error: string} + | {type: ACTION_TYPE.QUIZ_ATTEMPT_CLEAR} ; export type NOT_FOUND_TYPE = 404; diff --git a/src/app/services/constants.ts b/src/app/services/constants.ts index 6f177f189a..3525fd111e 100644 --- a/src/app/services/constants.ts +++ b/src/app/services/constants.ts @@ -225,6 +225,7 @@ export enum ACTION_TYPE { QUIZ_START_FREE_ATTEMPT_REQUEST = "QUIZ_START_FREE_ATTEMPT_REQUEST", QUIZ_LOAD_ATTEMPT_RESPONSE_SUCCESS = "QUIZ_LOAD_ATTEMPT_RESPONSE_SUCCESS", QUIZ_LOAD_ATTEMPT_RESPONSE_FAILURE = "QUIZ_LOAD_ATTEMPT_RESPONSE_FAILURE", + QUIZ_ATTEMPT_CLEAR = "QUIZ_ATTEMPT_CLEAR", } export enum PROGRAMMING_LANGUAGE { diff --git a/src/app/state/actions/quizzes.tsx b/src/app/state/actions/quizzes.tsx index ef5919fe9b..e6712d963a 100644 --- a/src/app/state/actions/quizzes.tsx +++ b/src/app/state/actions/quizzes.tsx @@ -21,7 +21,7 @@ export const loadQuizAssignmentAttempt = (quizAssignmentId: number) => async (di }; export const clearQuizAttempt = () => (dispatch: Dispatch) => { - dispatch({type: ACTION_TYPE.QUIZ_LOAD_ATTEMPT_RESPONSE_SUCCESS, attempt: {}}); + dispatch({type: ACTION_TYPE.QUIZ_ATTEMPT_CLEAR}); }; const debouncedDispatch = debounce(async (dispatch: Dispatch, quizAttemptId: number, questionId: string, attempt) => { diff --git a/src/app/state/reducers/quizState.ts b/src/app/state/reducers/quizState.ts index 520779723f..15b1f24bce 100644 --- a/src/app/state/reducers/quizState.ts +++ b/src/app/state/reducers/quizState.ts @@ -22,6 +22,8 @@ export const quizAttempt = (possibleAttempt: QuizAttemptState = null, action: Ac return {attempt: action.attempt}; case ACTION_TYPE.QUIZ_LOAD_ATTEMPT_RESPONSE_FAILURE: return {error: action.error}; + case ACTION_TYPE.QUIZ_ATTEMPT_CLEAR: + return null; case ACTION_TYPE.QUIZ_LOAD_ASSIGNMENT_ATTEMPT_REQUEST: if (possibleAttempt && 'attempt' in possibleAttempt && possibleAttempt.attempt.quizAssignmentId === action.quizAssignmentId) { // Optimistically keep current attempt From b8477c6f51fe51835c21bebcb5f6872eff9fbc9d Mon Sep 17 00:00:00 2001 From: Jaycie Brown Date: Mon, 15 Jun 2026 13:49:19 +0100 Subject: [PATCH 05/11] Fix remaining tests --- .../elements/quiz/QuizAttemptFooter.tsx | 4 +- .../elements/quiz/QuizContentsComponent.tsx | 38 ++++++++++++------- .../elements/sidebar/QuizSidebar.tsx | 2 +- .../pages/quizzes/QuizAttemptFeedback.tsx | 2 +- src/app/components/pages/quizzes/QuizView.tsx | 6 +-- src/test/pages/QuizView.test.tsx | 20 +--------- 6 files changed, 33 insertions(+), 39 deletions(-) diff --git a/src/app/components/elements/quiz/QuizAttemptFooter.tsx b/src/app/components/elements/quiz/QuizAttemptFooter.tsx index 2c038d0c54..995de6ff84 100644 --- a/src/app/components/elements/quiz/QuizAttemptFooter.tsx +++ b/src/app/components/elements/quiz/QuizAttemptFooter.tsx @@ -10,7 +10,7 @@ import React from "react"; import {Spacer} from "../Spacer"; import {IsaacSpinner} from "../../handlers/IsaacSpinner"; import {Button} from "reactstrap"; -import {confirmThen, siteSpecific} from "../../../services"; +import {confirmThen, isDefined, siteSpecific} from "../../../services"; import {QuizSidebarLayout} from "./QuizSidebarLayout"; function extractSectionIdFromQuizQuestionId(questionId: string) { @@ -38,7 +38,7 @@ export function QuizAttemptFooter(props: QuizProps & FullQuizInfo & {feedbackLin const sectionCount = Object.keys(sections).length; let controls; - if (page === null) { + if (!isDefined(page)) { let anyAnswered = false; const completedSections = Object.keys(sections).reduce((map, sectionId) => { map[sectionId] = true; diff --git a/src/app/components/elements/quiz/QuizContentsComponent.tsx b/src/app/components/elements/quiz/QuizContentsComponent.tsx index 44a1539f1c..c9471dfed5 100644 --- a/src/app/components/elements/quiz/QuizContentsComponent.tsx +++ b/src/app/components/elements/quiz/QuizContentsComponent.tsx @@ -63,7 +63,9 @@ export type QuizProps = { page?: number; } & (FullQuizInfo | QuizSummaryInfo); -const isFullQuiz = (quiz: IsaacQuizDTO | DetailedQuizSummaryDTO): quiz is IsaacQuizDTO => isDefined((quiz as IsaacQuizDTO).defaultFeedbackMode); +const isFullQuizProps = (props: QuizProps): props is QuizProps & FullQuizInfo => { + return isDefined((props as QuizProps & FullQuizInfo).attempt); +}; function inSection(section: IsaacQuizSectionDTO, questions: QuestionDTO[]) { @@ -123,10 +125,15 @@ function QuizDetails({quizContents: {sections, questions, pageLink}, attempt}: F } } -function QuizHeader(quizProps: QuizProps & FullQuizInfo) { - const {quiz, attempt, preview, user} = quizProps; +function QuizHeader(quizProps: QuizProps) { const dispatch = useAppDispatch(); + if (!isFullQuizProps(quizProps) && !quizProps.preview) { + return

You are freely attempting this test.

; + } + + const {quiz, preview, user, attempt} = quizProps as QuizProps & FullQuizInfo; + if (preview) { return <> @@ -234,12 +241,15 @@ export const myQuizzesCrumbs = [{title: siteSpecific("My tests", "Tests"), to: ` export const teacherQuizzesCrumbs = [{title: siteSpecific("Set / manage tests", "Tests"), to: `/set_tests`}]; export const rubricCrumbs = [{title: "Practice tests", to: "/practice_tests"}]; export const viewQuizzesCrumbs = [{title: "View tests", to: "/view_tests"}]; -const getCrumbs = (preview: boolean | undefined, user: RegisteredUserDTO) => { +const getCrumbs = (preview: boolean | undefined, isFreeAttempt: boolean, user: RegisteredUserDTO) => { if (preview && isTeacherOrAbove(user)) { return teacherQuizzesCrumbs; } - return viewQuizzesCrumbs; - // TODO + // TODO adjust with changes to test pages – remove isFreeAttempt entirely, just use viewQuizzesCrumbs from here down + // return viewQuizzesCrumbs; + if (isFreeAttempt) { + return rubricCrumbs; + } return myQuizzesCrumbs; }; @@ -260,10 +270,10 @@ const generateQuizTitle = (quiz: IsaacQuizDTO | DetailedQuizSummaryDTO | undefin }; const QuizTitle = (quizProps: QuizProps) => { - const {page, pageHelp, preview, studentUser, user, quiz} = quizProps; + const {page, pageHelp, preview, studentUser, user, quiz} = quizProps as QuizProps; - const crumbs = getCrumbs(preview, user); - if (page === null || page === undefined || !isFullQuiz(quiz)) { + const crumbs = getCrumbs(preview, window.location.pathname.includes('/view/'), user); + if (!isDefined(page) || !isFullQuizProps(quizProps)) { const quizTitle = generateQuizTitle(quiz, preview, undefined, studentUser); return - {isFullQuiz(quiz) && !isDefined(studentUser?.id) && } + {!isDefined(studentUser?.id) && } {viewingAsSomeoneElse &&
You are viewing this test as {studentUser?.givenName} {studentUser?.familyName}.{quizAssignmentId && <> Click here to return to the teacher test feedback page.}
} - {isFullQuiz(quiz) && } + {isFullQuizProps(props) && }
; } @@ -333,8 +343,8 @@ function QuizQuestions(props: QuizProps & FullQuizInfo) { export function QuizContentsComponent(props: QuizProps) { const {quiz, studentUser, user} = props; - const questions = isFullQuiz(quiz) ? (props as QuizProps & FullQuizInfo).quizContents.questions : []; - const sections = isFullQuiz(quiz) ? (props as QuizProps & FullQuizInfo).quizContents.sections : {}; + const questions = isFullQuizProps(props) ? (props as QuizProps & FullQuizInfo).quizContents.questions : []; + const sections = isFullQuizProps(props) ? (props as QuizProps & FullQuizInfo).quizContents.sections : {}; const sectionState = (section: IsaacQuizSectionDTO) => { const sectionQs = section ? inSection(section, questions) : undefined; @@ -359,7 +369,7 @@ export function QuizContentsComponent(props: QuizProps) { - {props.page === null || props.page == undefined + {!isDefined(props.page) ? : } diff --git a/src/app/components/elements/sidebar/QuizSidebar.tsx b/src/app/components/elements/sidebar/QuizSidebar.tsx index a60c10c4f4..7578456f4c 100644 --- a/src/app/components/elements/sidebar/QuizSidebar.tsx +++ b/src/app/components/elements/sidebar/QuizSidebar.tsx @@ -24,7 +24,7 @@ export const QuizSidebar = (props: QuizSidebarProps) => { const navigate = useNavigate(); const location = useLocation(); - const isFullQuiz = (quiz: QuizSidebarProps['quiz']): quiz is IsaacQuizDTO => isDefined((quiz as IsaacQuizDTO).defaultFeedbackMode); + const isFullQuiz = (quiz: QuizSidebarProps['quiz']): quiz is IsaacQuizDTO => isDefined((quiz as IsaacQuizDTO).canonicalSourceFile); const rubricPath = viewingAsSomeoneElse ? location.pathname.split("/").slice(0, 6).join("/") : diff --git a/src/app/components/pages/quizzes/QuizAttemptFeedback.tsx b/src/app/components/pages/quizzes/QuizAttemptFeedback.tsx index ed0d9f8fa6..e303fd62d8 100644 --- a/src/app/components/pages/quizzes/QuizAttemptFeedback.tsx +++ b/src/app/components/pages/quizzes/QuizAttemptFeedback.tsx @@ -20,7 +20,7 @@ function QuizAttemptFeedbackFooter(props: QuizProps & FullQuizInfo) { const {page, studentUser, quizContents: {pageLink}} = props; let controls; let prequel = null; - if (page === null) { + if (!isDefined(page)) { prequel =

Click on a section title or click ‘Next’ to look at {isDefined(studentUser) ? "their" : "your"} detailed feedback.

; controls = <> diff --git a/src/app/components/pages/quizzes/QuizView.tsx b/src/app/components/pages/quizzes/QuizView.tsx index af64820896..81b820eb70 100644 --- a/src/app/components/pages/quizzes/QuizView.tsx +++ b/src/app/components/pages/quizzes/QuizView.tsx @@ -1,6 +1,6 @@ import React from "react"; import {useGetQuizRubricQuery} from "../../../state"; -import {useParams} from "react-router-dom"; +import {Link, useParams} from "react-router-dom"; import {getThemeFromTags, isDefined, isTeacherOrAbove, tags} from "../../../services"; import {QuizContentsComponent, rubricCrumbs} from "../../elements/quiz/QuizContentsComponent"; import {Button, Container} from "reactstrap"; @@ -23,8 +23,8 @@ const QuizFooter = ({quizId, user}: {quizId: string, user: RegisteredUserDTO}) =
- {isTeacherOrAbove(user) && } - + {isTeacherOrAbove(user) && } +
; }; diff --git a/src/test/pages/QuizView.test.tsx b/src/test/pages/QuizView.test.tsx index 3c60dcf1c0..898d33225d 100644 --- a/src/test/pages/QuizView.test.tsx +++ b/src/test/pages/QuizView.test.tsx @@ -9,7 +9,6 @@ import { expectSidebarToggle, previewButton, renderQuizPage, - setTestButton, sideBarTestCases, testSectionsHeader } from "../helpers/quiz"; @@ -36,14 +35,9 @@ describe("QuizView", () => { expectH1(rubric.title); }); - it('does not show message about this page', async () => { + it('shows message about this page', async () => { await studentViewsQuiz(); - expect(screen.queryByTestId("quiz-action")).not.toBeInTheDocument(); - }); - - it('does not show Set Test button', async () => { - await studentViewsQuiz(); - expect(setTestButton()).toBe(null); + expect(screen.queryByTestId("quiz-action")).toBeInTheDocument(); }); it('shows quiz rubric', async () => { @@ -69,11 +63,6 @@ describe("QuizView", () => { describe('for teachers', () => { const teacherViewsQuiz = () => renderQuizView({ role: 'TEACHER', quizId }); - it('shows Set Test button', async () => { - await teacherViewsQuiz(); - expect(setTestButton()).toBeInTheDocument(); - }); - it('shows "Preview" button that loads the preview page and allows navigating back', async () => { await teacherViewsQuiz(); await expectLinkWithEnabledBackwardsNavigation("Preview", `/test/preview/${quizId}`, `/test/view/${quizId}`); @@ -83,11 +72,6 @@ describe("QuizView", () => { describe('for content editors', () => { const editorViewsQuiz = () => renderQuizView({ role: 'TEACHER', quizId }); - it('shows Set Test Button', async () => { - await editorViewsQuiz(); - expect(setTestButton()).toBeInTheDocument(); - }); - // It'd be more consistent with, eg. the `/preview` page to show the edit button. // However, we'd need `canonicalSourceFile` for this, which the `/rubric` endpoint doesn't return. // For now, James suggested this was not worth the effort. From 9e725b029c6152f6d03bb885e286f9642c242641 Mon Sep 17 00:00:00 2001 From: Jaycie Brown Date: Mon, 15 Jun 2026 13:58:35 +0100 Subject: [PATCH 06/11] Remove additional "Set test" button --- src/app/components/elements/quiz/QuizContentsComponent.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/app/components/elements/quiz/QuizContentsComponent.tsx b/src/app/components/elements/quiz/QuizContentsComponent.tsx index c9471dfed5..c2b966c2b3 100644 --- a/src/app/components/elements/quiz/QuizContentsComponent.tsx +++ b/src/app/components/elements/quiz/QuizContentsComponent.tsx @@ -126,21 +126,18 @@ function QuizDetails({quizContents: {sections, questions, pageLink}, attempt}: F } function QuizHeader(quizProps: QuizProps) { - const dispatch = useAppDispatch(); if (!isFullQuizProps(quizProps) && !quizProps.preview) { return

You are freely attempting this test.

; } - const {quiz, preview, user, attempt} = quizProps as QuizProps & FullQuizInfo; + const {quiz, preview, attempt} = quizProps as QuizProps & FullQuizInfo; if (preview) { return <> -
+

You are previewing this test.

- - {isTeacherOrAbove(user) && }
; } else if (isDefined(attempt.quizAssignment)) { From 456867914e699d9dddbe5ce3982056875f5c7c25 Mon Sep 17 00:00:00 2001 From: Jaycie Brown Date: Mon, 15 Jun 2026 14:00:17 +0100 Subject: [PATCH 07/11] Fix ESLint --- src/app/components/elements/quiz/QuizContentsComponent.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/components/elements/quiz/QuizContentsComponent.tsx b/src/app/components/elements/quiz/QuizContentsComponent.tsx index c2b966c2b3..93c846cae7 100644 --- a/src/app/components/elements/quiz/QuizContentsComponent.tsx +++ b/src/app/components/elements/quiz/QuizContentsComponent.tsx @@ -35,7 +35,6 @@ import {EditContentButton} from "../EditContentButton"; import {Markup} from "../markup"; import classNames from "classnames"; import { MainContent, SidebarLayout } from "../layout/SidebarLayout"; -import { SetQuizzesModal } from "../modals/SetQuizzesModal"; import { QuizSidebar, QuizSidebarProps } from "../sidebar/QuizSidebar"; type QuizContents = { From e692763db32b216ae6a3524ddbf5ece5baf0246a Mon Sep 17 00:00:00 2001 From: Jaycie Brown Date: Thu, 25 Jun 2026 10:13:30 +0100 Subject: [PATCH 08/11] Copy Practice Tests layout changes from `set-manage-all-work-types` --- .../list-groups/AbstractListViewItem.tsx | 8 +++---- .../elements/list-groups/ListView.tsx | 23 +++++++++++++++---- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/app/components/elements/list-groups/AbstractListViewItem.tsx b/src/app/components/elements/list-groups/AbstractListViewItem.tsx index fe822accbb..0baf5dcfdf 100644 --- a/src/app/components/elements/list-groups/AbstractListViewItem.tsx +++ b/src/app/components/elements/list-groups/AbstractListViewItem.tsx @@ -89,7 +89,7 @@ const LinkTags = ({linkTags, disabled}: LinkTagProps) => { const QuizLinks = (props: React.HTMLAttributes & {previewQuizUrl?: string, quizButton?: ReactNode}) => { const { previewQuizUrl, quizButton, ...rest } = props; return - {previewQuizUrl && } {quizButton} @@ -206,7 +206,7 @@ export const AbstractListViewItem = ({title, icon, subject, subtitle, breadcrumb const isLLM = tags?.includes("llm_question_page"); const flatLayout = style === "flat" && above['lg'](deviceSize); - const stackedLayout = style === "stacked" || below["sm"](deviceSize); + const stackedLayout = style === "stacked" || (isPhy && below["sm"](deviceSize)) || (isAda && below["xs"](deviceSize)); const wrapTitleTags = below["xs"](deviceSize); const cardBody = <> @@ -273,7 +273,7 @@ export const AbstractListViewItem = ({title, icon, subject, subtitle, breadcrumb {isItem && typedProps.linkTags &&
} - {isQuiz && stackedLayout &&
+ {isQuiz && stackedLayout &&
}
@@ -297,7 +297,7 @@ export const AbstractListViewItem = ({title, icon, subject, subtitle, breadcrumb {isGameboard && isTeacherOrAbove(user) && } - {isQuiz && + {isQuiz && } {(isItem || isBuilder) && isPhy && contentId && typedProps.allowBookmarking && isLoggedIn(user) && bookmarksFeatureFlag &&
& { export const QuizListViewItem = ({item, isQuizSetter, useViewQuizLink, ...rest}: QuizListViewItemProps) => { const dispatch = useAppDispatch(); + const user = useAppSelector(selectors.user.orNull); const itemSubject = tags.getSpecifiedTag(TAG_LEVEL.subject, item.tags as TAG_ID[])?.id as Subject; const quizButton = isQuizSetter ? - dispatch(openActiveModal(SetQuizzesModal({quiz: item})))} affix={{ affix: "icon-arrow-right", position: "suffix", type: "icon" }}> + dispatch(openActiveModal(SetQuizzesModal({quiz: item})))} affix={{ affix: "icon-arrow-right", position: "suffix", type: "icon", affixClassName: "ms-2 icon-color-white" }}> Set test : - + Take the test ; + + // If the user is event admin or above, and the quiz is hidden from teachers, then show that + // Otherwise, show if the quiz is visible to students + const roleVisibilitySummary = (quiz: QuizSummaryDTO): string | undefined => { + if (isEventLeaderOrStaff(user) && quiz.hiddenFromRoles && quiz.hiddenFromRoles?.includes("TEACHER")) { + return "Hidden from teachers"; + } + if (((quiz.hiddenFromRoles && !quiz.hiddenFromRoles?.includes("STUDENT")) || quiz.visibleToStudents)) { + return "Visible to students"; + } + }; + return ; }; From efeed837fc7d81701c57bf920699ba785dddd172 Mon Sep 17 00:00:00 2001 From: Jaycie Brown Date: Thu, 25 Jun 2026 10:54:24 +0100 Subject: [PATCH 09/11] Minor refactor of quiz components for code simplicity --- src/app/components/elements/quiz/QuizContentsComponent.tsx | 6 +++--- src/app/components/elements/sidebar/QuizSidebar.tsx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/components/elements/quiz/QuizContentsComponent.tsx b/src/app/components/elements/quiz/QuizContentsComponent.tsx index 93c846cae7..c466d24795 100644 --- a/src/app/components/elements/quiz/QuizContentsComponent.tsx +++ b/src/app/components/elements/quiz/QuizContentsComponent.tsx @@ -199,7 +199,7 @@ function QuizSection(quizProps: QuizProps & FullQuizInfo) { const {attempt, page, studentUser, user, quizAssignmentId} = quizProps; const deviceSize = useDeviceSize(); const sections = attempt.quiz?.children; - const section = !!(page && sections) && sections[page - 1]; + const section = page && sections?.[page - 1]; const attribution = attempt.quiz?.attribution; const viewingAsSomeoneElse = isDefined(studentUser) && studentUser?.id !== user?.id; @@ -339,8 +339,8 @@ function QuizQuestions(props: QuizProps & FullQuizInfo) { export function QuizContentsComponent(props: QuizProps) { const {quiz, studentUser, user} = props; - const questions = isFullQuizProps(props) ? (props as QuizProps & FullQuizInfo).quizContents.questions : []; - const sections = isFullQuizProps(props) ? (props as QuizProps & FullQuizInfo).quizContents.sections : {}; + const questions = isFullQuizProps(props) ? props.quizContents.questions : []; + const sections = isFullQuizProps(props) ? props.quizContents.sections : {}; const sectionState = (section: IsaacQuizSectionDTO) => { const sectionQs = section ? inSection(section, questions) : undefined; diff --git a/src/app/components/elements/sidebar/QuizSidebar.tsx b/src/app/components/elements/sidebar/QuizSidebar.tsx index 7578456f4c..f07e31ea67 100644 --- a/src/app/components/elements/sidebar/QuizSidebar.tsx +++ b/src/app/components/elements/sidebar/QuizSidebar.tsx @@ -18,14 +18,14 @@ export interface QuizSidebarProps { sectionTitles: string[]; } +const isFullQuiz = (quiz: QuizSidebarProps['quiz']): quiz is IsaacQuizDTO => isDefined((quiz as IsaacQuizDTO).canonicalSourceFile); + export const QuizSidebar = (props: QuizSidebarProps) => { const { quiz, viewingAsSomeoneElse, totalSections, currentSection, sectionStates, sectionTitles} = props; const deviceSize = useDeviceSize(); const navigate = useNavigate(); const location = useLocation(); - const isFullQuiz = (quiz: QuizSidebarProps['quiz']): quiz is IsaacQuizDTO => isDefined((quiz as IsaacQuizDTO).canonicalSourceFile); - const rubricPath = viewingAsSomeoneElse ? location.pathname.split("/").slice(0, 6).join("/") : isFullQuiz(quiz) ? location.pathname.split("/").slice(0, 5).join("/") : From 744108f2736250afc6f3513f9602a8f309e800ee Mon Sep 17 00:00:00 2001 From: Jaycie Brown Date: Thu, 25 Jun 2026 14:15:11 +0100 Subject: [PATCH 10/11] Fix and simplify `rubricPath` calculation --- .../elements/quiz/QuizContentsComponent.tsx | 1 - .../elements/sidebar/QuizSidebar.tsx | 29 +++++-------------- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/src/app/components/elements/quiz/QuizContentsComponent.tsx b/src/app/components/elements/quiz/QuizContentsComponent.tsx index c466d24795..c36d30089a 100644 --- a/src/app/components/elements/quiz/QuizContentsComponent.tsx +++ b/src/app/components/elements/quiz/QuizContentsComponent.tsx @@ -353,7 +353,6 @@ export function QuizContentsComponent(props: QuizProps) { const sidebarProps: QuizSidebarProps = { quiz, - viewingAsSomeoneElse, totalSections: Object.keys(sections).length, currentSection: props.page ? props.page : undefined, sectionStates: Object.values(sections).map(section => sectionState(section)), diff --git a/src/app/components/elements/sidebar/QuizSidebar.tsx b/src/app/components/elements/sidebar/QuizSidebar.tsx index f07e31ea67..eb700109be 100644 --- a/src/app/components/elements/sidebar/QuizSidebar.tsx +++ b/src/app/components/elements/sidebar/QuizSidebar.tsx @@ -11,25 +11,18 @@ import { Pill, KeyItem } from "./SidebarElements"; export interface QuizSidebarProps { quiz: IsaacQuizDTO | DetailedQuizSummaryDTO; - viewingAsSomeoneElse: boolean; totalSections: number; currentSection?: number; sectionStates: SectionProgress[]; sectionTitles: string[]; } -const isFullQuiz = (quiz: QuizSidebarProps['quiz']): quiz is IsaacQuizDTO => isDefined((quiz as IsaacQuizDTO).canonicalSourceFile); - export const QuizSidebar = (props: QuizSidebarProps) => { - const { quiz, viewingAsSomeoneElse, totalSections, currentSection, sectionStates, sectionTitles} = props; + const { quiz, totalSections, currentSection, sectionStates, sectionTitles } = props; const deviceSize = useDeviceSize(); - const navigate = useNavigate(); const location = useLocation(); - const rubricPath = - viewingAsSomeoneElse ? location.pathname.split("/").slice(0, 6).join("/") : - isFullQuiz(quiz) ? location.pathname.split("/").slice(0, 5).join("/") : - location.pathname.split("/page")[0]; + const rubricPath = location.pathname.split(new RegExp(/\/page\/\d+/))[0]; // i.e. "/test/{view|preview|attempt|assignment}/{id}". const hasSections = totalSections > 0; const subjects = tagsService.getSubjectTags(quiz.tags as TAG_ID[]); const topics = tagsService.getTopicTags(quiz.tags as TAG_ID[]); @@ -42,15 +35,6 @@ export const QuizSidebar = (props: QuizSidebarProps) => { : "icon icon-raw icon-not-started"; }; - const switchToPage = (page: string) => { - if (viewingAsSomeoneElse || isFullQuiz(quiz)) { - void navigate(rubricPath.concat("/", page)); - } - else { - void navigate(rubricPath.concat("/page/", page)); - } - }; - const SidebarContents = () => { return
@@ -69,12 +53,15 @@ export const QuizSidebar = (props: QuizSidebarProps) => {
Section(s)
  • - navigate(rubricPath)}/> +
  • {Array.from({length: totalSections}, (_, i) => i).map(section =>
  • - switchToPage(String(section+1))} - suffix={{icon: progressIcon(section), info: sectionStates[section]}}/> +
  • )}
From dcb6e46057cdff610c11242b929f24b9f5d16646 Mon Sep 17 00:00:00 2001 From: Jaycie Brown Date: Thu, 25 Jun 2026 14:19:09 +0100 Subject: [PATCH 11/11] Add "attempt yourself" button in test `/preview` --- src/app/components/pages/quizzes/QuizPreview.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/components/pages/quizzes/QuizPreview.tsx b/src/app/components/pages/quizzes/QuizPreview.tsx index 515f8d2155..0b58efffc3 100644 --- a/src/app/components/pages/quizzes/QuizPreview.tsx +++ b/src/app/components/pages/quizzes/QuizPreview.tsx @@ -20,9 +20,10 @@ const QuizFooter = (props: QuizProps & FullQuizInfo) => { ? : <>
- {isTeacherOrAbove(user) && } + {isTeacherOrAbove(user) && } - + +
} ;