Skip to content

Commit 22ad0ba

Browse files
committed
Start adding context-sensitive checkboxes
1 parent 2243515 commit 22ad0ba

File tree

7 files changed

+97
-41
lines changed

7 files changed

+97
-41
lines changed
Lines changed: 4 additions & 0 deletions
Loading
Lines changed: 4 additions & 0 deletions
Loading

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,8 @@ export interface QuestionFinderFilterPanelProps {
145145
searchStatuses: QuestionStatus, setSearchStatuses: Dispatch<SetStateAction<QuestionStatus>>;
146146
searchBooks: string[], setSearchBooks: Dispatch<SetStateAction<string[]>>;
147147
excludeBooks: boolean, setExcludeBooks: Dispatch<SetStateAction<boolean>>;
148-
tiers: Tier[], choices: ChoiceTree[]; selections: ChoiceTree[];
149-
setTierSelection: (tierIndex: number) => React.Dispatch<React.SetStateAction<ChoiceTree>>
148+
tiers: Tier[], choices: ChoiceTree[];
149+
selections: ChoiceTree[], setSelections: Dispatch<SetStateAction<ChoiceTree[]>>;
150150
applyFilters: () => void; clearFilters: () => void;
151151
validFiltersSelected: boolean;
152152
searchDisabled: boolean;
@@ -161,7 +161,7 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps)
161161
searchStatuses, setSearchStatuses,
162162
searchBooks, setSearchBooks,
163163
excludeBooks, setExcludeBooks,
164-
tiers, choices, selections, setTierSelection,
164+
tiers, choices, selections, setSelections,
165165
applyFilters, clearFilters, validFiltersSelected,
166166
searchDisabled, setSearchDisabled
167167
} = props;
@@ -268,8 +268,8 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps)
268268
<HierarchyFilterHexagonal {...{
269269
tier: pageContext.subject ? 1 : 0,
270270
index: pageContext.subject as TAG_ID ?? TAG_LEVEL.subject,
271-
tiers, choices, selections,
272-
questionFinderFilter: true, setTierSelection
271+
tiers, choices, selections, setSelections,
272+
questionFinderFilter: true
273273
}}/>
274274
</div>,
275275
groupBaseTagOptions.map((tag, index) => (

src/app/components/elements/svg/HierarchyFilter.tsx

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {StyledSelect} from "../inputs/StyledSelect";
2121
import { Label } from "reactstrap";
2222
import { StyledCheckbox } from "../inputs/StyledCheckbox";
2323
import { ChoiceTree } from "../panels/QuestionFinderFilterPanel";
24+
import { pruneTreeNode } from "../../pages/QuestionFinder";
2425

2526
export type TierID = "subjects" | "fields" | "topics";
2627
export interface Tier {id: TierID; name: string; for: string}
@@ -35,7 +36,7 @@ interface HierarchySummaryProps {
3536

3637
interface HierarchyFilterProps extends HierarchySummaryProps {
3738
questionFinderFilter?: boolean;
38-
setTierSelection: (tierIndex: number) => React.Dispatch<React.SetStateAction<ChoiceTree>>;
39+
setSelections: (selections: ChoiceTree[]) => void;
3940
tier: number;
4041
index: TAG_ID | TAG_LEVEL;
4142
className?: string;
@@ -49,38 +50,42 @@ function naturalLanguageList(list: string[]) {
4950
return `${lowerCaseList.slice(0, lastIndex).join(", ")} and ${lowerCaseList[lastIndex]}`;
5051
}
5152

52-
export function HierarchyFilterHexagonal({tier, index, tiers, choices, selections, questionFinderFilter, setTierSelection, className}: HierarchyFilterProps) {
53-
return <div className={className}>
53+
export function HierarchyFilterHexagonal({tier, index, tiers, choices, selections, questionFinderFilter, className, setSelections}: HierarchyFilterProps) {
54+
return <div className={classNames("ms-3", className)}>
5455
{choices[tier] && choices[tier][index] && choices[tier][index].map((choice) => {
5556
const isSelected = selections[tier] && selections[tier][index]?.map(s => s.value).includes(choice.value);
5657
function selectValue() {
58+
let newSelections = [...selections];
5759
if (selections[tier] && selections[tier][index]) {
58-
setTierSelection(tier)({...selections[tier], [index]: isSelected ?
59-
selections[tier][index].filter(s => s.value !== choice.value) :
60-
[...selections[tier][index], choice]});
60+
if (isSelected) {
61+
newSelections = pruneTreeNode(newSelections, choice.value);
62+
} else {
63+
newSelections[tier][index]?.push(choice);
64+
}
6165
}
6266
else {
63-
setTierSelection(tier)({...selections[tier], [index]: [choice]});
67+
newSelections[tier] = {...selections[tier], [index]: [choice]};
6468
}
69+
setSelections(newSelections);
6570
};
6671

67-
return <div key={choice.value} className={classNames("ps-3 ms-2", {"ms-3": tier===1, "ms-4 search-field": tier===2, "bg-white": tier===0 && isSelected, "bg-grey": tier===1 && isSelected})}>
72+
return <div key={choice.value} className={classNames("ps-3", {"ms-2": tier===0, "search-field": tier===2, "bg-white": tier===0 && isSelected, "bg-grey": tier===1 && isSelected})}>
6873
<StyledCheckbox
6974
color="primary"
7075
checked={isSelected}
7176
onChange={selectValue}
7277
label={<span>{choice.label}</span>}
7378
/>
7479
{tier < 2 && choices[tier+1] && choice.value in choices[tier+1] &&
75-
<HierarchyFilterHexagonal {...{tier: tier+1, index: choice.value, tiers, choices, selections, questionFinderFilter, setTierSelection}} className={classNames({"bg-white": tier===0})}/>
80+
<HierarchyFilterHexagonal {...{tier: tier+1, index: choice.value, tiers, choices, selections, questionFinderFilter, setSelections}} className={classNames({"bg-white": tier===0})}/>
7681
}
7782
</div>;
7883
}
7984
)}
8085
</div>;
8186
}
8287

83-
export function HierarchyFilterSummary({tiers, choices, selections}: HierarchySummaryProps) {
88+
/* export function HierarchyFilterSummary({tiers, choices, selections}: HierarchySummaryProps) {
8489
const hexagon = calculateHexagonProportions(10, 2);
8590
const hexKeyPoints = addHexagonKeyPoints(hexagon);
8691
const connection = {length: 60};
@@ -104,7 +109,7 @@ export function HierarchyFilterSummary({tiers, choices, selections}: HierarchySu
104109
{`${naturalLanguageList(selectionSummary)} filter${selectionSummary.length != 1 || selections[0]?.length != 1 ? "s" : ""} selected`}
105110
</title>
106111
<g id="hexagonal-filter-summary" transform={`translate(1,1)`}>
107-
{/* Connection & Hexagon */}
112+
108113
<g transform={`translate(${connection.length / 2 - hexKeyPoints.x.center}, 0)`}>
109114
{selectionSummary.map((selection, i) => {
110115
const yCenter = hexKeyPoints.y.center;
@@ -120,7 +125,7 @@ export function HierarchyFilterSummary({tiers, choices, selections}: HierarchySu
120125
})}
121126
</g>
122127
123-
{/* Text */}
128+
124129
{selectionSummary.map((selection, i) => {
125130
return <g key={selection} transform={`translate(${((hexagon.halfWidth + hexagon.padding) * 2 + connection.length) * i}, 0)`}>
126131
<g transform={`translate(0, ${hexagon.quarterHeight * 4 + hexagon.padding})`}>
@@ -143,4 +148,4 @@ export function HierarchyFilterSelects({tiers, choices, selections, setTierSelec
143148
<StyledSelect name={tier.for} onChange={selectOnChange(setTierSelection(i), false)} isMulti options={choices[i]} value={selections[i]} />
144149
</React.Fragment>)}
145150
</React.Fragment>;
146-
}
151+
} */

src/app/components/pages/QuestionFinder.tsx

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import { ShareLink } from "../elements/ShareLink";
4848
import { Spacer } from "../elements/Spacer";
4949
import { ListView } from "../elements/list-groups/ListView";
5050
import { ContentTypeVisibility, LinkToContentSummaryList } from "../elements/list-groups/ContentSummaryListGroupItem";
51+
import { get, set } from "lodash";
5152

5253
// Type is used to ensure that we check all query params if a new one is added in the future
5354
const FILTER_PARAMS = ["query", "topics", "fields", "subjects", "stages", "difficulties", "examBoards", "book", "excludeBooks", "statuses"] as const;
@@ -85,7 +86,7 @@ function processTagHierarchy(subjects: string[], fields: string[], topics: strin
8586
if (index === 0)
8687
selectionItems.push({[TAG_LEVEL.subject]: validTierTags.map(itemiseTag)} as ChoiceTree);
8788
else {
88-
const parents = Object.values(selectionItems[index-1]).flat();
89+
const parents = selectionItems[index-1] ? Object.values(selectionItems[index-1]).flat() : [];
8990
const validChildren = parents.map(p => tags.getChildren(p.value).filter(c => tier.includes(c.id)).map(itemiseTag));
9091

9192
const currentLayer: ChoiceTree = {};
@@ -100,6 +101,28 @@ function processTagHierarchy(subjects: string[], fields: string[], topics: strin
100101
return selectionItems;
101102
}
102103

104+
export function pruneTreeNode(tree: ChoiceTree[], filter: string, recursive?: boolean): ChoiceTree[] {
105+
let newTree = [...tree];
106+
newTree.forEach((tier, i) => {
107+
if (tier[filter as TAG_ID]) { // removing children of node
108+
Object.values(tier[filter as TAG_ID] || {}).forEach(v => pruneTreeNode(newTree, v.value, recursive));
109+
delete newTree[i][filter as TAG_ID];
110+
} else { // removing node itself
111+
const parents = Object.keys(tier);
112+
parents.forEach(parent => {
113+
if (newTree[i][parent as TAG_ID]?.some(c => c.value === filter)) {
114+
newTree[i][parent as TAG_ID] = newTree[i][parent as TAG_ID]?.filter(c => c.value !== filter);
115+
if (recursive && newTree[i][parent as TAG_ID]?.length === 0) {
116+
newTree = pruneTreeNode(newTree, parent, true);
117+
}
118+
}
119+
});
120+
}
121+
});
122+
123+
return newTree;
124+
}
125+
103126
function getInitialQuestionStatuses(params: ListParams<FilterParams>): QuestionStatus {
104127
const statuses = arrayFromPossibleCsv(params.statuses);
105128
if (statuses.length < 1) {
@@ -171,8 +194,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
171194
processTagHierarchy(
172195
arrayFromPossibleCsv(pageContext.subject ? [pageContext.subject] : params.subjects),
173196
arrayFromPossibleCsv(params.fields),
174-
arrayFromPossibleCsv(params.topics)
175-
)
197+
arrayFromPossibleCsv(params.topics))
176198
);
177199

178200
const choices: ChoiceTree[] = [];
@@ -199,14 +221,6 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
199221
{id: "topics" as TierID, name: "Topic"}
200222
].map(tier => ({...tier, for: "for_" + tier.id}));
201223

202-
const setTierSelection = (tierIndex: number) => {
203-
return ((values: ChoiceTree) => {
204-
const newSelections = selections.slice(0, 3);
205-
newSelections[tierIndex] = values;
206-
setSelections(newSelections);
207-
}) as React.Dispatch<React.SetStateAction<ChoiceTree>>;
208-
};
209-
210224
const {results: questions, totalResults: totalQuestions, nextSearchOffset} = useAppSelector((state: AppState) => state && state.questionSearchResult) || {};
211225
const nothingToSearchFor =
212226
[searchQuery, searchTopics, searchBooks, searchStages, searchDifficulties, searchExamBoards].every(v => v.length === 0) &&
@@ -376,7 +390,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
376390
|| searchStages.length > 0
377391
|| searchBooks.length > 0
378392
|| excludeBooks
379-
|| selections.some(tier => Object.keys(tier).length > 0)
393+
|| selections.some(tier => Object.values(tier).flat().length > 0)
380394
|| Object.entries(searchStatuses).some(e => e[1]));
381395
if (isPhy) applyFilters();
382396
}, [searchDifficulties, searchTopics, searchExamBoards, searchStages, searchBooks, excludeBooks, selections, searchStatuses]);
@@ -427,27 +441,44 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
427441
<IsaacSpinner />
428442
</div>;
429443

444+
function removeFilterTag(filter: string) {
445+
if (searchStages.includes(filter as STAGE)) {
446+
setSearchStages(searchStages.filter(f => f !== filter));
447+
} else if (getChoiceTreeLeaves(selections).some(leaf => leaf.value === filter)) {
448+
setSelections(pruneTreeNode(selections, filter, true));
449+
} else if (searchDifficulties.includes(filter as Difficulty)) {
450+
setSearchDifficulties(searchDifficulties.filter(f => f !== filter));
451+
} else if (searchExamBoards.includes(filter as ExamBoard)) {
452+
setSearchExamBoards(searchExamBoards.filter(f => f !== filter));
453+
} else if (searchBooks.includes(filter)) {
454+
setSearchBooks(searchBooks.filter(f => f !== filter));
455+
} else if (searchStatuses[filter as keyof QuestionStatus]) {
456+
setSearchStatuses({...searchStatuses, [filter as keyof QuestionStatus]: false});
457+
}
458+
};
459+
430460
const FilterTag = ({name}: {name: string}) => {
431461
return (
432-
<div className="quiz-level-1-tag me-2">
462+
<div data-bs-theme="neutral" className="filter-tag me-2 d-flex align-items-center">
433463
{name}
464+
<button className="icon icon-close" onClick={() => removeFilterTag(name)} aria-label="Close"/>
434465
</div>
435466
);
436467
};
437468

438469
const FilterSummary = () => {
439470
const stageList: string[] = searchStages.filter(stage => stage !== pageContext.stage);
440-
const selectionList: string[] = getChoiceTreeLeaves(selections).filter(leaf => leaf.value !== pageContext.subject).map(leaf => leaf.label);
471+
const selectionList: string[] = getChoiceTreeLeaves(selections).filter(leaf => leaf.value !== pageContext.subject).map(leaf => leaf.value); // value for now???
441472
const statusList: string[] = Object.keys(searchStatuses).filter(status => searchStatuses[status as keyof QuestionStatus]);
442473

443-
const categories = [searchDifficulties, searchTopics, stageList, searchExamBoards, statusList, searchBooks, selectionList].flat();
474+
const categories = [searchDifficulties, stageList, searchExamBoards, statusList, searchBooks, selectionList].flat();
444475

445476
return <div className="d-flex">
446477
{categories.map(c => <FilterTag key={c} name={c}/>)}
447-
{categories.length > 0 ?
478+
{categories.length > 0 ?
448479
<button className="text-black py-0 btn-link bg-transparent" onClick={(e) => { e.stopPropagation(); clearFilters(); }}>
449480
clear all filters
450-
</button>
481+
</button>
451482
: <div/>}
452483
</div>;
453484
};
@@ -482,7 +513,8 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
482513
searchStatuses, setSearchStatuses,
483514
searchBooks, setSearchBooks,
484515
excludeBooks, setExcludeBooks,
485-
tiers, choices, selections, setTierSelection,
516+
tiers, choices,
517+
selections, setSelections,
486518
applyFilters, clearFilters,
487519
validFiltersSelected, searchDisabled, setSearchDisabled
488520
}} />
@@ -518,7 +550,8 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
518550
searchStatuses, setSearchStatuses,
519551
searchBooks, setSearchBooks,
520552
excludeBooks, setExcludeBooks,
521-
tiers, choices, selections, setTierSelection,
553+
tiers, choices,
554+
selections, setSelections,
522555
applyFilters, clearFilters,
523556
validFiltersSelected, searchDisabled, setSearchDisabled
524557
}} /> {/* Temporarily disabled at >=lg to test list view until this filter is moved into the sidebar */}

src/scss/common/checkbox.scss

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@
3131
}
3232

3333
&[color="primary"] {
34-
background: $primary;
35-
border-color: $primary;
34+
background: $color-brand-500;
35+
border-color: $color-brand-500;
3636

3737
&:hover, &:disabled {
38-
background: darken($primary, 10%);
39-
border-color: darken($primary, 10%);
38+
background: darken($color-brand-500, 10%);
39+
border-color: darken($color-brand-500, 10%);
4040
}
4141
}
4242

src/scss/phy/list-groups.scss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@
7575
font-weight: bold;
7676
}
7777

78+
.filter-tag {
79+
@include pill-tag($color-neutral-900, $color-neutral-100, $color-neutral-200);
80+
border-width: 2px;
81+
82+
.icon {
83+
background-color: var(--buttons-light-icon);
84+
margin-left: 0.25rem;
85+
}
86+
}
87+
7888
.list-group-links li a.card-tag {
7989
@include pill-tag($color-neutral-900, var(--subject-color-50), var(--subject-color-100));
8090
font-family: $secondary-font;

0 commit comments

Comments
 (0)