@@ -48,6 +48,7 @@ import { ShareLink } from "../elements/ShareLink";
48
48
import { Spacer } from "../elements/Spacer" ;
49
49
import { ListView } from "../elements/list-groups/ListView" ;
50
50
import { ContentTypeVisibility , LinkToContentSummaryList } from "../elements/list-groups/ContentSummaryListGroupItem" ;
51
+ import { get , set } from "lodash" ;
51
52
52
53
// Type is used to ensure that we check all query params if a new one is added in the future
53
54
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
85
86
if ( index === 0 )
86
87
selectionItems . push ( { [ TAG_LEVEL . subject ] : validTierTags . map ( itemiseTag ) } as ChoiceTree ) ;
87
88
else {
88
- const parents = Object . values ( selectionItems [ index - 1 ] ) . flat ( ) ;
89
+ const parents = selectionItems [ index - 1 ] ? Object . values ( selectionItems [ index - 1 ] ) . flat ( ) : [ ] ;
89
90
const validChildren = parents . map ( p => tags . getChildren ( p . value ) . filter ( c => tier . includes ( c . id ) ) . map ( itemiseTag ) ) ;
90
91
91
92
const currentLayer : ChoiceTree = { } ;
@@ -100,6 +101,28 @@ function processTagHierarchy(subjects: string[], fields: string[], topics: strin
100
101
return selectionItems ;
101
102
}
102
103
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
+
103
126
function getInitialQuestionStatuses ( params : ListParams < FilterParams > ) : QuestionStatus {
104
127
const statuses = arrayFromPossibleCsv ( params . statuses ) ;
105
128
if ( statuses . length < 1 ) {
@@ -171,8 +194,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
171
194
processTagHierarchy (
172
195
arrayFromPossibleCsv ( pageContext . subject ? [ pageContext . subject ] : params . subjects ) ,
173
196
arrayFromPossibleCsv ( params . fields ) ,
174
- arrayFromPossibleCsv ( params . topics )
175
- )
197
+ arrayFromPossibleCsv ( params . topics ) )
176
198
) ;
177
199
178
200
const choices : ChoiceTree [ ] = [ ] ;
@@ -199,14 +221,6 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
199
221
{ id : "topics" as TierID , name : "Topic" }
200
222
] . map ( tier => ( { ...tier , for : "for_" + tier . id } ) ) ;
201
223
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
-
210
224
const { results : questions , totalResults : totalQuestions , nextSearchOffset} = useAppSelector ( ( state : AppState ) => state && state . questionSearchResult ) || { } ;
211
225
const nothingToSearchFor =
212
226
[ searchQuery , searchTopics , searchBooks , searchStages , searchDifficulties , searchExamBoards ] . every ( v => v . length === 0 ) &&
@@ -376,7 +390,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
376
390
|| searchStages . length > 0
377
391
|| searchBooks . length > 0
378
392
|| excludeBooks
379
- || selections . some ( tier => Object . keys ( tier ) . length > 0 )
393
+ || selections . some ( tier => Object . values ( tier ) . flat ( ) . length > 0 )
380
394
|| Object . entries ( searchStatuses ) . some ( e => e [ 1 ] ) ) ;
381
395
if ( isPhy ) applyFilters ( ) ;
382
396
} , [ searchDifficulties , searchTopics , searchExamBoards , searchStages , searchBooks , excludeBooks , selections , searchStatuses ] ) ;
@@ -427,27 +441,44 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
427
441
< IsaacSpinner />
428
442
</ div > ;
429
443
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
+
430
460
const FilterTag = ( { name} : { name : string } ) => {
431
461
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 " >
433
463
{ name }
464
+ < button className = "icon icon-close" onClick = { ( ) => removeFilterTag ( name ) } aria-label = "Close" />
434
465
</ div >
435
466
) ;
436
467
} ;
437
468
438
469
const FilterSummary = ( ) => {
439
470
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???
441
472
const statusList : string [ ] = Object . keys ( searchStatuses ) . filter ( status => searchStatuses [ status as keyof QuestionStatus ] ) ;
442
473
443
- const categories = [ searchDifficulties , searchTopics , stageList , searchExamBoards , statusList , searchBooks , selectionList ] . flat ( ) ;
474
+ const categories = [ searchDifficulties , stageList , searchExamBoards , statusList , searchBooks , selectionList ] . flat ( ) ;
444
475
445
476
return < div className = "d-flex" >
446
477
{ categories . map ( c => < FilterTag key = { c } name = { c } /> ) }
447
- { categories . length > 0 ?
478
+ { categories . length > 0 ?
448
479
< button className = "text-black py-0 btn-link bg-transparent" onClick = { ( e ) => { e . stopPropagation ( ) ; clearFilters ( ) ; } } >
449
480
clear all filters
450
- </ button >
481
+ </ button >
451
482
: < div /> }
452
483
</ div > ;
453
484
} ;
@@ -482,7 +513,8 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
482
513
searchStatuses, setSearchStatuses,
483
514
searchBooks, setSearchBooks,
484
515
excludeBooks, setExcludeBooks,
485
- tiers, choices, selections, setTierSelection,
516
+ tiers, choices,
517
+ selections, setSelections,
486
518
applyFilters, clearFilters,
487
519
validFiltersSelected, searchDisabled, setSearchDisabled
488
520
} } />
@@ -518,7 +550,8 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
518
550
searchStatuses, setSearchStatuses,
519
551
searchBooks, setSearchBooks,
520
552
excludeBooks, setExcludeBooks,
521
- tiers, choices, selections, setTierSelection,
553
+ tiers, choices,
554
+ selections, setSelections,
522
555
applyFilters, clearFilters,
523
556
validFiltersSelected, searchDisabled, setSearchDisabled
524
557
} } /> { /* Temporarily disabled at >=lg to test list view until this filter is moved into the sidebar */ }
0 commit comments