Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
66d5388
Merge branch 'main' into feature/manage-assignments
jacbn Jun 24, 2026
833344c
Fix RoutesPhy after merge
jacbn Jun 24, 2026
d8a0bab
Merge branch main
barna-isaac Jun 26, 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
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
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>;
};
3 changes: 2 additions & 1 deletion src/app/components/elements/MyAssignmentsContents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ const PhyAssignmentCard = ({assignment}: {assignment: AssignmentDTO}) => {
return <GameboardCard
gameboard={assignment.gameboard}
linkLocation={GameboardLinkLocation.Card}
assignment={assignment}
assignment={assignment}
usageDisplay={{type: "correctness"}}
openAssignModal={openAssignModal}
>
<Row className="w-100">
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/elements/ShareLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const ShareLink = ({linkUrl, reducedWidthLink, gameboardId, clickAwayClos
</a>
</div>}
<IconButton
icon={{name: "icon-share icon-color-black-hoverable", color: outline ? "" : "white"}}
icon={{name: classNames("icon-share", buttonProps?.disabled ? "icon-color-grey" : "icon-color-black-hoverable"), color: outline ? "" : "white"}}
className={classNames(innerClassName, "w-max-content h-max-content action-button", {"icon-button-sm": size == "sm"})}
aria-label={buttonAriaLabel}
title="Share page"
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/elements/cards/BoardCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ export const BoardCard = ({user, board, boardView, displayAssignmentInfo, setSel
// sci
<GameboardCard
gameboard={board} linkLocation={GameboardLinkLocation.Card} data-testid="gameboard-card"
openAssignModal={openAssignModal} groupCount={isSetAssignments ? assignees?.length : undefined}
openAssignModal={openAssignModal} usageDisplay={isSetAssignments ? {type: "group", groupCount: assignees?.length} : {type: "correctness"}}
>
<Row>
<Col>
Expand Down
129 changes: 87 additions & 42 deletions src/app/components/elements/cards/GameboardCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useMemo, useState } from "react";
import { AssignmentDTO, GameboardDTO } from "../../../../IsaacApiTypes";
import { Row, Col, Button, Label, Collapse, Badge } from "reactstrap";
import { generateGameboardSubjectHexagons, isDefined, above, HUMAN_SUBJECTS, stageLabelMap, difficultyShortLabelMap, PATHS, tags, determineGameboardStagesAndDifficulties, determineGameboardSubjects, TAG_ID, useDeviceSize, Subject, isPhy, below, isTutorOrAbove } from "../../../services";
import { generateGameboardSubjectHexagons, isDefined, above, HUMAN_SUBJECTS, stageLabelMap, difficultyShortLabelMap, PATHS, tags, determineGameboardStagesAndDifficulties, determineGameboardSubjects, TAG_ID, useDeviceSize, Subject, isPhy, below, isTutorOrAbove, siteSpecific, TODAY } from "../../../services";

Check failure on line 4 in src/app/components/elements/cards/GameboardCard.tsx

View workflow job for this annotation

GitHub Actions / build-and-test (24)

'siteSpecific' is defined but never used. Allowed unused vars must match /^_/u

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import siteSpecific.
import { HexIcon } from "../svg/HexIcon";
import { Link } from "react-router-dom";
import classNames from "classnames";
Expand All @@ -11,6 +11,7 @@
import { selectors, useAppSelector } from "../../../state";
import { SupersededDeprecatedBoardContentWarning } from "../../navigation/SupersededDeprecatedWarning";
import { FeatureFlag, useFeatureFlag } from "../../../services/featureFlag";
import { getFriendlyDaysUntil } from "../DateString";

export enum GameboardLinkLocation {
// where on the card can the user click to navigate to the gameboard
Expand All @@ -31,50 +32,74 @@
</Badge>;
};

type GameboardCardUsageDisplay = {
type: "correctness";
} | {
type: "group";
groupCount: number;
} | {
type: "progressLink";
assignment: AssignmentDTO;
}
interface CardUsageInfoProps extends React.HTMLAttributes<HTMLDivElement> {
gameboard?: GameboardDTO;
groupCount?: number;
isSetAssignments?: boolean;
usageDisplay?: GameboardCardUsageDisplay;
}

// "Attempted/Correct" percentages or "Assigned to X groups"
const CardUsageInfo = ({ gameboard, groupCount, isSetAssignments, className, ...rest }: CardUsageInfoProps) => {
return <div {...rest} className={classNames(className, "d-flex justify-content-center justify-content-md-end column-gap-7 column-gap-md-4")}>
{!isSetAssignments
? <>
<Label className="d-block w-max-content text-center text-nowrap pt-3">
{isDefined(gameboard) &&<div className="board-percent-completed">{gameboard.percentageAttempted ?? 0}</div>}
Attempted
</Label>
<Label className="d-block w-max-content text-center text-nowrap pt-3">
{isDefined(gameboard) && <div className="board-percent-completed">{gameboard.percentageCorrect ?? 0}</div>}
Correct
</Label>
</>
: <>
<Label className="d-block w-max-content text-center text-nowrap pt-3 pt-md-1" title="Number of groups assigned">
Assigned to
<div className="board-bubble-info">{groupCount ?? 0}</div>
group{groupCount !== 1 && "s"}
</Label>
</>
}
const CardUsageInfo = ({ gameboard, usageDisplay, className, ...rest }: CardUsageInfoProps) => {
return <div {...rest} className={classNames(className, "d-flex justify-content-center justify-content-md-end align-self-start column-gap-7 column-gap-md-4", {"card-usage-branded-corner": usageDisplay?.type === "progressLink"})}>
{usageDisplay?.type === "correctness" && <>
<Label className="d-block w-max-content text-center text-nowrap pt-3">
{isDefined(gameboard) &&<div className="board-percent-completed">{gameboard.percentageAttempted ?? 0}</div>}
Attempted
</Label>
<Label className="d-block w-max-content text-center text-nowrap pt-3">
{isDefined(gameboard) && <div className="board-percent-completed">{gameboard.percentageCorrect ?? 0}</div>}
Correct
</Label>
</>}
{usageDisplay?.type === "group" && <>
<Label className="d-block w-max-content text-center text-nowrap pt-3 pt-md-1" title="Number of groups assigned">
Assigned to
<div className="board-bubble-info">{usageDisplay.groupCount ?? 0}</div>
group{usageDisplay.groupCount !== 1 && "s"}
</Label>
</>}
{usageDisplay?.type === "progressLink" && <>
{isDefined(usageDisplay.assignment.scheduledStartDate) && usageDisplay.assignment.scheduledStartDate >= TODAY()
? <div className="d-flex align-items-center">
<span>
Begins&nbsp;
<b>{getFriendlyDaysUntil(usageDisplay.assignment.scheduledStartDate)}</b>
</span>
</div>
: <Link to={`${PATHS.ASSIGNMENT_PROGRESS}/${usageDisplay.assignment.id}`} target="_blank" className="d-flex align-items-center gap-2">
<b>View group progress</b>
<span className={"visually-hidden"}>(opens in new tab)</span>
<i className="icon icon-arrow-right icon-color-white" aria-hidden="true" />
</Link>
}
</>}
</div>;
};

interface GameboardCardProps extends React.HTMLAttributes<HTMLElement> {
type GameboardCardProps = React.HTMLAttributes<HTMLElement> & {
gameboard?: GameboardDTO;
linkLocation?: GameboardLinkLocation;
assignment?: AssignmentDTO;
openAssignModal?: () => void;
groupCount?: number;
}
unassign?: () => void;
useAssignmentLink?: boolean; // whether to use /assignment/:id over /gameboards#:id
allowManaging?: boolean; // replaces "assign" with both "unset" and "set again" buttons for more precise assignment management
usageDisplay?: GameboardCardUsageDisplay;
};


// any children passed into this component will be rendered in the card body
export const GameboardCard = (props: GameboardCardProps) => {
const {gameboard, linkLocation, children, assignment, openAssignModal, groupCount, ...rest} = props;
const {gameboard, linkLocation, children, assignment, openAssignModal, unassign, useAssignmentLink, allowManaging, usageDisplay, ...rest} = props;

const isSetAssignments = isDefined(groupCount);
const user = useAppSelector(selectors.user.orNull);

const [showMore, setShowMore] = useState(false);
Expand All @@ -94,7 +119,7 @@

const boardLink = assignment && isAssignmentsV2Link
? `/assignment/${assignment.id}/view`
: gameboard && (isSetAssignments
: gameboard && (useAssignmentLink
? `/assignment/${gameboard.id}`
: `${PATHS.GAMEBOARD}#${gameboard.id}`
);
Expand All @@ -114,15 +139,18 @@
<h4 className="text-break m-0">
{isDefined(gameboard) && (
linkLocation === GameboardLinkLocation.Title
? <Link to={`${PATHS.GAMEBOARD}#${gameboard.id}`}>{gameboard.title}</Link>
? <Link to={`${PATHS.GAMEBOARD}#${gameboard.id}`} target="_blank">
{gameboard.title}
<i className="icon icon-new-tab ms-2 icon-color-black" />
</Link>
: gameboard.title
)}
</h4>
{above['sm'](deviceSize) && boardSubjects.length > 0 && <div className="d-flex align-items-center mb-2">
{boardSubjects.map((subject) => <span key={subject} className="badge rounded-pill bg-theme me-1" data-bs-theme={subject}>{HUMAN_SUBJECTS[subject]}</span>)}
</div>}
</div>
{!below['xs'](deviceSize) && <CardUsageInfo className="float-end" gameboard={gameboard} groupCount={groupCount} isSetAssignments={isSetAssignments} />}
{!below['xs'](deviceSize) && <CardUsageInfo className="float-end" gameboard={gameboard} usageDisplay={usageDisplay} />}
</div>

{children}
Expand All @@ -133,29 +161,46 @@
</Col>
</Row>

{below['xs'](deviceSize) && <CardUsageInfo className="d-flex w-100 justify-content-around" gameboard={gameboard} groupCount={groupCount} isSetAssignments={isSetAssignments} />}
{below['xs'](deviceSize) && <CardUsageInfo className="d-flex w-100 justify-content-around" gameboard={gameboard} usageDisplay={usageDisplay} />}

<div className="d-flex flex-column flex-sm-row align-items-start mt-2">
<Button className="my-2 btn-underline order-1 order-sm-0" color="link" onClick={(e) => {e.preventDefault(); setShowMore(!showMore);}}>
{gameboard?.contents?.length && <Button className="my-2 btn-underline order-1 order-sm-0" color="link" onClick={(e) => {e.preventDefault(); setShowMore(!showMore);}}>
{showMore ? "Hide details" : "Show details"}
</Button>
</Button>}
<Spacer />
<div className="d-flex gap-3 align-self-stretch align-items-center mb-2 order-0 order-sm-1">
{isPhy && gameboard && <SaveBoardButton board={gameboard} color="keyline" size="sm" />}
{isPhy && boardLink && <div className="card-share-link">
<ShareLink linkUrl={boardLink} reducedWidthLink clickAwayClose size="sm" buttonProps={{color: "keyline"}} />
</div>}
{isTutorOrAbove(user) && <Button className="flex-grow-1" color="keyline" onClick={(e) => {e.preventDefault(); openAssignModal?.();}}>
{isSetAssignments ? "Assign / Unassign" : "Assign"}
</Button>}
{allowManaging
? isTutorOrAbove(user) && <>
<Button className="flex-grow-1" color="keyline" onClick={(e) => {e.preventDefault(); unassign?.();}}>
Unassign
</Button>
<Button className="flex-grow-1" color="keyline" onClick={(e) => {e.preventDefault(); openAssignModal?.();}}>
Set again
</Button>
</>
: isTutorOrAbove(user) && <>
<Button className="flex-grow-1" color="keyline" onClick={(e) => {e.preventDefault(); openAssignModal?.();}}>
Assign
</Button>
</>
}
</div>
</div>

{/* collapsed info */}
<Collapse isOpen={showMore} className="w-100">
{/* collapsed info -- hidden if no contents */}
{gameboard?.contents?.length && <Collapse isOpen={showMore} className="w-100">
<Row>
<Col xs={12} md={8} className="mt-sm-2">
<p className="mb-0"><strong>Questions:</strong> {gameboard?.contents?.length || "0"}</p>
<p className="mb-0 d-flex align-items-center gap-2">
<span>
<strong>Questions:</strong>{" "}
{gameboard?.contents?.length || "0"}
</span>
</p>
{isDefined(topics) && topics.length > 0 && <p className="mb-0">
<strong>{topics.length === 1 ? "Topic" : "Topics"}:</strong>{" "}
{topics.join(", ")}
Expand Down Expand Up @@ -188,7 +233,7 @@
}
</Col>
</Row>
</Collapse>
</Collapse>}
</div>;

if (gameboard && linkLocation === GameboardLinkLocation.Card && boardLink) {
Expand Down
Loading
Loading