Skip to content

Commit 71878d0

Browse files
authored
Merge pull request #1407 from isaacphysics/redesign/general-improvements-4
Redesign: General improvements 4
2 parents 20b3323 + d20ed4a commit 71878d0

12 files changed

+197
-132
lines changed

src/app/components/elements/TeacherDashboard.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ const BooksPanel = () => {
146146
<div ref={setScrollRef} className="row position-relative mt-sm-3 mt-md-0 mt-xl-3 row-cols-3 row-cols-md-4 row-cols-lg-8 row-cols-xl-2 row-cols-xxl-auto flex-nowrap overflow-x-scroll overflow-y-hidden">
147147
{/* ScrollShadows uses ResizeObserver, which doesn't exist on Safari <= 13 */}
148148
{window.ResizeObserver && <ScrollShadows element={scrollRef ?? undefined} shadowType="dashboard-scroll-shadow" />}
149-
{ISAAC_BOOKS.filter(book => book.subject === subject || subject === "all")
149+
{ISAAC_BOOKS.filter(b => !b.hidden).filter(book => book.subject === subject || subject === "all")
150150
.map((book) =>
151151
<Col key={book.title} className="mb-2 me-1 p-0">
152152
<BookCard {...book}/>

src/app/components/elements/layout/SidebarLayout.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -307,13 +307,14 @@ interface FilterCheckboxProps extends React.HTMLAttributes<HTMLElement> {
307307
incompatibleTags?: Tag[]; // tags that are removed when this tag is added
308308
dependentTags?: Tag[]; // tags that are removed when this tag is removed
309309
baseTag?: Tag; // tag to add when all tags are removed
310+
partial?: boolean; // if true, the checkbox can be partially selected
310311
partiallySelected?: boolean;
311312
checkboxStyle?: "tab" | "button";
312313
bsSize?: "sm" | "lg";
313314
}
314315

315316
const FilterCheckbox = (props : FilterCheckboxProps) => {
316-
const {tag, conceptFilters, setConceptFilters, tagCounts, checkboxStyle, incompatibleTags, dependentTags, baseTag, partiallySelected, ...rest} = props;
317+
const {tag, conceptFilters, setConceptFilters, tagCounts, checkboxStyle, incompatibleTags, dependentTags, baseTag, partial, partiallySelected, ...rest} = props;
317318
const [checked, setChecked] = useState(conceptFilters.includes(tag));
318319

319320
useEffect(() => {
@@ -331,6 +332,7 @@ const FilterCheckbox = (props : FilterCheckboxProps) => {
331332
? <StyledCheckbox {...rest} id={tag.id} checked={checked}
332333
onChange={(e: ChangeEvent<HTMLInputElement>) => handleCheckboxChange(e.target.checked)}
333334
label={<span>{tag.title} {tagCounts && isDefined(tagCounts[tag.id]) && <span className="text-muted">({tagCounts[tag.id]})</span>}</span>}
335+
partial={partial}
334336
/>
335337
: <StyledTabPicker {...rest} id={tag.id} checked={checked}
336338
onInputChange={(e: ChangeEvent<HTMLInputElement>) => handleCheckboxChange(e.target.checked)}
@@ -454,8 +456,8 @@ export const GenericConceptsSidebar = (props: ConceptListSidebarProps) => {
454456
<FilterCheckbox
455457
checkboxStyle="button" color="theme" data-bs-theme={subject} tag={subjectTag} conceptFilters={conceptFilters}
456458
setConceptFilters={setConceptFilters} tagCounts={tagCounts} dependentTags={descendentTags} incompatibleTags={descendentTags}
457-
partiallySelected={descendentTags.some(tag => conceptFilters.includes(tag))} // not quite isPartial; this is also true if all descendents selected
458-
className={classNames({"icon-checkbox-off": !isSelected, "icon icon-checkbox-partial-alt": isSelected && isPartial, "icon-checkbox-selected": isSelected && !isPartial})}
459+
partial partiallySelected={descendentTags.some(tag => conceptFilters.includes(tag))} // not quite isPartial; this is also true if all descendents selected
460+
className={classNames("icon", {"icon-checkbox-off": !isSelected, "icon-checkbox-partial-alt": isSelected && isPartial, "icon-checkbox-selected": isSelected && !isPartial})}
459461
/>
460462
{isSelected && <div className="ms-3 ps-2">
461463
{descendentTags

src/app/components/elements/modals/QuestionSearchModal.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export const QuestionSearchModal = (
190190
selectOnChange(setSearchBook, true)(e);
191191
sortableTableHeaderUpdateState(questionsSort, setQuestionsSort, "title");
192192
}}
193-
options={ISAAC_BOOKS.map(book => ({value: book.value, label: book.label}))}
193+
options={ISAAC_BOOKS.filter(b => !b.hidden).map(book => ({value: book.tag, label: book.shortTitle}))}
194194
/>
195195
</Col>}
196196
<Col lg={siteSpecific(9, 12)} className={`text-wrap mt-2 ${isBookSearch ? "d-none" : ""}`}>

src/app/components/elements/panels/QuestionFinderFilterPanel.tsx

+10-7
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,10 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps)
158158
const deviceSize = useDeviceSize();
159159
const dispatch = useAppDispatch();
160160
const pageContext = useAppSelector(selectors.pageContext.context);
161-
const bookOptions = ISAAC_BOOKS.filter(book => !pageContext?.subject || book.subject === pageContext?.subject);
161+
const bookOptions = ISAAC_BOOKS.filter(b => !b.hidden).filter(book =>
162+
(!pageContext?.subject || book.subject === pageContext?.subject)
163+
&& (!pageContext?.stage?.length || book.stages.some(x => pageContext?.stage?.includes(x)))
164+
);
162165

163166
const [filtersVisible, setFiltersVisible] = useState<boolean>(above["lg"](deviceSize));
164167

@@ -331,16 +334,16 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps)
331334
/>
332335
</div>
333336
{bookOptions.map((book, index) => (
334-
<div className={classNames("w-100 ps-3 py-1 ms-2", {"checkbox-region": searchBooks.includes(book.value) && !excludeBooks})} key={index}>
337+
<div className={classNames("w-100 ps-3 py-1 ms-2", {"checkbox-region": searchBooks.includes(book.tag) && !excludeBooks})} key={index}>
335338
<StyledCheckbox
336339
color="primary" disabled={excludeBooks}
337-
checked={searchBooks.includes(book.value) && !excludeBooks}
340+
checked={searchBooks.includes(book.tag) && !excludeBooks}
338341
onChange={() => setSearchBooks(
339-
s => s.includes(book.value)
340-
? s.filter(v => v !== book.value)
341-
: [...s, book.value]
342+
s => s.includes(book.tag)
343+
? s.filter(v => v !== book.tag)
344+
: [...s, book.tag]
342345
)}
343-
label={<span className="me-2">{book.label ?? book.title}</span>}
346+
label={<span className="me-2">{book.shortTitle ?? book.title}</span>}
344347
/>
345348
</div>
346349
))}

src/app/components/pages/Concepts.tsx

+54-39
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import React, {FormEvent, MutableRefObject, useEffect, useRef, useState} from "react";
1+
import React, {FormEvent, MutableRefObject, useEffect, useMemo, useRef, useState} from "react";
22
import {RouteComponentProps, withRouter} from "react-router-dom";
33
import {selectors, useAppSelector} from "../../state";
44
import {Badge, Card, CardBody, CardHeader, Container} from "reactstrap";
55
import queryString from "query-string";
6-
import {isAda, isPhy, matchesAllWordsInAnyOrder, pushConceptsToHistory, searchResultIsPublic, shortcuts, TAG_ID, tags} from "../../services";
6+
import {isAda, isPhy, isRelevantToPageContext, matchesAllWordsInAnyOrder, pushConceptsToHistory, searchResultIsPublic, shortcuts, TAG_ID, tags} from "../../services";
77
import {generateSubjectLandingPageCrumbFromContext, TitleAndBreadcrumb} from "../elements/TitleAndBreadcrumb";
88
import {ShortcutResponse, Tag} from "../../../IsaacAppTypes";
99
import {IsaacSpinner} from "../handlers/IsaacSpinner";
@@ -14,41 +14,72 @@ import { isFullyDefinedContext, useUrlPageTheme } from "../../services/pageConte
1414
import { useListConceptsQuery } from "../../state/slices/api/conceptsApi";
1515
import { ShowLoadingQuery } from "../handlers/ShowLoadingQuery";
1616
import { ContentSummaryDTO } from "../../../IsaacApiTypes";
17+
import { skipToken } from "@reduxjs/toolkit/query";
18+
19+
const subjectToTagMap = {
20+
physics: TAG_ID.physics,
21+
chemistry: TAG_ID.chemistry,
22+
biology: TAG_ID.biology,
23+
maths: TAG_ID.maths,
24+
};
1725

1826
// This component is Isaac Physics only (currently)
1927
export const Concepts = withRouter((props: RouteComponentProps) => {
2028
const {location, history} = props;
2129
const user = useAppSelector(selectors.user.orNull);
2230
const pageContext = useUrlPageTheme();
2331

24-
const listConceptsQuery = useListConceptsQuery({conceptIds: undefined, tagIds: pageContext?.subject});
32+
const searchParsed = queryString.parse(location.search, {arrayFormat: "comma"});
2533

26-
const subjectToTagMap = {
27-
physics: TAG_ID.physics,
28-
chemistry: TAG_ID.chemistry,
29-
biology: TAG_ID.biology,
30-
maths: TAG_ID.maths,
31-
};
34+
const [query, filters] = useMemo(() => {
35+
const queryParsed = searchParsed.query || null;
36+
const query = Array.isArray(queryParsed) ? queryParsed.join(",") : queryParsed;
3237

33-
const applicableTags = pageContext?.subject ? tags.getDirectDescendents(subjectToTagMap[pageContext.subject]) : tags.allFieldTags;
34-
35-
const tagCounts : Record<string, number> = [...applicableTags, ...(pageContext?.subject ? [tags.getById(pageContext?.subject as TAG_ID)] : [])].reduce((acc, t) => ({...acc, [t.id]: listConceptsQuery?.data?.results?.filter(c => c.tags?.includes(t.id)).length || 0}), {});
38+
const filterParsed = searchParsed.types || null;
39+
const filters = Array.isArray(filterParsed) ? filterParsed.filter(x => !!x) as string[] : filterParsed?.split(",") ?? [];
40+
return [query, filters];
41+
}, [searchParsed]);
3642

37-
const searchParsed = queryString.parse(location.search);
43+
const applicableTags = pageContext?.subject
44+
// this includes all subject tags and all field tags
45+
? [tags.getById(subjectToTagMap[pageContext.subject]), ...tags.getDirectDescendents(subjectToTagMap[pageContext.subject])]
46+
: [...tags.allSubjectTags, ...tags.allFieldTags];
3847

39-
const queryParsed = searchParsed.query || "";
40-
const query = queryParsed instanceof Array ? queryParsed[0] : queryParsed;
48+
const [searchText, setSearchText] = useState(query);
49+
const [conceptFilters, setConceptFilters] = useState<Tag[]>(
50+
applicableTags.filter(f => filters.includes(f.id))
51+
);
52+
const [shortcutResponse, setShortcutResponse] = useState<ShortcutResponse[]>();
4153

42-
const filterParsed = (searchParsed.types || (TAG_ID.physics + "," + TAG_ID.maths + "," + TAG_ID.chemistry + "," + TAG_ID.biology));
43-
const filters = (Array.isArray(filterParsed) ? filterParsed[0] || "" : filterParsed || "").split(",");
54+
const listConceptsQuery = useListConceptsQuery(pageContext
55+
? {conceptIds: undefined, tagIds: pageContext?.subject ?? tags.allSubjectTags.map(t => t.id).join(",")}
56+
: skipToken
57+
);
4458

45-
const [searchText, setSearchText] = useState(query);
46-
const [conceptFilters, setConceptFilters] = useState<Tag[]>([
47-
...(pageContext?.subject ? [tags.getById(subjectToTagMap[pageContext.subject])] : []),
48-
...applicableTags.filter(f => filters.includes(f.id))
49-
]);
59+
const shortcutAndFilter = (concepts?: ContentSummaryDTO[], excludeTopicFiltering?: boolean) => {
60+
const searchResults = concepts?.filter(c =>
61+
matchesAllWordsInAnyOrder(c.title, searchText || "") ||
62+
matchesAllWordsInAnyOrder(c.summary, searchText || "")
63+
);
64+
65+
const filteredSearchResults = searchResults
66+
?.filter((result) => excludeTopicFiltering || !filters.length || result?.tags?.some(t => filters.includes(t)))
67+
.filter((result) => !pageContext?.stage?.length || isRelevantToPageContext(result.audience, pageContext))
68+
.filter((result) => searchResultIsPublic(result, user));
69+
70+
const shortcutAndFilteredSearchResults = (shortcutResponse || []).concat(filteredSearchResults || []);
5071

51-
const [shortcutResponse, setShortcutResponse] = useState<ShortcutResponse[]>();
72+
return shortcutAndFilteredSearchResults;
73+
};
74+
75+
const tagCounts : Record<string, number> = [
76+
...applicableTags,
77+
...(pageContext?.subject ? [tags.getById(pageContext?.subject as TAG_ID)] : [])
78+
].reduce((acc, t) => ({
79+
...acc,
80+
// we exclude topics when filtering here to avoid selecting a filter changing the tag counts
81+
[t.id]: shortcutAndFilter(listConceptsQuery?.data?.results, true)?.filter(c => c.tags?.includes(t.id)).length || 0
82+
}), {});
5283

5384
function doSearch(e?: FormEvent<HTMLFormElement>) {
5485
if (e) {
@@ -73,22 +104,6 @@ export const Concepts = withRouter((props: RouteComponentProps) => {
73104

74105
useEffect(() => {doSearch();}, [conceptFilters]);
75106

76-
const shortcutAndFilter = (concepts?: ContentSummaryDTO[]) => {
77-
const searchResults = concepts
78-
?.filter(c =>
79-
matchesAllWordsInAnyOrder(c.title, searchText || "") ||
80-
matchesAllWordsInAnyOrder(c.summary, searchText || "")
81-
);
82-
83-
const filteredSearchResults = searchResults
84-
?.filter((result) => result?.tags?.some(t => filters.includes(t)))
85-
.filter((result) => searchResultIsPublic(result, user));
86-
87-
const shortcutAndFilteredSearchResults = (shortcutResponse || []).concat(filteredSearchResults || []);
88-
89-
return shortcutAndFilteredSearchResults;
90-
};
91-
92107
const crumb = isPhy && isFullyDefinedContext(pageContext) && generateSubjectLandingPageCrumbFromContext(pageContext);
93108

94109
const sidebarProps = {searchText, setSearchText, conceptFilters, setConceptFilters, applicableTags, tagCounts};

0 commit comments

Comments
 (0)