1
- import React , { ChangeEvent , Dispatch , RefObject , SetStateAction , useEffect , useRef , useState } from "react" ;
1
+ import React , { ChangeEvent , Dispatch , RefObject , SetStateAction , useEffect , useMemo , useRef , useState } from "react" ;
2
2
import { Col , ColProps , RowProps , Input , Offcanvas , OffcanvasBody , OffcanvasHeader , Row , DropdownItem , DropdownMenu , DropdownToggle , UncontrolledDropdown , Form , Label } from "reactstrap" ;
3
3
import partition from "lodash/partition" ;
4
4
import classNames from "classnames" ;
@@ -9,7 +9,7 @@ import { above, ACCOUNT_TAB, ACCOUNT_TABS, AUDIENCE_DISPLAY_FIELDS, below, BOARD
9
9
Item , stageLabelMap , extractTeacherName , determineGameboardSubjects , PATHS , getQuestionPlaceholder , getFilteredStageOptions ,
10
10
isPhy ,
11
11
ISAAC_BOOKS ,
12
- BookHiddenState } from "../../../services" ;
12
+ BookHiddenState , TAG_LEVEL } from "../../../services" ;
13
13
import { StageAndDifficultySummaryIcons } from "../StageAndDifficultySummaryIcons" ;
14
14
import { mainContentIdSlice , selectors , useAppDispatch , useAppSelector , useGetQuizAssignmentsAssignedToMeQuery } from "../../../state" ;
15
15
import { Link , useHistory , useLocation } from "react-router-dom" ;
@@ -337,9 +337,13 @@ const FilterCheckbox = (props : FilterCheckboxProps) => {
337
337
} , [ conceptFilters , tag ] ) ;
338
338
339
339
const handleCheckboxChange = ( checked : boolean ) => {
340
+ // Reselect parent if all children are deselected
341
+ const siblingTags = tag . type === TAG_LEVEL . field && tag . parent ? tags . getDirectDescendents ( tag . parent ) . filter ( t => t !== tag ) : [ ] ;
342
+ const reselectParent = tag . parent && siblingTags . every ( t => ! conceptFilters . includes ( t ) ) ;
343
+
340
344
const newConceptFilters = checked
341
345
? [ ...conceptFilters . filter ( c => ! incompatibleTags ?. includes ( c ) ) , ...( ! partiallySelected ? [ tag ] : [ ] ) ]
342
- : conceptFilters . filter ( c => ! [ tag , ...( dependentTags ?? [ ] ) ] . includes ( c ) ) ;
346
+ : [ ... conceptFilters . filter ( c => ! [ tag , ...( dependentTags ?? [ ] ) ] . includes ( c ) ) , ... ( reselectParent ? [ tags . getById ( tag . parent ! ) ] : [ ] ) ] ;
343
347
setConceptFilters ( newConceptFilters . length > 0 ? newConceptFilters : ( baseTag ? [ baseTag ] : [ ] ) ) ;
344
348
} ;
345
349
@@ -428,27 +432,32 @@ export const SubjectSpecificConceptListSidebar = (props: ConceptListSidebarProps
428
432
}
429
433
</ div >
430
434
</ search >
431
-
432
- < div className = "section-divider" />
433
-
434
- < div className = "sidebar-help" >
435
- < p > The concepts shown on this page have been filtered to only show those that are relevant to { getHumanContext ( pageContext ) } .</ p >
436
- < p > If you want to explore broader concepts across multiple subjects or learning stages, you can use the main concept browser:</ p >
437
- < AffixButton size = "md" color = "keyline" tag = { Link } to = "/concepts" affix = { {
438
- affix : "icon-right" ,
439
- position : "suffix" ,
440
- type : "icon"
441
- } } >
442
- Browse concepts
443
- </ AffixButton >
444
- </ div >
445
435
</ ContentSidebar > ;
446
436
} ;
447
437
448
- export const GenericConceptsSidebar = ( props : ConceptListSidebarProps ) => {
449
- const { searchText, setSearchText, conceptFilters, setConceptFilters, applicableTags, tagCounts, ...rest } = props ;
438
+ interface GenericConceptsSidebarProps extends ConceptListSidebarProps {
439
+ searchStages : Stage [ ] ;
440
+ setSearchStages : React . Dispatch < React . SetStateAction < Stage [ ] > > ;
441
+ stageCounts : Record < string , number > ;
442
+ }
450
443
451
- const pageContext = useAppSelector ( selectors . pageContext . context ) ;
444
+ export const GenericConceptsSidebar = ( props : GenericConceptsSidebarProps ) => {
445
+ const { searchText, setSearchText, conceptFilters, setConceptFilters, tagCounts, searchStages, setSearchStages, stageCounts, ...rest } = props ;
446
+
447
+ const updateSearchStages = ( stage : Stage ) => {
448
+ if ( searchStages . includes ( stage ) ) {
449
+ setSearchStages ( searchStages . filter ( s => s !== stage ) ) ;
450
+ } else {
451
+ setSearchStages ( [ ...( searchStages ?? [ ] ) , stage ] ) ;
452
+ }
453
+ } ;
454
+
455
+ // If exactly one subject is selected, infer a colour for the stage checkboxes
456
+ const singleSubjectColour = useMemo ( ( ) => {
457
+ return conceptFilters . length === 1 && conceptFilters [ 0 ] . type === TAG_LEVEL . subject ? conceptFilters [ 0 ] . id
458
+ : conceptFilters . length && conceptFilters . every ( tag => tag . parent === conceptFilters [ 0 ] . parent ) ? conceptFilters [ 0 ] . parent
459
+ : undefined ;
460
+ } , [ conceptFilters ] ) ;
452
461
453
462
return < ContentSidebar { ...rest } >
454
463
< div className = "section-divider" />
@@ -479,7 +488,7 @@ export const GenericConceptsSidebar = (props: ConceptListSidebarProps) => {
479
488
/>
480
489
{ isSelected && < div className = "ms-3 ps-2" >
481
490
{ descendentTags
482
- . filter ( tag => ! isDefined ( tagCounts ) || tagCounts [ tag . id ] > 0 )
491
+ . filter ( tag => ! isDefined ( tagCounts ) || tagCounts [ tag . id ] > 0 || conceptFilters . includes ( tag ) )
483
492
// .sort((a, b) => tagCounts ? tagCounts[b.id] - tagCounts[a.id] : 0)
484
493
. map ( ( tag , j ) => < FilterCheckbox key = { j }
485
494
checkboxStyle = "button" color = "theme" bsSize = "sm" data-bs-theme = { subject } tag = { tag } conceptFilters = { conceptFilters }
@@ -489,26 +498,20 @@ export const GenericConceptsSidebar = (props: ConceptListSidebarProps) => {
489
498
</ div > }
490
499
</ div > ;
491
500
} ) }
501
+ < div className = "section-divider" />
502
+ < h5 > Filter by stage</ h5 >
503
+ < ul className = "ps-2" >
504
+ { getFilteredStageOptions ( ) . filter ( s => stageCounts [ s . value ] > 0 || searchStages . includes ( s . value ) ) . map ( ( stage ) =>
505
+ < li key = { stage . value } >
506
+ < StyledCheckbox checked = { searchStages . includes ( stage . value ) }
507
+ label = { < > { stage . label } < span className = "text-muted" > ({ stageCounts [ stage . value ] } )</ span > </ > }
508
+ data-bs-theme = { singleSubjectColour }
509
+ color = "theme" onChange = { ( ) => { updateSearchStages ( stage . value ) ; } } />
510
+ </ li > ) }
511
+ </ ul >
492
512
</ div >
493
513
</ search >
494
514
495
- < div className = "section-divider" />
496
-
497
- { pageContext ?. subject && < >
498
- < div className = "section-divider" />
499
-
500
- < div className = "sidebar-help" >
501
- < p > The concepts shown on this page have been filtered to only show those that are relevant to { getHumanContext ( pageContext ) } .</ p >
502
- < p > If you want to explore broader concepts across multiple subjects or learning stages, you can use the main concept browser:</ p >
503
- < AffixButton size = "md" color = "keyline" tag = { Link } to = "/concepts" affix = { {
504
- affix : "icon-right" ,
505
- position : "suffix" ,
506
- type : "icon"
507
- } } >
508
- Browse concepts
509
- </ AffixButton >
510
- </ div >
511
- </ > }
512
515
</ ContentSidebar > ;
513
516
} ;
514
517
@@ -530,7 +533,7 @@ export const QuestionFinderSidebar = (props: QuestionFinderSidebarProps) => {
530
533
return < ContentSidebar { ...rest } >
531
534
< div className = "section-divider" />
532
535
< search >
533
- < h5 > Search Questions </ h5 >
536
+ < h5 > Search questions </ h5 >
534
537
< Input
535
538
className = 'search--filter-input my-4'
536
539
type = "search" value = { internalSearchText || "" }
@@ -543,22 +546,6 @@ export const QuestionFinderSidebar = (props: QuestionFinderSidebarProps) => {
543
546
544
547
< QuestionFinderFilterPanel { ...questionFinderFilterPanelProps } />
545
548
</ search >
546
-
547
- { pageContext ?. subject && pageContext ?. stage && < >
548
- < div className = "section-divider" />
549
-
550
- < div className = "sidebar-help" >
551
- < p > The questions shown here have been filtered to only show those that are relevant to { getHumanContext ( pageContext ) } .</ p >
552
- < p > If you want to explore our full range of questions across multiple subjects or learning stages, you can use the main question finder:</ p >
553
- < AffixButton size = "md" color = "keyline" tag = { Link } to = "/questions" affix = { {
554
- affix : "icon-right" ,
555
- position : "suffix" ,
556
- type : "icon"
557
- } } >
558
- Browse all questions
559
- </ AffixButton >
560
- </ div >
561
- </ > }
562
549
</ ContentSidebar > ;
563
550
} ;
564
551
@@ -662,20 +649,6 @@ export const PracticeQuizzesSidebar = (props: PracticeQuizzesSidebarProps) => {
662
649
</ ul >
663
650
</ > }
664
651
665
- { isFullyDefinedContext ( pageContext ) && < >
666
- < div className = "section-divider" />
667
- < div className = "sidebar-help" >
668
- < p > The practice tests shown here have been filtered to only show those that are relevant to { getHumanContext ( pageContext ) } .</ p >
669
- < p > If you want to explore our full range of practice tests, you can view the main practice tests page:</ p >
670
- < AffixButton size = "md" color = "keyline" tag = { Link } to = "/practice_tests" affix = { {
671
- affix : "icon-right" ,
672
- position : "suffix" ,
673
- type : "icon"
674
- } } >
675
- Browse all practice tests
676
- </ AffixButton >
677
- </ div >
678
- </ > }
679
652
< div className = "section-divider" />
680
653
< div className = "sidebar-help" >
681
654
< p > You can see all of the tests that you have in progress or have completed in your My Isaac:</ p >
0 commit comments