From fde84df8f44bca189be9dff00c0b7cee656ee30c Mon Sep 17 00:00:00 2001 From: Alex Lewin Date: Wed, 14 May 2025 16:22:00 +0100 Subject: [PATCH 01/18] Move "browse all questions/concepts" to top of page On subject-specific question finders and concept listings, move the "browse all" button out of the sidebar to the top of the page. Remove this option entirely for practice tests because we don't want to link to the main practice tests listing anywhere. --- .../elements/layout/SidebarLayout.tsx | 61 ------------------- src/app/components/pages/Concepts.tsx | 17 +++++- src/app/components/pages/QuestionFinder.tsx | 20 ++++-- 3 files changed, 31 insertions(+), 67 deletions(-) diff --git a/src/app/components/elements/layout/SidebarLayout.tsx b/src/app/components/elements/layout/SidebarLayout.tsx index 89c61930a4..46e7742c9c 100644 --- a/src/app/components/elements/layout/SidebarLayout.tsx +++ b/src/app/components/elements/layout/SidebarLayout.tsx @@ -426,20 +426,6 @@ export const SubjectSpecificConceptListSidebar = (props: ConceptListSidebarProps } - -
- -
-

The concepts shown on this page have been filtered to only show those that are relevant to {getHumanContext(pageContext)}.

-

If you want to explore broader concepts across multiple subjects or learning stages, you can use the main concept browser:

- - Browse concepts - -
; }; @@ -490,23 +476,6 @@ export const GenericConceptsSidebar = (props: ConceptListSidebarProps) => {
-
- - {pageContext?.subject && <> -
- -
-

The concepts shown on this page have been filtered to only show those that are relevant to {getHumanContext(pageContext)}.

-

If you want to explore broader concepts across multiple subjects or learning stages, you can use the main concept browser:

- - Browse concepts - -
- } ; }; @@ -541,22 +510,6 @@ export const QuestionFinderSidebar = (props: QuestionFinderSidebarProps) => { - - {pageContext?.subject && pageContext?.stage && <> -
- -
-

The questions shown here have been filtered to only show those that are relevant to {getHumanContext(pageContext)}.

-

If you want to explore our full range of questions across multiple subjects or learning stages, you can use the main question finder:

- - Browse all questions - -
- } ; }; @@ -660,20 +613,6 @@ export const PracticeQuizzesSidebar = (props: PracticeQuizzesSidebarProps) => { } - {isFullyDefinedContext(pageContext) && <> -
-
-

The practice tests shown here have been filtered to only show those that are relevant to {getHumanContext(pageContext)}.

-

If you want to explore our full range of practice tests, you can view the main practice tests page:

- - Browse all practice tests - -
- }

You can see all of the tests that you have in progress or have completed in your My Isaac:

diff --git a/src/app/components/pages/Concepts.tsx b/src/app/components/pages/Concepts.tsx index 89c1f01841..a7e45fd0d3 100644 --- a/src/app/components/pages/Concepts.tsx +++ b/src/app/components/pages/Concepts.tsx @@ -1,5 +1,5 @@ import React, {FormEvent, MutableRefObject, useEffect, useMemo, useRef, useState} from "react"; -import {RouteComponentProps, withRouter} from "react-router-dom"; +import {Link, RouteComponentProps, withRouter} from "react-router-dom"; import {selectors, useAppSelector} from "../../state"; import {Badge, Card, CardBody, CardHeader, Container} from "reactstrap"; import queryString from "query-string"; @@ -10,11 +10,12 @@ import {IsaacSpinner} from "../handlers/IsaacSpinner"; import { ListView } from "../elements/list-groups/ListView"; import { ContentTypeVisibility, LinkToContentSummaryList } from "../elements/list-groups/ContentSummaryListGroupItem"; import { SubjectSpecificConceptListSidebar, MainContent, SidebarLayout, GenericConceptsSidebar } from "../elements/layout/SidebarLayout"; -import { isFullyDefinedContext, useUrlPageTheme } from "../../services/pageContext"; +import { getHumanContext, isFullyDefinedContext, useUrlPageTheme } from "../../services/pageContext"; import { useListConceptsQuery } from "../../state/slices/api/conceptsApi"; import { ShowLoadingQuery } from "../handlers/ShowLoadingQuery"; import { ContentSummaryDTO } from "../../../IsaacApiTypes"; import { skipToken } from "@reduxjs/toolkit/query"; +import { AffixButton } from "../elements/AffixButton"; const subjectToTagMap = { physics: TAG_ID.physics, @@ -113,6 +114,7 @@ export const Concepts = withRouter((props: RouteComponentProps) => { currentPageTitle="Concepts" intermediateCrumbs={crumb ? [crumb] : undefined} icon={{type: "hex", icon: "icon-concept"}} + className="mb-4" /> {pageContext?.subject @@ -120,6 +122,17 @@ export const Concepts = withRouter((props: RouteComponentProps) => { : } + {pageContext?.subject &&
+

The concepts shown on this page have been filtered to only show those that are relevant to {getHumanContext(pageContext)}.

+ + Browse all concepts + +
} {isPhy &&
{ {siteSpecific(
- {(pageContext?.subject && pageContext.stage - ? `Use our question finder to find questions to try on ${getHumanContext(pageContext)} topics.` - : "Use our question finder to find questions to try on topics in Physics, Maths, Chemistry and Biology." - ) + " Use our practice questions to become fluent in topics and then take your understanding and problem solving skills to the next level with our challenge questions."} + {(pageContext?.subject && pageContext.stage) + ?
+

The questions shown on this page have been filtered to only show those that are relevant to {getHumanContext(pageContext)}.

+ + Browse all questions + +
+ : <>Use our question finder to find questions to try on topics in Physics, Maths, Chemistry and Biology. + Use our practice questions to become fluent in topics and then take your understanding and problem solving skills to the next level with our challenge questions.}
, )} From 8ce7acfbabaddd3ab9107a9355e4dbbf14136ba3 Mon Sep 17 00:00:00 2001 From: Alex Lewin Date: Thu, 15 May 2025 17:19:47 +0100 Subject: [PATCH 02/18] Add stage filters to main concepts listing --- .../elements/layout/SidebarLayout.tsx | 29 +++++++++++++++++-- src/app/components/pages/Concepts.tsx | 14 ++++++--- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/app/components/elements/layout/SidebarLayout.tsx b/src/app/components/elements/layout/SidebarLayout.tsx index 46e7742c9c..c3376ebcf0 100644 --- a/src/app/components/elements/layout/SidebarLayout.tsx +++ b/src/app/components/elements/layout/SidebarLayout.tsx @@ -429,11 +429,23 @@ export const SubjectSpecificConceptListSidebar = (props: ConceptListSidebarProps ; }; -export const GenericConceptsSidebar = (props: ConceptListSidebarProps) => { - const { searchText, setSearchText, conceptFilters, setConceptFilters, applicableTags, tagCounts, ...rest } = props; +interface GenericConceptsSidebarProps extends ConceptListSidebarProps { + searchStages: STAGE[]; + setSearchStages: React.Dispatch>; + stageCounts: Record; +} - const pageContext = useAppSelector(selectors.pageContext.context); +export const GenericConceptsSidebar = (props: GenericConceptsSidebarProps) => { + const { searchText, setSearchText, conceptFilters, setConceptFilters, applicableTags, tagCounts, searchStages, setSearchStages, stageCounts, ...rest } = props; + const updateSearchStages = (stage: STAGE) => { + if (searchStages.includes(stage)) { + setSearchStages(searchStages.filter(s => s !== stage)); + } else { + setSearchStages([...(searchStages ?? []), stage]); + } + }; + return
@@ -473,6 +485,17 @@ export const GenericConceptsSidebar = (props: ConceptListSidebarProps) => {
}
; })} +
+
Filter by stage
+
    + {getFilteredStageOptions().map((stage, i) => +
  • + {stage.label} ({stageCounts[stage.value]})} + data-bs-theme={conceptFilters.length === 1 ? conceptFilters[0].id : undefined} + color="theme" onChange={() => {updateSearchStages(stage.value);}}/> +
  • )} +
diff --git a/src/app/components/pages/Concepts.tsx b/src/app/components/pages/Concepts.tsx index a7e45fd0d3..01fc60cad0 100644 --- a/src/app/components/pages/Concepts.tsx +++ b/src/app/components/pages/Concepts.tsx @@ -3,7 +3,7 @@ import {Link, RouteComponentProps, withRouter} from "react-router-dom"; import {selectors, useAppSelector} from "../../state"; import {Badge, Card, CardBody, CardHeader, Container} from "reactstrap"; import queryString from "query-string"; -import {isAda, isPhy, isRelevantToPageContext, matchesAllWordsInAnyOrder, pushConceptsToHistory, searchResultIsPublic, shortcuts, TAG_ID, tags} from "../../services"; +import {getFilteredStageOptions, isAda, isPhy, isRelevantToPageContext, matchesAllWordsInAnyOrder, pushConceptsToHistory, searchResultIsPublic, shortcuts, STAGE, STAGE_TO_LEARNING_STAGE, TAG_ID, tags} from "../../services"; import {generateSubjectLandingPageCrumbFromContext, TitleAndBreadcrumb} from "../elements/TitleAndBreadcrumb"; import {ShortcutResponse, Tag} from "../../../IsaacAppTypes"; import {IsaacSpinner} from "../handlers/IsaacSpinner"; @@ -49,6 +49,7 @@ export const Concepts = withRouter((props: RouteComponentProps) => { const [conceptFilters, setConceptFilters] = useState( applicableTags.filter(f => filters.includes(f.id)) ); + const [searchStages, setSearchStages] = useState([]); const [shortcutResponse, setShortcutResponse] = useState(); const listConceptsQuery = useListConceptsQuery(pageContext @@ -58,8 +59,8 @@ export const Concepts = withRouter((props: RouteComponentProps) => { const shortcutAndFilter = (concepts?: ContentSummaryDTO[], excludeTopicFiltering?: boolean) => { const searchResults = concepts?.filter(c => - matchesAllWordsInAnyOrder(c.title, searchText || "") || - matchesAllWordsInAnyOrder(c.summary, searchText || "") + (matchesAllWordsInAnyOrder(c.title, searchText || "") || matchesAllWordsInAnyOrder(c.summary, searchText || "")) + && (searchStages.length === 0 || searchStages.some(s => c.audience?.some(a => a.stage?.includes(s)))) ); const filteredSearchResults = searchResults @@ -81,6 +82,11 @@ export const Concepts = withRouter((props: RouteComponentProps) => { [t.id]: shortcutAndFilter(listConceptsQuery?.data?.results, true)?.filter(c => c.tags?.includes(t.id)).length || 0 }), {}); + const stageCounts = getFilteredStageOptions().reduce((acc, s) => ({ + ...acc, + [s.value]: shortcutAndFilter(listConceptsQuery?.data?.results, true)?.filter(c => c.audience?.some(a => a.stage?.includes(s.value))).length || 0 + }), {}); + function doSearch(e?: FormEvent) { if (e) { e.preventDefault(); @@ -119,7 +125,7 @@ export const Concepts = withRouter((props: RouteComponentProps) => { {pageContext?.subject ? - : + : } {pageContext?.subject &&
From 0ccce161bb443e70791701b68555d20a12f5bc36 Mon Sep 17 00:00:00 2001 From: Alex Lewin Date: Fri, 16 May 2025 11:45:55 +0100 Subject: [PATCH 03/18] Add stage filters to concept finder URL --- .../elements/layout/SidebarLayout.tsx | 6 +++--- src/app/components/pages/Concepts.tsx | 18 +++++++++++------- src/app/services/search.ts | 5 +++-- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/app/components/elements/layout/SidebarLayout.tsx b/src/app/components/elements/layout/SidebarLayout.tsx index c3376ebcf0..64a6c3c989 100644 --- a/src/app/components/elements/layout/SidebarLayout.tsx +++ b/src/app/components/elements/layout/SidebarLayout.tsx @@ -430,15 +430,15 @@ export const SubjectSpecificConceptListSidebar = (props: ConceptListSidebarProps }; interface GenericConceptsSidebarProps extends ConceptListSidebarProps { - searchStages: STAGE[]; - setSearchStages: React.Dispatch>; + searchStages: Stage[]; + setSearchStages: React.Dispatch>; stageCounts: Record; } export const GenericConceptsSidebar = (props: GenericConceptsSidebarProps) => { const { searchText, setSearchText, conceptFilters, setConceptFilters, applicableTags, tagCounts, searchStages, setSearchStages, stageCounts, ...rest } = props; - const updateSearchStages = (stage: STAGE) => { + const updateSearchStages = (stage: Stage) => { if (searchStages.includes(stage)) { setSearchStages(searchStages.filter(s => s !== stage)); } else { diff --git a/src/app/components/pages/Concepts.tsx b/src/app/components/pages/Concepts.tsx index 01fc60cad0..7e3d410cd7 100644 --- a/src/app/components/pages/Concepts.tsx +++ b/src/app/components/pages/Concepts.tsx @@ -3,7 +3,7 @@ import {Link, RouteComponentProps, withRouter} from "react-router-dom"; import {selectors, useAppSelector} from "../../state"; import {Badge, Card, CardBody, CardHeader, Container} from "reactstrap"; import queryString from "query-string"; -import {getFilteredStageOptions, isAda, isPhy, isRelevantToPageContext, matchesAllWordsInAnyOrder, pushConceptsToHistory, searchResultIsPublic, shortcuts, STAGE, STAGE_TO_LEARNING_STAGE, TAG_ID, tags} from "../../services"; +import {getFilteredStageOptions, isAda, isPhy, isRelevantToPageContext, matchesAllWordsInAnyOrder, pushConceptsToHistory, searchResultIsPublic, shortcuts, TAG_ID, tags} from "../../services"; import {generateSubjectLandingPageCrumbFromContext, TitleAndBreadcrumb} from "../elements/TitleAndBreadcrumb"; import {ShortcutResponse, Tag} from "../../../IsaacAppTypes"; import {IsaacSpinner} from "../handlers/IsaacSpinner"; @@ -13,7 +13,7 @@ import { SubjectSpecificConceptListSidebar, MainContent, SidebarLayout, GenericC import { getHumanContext, isFullyDefinedContext, useUrlPageTheme } from "../../services/pageContext"; import { useListConceptsQuery } from "../../state/slices/api/conceptsApi"; import { ShowLoadingQuery } from "../handlers/ShowLoadingQuery"; -import { ContentSummaryDTO } from "../../../IsaacApiTypes"; +import { ContentSummaryDTO, Stage } from "../../../IsaacApiTypes"; import { skipToken } from "@reduxjs/toolkit/query"; import { AffixButton } from "../elements/AffixButton"; @@ -32,13 +32,17 @@ export const Concepts = withRouter((props: RouteComponentProps) => { const searchParsed = queryString.parse(location.search, {arrayFormat: "comma"}); - const [query, filters] = useMemo(() => { + const [query, filters, stages] = useMemo(() => { const queryParsed = searchParsed.query || null; const query = Array.isArray(queryParsed) ? queryParsed.join(",") : queryParsed; const filterParsed = searchParsed.types || null; const filters = Array.isArray(filterParsed) ? filterParsed.filter(x => !!x) as string[] : filterParsed?.split(",") ?? []; - return [query, filters]; + + const stagesParsed = searchParsed.stages || null; + const stages = Array.isArray(stagesParsed) ? stagesParsed.filter(x => !!x) as string[] : stagesParsed?.split(",") ?? []; + + return [query, filters, stages]; }, [searchParsed]); const applicableTags = pageContext?.subject @@ -49,7 +53,7 @@ export const Concepts = withRouter((props: RouteComponentProps) => { const [conceptFilters, setConceptFilters] = useState( applicableTags.filter(f => filters.includes(f.id)) ); - const [searchStages, setSearchStages] = useState([]); + const [searchStages, setSearchStages] = useState(stages as Stage[]); const [shortcutResponse, setShortcutResponse] = useState(); const listConceptsQuery = useListConceptsQuery(pageContext @@ -91,7 +95,7 @@ export const Concepts = withRouter((props: RouteComponentProps) => { if (e) { e.preventDefault(); } - pushConceptsToHistory(history, searchText || "", [...conceptFilters.map(f => f.id)]); + pushConceptsToHistory(history, searchText || "", [...conceptFilters.map(f => f.id)], searchStages); if (searchText) { setShortcutResponse(shortcuts(searchText)); @@ -108,7 +112,7 @@ export const Concepts = withRouter((props: RouteComponentProps) => { }; }, [searchText]); - useEffect(() => {doSearch();}, [conceptFilters]); + useEffect(() => {doSearch();}, [conceptFilters, searchStages]); const crumb = isPhy && isFullyDefinedContext(pageContext) && generateSubjectLandingPageCrumbFromContext(pageContext); diff --git a/src/app/services/search.ts b/src/app/services/search.ts index ff6f3ba1d4..06732562a8 100644 --- a/src/app/services/search.ts +++ b/src/app/services/search.ts @@ -1,6 +1,6 @@ import {History} from "history"; import {DOCUMENT_TYPE, isStaff, TAG_ID} from "."; -import {ContentSummaryDTO} from "../../IsaacApiTypes"; +import {ContentSummaryDTO, Stage} from "../../IsaacApiTypes"; import {PotentialUser} from "../../IsaacAppTypes"; import queryString from "query-string"; import {Immutable} from "immer"; @@ -18,10 +18,11 @@ export const pushSearchToHistory = function(history: History, searchQuery: strin }); }; -export const pushConceptsToHistory = function(history: History, searchText: string, subjects: TAG_ID[]) { +export const pushConceptsToHistory = function(history: History, searchText: string, subjects: TAG_ID[], stages: Stage[]) { const queryOptions = { "query": encodeURIComponent(searchText), "types": subjects.join(","), + "stages": stages.join(","), }; history.push({ pathname: history.location.pathname, From af99f430adb99bde75661d7550158c48b6c70e21 Mon Sep 17 00:00:00 2001 From: Alex Lewin Date: Fri, 16 May 2025 14:43:45 +0100 Subject: [PATCH 04/18] Reselect subject when fields deselected On the generic concept finder: if a subject and fields are selected, and the fields are then deselected, reselect the subject. This now matches the behaviour of the question finder. --- src/app/components/elements/layout/SidebarLayout.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/components/elements/layout/SidebarLayout.tsx b/src/app/components/elements/layout/SidebarLayout.tsx index 64a6c3c989..8eeb98f265 100644 --- a/src/app/components/elements/layout/SidebarLayout.tsx +++ b/src/app/components/elements/layout/SidebarLayout.tsx @@ -335,9 +335,13 @@ const FilterCheckbox = (props : FilterCheckboxProps) => { }, [conceptFilters, tag]); const handleCheckboxChange = (checked: boolean) => { + // Reselect parent if all children are deselected + const siblingTags = tag.type === "field" && incompatibleTags ? tags.getDirectDescendents(incompatibleTags[0].id).filter(t => t !== tag) : []; + const reselectParent = siblingTags.length && !siblingTags.some(t => conceptFilters.includes(t)); + const newConceptFilters = checked ? [...conceptFilters.filter(c => !incompatibleTags?.includes(c)), ...(!partiallySelected ? [tag] : [])] - : conceptFilters.filter(c => ![tag, ...(dependentTags ?? [])].includes(c)); + : [...conceptFilters.filter(c => ![tag, ...(dependentTags ?? [])].includes(c)), ...(reselectParent ? [incompatibleTags![0]] : [])]; setConceptFilters(newConceptFilters.length > 0 ? newConceptFilters : (baseTag ? [baseTag] : [])); }; From d4458366a58d3e1bf1bc6d1e4b198329f77fcfdc Mon Sep 17 00:00:00 2001 From: Alex Lewin Date: Fri, 16 May 2025 15:22:04 +0100 Subject: [PATCH 05/18] Correct stage count Selecting other stages shouldn't change the stage count, but selecting subjects/topics should. --- src/app/components/pages/Concepts.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/components/pages/Concepts.tsx b/src/app/components/pages/Concepts.tsx index 7e3d410cd7..40e7e77c08 100644 --- a/src/app/components/pages/Concepts.tsx +++ b/src/app/components/pages/Concepts.tsx @@ -88,7 +88,8 @@ export const Concepts = withRouter((props: RouteComponentProps) => { const stageCounts = getFilteredStageOptions().reduce((acc, s) => ({ ...acc, - [s.value]: shortcutAndFilter(listConceptsQuery?.data?.results, true)?.filter(c => c.audience?.some(a => a.stage?.includes(s.value))).length || 0 + [s.value]: listConceptsQuery?.data?.results?.filter(c => c.audience?.some(a => a.stage?.includes(s.value)) + && (!filters.length || c.tags?.some(t => filters.includes(t))))?.length || 0 }), {}); function doSearch(e?: FormEvent) { From 61371f7e9d5a1371860c2fbf64163f3107479f2e Mon Sep 17 00:00:00 2001 From: Alex Lewin Date: Wed, 21 May 2025 09:42:00 +0100 Subject: [PATCH 06/18] Prevent button wrapping to 3 lines --- src/app/components/pages/Concepts.tsx | 2 +- src/app/components/pages/QuestionFinder.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/pages/Concepts.tsx b/src/app/components/pages/Concepts.tsx index 40e7e77c08..929be8ddf1 100644 --- a/src/app/components/pages/Concepts.tsx +++ b/src/app/components/pages/Concepts.tsx @@ -135,7 +135,7 @@ export const Concepts = withRouter((props: RouteComponentProps) => { {pageContext?.subject &&

The concepts shown on this page have been filtered to only show those that are relevant to {getHumanContext(pageContext)}.

- { {(pageContext?.subject && pageContext.stage) ?

The questions shown on this page have been filtered to only show those that are relevant to {getHumanContext(pageContext)}.

- Date: Wed, 21 May 2025 14:44:09 +0100 Subject: [PATCH 07/18] Update concept stage counts according to search query On the generic concept finder: fixes and neatens the logic for calculating how many concepts match a stage filter by reusing the logic used for topic counts. --- src/app/components/pages/Concepts.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/components/pages/Concepts.tsx b/src/app/components/pages/Concepts.tsx index 929be8ddf1..24f04b974c 100644 --- a/src/app/components/pages/Concepts.tsx +++ b/src/app/components/pages/Concepts.tsx @@ -61,14 +61,14 @@ export const Concepts = withRouter((props: RouteComponentProps) => { : skipToken ); - const shortcutAndFilter = (concepts?: ContentSummaryDTO[], excludeTopicFiltering?: boolean) => { + const shortcutAndFilter = (concepts?: ContentSummaryDTO[], excludeTopicFiltering?: boolean, excludeStageFiltering?: boolean) => { const searchResults = concepts?.filter(c => (matchesAllWordsInAnyOrder(c.title, searchText || "") || matchesAllWordsInAnyOrder(c.summary, searchText || "")) - && (searchStages.length === 0 || searchStages.some(s => c.audience?.some(a => a.stage?.includes(s)))) ); const filteredSearchResults = searchResults ?.filter((result) => excludeTopicFiltering || !filters.length || result?.tags?.some(t => filters.includes(t))) + .filter((result) => excludeStageFiltering || !searchStages.length || searchStages.some(s => result.audience?.some(a => a.stage?.includes(s)))) .filter((result) => !pageContext?.stage?.length || isRelevantToPageContext(result.audience, pageContext)) .filter((result) => searchResultIsPublic(result, user)); @@ -83,13 +83,13 @@ export const Concepts = withRouter((props: RouteComponentProps) => { ].reduce((acc, t) => ({ ...acc, // we exclude topics when filtering here to avoid selecting a filter changing the tag counts - [t.id]: shortcutAndFilter(listConceptsQuery?.data?.results, true)?.filter(c => c.tags?.includes(t.id)).length || 0 + [t.id]: shortcutAndFilter(listConceptsQuery?.data?.results, true, false)?.filter(c => c.tags?.includes(t.id)).length || 0 }), {}); const stageCounts = getFilteredStageOptions().reduce((acc, s) => ({ - ...acc, - [s.value]: listConceptsQuery?.data?.results?.filter(c => c.audience?.some(a => a.stage?.includes(s.value)) - && (!filters.length || c.tags?.some(t => filters.includes(t))))?.length || 0 + ...acc, + // we exclude stages when filtering here to avoid selecting a filter changing the tag counts + [s.value]: shortcutAndFilter(listConceptsQuery?.data?.results, false, true)?.filter(c => c.audience?.some(a => a.stage?.includes(s.value)))?.length || 0 }), {}); function doSearch(e?: FormEvent) { From 41667368494b36e83db183c3503eb599a98777c1 Mon Sep 17 00:00:00 2001 From: Alex Lewin Date: Wed, 21 May 2025 16:59:20 +0100 Subject: [PATCH 08/18] Use stage value as list key --- src/app/components/elements/layout/SidebarLayout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/elements/layout/SidebarLayout.tsx b/src/app/components/elements/layout/SidebarLayout.tsx index 8eeb98f265..4f5b69f5bb 100644 --- a/src/app/components/elements/layout/SidebarLayout.tsx +++ b/src/app/components/elements/layout/SidebarLayout.tsx @@ -492,8 +492,8 @@ export const GenericConceptsSidebar = (props: GenericConceptsSidebarProps) => {
Filter by stage
    - {getFilteredStageOptions().map((stage, i) => -
  • + {getFilteredStageOptions().map((stage) => +
  • {stage.label} ({stageCounts[stage.value]})} data-bs-theme={conceptFilters.length === 1 ? conceptFilters[0].id : undefined} From 618f5993be38e4ab0d321418227368a86d582d17 Mon Sep 17 00:00:00 2001 From: Alex Lewin Date: Wed, 21 May 2025 17:16:46 +0100 Subject: [PATCH 09/18] Improve parent/child tag logic --- src/app/components/elements/layout/SidebarLayout.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/components/elements/layout/SidebarLayout.tsx b/src/app/components/elements/layout/SidebarLayout.tsx index 4f5b69f5bb..0ebe664e03 100644 --- a/src/app/components/elements/layout/SidebarLayout.tsx +++ b/src/app/components/elements/layout/SidebarLayout.tsx @@ -7,7 +7,7 @@ import { above, ACCOUNT_TAB, ACCOUNT_TABS, AUDIENCE_DISPLAY_FIELDS, below, BOARD EventStatusFilter, EventTypeFilter, filterAssignmentsByStatus, filterAudienceViewsByProperties, getDistinctAssignmentGroups, getDistinctAssignmentSetters, getHumanContext, getThemeFromContextAndTags, HUMAN_STAGES, ifKeyIsEnter, isAda, isDefined, PHY_NAV_SUBJECTS, isTeacherOrAbove, QuizStatus, siteSpecific, TAG_ID, tags, STAGE, useDeviceSize, LearningStage, HUMAN_SUBJECTS, ArrayElement, isFullyDefinedContext, isSingleStageContext, Item, stageLabelMap, extractTeacherName, determineGameboardSubjects, PATHS, getQuestionPlaceholder, getFilteredStageOptions, - isPhy} from "../../../services"; + isPhy, TAG_LEVEL} from "../../../services"; import { StageAndDifficultySummaryIcons } from "../StageAndDifficultySummaryIcons"; import { mainContentIdSlice, selectors, useAppDispatch, useAppSelector, useGetQuizAssignmentsAssignedToMeQuery } from "../../../state"; import { Link, useHistory, useLocation } from "react-router-dom"; @@ -336,12 +336,12 @@ const FilterCheckbox = (props : FilterCheckboxProps) => { const handleCheckboxChange = (checked: boolean) => { // Reselect parent if all children are deselected - const siblingTags = tag.type === "field" && incompatibleTags ? tags.getDirectDescendents(incompatibleTags[0].id).filter(t => t !== tag) : []; - const reselectParent = siblingTags.length && !siblingTags.some(t => conceptFilters.includes(t)); + const siblingTags = tag.type === TAG_LEVEL.field && tag.parent ? tags.getDirectDescendents(tag.parent).filter(t => t !== tag) : []; + const reselectParent = tag.parent && siblingTags.every(t => !conceptFilters.includes(t)); const newConceptFilters = checked ? [...conceptFilters.filter(c => !incompatibleTags?.includes(c)), ...(!partiallySelected ? [tag] : [])] - : [...conceptFilters.filter(c => ![tag, ...(dependentTags ?? [])].includes(c)), ...(reselectParent ? [incompatibleTags![0]] : [])]; + : [...conceptFilters.filter(c => ![tag, ...(dependentTags ?? [])].includes(c)), ...(reselectParent ? [tags.getById(tag.parent!)] : [])]; setConceptFilters(newConceptFilters.length > 0 ? newConceptFilters : (baseTag ? [baseTag] : [])); }; From d0c399f3f891b2650471bd7140965feee09e50fe Mon Sep 17 00:00:00 2001 From: Alex Lewin Date: Thu, 22 May 2025 12:18:25 +0100 Subject: [PATCH 10/18] Use subject colour on stage checkboxes whenever possible --- .../components/elements/layout/SidebarLayout.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/app/components/elements/layout/SidebarLayout.tsx b/src/app/components/elements/layout/SidebarLayout.tsx index 0ebe664e03..54ed938f5e 100644 --- a/src/app/components/elements/layout/SidebarLayout.tsx +++ b/src/app/components/elements/layout/SidebarLayout.tsx @@ -1,4 +1,4 @@ -import React, { ChangeEvent, Dispatch, RefObject, SetStateAction, useEffect, useRef, useState } from "react"; +import React, { ChangeEvent, Dispatch, RefObject, SetStateAction, useEffect, useMemo, useRef, useState } from "react"; import { Col, ColProps, RowProps, Input, Offcanvas, OffcanvasBody, OffcanvasHeader, Row, DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown, Form, Label } from "reactstrap"; import partition from "lodash/partition"; import classNames from "classnames"; @@ -440,7 +440,7 @@ interface GenericConceptsSidebarProps extends ConceptListSidebarProps { } export const GenericConceptsSidebar = (props: GenericConceptsSidebarProps) => { - const { searchText, setSearchText, conceptFilters, setConceptFilters, applicableTags, tagCounts, searchStages, setSearchStages, stageCounts, ...rest } = props; + const { searchText, setSearchText, conceptFilters, setConceptFilters, tagCounts, searchStages, setSearchStages, stageCounts, ...rest } = props; const updateSearchStages = (stage: Stage) => { if (searchStages.includes(stage)) { @@ -450,6 +450,13 @@ export const GenericConceptsSidebar = (props: GenericConceptsSidebarProps) => { } }; + // If exactly one subject is selected, infer a colour for the stage checkboxes + const singleSubjectColour = useMemo(() => { + return conceptFilters.length === 1 && conceptFilters[0].type === TAG_LEVEL.subject ? conceptFilters[0].id + : conceptFilters.length && conceptFilters.every(tag => tag.parent === conceptFilters[0].parent) ? conceptFilters[0].parent + : undefined; + }, [conceptFilters]); + return
    @@ -496,7 +503,7 @@ export const GenericConceptsSidebar = (props: GenericConceptsSidebarProps) => {
  • {stage.label} ({stageCounts[stage.value]})} - data-bs-theme={conceptFilters.length === 1 ? conceptFilters[0].id : undefined} + data-bs-theme={singleSubjectColour} color="theme" onChange={() => {updateSearchStages(stage.value);}}/>
  • )}
From 48a797bbf292f521c6fb8fcca382ec745b6c57d9 Mon Sep 17 00:00:00 2001 From: Alex Lewin Date: Thu, 22 May 2025 13:41:55 +0100 Subject: [PATCH 11/18] Don't show stage options with no results Resolves an issue where selecting a stage with no results collapsed the field menu and made it impossible to deselect fields. --- src/app/components/elements/layout/SidebarLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/elements/layout/SidebarLayout.tsx b/src/app/components/elements/layout/SidebarLayout.tsx index 54ed938f5e..dc7227938e 100644 --- a/src/app/components/elements/layout/SidebarLayout.tsx +++ b/src/app/components/elements/layout/SidebarLayout.tsx @@ -499,7 +499,7 @@ export const GenericConceptsSidebar = (props: GenericConceptsSidebarProps) => {
Filter by stage
    - {getFilteredStageOptions().map((stage) => + {getFilteredStageOptions().filter(s => stageCounts[s.value] > 0).map((stage) =>
  • {stage.label} ({stageCounts[stage.value]})} From 9f45b8f3302a22cd9fae9bbc5f5d3cdab3234d4a Mon Sep 17 00:00:00 2001 From: Alex Lewin Date: Thu, 22 May 2025 13:49:21 +0100 Subject: [PATCH 12/18] Decrease padding above concepts listing This also moves the sidebar up and is consistent with the question finder. --- src/app/components/pages/Concepts.tsx | 5 ++--- src/app/components/pages/QuestionFinder.tsx | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/components/pages/Concepts.tsx b/src/app/components/pages/Concepts.tsx index 24f04b974c..baddf3a1db 100644 --- a/src/app/components/pages/Concepts.tsx +++ b/src/app/components/pages/Concepts.tsx @@ -125,7 +125,6 @@ export const Concepts = withRouter((props: RouteComponentProps) => { currentPageTitle="Concepts" intermediateCrumbs={crumb ? [crumb] : undefined} icon={{type: "hex", icon: "icon-concept"}} - className="mb-4" /> {pageContext?.subject @@ -133,8 +132,8 @@ export const Concepts = withRouter((props: RouteComponentProps) => { : } - {pageContext?.subject &&
    -

    The concepts shown on this page have been filtered to only show those that are relevant to {getHumanContext(pageContext)}.

    + {pageContext?.subject &&
    +

    The concepts shown on this page have been filtered to only show those that are relevant to {getHumanContext(pageContext)}.

    {
    {(pageContext?.subject && pageContext.stage) ?
    -

    The questions shown on this page have been filtered to only show those that are relevant to {getHumanContext(pageContext)}.

    +

    The questions shown on this page have been filtered to only show those that are relevant to {getHumanContext(pageContext)}.

    Date: Thu, 22 May 2025 13:54:15 +0100 Subject: [PATCH 13/18] Change "Search questions" capitalisation This is now consistent with the concepts/practice tests sidebars and the designs. --- src/app/components/elements/layout/SidebarLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/elements/layout/SidebarLayout.tsx b/src/app/components/elements/layout/SidebarLayout.tsx index dc7227938e..6d0feae9b9 100644 --- a/src/app/components/elements/layout/SidebarLayout.tsx +++ b/src/app/components/elements/layout/SidebarLayout.tsx @@ -531,7 +531,7 @@ export const QuestionFinderSidebar = (props: QuestionFinderSidebarProps) => { return
    -
    Search Questions
    +
    Search questions
    Date: Thu, 22 May 2025 14:13:02 +0100 Subject: [PATCH 14/18] Reject invalid stage in url --- src/app/components/pages/Concepts.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/pages/Concepts.tsx b/src/app/components/pages/Concepts.tsx index baddf3a1db..aad40697c4 100644 --- a/src/app/components/pages/Concepts.tsx +++ b/src/app/components/pages/Concepts.tsx @@ -53,7 +53,7 @@ export const Concepts = withRouter((props: RouteComponentProps) => { const [conceptFilters, setConceptFilters] = useState( applicableTags.filter(f => filters.includes(f.id)) ); - const [searchStages, setSearchStages] = useState(stages as Stage[]); + const [searchStages, setSearchStages] = useState(getFilteredStageOptions().filter(s => stages.includes(s.value)).map(s => s.value)); const [shortcutResponse, setShortcutResponse] = useState(); const listConceptsQuery = useListConceptsQuery(pageContext From 7401ef6400b7cf64bd2a3e49ca2278f7da22b061 Mon Sep 17 00:00:00 2001 From: Barna Magyarkuti Date: Thu, 22 May 2025 16:52:29 +0100 Subject: [PATCH 15/18] remove inline style on my machine, it didnt work on Concepts: the button still used up three lines. it was fine on the QuestionFinder but I wanted to be consistent across the two pages. Even if I got it to work on my machine's particular settings, using fixed widths, I don't think it would have worked reliable across different font sizes. --- src/app/components/pages/Concepts.tsx | 20 +++++++++++--------- src/app/components/pages/QuestionFinder.tsx | 20 +++++++++++--------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/app/components/pages/Concepts.tsx b/src/app/components/pages/Concepts.tsx index aad40697c4..ffb8526f45 100644 --- a/src/app/components/pages/Concepts.tsx +++ b/src/app/components/pages/Concepts.tsx @@ -132,16 +132,18 @@ export const Concepts = withRouter((props: RouteComponentProps) => { : } - {pageContext?.subject &&
    -

    The concepts shown on this page have been filtered to only show those that are relevant to {getHumanContext(pageContext)}.

    - + {pageContext?.subject &&
    +

    The concepts shown on this page have been filtered to only show those that are relevant to {getHumanContext(pageContext)}.

    +
    + Browse all concepts - + +
    } {isPhy &&
    { {siteSpecific(
    {(pageContext?.subject && pageContext.stage) - ?
    -

    The questions shown on this page have been filtered to only show those that are relevant to {getHumanContext(pageContext)}.

    - + ?
    +

    The questions shown on this page have been filtered to only show those that are relevant to {getHumanContext(pageContext)}.

    +
    + Browse all questions - + +
    : <>Use our question finder to find questions to try on topics in Physics, Maths, Chemistry and Biology. Use our practice questions to become fluent in topics and then take your understanding and problem solving skills to the next level with our challenge questions.} From 29154e3f6d668d22f1d516e18ccf9a7225ee3151 Mon Sep 17 00:00:00 2001 From: Alex Lewin Date: Fri, 23 May 2025 12:16:54 +0100 Subject: [PATCH 16/18] Prevent text wrapping to 3 lines --- src/app/components/pages/Concepts.tsx | 20 +++++++++----------- src/app/components/pages/QuestionFinder.tsx | 20 +++++++++----------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/app/components/pages/Concepts.tsx b/src/app/components/pages/Concepts.tsx index ffb8526f45..5dd521cc0d 100644 --- a/src/app/components/pages/Concepts.tsx +++ b/src/app/components/pages/Concepts.tsx @@ -132,18 +132,16 @@ export const Concepts = withRouter((props: RouteComponentProps) => { : } - {pageContext?.subject &&
    -

    The concepts shown on this page have been filtered to only show those that are relevant to {getHumanContext(pageContext)}.

    -
    - + {pageContext?.subject &&
    +

    The concepts shown on this page have been filtered to only show those that are relevant to {getHumanContext(pageContext)}.

    + Browse all concepts - -
    +
    } {isPhy &&
    { {siteSpecific(
    {(pageContext?.subject && pageContext.stage) - ?
    -

    The questions shown on this page have been filtered to only show those that are relevant to {getHumanContext(pageContext)}.

    -
    - + ?
    +

    The questions shown on this page have been filtered to only show those that are relevant to {getHumanContext(pageContext)}.

    + Browse all questions - -
    +
    : <>Use our question finder to find questions to try on topics in Physics, Maths, Chemistry and Biology. Use our practice questions to become fluent in topics and then take your understanding and problem solving skills to the next level with our challenge questions.} From a5ff701bb8199a0948b6d57ce7b226d96a3b7854 Mon Sep 17 00:00:00 2001 From: Alex Lewin Date: Fri, 23 May 2025 12:27:09 +0100 Subject: [PATCH 17/18] Always show checkbox for selected stage(s) --- src/app/components/elements/layout/SidebarLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/elements/layout/SidebarLayout.tsx b/src/app/components/elements/layout/SidebarLayout.tsx index b5d1da8435..c4db1f7448 100644 --- a/src/app/components/elements/layout/SidebarLayout.tsx +++ b/src/app/components/elements/layout/SidebarLayout.tsx @@ -501,7 +501,7 @@ export const GenericConceptsSidebar = (props: GenericConceptsSidebarProps) => {
    Filter by stage
      - {getFilteredStageOptions().filter(s => stageCounts[s.value] > 0).map((stage) => + {getFilteredStageOptions().filter(s => stageCounts[s.value] > 0 || searchStages.includes(s.value)).map((stage) =>
    • {stage.label} ({stageCounts[stage.value]})} From ee8b99ca7a0c4015263e04fb533539a21f7a7ab3 Mon Sep 17 00:00:00 2001 From: Alex Lewin Date: Fri, 23 May 2025 13:43:41 +0100 Subject: [PATCH 18/18] Always show checkbox for selected field(s) --- src/app/components/elements/layout/SidebarLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/elements/layout/SidebarLayout.tsx b/src/app/components/elements/layout/SidebarLayout.tsx index c4db1f7448..c7914991c6 100644 --- a/src/app/components/elements/layout/SidebarLayout.tsx +++ b/src/app/components/elements/layout/SidebarLayout.tsx @@ -488,7 +488,7 @@ export const GenericConceptsSidebar = (props: GenericConceptsSidebarProps) => { /> {isSelected &&
      {descendentTags - .filter(tag => !isDefined(tagCounts) || tagCounts[tag.id] > 0) + .filter(tag => !isDefined(tagCounts) || tagCounts[tag.id] > 0 || conceptFilters.includes(tag)) // .sort((a, b) => tagCounts ? tagCounts[b.id] - tagCounts[a.id] : 0) .map((tag, j) =>