@@ -7,12 +7,31 @@ import {
7
7
SOURCE_CONTAINER
8
8
} from "../constants" ;
9
9
import { getContainerIdFromUrl } from "../utils" ;
10
- import { Button , Menu , MenuItem , TextField , Theme } from "@mui/material" ;
10
+ import {
11
+ Button ,
12
+ FormControlLabel ,
13
+ Grid ,
14
+ IconButton ,
15
+ Input ,
16
+ InputAdornment ,
17
+ Menu ,
18
+ MenuItem ,
19
+ Switch ,
20
+ Theme
21
+ } from "@mui/material" ;
11
22
import { PREFERRED_SOURCES_VIEW_ONLY , useAnchor } from "../../../utils" ;
12
23
import { APISource } from "../../sources" ;
13
- import { AccountTreeOutlined , FolderOpen } from "@mui/icons-material" ;
24
+ import {
25
+ AccountTreeOutlined ,
26
+ FolderOpen ,
27
+ Search as SearchIcon
28
+ } from "@mui/icons-material" ;
14
29
import { APIDictionary } from "../../dictionaries/types" ;
15
30
import { createStyles , makeStyles } from "@mui/styles" ;
31
+ import InfiniteScroll from "react-infinite-scroll-component" ;
32
+ import { partialRight , pick } from "lodash" ;
33
+ import dictionaryApi from "../../dictionaries/api" ;
34
+ import sourceApi from "../../sources/api" ;
16
35
17
36
interface Props {
18
37
containerType : string ;
@@ -22,6 +41,23 @@ interface Props {
22
41
children ?: React . ReactNode [ ] ;
23
42
sources : APISource [ ] ;
24
43
dictionaries : APIDictionary [ ] ;
44
+ showOnlyVerified : boolean ;
45
+ toggleShowVerified : React . ChangeEventHandler < HTMLInputElement > ;
46
+ goTo : Function ;
47
+ initialSearch : string ;
48
+ pathUrl : Function ;
49
+ dictionaryMeta ?: {
50
+ num_found ?: number ;
51
+ page_number ?: number ;
52
+ pages ?: number ;
53
+ num_returned ?: number ;
54
+ } ;
55
+ sourcesMeta ?: {
56
+ num_found ?: number ;
57
+ page_number ?: number ;
58
+ pages ?: number ;
59
+ num_returned ?: number ;
60
+ } ;
25
61
}
26
62
27
63
const useStyles = makeStyles ( ( theme : Theme ) =>
@@ -33,6 +69,9 @@ const useStyles = makeStyles((theme: Theme) =>
33
69
padding : "0.2rem 1rem" ,
34
70
cursor : "none"
35
71
} ,
72
+ sourcesDropdownHeader : {
73
+ padding : "0.5rem 1rem"
74
+ } ,
36
75
input : {
37
76
cursor : "pointer" ,
38
77
borderBottom : "1px dotted black" ,
@@ -48,37 +87,97 @@ const useStyles = makeStyles((theme: Theme) =>
48
87
} ,
49
88
sourceIcon : {
50
89
marginRight : "0.2rem" ,
51
- fill : "#8080809c" ,
52
- color :"#000000ad"
90
+ fill : "#8080809c"
91
+ } ,
92
+ searchInput : {
93
+ textAlign : "center" ,
94
+ fontSize : "larger"
53
95
}
54
96
} )
55
97
) ;
56
-
57
98
const ViewConceptsHeader : React . FC < Props > = ( {
58
99
containerType,
59
100
containerUrl,
60
101
gimmeAUrl,
61
102
addConceptToDictionary,
62
103
children,
63
104
sources,
64
- dictionaries
105
+ dictionaries,
106
+ goTo,
107
+ initialSearch,
108
+ pathUrl
65
109
} ) => {
66
- const [ showSources , setShowSources ] = useState ( false ) ;
67
- const [ preferredSources , setPreferredSources ] = useState <
110
+ const [ showAllSources , setShowAllSources ] = useState ( false ) ;
111
+ const [ queryString , setQueryString ] = useState ( initialSearch ) ;
112
+ const [ currentSources , setCurrentSources ] = useState <
68
113
{ name : string ; url : string } [ ]
69
114
> ( ) ;
115
+ const [ useSources , setUseSources ] = useState ( true ) ;
116
+ const [ currentPage , setCurrentPage ] = useState ( 1 ) ;
117
+ const [ apiMethod , setApiMethod ] = useState <
118
+ | typeof sourceApi . sources . retrieve . private
119
+ | typeof dictionaryApi . dictionaries . retrieve . private
120
+ > ( sourceApi . sources . retrieve . private ) ;
121
+ const [ totalResultCount , setTotalResultCount ] = useState ( 0 ) ;
122
+ const [ currentResultCount , setCurrentResultCount ] = useState ( 0 ) ;
123
+ const [ resultsLoadedCount , setResultsLoadedCount ] = useState ( 0 ) ;
124
+
70
125
useEffect ( ( ) => {
71
126
const defaultSources = Object . entries (
72
127
PREFERRED_SOURCES_VIEW_ONLY
73
128
) . map ( ( [ key , value ] ) => ( { name : key , url : value } ) ) ;
74
- if ( showSources ) {
129
+ if ( showAllSources ) {
75
130
const allSources = defaultSources
76
- . concat ( sources . map ( s => ( { name : s . name , url : s . url } ) ) )
77
- . concat ( dictionaries . map ( d => ( { name : d . name , url : d . url } ) ) ) ;
78
- setPreferredSources ( allSources ) ;
79
- } else setPreferredSources ( defaultSources ) ;
80
- } , [ showSources , sources , dictionaries ] ) ;
131
+ . concat ( sources . map ( ( s ) => ( { name : s . name , url : s . url } ) ) )
132
+ . concat ( dictionaries . map ( ( d ) => ( { name : d . name , url : d . url } ) ) ) ;
133
+ setCurrentSources ( allSources ) ;
134
+ } else setCurrentSources ( defaultSources ) ;
135
+ } , [ showAllSources , sources , dictionaries , initialSearch ] ) ;
136
+
137
+ useEffect ( ( ) => setCurrentPage ( 0 ) , [ useSources ] ) ;
138
+ useEffect (
139
+ ( ) =>
140
+ useSources
141
+ ? setApiMethod ( sourceApi . sources . retrieve . private )
142
+ : setApiMethod ( dictionaryApi . dictionaries . retrieve . private ) ,
143
+ [ useSources ]
144
+ ) ;
145
+
146
+ const fetchMoreData = ( page : number ) => {
147
+ setCurrentPage ( page ) ;
148
+ apiMethod (
149
+ useSources ? "/sources/" : "/collections/" ,
150
+ "" ,
151
+ 25 ,
152
+ currentPage
153
+ ) . then ( ( results ) => {
154
+ const sources = results . data . map ( partialRight ( pick , "name" , "url" ) ) as {
155
+ name : string ;
156
+ url : string ;
157
+ } [ ] ;
81
158
159
+ if ( currentPage === 1 ) {
160
+ setTotalResultCount ( results . headers . num_found ) ;
161
+ }
162
+
163
+ setCurrentResultCount ( results . headers . num_returned ) ;
164
+ setResultsLoadedCount (
165
+ results . headers . offset + results . headers . num_returned
166
+ ) ;
167
+
168
+ const existingSources = currentSources ? currentSources : [ ] ;
169
+ setCurrentSources ( [ ...existingSources , ...sources ] ) ;
170
+ } ) ;
171
+ } ;
172
+
173
+ const loadData = ( ) => {
174
+ fetchMoreData ( currentPage + 1 ) ;
175
+ } ;
176
+
177
+ const handleSearch = ( q : string ) => goTo ( pathUrl ( { q } ) ) ;
178
+ const handleShowSources = ( event : React . ChangeEvent < HTMLInputElement > ) => {
179
+ setShowAllSources ( event . target . checked ) ;
180
+ } ;
82
181
const classes = useStyles ( ) ;
83
182
const isSourceContainer = containerType === SOURCE_CONTAINER ;
84
183
const isAddToDictionary = isSourceContainer && ! ! addConceptToDictionary ;
@@ -87,7 +186,6 @@ const ViewConceptsHeader: React.FC<Props> = ({
87
186
handleSwitchSourceClick ,
88
187
handleSwitchSourceClose
89
188
] = useAnchor ( ) ;
90
-
91
189
const getTitleBasedOnContainerType = ( ) => {
92
190
return isAddToDictionary
93
191
? `Import existing concept from ${ getContainerIdFromUrl ( containerUrl ) } `
@@ -118,48 +216,103 @@ const ViewConceptsHeader: React.FC<Props> = ({
118
216
} }
119
217
anchorEl = { switchSourceAnchor }
120
218
keepMounted
121
- open = { Boolean ( switchSourceAnchor ) }
219
+ open = { ! ! switchSourceAnchor }
122
220
onClose = { handleSwitchSourceClose }
123
221
>
124
- < TextField
125
- multiline
126
- className = { classes . textField }
127
- InputProps = { {
128
- className : classes . underline
129
- } }
130
- inputProps = { {
131
- className : classes . input
132
- } }
133
- value = {
134
- showSources
135
- ? "Choose a source/dictionary"
136
- : "Select a different source/dictionary"
137
- }
138
- onClick = { ( ) => setShowSources ( ! showSources ) }
139
- />
140
- { preferredSources ?. map ( ( { name, url } ) => (
141
- < MenuItem
142
- // replace because we want to keep the back button useful
143
- replace
144
- to = { gimmeAUrl ( { } , `${ url } concepts/` ) }
145
- key = { name }
146
- component = { Link }
147
- onClick = { handleSwitchSourceClose }
148
- data-testid = { name }
222
+ < Grid
223
+ container
224
+ direction = "column"
225
+ className = { classes . sourcesDropdownHeader }
226
+ >
227
+ < FormControlLabel
228
+ control = {
229
+ < Switch
230
+ checked = { showAllSources }
231
+ onChange = { handleShowSources }
232
+ color = "primary"
233
+ name = "displayVerified"
234
+ />
235
+ }
236
+ label = {
237
+ showAllSources ? `Showing all Sources` : `Show all Sources`
238
+ }
239
+ />
240
+ { showAllSources && (
241
+ < form
242
+ onSubmit = { ( e : React . SyntheticEvent ) => {
243
+ e . preventDefault ( ) ;
244
+ handleSearch ( queryString ) ;
245
+ } }
246
+ >
247
+ < Input
248
+ color = "primary"
249
+ type = "search"
250
+ fullWidth
251
+ placeholder = { "Select an alternative source" }
252
+ value = { queryString }
253
+ onChange = { ( e ) => setQueryString ( e . target . value ) }
254
+ endAdornment = {
255
+ < InputAdornment position = "end" >
256
+ < IconButton onClick = { ( ) => handleSearch ( queryString ) } >
257
+ < SearchIcon />
258
+ </ IconButton >
259
+ </ InputAdornment >
260
+ }
261
+ />
262
+ </ form >
263
+ ) }
264
+ </ Grid >
265
+ { showAllSources ? (
266
+ < InfiniteScroll
267
+ dataLength = { currentResultCount }
268
+ next = { loadData }
269
+ hasMore = { resultsLoadedCount < totalResultCount }
270
+ loader = { < h4 > Loading...</ h4 > }
271
+ endMessage = { < h4 > end</ h4 > }
272
+ scrollableTarget = "scrollableDiv"
149
273
>
150
- { url ?. includes ( "/collection" ) ? (
151
- < FolderOpen className = { classes . sourceIcon } />
152
- ) : (
153
- < AccountTreeOutlined className = { classes . sourceIcon } />
154
- ) }
155
- { name }
156
- </ MenuItem >
157
- ) ) }
274
+ { currentSources ?. map ( ( { name, url } ) => (
275
+ < MenuItem
276
+ // replace because we want to keep the back button useful
277
+ replace
278
+ to = { gimmeAUrl ( { } , `${ url } concepts/` ) }
279
+ key = { name }
280
+ component = { Link }
281
+ onClick = { handleSwitchSourceClose }
282
+ >
283
+ { url ?. includes ( "/collection" ) ? (
284
+ < FolderOpen className = { classes . sourceIcon } />
285
+ ) : (
286
+ < AccountTreeOutlined className = { classes . sourceIcon } />
287
+ ) }
288
+ { name }
289
+ </ MenuItem >
290
+ ) ) }
291
+ </ InfiniteScroll >
292
+ ) : (
293
+ currentSources ?. map ( ( { name, url } ) => (
294
+ < MenuItem
295
+ // replace because we want to keep the back button useful
296
+ replace
297
+ to = { gimmeAUrl ( { } , `${ url } concepts/` ) }
298
+ key = { name }
299
+ component = { Link }
300
+ onClick = { handleSwitchSourceClose }
301
+ data-testid = { name }
302
+ >
303
+ { url ?. includes ( "/collection" ) ? (
304
+ < FolderOpen className = { classes . sourceIcon } />
305
+ ) : (
306
+ < AccountTreeOutlined className = { classes . sourceIcon } />
307
+ ) }
308
+ { name }
309
+ </ MenuItem >
310
+ ) )
311
+ ) }
158
312
</ Menu >
159
313
</ >
160
314
) ;
161
315
} ;
162
-
163
316
return (
164
317
< Header
165
318
title = { getTitleBasedOnContainerType ( ) }
@@ -177,5 +330,4 @@ const ViewConceptsHeader: React.FC<Props> = ({
177
330
</ Header >
178
331
) ;
179
332
} ;
180
-
181
333
export default ViewConceptsHeader ;
0 commit comments