Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
6f4f250
Prevent decks automatically saving to My QDecks on attempt
jacbn Apr 22, 2026
4177461
Move assignment-setting hooks etc inside cards
jacbn Apr 22, 2026
c14bc5a
Rename My q. decks => My saved decks
jacbn Apr 22, 2026
167d888
Add manual saving/unsaving for decks (sci-only)
jacbn Apr 23, 2026
dde2c73
Restructure board cards to better fit new content
jacbn Apr 24, 2026
7d7aec7
Share manual deck save changes with Ada
jacbn Apr 24, 2026
7f85984
Align expanded board views with board ALVIs
jacbn Apr 24, 2026
5387fc2
Fix board hex indicator position
jacbn Apr 24, 2026
0c53946
Allow additional action buttons via `additionalActionButtons`
jacbn Apr 24, 2026
18b1fcf
Merge branch 'improvement/icon-colors-css-vars' into feature/my-saved…
jacbn Apr 27, 2026
4d4d4d7
Use new fillable icon format for star
jacbn Apr 27, 2026
9d6cd42
Hide assign / save buttons when not permitted by role
jacbn Apr 27, 2026
1711da5
Restore card usage info at `<=sm`
jacbn Apr 27, 2026
7cc9891
Fix ESLint warnings
jacbn Apr 27, 2026
d427884
Wrap animation in `not-reduced-motion`
jacbn Apr 27, 2026
0653504
Move Ada student save button hiding to the board only
jacbn Apr 27, 2026
30c302e
Restore circle / hex contents on Set Assignments
jacbn Apr 27, 2026
9e636be
Fix ESLint
jacbn Apr 27, 2026
b1528f2
Rearrange SCSS files so access mixins always defined
jacbn Apr 28, 2026
1e31038
Merge branch 'main' into feature/my-saved-decks
jacbn Apr 28, 2026
c8937d1
Provide RTK user in Cypress tests with new param
jacbn Apr 28, 2026
88d35da
Add RTK user to My & Set Assignments pages
jacbn Apr 28, 2026
338f9bd
Update VRT baselines
actions-user Apr 28, 2026
0c743b7
Merge pull request #2118 from isaacphysics/vrt/feature/my-saved-decks
jacbn Apr 28, 2026
90cb196
Merge branch 'main' into feature/my-saved-decks
sjd210 May 5, 2026
fd52424
Correct comment to match new variable name
sjd210 May 5, 2026
df61a78
Fix names in `unlink => unsave` inconsistency
jacbn May 8, 2026
6f88d73
Improve table view columns for MSD+SA
jacbn May 8, 2026
ff43104
Fix position of icons inside scrollable tables
jacbn May 8, 2026
616dcda
Add save deck button to Manage section in SA
jacbn May 8, 2026
0a4b7a2
Remove overlapping warn-on-remove-deck popup
jacbn May 8, 2026
160ebcd
Show success toast on saving for better user feedback
jacbn May 8, 2026
1fffd29
Merge branch 'main' into feature/my-saved-decks
jacbn Jun 1, 2026
2015c0a
Copy assignment schedule as base for manage assignments
jacbn Jun 2, 2026
f537929
Migrate to sidebar layout
jacbn Jun 2, 2026
a93be00
Migrate from AssignmentListEntry to GameboardCard-based entries
jacbn Jun 3, 2026
81745fb
Add stylised group progress link to assignment cards
jacbn Jun 3, 2026
c081e86
Curve lower corner of management card info
jacbn Jun 4, 2026
c49ada9
Add preview button to assignment card
jacbn Jun 4, 2026
ff7ae33
Remove unused schedule pieces from Manage Assignments
jacbn Jun 4, 2026
e4826b6
Include revamped Set Assignment flow at top of page
jacbn Jun 4, 2026
1572a93
Move "set new" modal text to content
jacbn Jun 4, 2026
8e8b9f5
Push rest of manage assignments behind a feature flag
jacbn Jun 4, 2026
a92b1ad
Include tests in assigned work; rename page
jacbn Jun 5, 2026
2517a80
Implement TestCards for test management functionality
jacbn Jun 5, 2026
7f23edb
Extract Set/Manage Tests logic into hook for reuse in Manage Assigned
jacbn Jun 5, 2026
8895d29
Add work type filter
jacbn Jun 5, 2026
689d8eb
Show "due date passed" indicator; fix display if passed
jacbn Jun 8, 2026
4f99f40
Merge branch 'main' into feature/manage-assignments
jacbn Jun 8, 2026
32101d8
Combine work set to same group into group list
jacbn Jun 10, 2026
badaf34
Include additional title and subject filters
jacbn Jun 10, 2026
c7e2ccc
Improve dark mode styling
jacbn Jun 12, 2026
c21699a
Add open-in-new icon to manage work cards
jacbn Jun 12, 2026
091eb78
Overhaul test pages logic for consistency and cleanliness
jacbn Jun 12, 2026
a4c54a7
Tidy up imports
jacbn Jun 12, 2026
ae9de2a
Merge branch 'feature/test-pages-overhaul' of https://github.com/isaa…
jacbn Jun 15, 2026
1d845e6
Update VRT baselines
actions-user Jun 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions cypress/support/commands.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
// }

import {mount, MountOptions} from 'cypress/react';
import { RegisteredUserDTO } from '../../src/IsaacApiTypes';

// Augment the Cypress namespace to include type definitions for
// your custom command.
Expand All @@ -46,7 +47,7 @@ declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
mountWithStoreAndRouter(component: ReactNode, routes: string[], initialRoute?: To, mountOptions?: MountOptions): Chainable<Element>;
mountWithStoreAndRouter(component: ReactNode, routes: string[], initialRoute?: To, user?: RegisteredUserDTO, mountOptions?: MountOptions): Chainable<Element>;

openSidebar(): Chainable<JQuery<HTMLElement>>;
closeSidebar(): Chainable<JQuery<HTMLElement>>;
Expand All @@ -60,15 +61,20 @@ import {Provider} from "react-redux";
import {store} from "../../src/app/state";
import {createBrowserRouter, createRoutesFromElements, Route, To} from "react-router";
import { RouterProvider } from 'react-router-dom';
import { ACTION_TYPE } from '../../src/app/services';

Cypress.Commands.add('mountWithStoreAndRouter', (component, routes, initialRoute=routes?.[0], mountOptions) => {
Cypress.Commands.add('mountWithStoreAndRouter', (component, routes, initialRoute=routes?.[0], user, mountOptions) => {
const router = createBrowserRouter(createRoutesFromElements(<>
{routes?.length
? routes.map(route => <Route key={route} element={component} path={route} />)
: <Route path="*" element={component} />
}
</>));

if (user) {
void store.dispatch({type: ACTION_TYPE.CURRENT_USER_RESPONSE_SUCCESS, user});
}

void router.navigate(initialRoute || '/');

mount(
Expand Down
3 changes: 3 additions & 0 deletions public/assets/common/icons/star-fill.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions public/assets/common/icons/star.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions src/IsaacAppTypes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Difficulty,
GameboardDTO,
GameboardItem,
IAssignmentLike,
ItemDTO,
QuestionDTO,
QuestionValidationResponseDTO,
Expand Down Expand Up @@ -421,6 +422,13 @@ export interface ValidAssignmentWithListingDate extends AssignmentDTO {
listingDate: Date;
}

export interface ValidWorkWithListingDate extends IAssignmentLike {
groupId: number;
additionalManagerPrivileges: boolean;
id: number;
listingDate: Date;
}

export interface AssignmentProgressPageSettings {
colourBlind: boolean;
setColourBlind: (colourBlind: boolean) => void;
Expand Down Expand Up @@ -486,6 +494,16 @@ export const ExpandableParentContext = React.createContext<boolean>(false);
export const ConfidenceContext = React.createContext<{recordConfidence: boolean}>({recordConfidence: false});
export const AssignmentProgressPageSettingsContext = React.createContext<AssignmentProgressPageSettings | undefined>(undefined);
export const GameboardContext = React.createContext<GameboardDTO | undefined>(undefined);

export const ManageAssignmentsContext = React.createContext<{
groupsById: {[id: number]: AppGroup | undefined};
workByGroup: {[id: number]: {boards?: IAssignmentLike[], tests?: IAssignmentLike[]} | undefined};
groups: AppGroup[];
collapsed: boolean;
setCollapsed: (b: boolean) => void;
viewBy: "startDate" | "dueDate";
}>({groupsById: {}, workByGroup: {}, groups: [], collapsed: false, setCollapsed: () => {}, viewBy: "startDate"});

export const AssignmentScheduleContext = React.createContext<{
boardsById: {[id: string]: GameboardDTO | undefined};
groupsById: {[id: number]: AppGroup | undefined};
Expand All @@ -498,6 +516,7 @@ export const AssignmentScheduleContext = React.createContext<{
setCollapsed: (b: boolean) => void;
viewBy: "startDate" | "dueDate";
}>({boardsById: {}, groupsById: {}, groupFilter: {}, boardIdsByGroupId: {}, groups: [], gameboards: [], openAssignmentModal: () => {}, collapsed: false, setCollapsed: () => {}, viewBy: "startDate"});

export const SidebarContext = React.createContext<{sidebarPresent: boolean} | undefined>(undefined);
export const ContentSidebarContext = React.createContext<{ toggle: () => void; close: () => void; } | undefined>(undefined);

Expand Down
10 changes: 7 additions & 3 deletions src/app/components/elements/Gameboards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,18 @@ const CSTable = (props: GameboardsTableProps) => {
</SortItemHeader>
{siteSpecific(
<>
<th className="text-center align-middle">Delete</th>
<th className="text-center align-middle">
{boardView === BoardViews.card ? "Unsave" : "Manage"}
</th>
</>,
<>
<th>Share</th>
<th>
{selectedBoards.length
? <Button size={"sm"} color={"link"} onClick={confirmDeleteMultipleBoards}>
Delete ({selectedBoards.length})
Unsave ({selectedBoards.length})
</Button>
: "Delete"
: "Unsave"
}
</th>
</>
Expand Down Expand Up @@ -115,6 +117,7 @@ const CSTable = (props: GameboardsTableProps) => {
boardView={boardView}
user={user}
boards={boards}
displayAssignmentInfo={false}
/>)
}
</tbody>
Expand Down Expand Up @@ -144,6 +147,7 @@ const Cards = (props: GameboardsCardsProps) => {
boardView={boardView}
user={user}
boards={boards}
displayAssignmentInfo={false}
/>
</Col>)}
</Row>}
Expand Down
82 changes: 82 additions & 0 deletions src/app/components/elements/ManageAssignedCards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from "react";
import { Row, Col } from "reactstrap";
import { AssignmentDTO, QuizAssignmentDTO } from "../../../IsaacApiTypes";
import { isDefined, isOverdue, useManageQuizAssignments } from "../../services";
import { useManageAssignment } from "../../services/setAssignment";
import { GameboardCard, GameboardLinkLocation } from "./cards/GameboardCard";
import { getFriendlyDaysUntil } from "./DateString";
import { TestCard } from "./cards/TestCard";

// this is similar to MyAssignmentsContents/AssignmentCard, but:
// - GameboardCard.usageDisplay is undefined, so no completion / group statistics are shown in the top right.
// - inside the card's children, does not highlight past deadlines.
// - GameboardCard.allowManaging is set
export const ManageAssignmentCard = ({assignment}: {assignment: AssignmentDTO}) => {
const assignmentStartDate = assignment.scheduledStartDate ?? assignment.creationDate;

const { openAssignModal, unassign } = useManageAssignment(assignment);

return <GameboardCard
className="mt-2"
gameboard={assignment.gameboard}
linkLocation={GameboardLinkLocation.Title}
assignment={assignment}
openAssignModal={openAssignModal}
usageDisplay={{type: "progressLink", assignment}}
unassign={unassign}
allowManaging
>
<Row className="w-100">
<Col>
{isDefined(assignment.groupName) &&
<p className="mb-0">Set to <strong>{assignment.groupName}</strong></p>
}
{isDefined(assignmentStartDate) &&
<p className="mb-0" data-testid={"gameboard-assigned"}>
Assigned <strong>{getFriendlyDaysUntil(assignmentStartDate)}</strong>
</p>
}
{isDefined(assignment.dueDate) && isDefined(assignment.gameboard) && <p className="mb-0">
Due <strong>{getFriendlyDaysUntil(assignment.dueDate)}</strong>
{isOverdue(assignment) && <span className="overdue ms-1">(passed)</span>}
</p>}
</Col>
</Row>

{assignment.notes && <p className="mb-0"><strong>Notes:</strong> {assignment.notes}</p>}
</GameboardCard>;
};

export const ManageTestCard = ({quizAssignment}: {quizAssignment: QuizAssignmentDTO}) => {

const { cancel, openExtendDueDateModal, openAssignModal } = useManageQuizAssignments();

return <TestCard
className="mt-2"
quizAssignment={quizAssignment}
linkLocation={!isOverdue(quizAssignment) ? GameboardLinkLocation.Title : undefined}
usageDisplay={{type: "progressLink"}}
openAssignModal={() => openAssignModal(quizAssignment)}
cancel={() => cancel(quizAssignment)}
extendDueDate={() => openExtendDueDateModal(quizAssignment)}
allowManaging
>
<Row className="w-100">
<Col>
{/* // TODO groupName is not defined anywhere in the quiz summary – can we add this in the API? */}
{/* {isDefined(quizAssignment.quizSummary?.groupName) &&
<p className="mb-0">Set to <strong>{quizAssignment.quizSummary?.groupName}</strong></p>
} */}
{isDefined(quizAssignment?.scheduledStartDate) || isDefined(quizAssignment?.creationDate) &&
<p className="mb-0" data-testid={"gameboard-assigned"}>
Assigned <strong>{getFriendlyDaysUntil(quizAssignment.scheduledStartDate || quizAssignment.creationDate)}</strong>
</p>
}
{isDefined(quizAssignment?.dueDate) && <p className="mb-0">
Due <strong>{getFriendlyDaysUntil(quizAssignment.dueDate)}</strong>
{isOverdue(quizAssignment) && <span className="overdue ms-1">(passed)</span>}
</p>}
</Col>
</Row>
</TestCard>;
};
2 changes: 1 addition & 1 deletion src/app/components/elements/MyAssignmentsContents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const CSCircle = ({label, percentage}: {percentage: number | unknown, label: str
const PhyAssignmentCard = ({assignment}: {assignment: AssignmentDTO}) => {
const assignmentStartDate = assignment.scheduledStartDate ?? assignment.creationDate;

return <GameboardCard gameboard={assignment.gameboard} linkLocation={GameboardLinkLocation.Card} assignment={assignment}>
return <GameboardCard gameboard={assignment.gameboard} linkLocation={GameboardLinkLocation.Card} assignment={assignment} usageDisplay={{type: "correctness"}}>
<Row className="w-100">
<Col xs={12} md={6}>
{isDefined(assignmentStartDate) &&
Expand Down
15 changes: 9 additions & 6 deletions src/app/components/elements/PageMetadata.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type PageMetadataProps = {
children?: ReactNode; // any content-type specific metadata that may require information outside of `doc`; e.g. question completion state, event info, etc.
noTitle?: boolean; // if true, any children (usually text) will be rendered in place of the title, with any action buttons (e.g. share, print, report) rendered to the side
helpModalId?: string;
additionalActionButtons?: ReactNode; // pages can extend the standard action buttons with their own. they will be placed to the left of the main ones.
pageContainsLLMFreeTextQuestion?: boolean;
} & (
{
Expand All @@ -45,14 +46,16 @@ interface ActionButtonsProps extends React.HTMLAttributes<HTMLDivElement> {
isQuestion: boolean;
helpModalId?: string;
doc?: SeguePageDTO;
additionalActionButtons?: ReactNode;
}

export const ActionButtons = ({location, isQuestion, helpModalId, doc, ...rest}: ActionButtonsProps) => {
export const ActionButtons = ({location, isQuestion, helpModalId, doc, additionalActionButtons, ...rest}: ActionButtonsProps) => {
const deviceSize = useDeviceSize();

const anyActionButtonShown = isPhy && helpModalId || above['sm'](deviceSize) || doc?.id;

return anyActionButtonShown && <div {...rest} className={classNames("d-flex no-print gap-2", rest.className)}>
{additionalActionButtons}
{isPhy && isQuestion && <FeatureFlagWrapper flag={FeatureFlag.ENABLE_SCI_BOOKMARKS}>
<BookmarkButton doc={doc} />
</FeatureFlagWrapper>
Expand Down Expand Up @@ -110,7 +113,7 @@ const MetadataTitle = ({doc, title, subtitle, badges}: MetadataTitleProps) => {
};

export const PageMetadata = (props: PageMetadataProps) => {
const { doc, title, subtitle, badges, children, noTitle, helpModalId, showSidebarButton, sidebarButtonText, sidebarInTitle } = props;
const { doc, title, subtitle, badges, children, noTitle, helpModalId, showSidebarButton, sidebarButtonText, sidebarInTitle, additionalActionButtons } = props;
const isQuestion = doc?.type === "isaacQuestionPage";
const isConcept = doc?.type === "isaacConceptPage";
const location = useLocation();
Expand All @@ -121,16 +124,16 @@ export const PageMetadata = (props: PageMetadataProps) => {
{isPhy && showSidebarButton && sidebarInTitle && below['md'](deviceSize) && <SidebarButton buttonTitle={sidebarButtonText} absolute/>}
<div className="page-metadata">
{isPhy && <div className={classNames("title-action-bar", {"d-flex align-items-center": !actionButtonsFloat})}>
{actionButtonsFloat && <ActionButtons location={location} isQuestion={isQuestion} helpModalId={helpModalId} doc={doc} className="float-end ms-3 mb-2"/>}
{actionButtonsFloat && <ActionButtons location={location} isQuestion={isQuestion} helpModalId={helpModalId} doc={doc} additionalActionButtons={additionalActionButtons} className="float-end ms-3 mb-2"/>}
{noTitle ? children : <MetadataTitle doc={doc} title={title} subtitle={subtitle} badges={badges}/>}
{!actionButtonsFloat && <ActionButtons location={location} isQuestion={isQuestion} helpModalId={helpModalId} doc={doc} className={classNames("ms-auto", {"mb-auto": !noTitle && badges})}/>}
{!actionButtonsFloat && <ActionButtons location={location} isQuestion={isQuestion} helpModalId={helpModalId} doc={doc} additionalActionButtons={additionalActionButtons} className={classNames("ms-auto", {"mb-auto": !noTitle && badges})}/>}
</div>}

{isAda && <div className={classNames("title-action-bar", {"d-flex align-items-end": !children})}>
{children && <ActionButtons location={location} isQuestion={isQuestion} helpModalId={helpModalId} doc={doc} className="float-end ms-3 mb-3"/>}
{children && <ActionButtons location={location} isQuestion={isQuestion} helpModalId={helpModalId} doc={doc} additionalActionButtons={additionalActionButtons} className="float-end ms-3 mb-3"/>}
<TagStack doc={doc} className={classNames({"mb-3": children, "d-flex align-items-end": !children})}/>
{children}
{!children && <ActionButtons location={location} isQuestion={isQuestion} helpModalId={helpModalId} doc={doc} className="ms-auto"/>}
{!children && <ActionButtons location={location} isQuestion={isQuestion} helpModalId={helpModalId} doc={doc} additionalActionButtons={additionalActionButtons} className="ms-auto"/>}
</div>}

{isPhy && !noTitle && children}
Expand Down
66 changes: 66 additions & 0 deletions src/app/components/elements/SaveBoardButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { useCallback, useMemo, useState } from "react";
import { IconButton } from "./AffixButton";
import { GameboardDTO } from "../../../IsaacApiTypes";
import classNames from "classnames";
import { ButtonProps } from "reactstrap";
import { saveGameboard, selectors, unlinkUserFromGameboard, useAppDispatch, useAppSelector } from "../../state";
import { isLoggedIn, siteSpecific } from "../../services";

interface SaveBoardButtonProps extends ButtonProps {
board: GameboardDTO;
size?: "sm" | "md"; // "md" default (as used for PageMetadata buttons); "sm" aligns with regular .btn padding
}

export const SaveBoardButton = (props: SaveBoardButtonProps) => {
const { board, size, className, ...rest } = props;

const dispatch = useAppDispatch();
const user = useAppSelector(selectors.user.loggedInOrNull);

const [justLinked, setJustLinked] = useState(false);
const isLinked = useMemo(() => board.savedToCurrentUser || justLinked, [board, justLinked]);

const linkBoard = useCallback(() => {
if (!user || !board) return;
setJustLinked(true);
void dispatch(saveGameboard({
boardId: board.id ?? "",
boardTitle: board.title,
user,
}));
}, [user, board, dispatch]);

const unlinkBoard = useCallback(() => {
if (!user || !board) return;
const confirmMessage = board.ownerUserId === user.id && !board.tags?.includes("ISAAC_BOARD")
? `Are you sure you want to unsave your board '${board.title}' from your account? You'll only be able to find it again if you've set it as an assignment.`
: `Are you sure you want to unsave '${board.title}' from your account?`;
if (confirm(confirmMessage)) {
setJustLinked(false);
void dispatch(unlinkUserFromGameboard({
boardId: board.id ?? "",
boardTitle: board.title
}));
}
}, [user, board, dispatch]);

if (!isLoggedIn(user)) return null; // anon users should not be able to save boards

return <IconButton
icon={{
name: classNames("icon-star", siteSpecific("icon-color-black-hoverable", undefined), { "fill": isLinked, "anim-star-select": justLinked }),
color: siteSpecific(undefined, props.color === "solid" ? "white" : "primary")
}}
className={classNames(className, "w-max-content h-max-content action-button", {"icon-button-sm": size === "sm"})}
title={isLinked ? "Unsave board" : "Save board"}
onClick={(e) => {
e.preventDefault();
if (isLinked) {
unlinkBoard();
} else {
linkBoard();
}
}}
{...rest}
/>;
};
Loading