1
- import React , { FormEvent , MutableRefObject , useEffect , useRef , useState } from "react" ;
1
+ import React , { FormEvent , MutableRefObject , useEffect , useMemo , useRef , useState } from "react" ;
2
2
import { RouteComponentProps , withRouter } from "react-router-dom" ;
3
3
import { selectors , useAppSelector } from "../../state" ;
4
4
import { Badge , Card , CardBody , CardHeader , Container } from "reactstrap" ;
5
5
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" ;
7
7
import { generateSubjectLandingPageCrumbFromContext , TitleAndBreadcrumb } from "../elements/TitleAndBreadcrumb" ;
8
8
import { ShortcutResponse , Tag } from "../../../IsaacAppTypes" ;
9
9
import { IsaacSpinner } from "../handlers/IsaacSpinner" ;
@@ -14,41 +14,72 @@ import { isFullyDefinedContext, useUrlPageTheme } from "../../services/pageConte
14
14
import { useListConceptsQuery } from "../../state/slices/api/conceptsApi" ;
15
15
import { ShowLoadingQuery } from "../handlers/ShowLoadingQuery" ;
16
16
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
+ } ;
17
25
18
26
// This component is Isaac Physics only (currently)
19
27
export const Concepts = withRouter ( ( props : RouteComponentProps ) => {
20
28
const { location, history} = props ;
21
29
const user = useAppSelector ( selectors . user . orNull ) ;
22
30
const pageContext = useUrlPageTheme ( ) ;
23
31
24
- const listConceptsQuery = useListConceptsQuery ( { conceptIds : undefined , tagIds : pageContext ?. subject } ) ;
32
+ const searchParsed = queryString . parse ( location . search , { arrayFormat : "comma" } ) ;
25
33
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 ;
32
37
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 ] ) ;
36
42
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 ] ;
38
47
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 [ ] > ( ) ;
41
53
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
+ ) ;
44
58
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 || [ ] ) ;
50
71
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
+ } ) , { } ) ;
52
83
53
84
function doSearch ( e ?: FormEvent < HTMLFormElement > ) {
54
85
if ( e ) {
@@ -73,22 +104,6 @@ export const Concepts = withRouter((props: RouteComponentProps) => {
73
104
74
105
useEffect ( ( ) => { doSearch ( ) ; } , [ conceptFilters ] ) ;
75
106
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
-
92
107
const crumb = isPhy && isFullyDefinedContext ( pageContext ) && generateSubjectLandingPageCrumbFromContext ( pageContext ) ;
93
108
94
109
const sidebarProps = { searchText, setSearchText, conceptFilters, setConceptFilters, applicableTags, tagCounts} ;
0 commit comments