Skip to content

Allow for multiple registered contexts when deciding if content is relevant #1574

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 7 additions & 4 deletions src/app/components/content/IsaacAccordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,11 @@ export const IsaacAccordion = ({doc}: {doc: ContentDTO}) => {
// at this point, there exists a stageIndex for each unique stage.
// now collapse indices before/after the userContext stage into "additional learning stages" when there are multiple

// only consider the first stage (even though teachers may have multiple) so the before/after logic works
const userContextStage = userContext.contexts[0].stage ?? STAGE.ALL;

const getMaxRelevantIndex = () => {
const learningStage = STAGE_TO_LEARNING_STAGE[userContext.stage];
const learningStage = STAGE_TO_LEARNING_STAGE[userContextStage];
if (learningStage) {
const applicableStages = LEARNING_STAGE_TO_STAGES[learningStage];
return Math.max(...applicableStages.map(stage => stageToFirstIndexMap[stage] ?? -1));
Expand All @@ -128,9 +131,9 @@ export const IsaacAccordion = ({doc}: {doc: ContentDTO}) => {
};

// if we need to show everything, don't remove anything
if (userContext.stage && isDefined(stageToFirstIndexMap[userContext.stage]) && userContext.stage !== STAGE.ALL) {
if (userContextStage && isDefined(stageToFirstIndexMap[userContextStage]) && userContextStage !== STAGE.ALL) {
const beforeUserStage = Object.keys(stageToFirstIndexMap).filter(stage => {
return stageToFirstIndexMap[stage] < stageToFirstIndexMap[userContext.stage];
return stageToFirstIndexMap[stage] < stageToFirstIndexMap[userContextStage];
});
const afterUserStage = Object.keys(stageToFirstIndexMap).filter(stage => {
return stageToFirstIndexMap[stage] > getMaxRelevantIndex();
Expand Down Expand Up @@ -158,7 +161,7 @@ export const IsaacAccordion = ({doc}: {doc: ContentDTO}) => {
}

return stageToFirstIndexMap;
}, [isConceptPage, sections, userContext.stage]);
}, [isConceptPage, sections, userContext.contexts[0].stage]);

const stageInserts = useMemo(() => {
// flip key and value; we don't construct it this way as when building we need fast lookup of audience
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/elements/PageTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { PhyHexIcon, PhyHexIconProps } from "./svg/PhyHexIcon";

function AudienceViewer({audienceViews}: {audienceViews: ViewingContext[]}) {
const userContext = useUserViewingContext();
const viewsWithMyStage = audienceViews.filter(vc => vc.stage === userContext.stage);
const viewsWithMyStage = audienceViews.filter(vc => userContext.contexts.some(uc => uc.stage === vc.stage));
// If there is a possible audience view that is correct for our user context, show that specific one
const viewsToUse = viewsWithMyStage.length > 0 ? viewsWithMyStage.slice(0, 1) : audienceViews;
const filteredViews = filterAudienceViewsByProperties(viewsToUse, AUDIENCE_DISPLAY_FIELDS);
Expand Down
14 changes: 7 additions & 7 deletions src/app/components/elements/inputs/UserContextPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,18 @@ export const UserContextPicker = ({className, hideLabels = true}: {className?: s
const user = useAppSelector(selectors.user.orNull);
const userContext = useUserViewingContext();

const filteredExamBoardOptions = getFilteredExamBoardOptions({byUser: user, byStages: [userContext.stage], includeNullOptions: true});
const [currentStage, setCurrentStage] = useState<STAGE>(userContext.contexts[0].stage as STAGE ?? STAGE.ALL);
const filteredExamBoardOptions = getFilteredExamBoardOptions({byUser: user, byStages: [currentStage], includeNullOptions: true});
const allStages = getFilteredStageOptions({includeNullOptions: true});

const onlyOneBoard : {label: string, value: EXAM_BOARD} | undefined = filteredExamBoardOptions.length === 2 && filteredExamBoardOptions.map(eb => eb.value).includes(EXAM_BOARD.ALL)
? filteredExamBoardOptions.filter(eb => eb.value !== EXAM_BOARD.ALL)[0]
: undefined;

const [currentStage, setCurrentStage] = useState<STAGE>(userContext.stage);

useEffect(() => {
setCurrentStage(userContext.stage);
}, [userContext.stage]);
setCurrentStage(userContext.contexts[0].stage as STAGE);
}, [userContext.contexts]);

if (isAda && !isLoggedIn(user) || isStaff(user)) {
return <Col className={`d-flex flex-column w-100 px-0 mt-2 context-picker-container no-print ${className}`}>
Expand All @@ -77,7 +77,7 @@ export const UserContextPicker = ({className, hideLabels = true}: {className?: s
className={classNames("flex-grow-1 d-inline-block ps-2 pe-0", { "mb-2 me-1": isAda })}
type="select" id="uc-stage-select"
aria-label={hideLabels ? "Stage" : undefined}
value={userContext.stage}
value={currentStage}
disabled={userContext.isFixedContext}
onChange={e => {
const newParams: { [key: string]: unknown } = {...qParams, stage: e.target.value};
Expand Down Expand Up @@ -113,7 +113,7 @@ export const UserContextPicker = ({className, hideLabels = true}: {className?: s
className={`flex-grow-1 d-inline-block ps-2 pe-0 mb-2 ms-1`}
type="select" id="uc-exam-board-select"
aria-label={hideLabels ? "Exam Board" : undefined}
value={userContext.examBoard}
value={userContext.contexts[0].examBoard}
onChange={e => {
dispatch(transientUserContextSlice.actions.setExamBoard(e.target.value as EXAM_BOARD));
}}
Expand All @@ -130,7 +130,7 @@ export const UserContextPicker = ({className, hideLabels = true}: {className?: s
<div className="mt-2 ms-1">
<i id={`viewing-context-explanation`} className={siteSpecific("icon icon-info icon-color-grey ms-1", "icon-help mx-1")}/>
<UncontrolledTooltip placement="bottom" target={`viewing-context-explanation`}>
You are seeing {stageLabelMap[userContext.stage]}{isAda ? ` - ${examBoardLabelMap[userContext.examBoard]}` : ""}
You are seeing {stageLabelMap[currentStage]}{isAda && userContext.contexts[0].examBoard ? ` - ${examBoardLabelMap[userContext.contexts[0].examBoard]}` : ""}
&nbsp;content.&nbsp;
{formatContextExplanation(userContext.explanation.stage, userContext.explanation.examBoard)}&nbsp;
{isAda && !isLoggedIn(user) && !userContext.hasDefaultPreferences ?
Expand Down
8 changes: 5 additions & 3 deletions src/app/components/elements/modals/QuestionSearchModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ import {
ISAAC_BOOKS,
TAG_LEVEL,
below,
useDeviceSize
useDeviceSize,
EXAM_BOARD
} from "../../../services";
import {ContentSummary, GameboardBuilderQuestions, GameboardBuilderQuestionsStackProps} from "../../../../IsaacAppTypes";
import {AudienceContext, Difficulty, ExamBoard} from "../../../../IsaacApiTypes";
Expand Down Expand Up @@ -78,8 +79,9 @@ export const QuestionSearchModal = (
const [searchDifficulties, setSearchDifficulties] = useState<Difficulty[]>([]);
const [searchExamBoards, setSearchExamBoards] = useState<ExamBoard[]>([]);
useEffect(function populateExamBoardFromUserContext() {
if (!EXAM_BOARD_NULL_OPTIONS.includes(userContext.examBoard)) setSearchExamBoards([userContext.examBoard]);
}, [userContext.examBoard]);
const userExamBoard = userContext.contexts[0].examBoard as EXAM_BOARD;
if (userContext.contexts.length === 1 && !EXAM_BOARD_NULL_OPTIONS.includes(userExamBoard)) setSearchExamBoards([userExamBoard]);
}, [userContext.contexts[0].examBoard]);

const [isSearching, setIsSearching] = useState(false);
const [searchBook, setSearchBook] = useState<string[]>([]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";
import {ContentBaseDTO} from "../../../IsaacApiTypes";
import {examBoardLabelMap, isIntendedAudience, isLoggedIn, stageLabelMap, useUserViewingContext} from "../../services";
import {examBoardLabelMap, isAda, isIntendedAudience, isLoggedIn, stageLabelMap, useUserViewingContext} from "../../services";
import {selectors, useAppSelector} from "../../state";
import {RenderNothing} from "../elements/RenderNothing";
import { Link } from "react-router-dom";
Expand All @@ -16,7 +16,10 @@ export function IntendedAudienceWarningBanner({doc}: {doc: ContentBaseDTO}) {
}

return <Alert color="warning" className={"no-print"}>
{`There is no content on this page for ${examBoardLabelMap[userContext.examBoard]} ${stageLabelMap[userContext.stage]}. `}
{userContext.contexts.length === 1 && userContext.contexts[0].examBoard && userContext.contexts[0].stage ?
`There is no content on this page for ${examBoardLabelMap[userContext.contexts[0].examBoard]} ${stageLabelMap[userContext.contexts[0].stage]}. ` :
`There is no content on this page for your stage ${isAda && "and exam board"} preferences. `
}
{ isLoggedIn(user) &&
<>
You can change your preferences <strong>by updating your profile <Link to="/account">here</Link>.</strong>
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/pages/Gameboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const GameboardItemComponent = ({gameboard, question}: {gameboard: GameboardDTO,
const userViewingContext = useUserViewingContext();
const deviceSize = useDeviceSize();
const currentUser = useAppSelector((state: AppState) => state?.user?.loggedIn && state.user || null);
const uniqueStage = questionViewingContexts.find(context => context.stage === userViewingContext.stage);
const uniqueStage = questionViewingContexts.find(context => userViewingContext.contexts.map(c => c.stage).includes(context.stage));
return <ListGroupItem key={question.id} className={itemClasses}>
<Link to={`/questions/${question.id}?board=${gameboard.id}`} className={classNames("position-relative", {"align-items-center": isPhy, "justify-content-center": isAda})}>
<span className={"question-progress-icon"}>
Expand Down
10 changes: 5 additions & 5 deletions src/app/components/pages/GameboardBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,11 +237,11 @@ const GameboardBuilder = ({user}: {user: RegisteredUserDTO}) => {
if (concepts && (!baseGameboardId)) {
const params: { [key: string]: string } = {};
params.concepts = concepts;
if (userContext.stage !== STAGE.ALL) {
params.stages = userContext.stage;
if (!userContext.contexts.map(c => c.stage).includes(STAGE.ALL)) {
params.stages = userContext.contexts[0].stage ?? "";
}
if (userContext.examBoard !== EXAM_BOARD.ALL) {
params.examBoards = userContext.examBoard;
if (!userContext.contexts.map(c => c.examBoard).includes(EXAM_BOARD.ALL)) {
params.examBoards = userContext.contexts[0].examBoard ?? "";
}
generateTemporaryGameboard(params).then((gameboardResponse) => {
if (mutationSucceeded(gameboardResponse)) {
Expand All @@ -251,7 +251,7 @@ const GameboardBuilder = ({user}: {user: RegisteredUserDTO}) => {
}
});
}
}, [dispatch, concepts, baseGameboardId, cloneGameboard, generateTemporaryGameboard, userContext.examBoard, userContext.stage]);
}, [dispatch, concepts, baseGameboardId, cloneGameboard, generateTemporaryGameboard, userContext.contexts[0]]);
useEffect(() => {
return history.block(() => {
logEvent(eventLog, "LEAVE_GAMEBOARD_BUILDER", {});
Expand Down
6 changes: 3 additions & 3 deletions src/app/services/userPreferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface UseUserPreferencesReturnType {
}

export function useUserPreferences(): UseUserPreferencesReturnType {
const {examBoard} = useUserViewingContext();
const examBoard = useUserViewingContext().contexts[0].examBoard;

const {PROGRAMMING_LANGUAGE: programmingLanguage, BOOLEAN_NOTATION: booleanNotation, DISPLAY_SETTING: displaySettings} =
useAppSelector((state: AppState) => state?.userPreferences) || {};
Expand All @@ -24,8 +24,8 @@ export function useUserPreferences(): UseUserPreferencesReturnType {
if (booleanNotation) {
preferredBooleanNotation = Object.values(BOOLEAN_NOTATION).find(key => booleanNotation[key] === true);
}
// if we don't have a boolean notation preference for the user, then set it based on the exam board
if (preferredBooleanNotation === undefined) {
// if we don't have a boolean notation preference for the user, then set it based on the (first) exam board
if (preferredBooleanNotation === undefined && examBoard) {
preferredBooleanNotation = examBoardBooleanNotationMap[examBoard];
}

Expand Down
Loading