@@ -3,20 +3,20 @@ import { Col, ColProps, RowProps, Input, Offcanvas, OffcanvasBody, OffcanvasHead
3
3
import partition from "lodash/partition" ;
4
4
import classNames from "classnames" ;
5
5
import { AssignmentDTO , ContentSummaryDTO , IsaacConceptPageDTO , QuestionDTO , QuizAttemptDTO , RegisteredUserDTO } from "../../../../IsaacApiTypes" ;
6
- import { above , ACCOUNT_TAB , ACCOUNT_TABS , AUDIENCE_DISPLAY_FIELDS , below , BOARD_ORDER_NAMES , BoardCompletions , BoardCreators , BoardLimit , BoardSubjects , BoardViews , confirmThen , determineAudienceViews , filterAssignmentsByStatus , filterAudienceViewsByProperties , getDistinctAssignmentGroups , getDistinctAssignmentSetters , getThemeFromContextAndTags , HUMAN_STAGES , ifKeyIsEnter , isAda , isDefined , siteSpecific , useDeviceSize } from "../../../services" ;
6
+ import { above , ACCOUNT_TAB , ACCOUNT_TABS , AUDIENCE_DISPLAY_FIELDS , below , BOARD_ORDER_NAMES , BoardCompletions , BoardCreators , BoardLimit , BoardSubjects , BoardViews , confirmThen , determineAudienceViews , filterAssignmentsByStatus , filterAudienceViewsByProperties , getDistinctAssignmentGroups , getDistinctAssignmentSetters , getHumanContext , getThemeFromContextAndTags , HUMAN_STAGES , ifKeyIsEnter , isAda , isDefined , PHY_NAV_SUBJECTS , siteSpecific , TAG_ID , tags , useDeviceSize } from "../../../services" ;
7
7
import { StageAndDifficultySummaryIcons } from "../StageAndDifficultySummaryIcons" ;
8
8
import { selectors , useAppSelector } from "../../../state" ;
9
9
import { Link , useHistory } from "react-router-dom" ;
10
10
import { AppGroup , AssignmentBoardOrder , Tag } from "../../../../IsaacAppTypes" ;
11
11
import { AffixButton } from "../AffixButton" ;
12
- import { getHumanContext } from "../../../services/pageContext" ;
13
12
import { QuestionFinderFilterPanel , QuestionFinderFilterPanelProps } from "../panels/QuestionFinderFilterPanel" ;
14
13
import { AssignmentState } from "../../pages/MyAssignments" ;
15
14
import { ShowLoadingQuery } from "../../handlers/ShowLoadingQuery" ;
16
15
import { Spacer } from "../Spacer" ;
17
16
import { StyledTabPicker } from "../inputs/StyledTabPicker" ;
18
17
import { GroupSelector } from "../../pages/Groups" ;
19
18
import { QuizRubricButton , SectionProgress } from "../quiz/QuizAttemptComponent" ;
19
+ import { StyledCheckbox } from "../inputs/StyledCheckbox" ;
20
20
import { formatISODateOnly } from "../DateString" ;
21
21
22
22
export const SidebarLayout = ( props : RowProps ) => {
@@ -29,11 +29,12 @@ export const MainContent = (props: ColProps) => {
29
29
return siteSpecific ( < Col xs = { 12 } lg = { 8 } xl = { 9 } { ...rest } className = { classNames ( className , "order-0 order-lg-1" ) } /> , props . children ) ;
30
30
} ;
31
31
32
- const QuestionLink = ( props : React . HTMLAttributes < HTMLLIElement > & { question : QuestionDTO , sidebarRef : RefObject < HTMLDivElement > } ) => {
33
- const { question, sidebarRef, ...rest } = props ;
32
+ const QuestionLink = ( props : React . HTMLAttributes < HTMLLIElement > & { question : QuestionDTO } ) => {
33
+ const { question, ...rest } = props ;
34
+ const subject = useAppSelector ( selectors . pageContext . subject ) ;
34
35
const audienceFields = filterAudienceViewsByProperties ( determineAudienceViews ( question . audience ) , AUDIENCE_DISPLAY_FIELDS ) ;
35
36
36
- return < li key = { question . id } { ...rest } data-bs-theme = { getThemeFromContextAndTags ( sidebarRef , question . tags ?? [ ] ) } >
37
+ return < li key = { question . id } { ...rest } data-bs-theme = { getThemeFromContextAndTags ( subject , question . tags ?? [ ] ) } >
37
38
< Link to = { `/questions/${ question . id } ` } className = "py-2" >
38
39
< i className = "icon icon-question" />
39
40
< div className = "d-flex flex-column w-100" >
@@ -44,10 +45,11 @@ const QuestionLink = (props: React.HTMLAttributes<HTMLLIElement> & {question: Qu
44
45
</ li > ;
45
46
} ;
46
47
47
- const ConceptLink = ( props : React . HTMLAttributes < HTMLLIElement > & { concept : IsaacConceptPageDTO , sidebarRef : RefObject < HTMLDivElement > } ) => {
48
- const { concept, sidebarRef, ...rest } = props ;
48
+ const ConceptLink = ( props : React . HTMLAttributes < HTMLLIElement > & { concept : IsaacConceptPageDTO } ) => {
49
+ const { concept, ...rest } = props ;
50
+ const subject = useAppSelector ( selectors . pageContext . subject ) ;
49
51
50
- return < li key = { concept . id } { ...rest } data-bs-theme = { getThemeFromContextAndTags ( sidebarRef , concept . tags ?? [ ] ) } >
52
+ return < li key = { concept . id } { ...rest } data-bs-theme = { getThemeFromContextAndTags ( subject , concept . tags ?? [ ] ) } >
51
53
< Link to = { `/concepts/${ concept . id } ` } className = "py-2" >
52
54
< i className = "icon icon-lightbulb" />
53
55
< span className = "hover-underline link-title" > { concept . title } </ span >
@@ -141,7 +143,7 @@ export const QuestionSidebar = (props: QuestionSidebarProps) => {
141
143
< div className = "section-divider" />
142
144
< h5 > Related concepts</ h5 >
143
145
< ul className = "link-list" >
144
- { relatedConcepts . map ( ( concept , i ) => < ConceptLink key = { i } concept = { concept } sidebarRef = { sidebarRef } /> ) }
146
+ { relatedConcepts . map ( ( concept , i ) => < ConceptLink key = { i } concept = { concept } /> ) }
145
147
</ ul >
146
148
</ > }
147
149
{ relatedQuestions && relatedQuestions . length > 0 && < >
@@ -150,19 +152,19 @@ export const QuestionSidebar = (props: QuestionSidebarProps) => {
150
152
< div className = "section-divider" />
151
153
< h5 > Related questions</ h5 >
152
154
< ul className = "link-list" >
153
- { relatedQuestions . map ( ( question , i ) => < QuestionLink key = { i } sidebarRef = { sidebarRef } question = { question } /> ) }
155
+ { relatedQuestions . map ( ( question , i ) => < QuestionLink key = { i } question = { question } /> ) }
154
156
</ ul >
155
157
</ >
156
158
: < >
157
159
< div className = "section-divider" />
158
160
< h5 > Related { HUMAN_STAGES [ pageContextStage [ 0 ] ] } questions</ h5 >
159
161
< ul className = "link-list" >
160
- { relatedQuestionsForContextStage . map ( ( question , i ) => < QuestionLink key = { i } sidebarRef = { sidebarRef } question = { question } /> ) }
162
+ { relatedQuestionsForContextStage . map ( ( question , i ) => < QuestionLink key = { i } question = { question } /> ) }
161
163
</ ul >
162
164
< div className = "section-divider" />
163
165
< h5 > Related questions for other learning stages</ h5 >
164
166
< ul className = "link-list" >
165
- { relatedQuestionsForOtherStages . map ( ( question , i ) => < QuestionLink key = { i } sidebarRef = { sidebarRef } question = { question } /> ) }
167
+ { relatedQuestionsForOtherStages . map ( ( question , i ) => < QuestionLink key = { i } question = { question } /> ) }
166
168
</ ul >
167
169
</ >
168
170
}
@@ -186,36 +188,53 @@ export const ConceptSidebar = (props: QuestionSidebarProps) => {
186
188
187
189
188
190
189
- interface FilterCheckboxProps extends React . HTMLAttributes < HTMLLabelElement > {
191
+ interface FilterCheckboxProps extends React . HTMLAttributes < HTMLElement > {
190
192
tag : Tag ;
191
193
conceptFilters : Tag [ ] ;
192
194
setConceptFilters : React . Dispatch < React . SetStateAction < Tag [ ] > > ;
193
195
tagCounts ?: Record < string , number > ;
196
+ incompatibleTags ?: Tag [ ] ; // tags that are removed when this tag is added
197
+ dependentTags ?: Tag [ ] ; // tags that are removed when this tag is removed
198
+ baseTag ?: Tag ; // tag to add when all tags are removed
199
+ checkboxStyle ?: "tab" | "button" ;
200
+ bsSize ?: "sm" | "lg" ;
194
201
}
195
202
196
203
const FilterCheckbox = ( props : FilterCheckboxProps ) => {
197
- const { tag, conceptFilters, setConceptFilters, tagCounts, ...rest } = props ;
204
+ const { tag, conceptFilters, setConceptFilters, tagCounts, checkboxStyle , incompatibleTags , dependentTags , baseTag , ...rest } = props ;
198
205
const [ checked , setChecked ] = useState ( conceptFilters . includes ( tag ) ) ;
199
206
200
207
useEffect ( ( ) => {
201
208
setChecked ( conceptFilters . includes ( tag ) ) ;
202
209
} , [ conceptFilters , tag ] ) ;
203
210
204
- return < StyledTabPicker { ...rest } id = { tag . id } checked = { checked }
205
- onInputChange = { ( e : ChangeEvent < HTMLInputElement > ) => setConceptFilters ( f => e . target . checked ? [ ...f , tag ] : f . filter ( c => c !== tag ) ) }
206
- checkboxTitle = { tag . title } count = { tagCounts && isDefined ( tagCounts [ tag . id ] ) ? tagCounts [ tag . id ] : undefined }
207
- /> ;
211
+ const handleCheckboxChange = ( checked : boolean ) => {
212
+ const newConceptFilters = checked
213
+ ? [ ...conceptFilters . filter ( c => ! incompatibleTags ?. includes ( c ) ) , tag ]
214
+ : conceptFilters . filter ( c => ! [ tag , ...( dependentTags ?? [ ] ) ] . includes ( c ) ) ;
215
+ setConceptFilters ( newConceptFilters . length > 0 ? newConceptFilters : ( baseTag ? [ baseTag ] : [ ] ) ) ;
216
+ } ;
217
+
218
+ return checkboxStyle === "button"
219
+ ? < StyledCheckbox { ...rest } id = { tag . id } checked = { checked }
220
+ onChange = { ( e : ChangeEvent < HTMLInputElement > ) => handleCheckboxChange ( e . target . checked ) }
221
+ label = { < span > { tag . title } { tagCounts && isDefined ( tagCounts [ tag . id ] ) && < span className = "text-muted" > ({ tagCounts [ tag . id ] } )</ span > } </ span > }
222
+ />
223
+ : < StyledTabPicker { ...rest } id = { tag . id } checked = { checked }
224
+ onInputChange = { ( e : ChangeEvent < HTMLInputElement > ) => handleCheckboxChange ( e . target . checked ) }
225
+ checkboxTitle = { tag . title } count = { tagCounts && isDefined ( tagCounts [ tag . id ] ) ? tagCounts [ tag . id ] : undefined }
226
+ /> ;
208
227
} ;
209
228
210
229
const AllFiltersCheckbox = ( props : Omit < FilterCheckboxProps , "tag" > ) => {
211
- const { conceptFilters, setConceptFilters, tagCounts, ...rest } = props ;
212
- const [ previousFilters , setPreviousFilters ] = useState < Tag [ ] > ( [ ] ) ;
230
+ const { conceptFilters, setConceptFilters, tagCounts, baseTag , ...rest } = props ;
231
+ const [ previousFilters , setPreviousFilters ] = useState < Tag [ ] > ( baseTag ? [ baseTag ] : [ ] ) ;
213
232
return < StyledTabPicker { ...rest }
214
- id = "all" checked = { ! conceptFilters . length } checkboxTitle = "All" count = { tagCounts && Object . values ( tagCounts ) . reduce ( ( a , b ) => a + b , 0 ) }
233
+ id = "all" checked = { baseTag ? conceptFilters . length === 1 && conceptFilters [ 0 ] === baseTag : ! conceptFilters . length } checkboxTitle = "All" count = { tagCounts && Object . values ( tagCounts ) . reduce ( ( a , b ) => a + b , 0 ) }
215
234
onInputChange = { ( e ) => {
216
235
if ( e . target . checked ) {
217
236
setPreviousFilters ( conceptFilters ) ;
218
- setConceptFilters ( [ ] ) ;
237
+ setConceptFilters ( baseTag ? [ baseTag ] : [ ] ) ;
219
238
} else {
220
239
setConceptFilters ( previousFilters ) ;
221
240
}
@@ -237,6 +256,8 @@ export const SubjectSpecificConceptListSidebar = (props: ConceptListSidebarProps
237
256
238
257
const pageContext = useAppSelector ( selectors . pageContext . context ) ;
239
258
259
+ const subjectTag = tags . getById ( pageContext ?. subject as TAG_ID ) ;
260
+
240
261
return < ContentSidebar { ...rest } >
241
262
< div className = "section-divider" />
242
263
< h5 > Search concepts</ h5 >
@@ -251,9 +272,19 @@ export const SubjectSpecificConceptListSidebar = (props: ConceptListSidebarProps
251
272
252
273
< div className = "d-flex flex-column" >
253
274
< h5 > Filter by topic</ h5 >
254
- < AllFiltersCheckbox conceptFilters = { conceptFilters } setConceptFilters = { setConceptFilters } tagCounts = { tagCounts } />
275
+ < AllFiltersCheckbox conceptFilters = { conceptFilters } setConceptFilters = { setConceptFilters } tagCounts = { tagCounts } baseTag = { subjectTag } />
255
276
< div className = "section-divider-small" />
256
- { applicableTags . map ( tag => < FilterCheckbox key = { tag . id } tag = { tag } conceptFilters = { conceptFilters } setConceptFilters = { setConceptFilters } tagCounts = { tagCounts } /> ) }
277
+ { applicableTags . map ( tag =>
278
+ < FilterCheckbox
279
+ key = { tag . id }
280
+ tag = { tag }
281
+ conceptFilters = { conceptFilters }
282
+ setConceptFilters = { setConceptFilters }
283
+ tagCounts = { tagCounts }
284
+ incompatibleTags = { [ subjectTag ] }
285
+ baseTag = { subjectTag }
286
+ />
287
+ ) }
257
288
</ div >
258
289
259
290
< div className = "section-divider" />
@@ -272,9 +303,68 @@ export const SubjectSpecificConceptListSidebar = (props: ConceptListSidebarProps
272
303
</ ContentSidebar > ;
273
304
} ;
274
305
275
- export const GenericConceptsSidebar = ( props : SidebarProps ) => {
276
- // TODO
277
- return < ContentSidebar { ...props } /> ;
306
+ export const GenericConceptsSidebar = ( props : ConceptListSidebarProps ) => {
307
+ const { searchText, setSearchText, conceptFilters, setConceptFilters, applicableTags, tagCounts, ...rest } = props ;
308
+
309
+ const pageContext = useAppSelector ( selectors . pageContext . context ) ;
310
+
311
+ return < ContentSidebar { ...rest } >
312
+ < div className = "section-divider" />
313
+ < h5 > Search concepts</ h5 >
314
+ < Input
315
+ className = 'search--filter-input my-4'
316
+ type = "search" value = { searchText || "" }
317
+ placeholder = "e.g. Forces"
318
+ onChange = { ( e : ChangeEvent < HTMLInputElement > ) => setSearchText ( e . target . value ) }
319
+ />
320
+
321
+ < div className = "section-divider" />
322
+
323
+ < div className = "d-flex flex-column" >
324
+ < h5 > Filter by subject</ h5 >
325
+ { Object . keys ( PHY_NAV_SUBJECTS ) . map ( ( subject , i ) => {
326
+ const subjectTag = tags . getById ( subject as TAG_ID ) ;
327
+ const descendentTags = tags . getDirectDescendents ( subjectTag . id ) ;
328
+ const isSelected = conceptFilters . includes ( subjectTag ) || descendentTags . some ( tag => conceptFilters . includes ( tag ) ) ;
329
+ const isPartial = descendentTags . some ( tag => conceptFilters . includes ( tag ) ) && descendentTags . some ( tag => ! conceptFilters . includes ( tag ) ) ;
330
+ return < div key = { i } className = { classNames ( "ps-2" , { "checkbox-region" : isSelected } ) } >
331
+ < FilterCheckbox
332
+ checkboxStyle = "button" color = "theme" data-bs-theme = { subject } tag = { subjectTag } conceptFilters = { conceptFilters }
333
+ setConceptFilters = { setConceptFilters } tagCounts = { tagCounts } dependentTags = { descendentTags } incompatibleTags = { descendentTags }
334
+ className = { classNames ( { "icon-checkbox-off" : ! isSelected , "icon icon-checkbox-partial-alt" : isSelected && isPartial , "icon-checkbox-selected" : isSelected && ! isPartial } ) }
335
+ />
336
+ { isSelected && < div className = "ms-3 ps-2" >
337
+ { descendentTags
338
+ . filter ( tag => ! isDefined ( tagCounts ) || tagCounts [ tag . id ] > 0 )
339
+ // .sort((a, b) => tagCounts ? tagCounts[b.id] - tagCounts[a.id] : 0)
340
+ . map ( ( tag , j ) => < FilterCheckbox key = { j }
341
+ checkboxStyle = "button" color = "theme" bsSize = "sm" data-bs-theme = { subject } tag = { tag } conceptFilters = { conceptFilters }
342
+ setConceptFilters = { setConceptFilters } tagCounts = { tagCounts } incompatibleTags = { [ subjectTag ] }
343
+ /> )
344
+ }
345
+ </ div > }
346
+ </ div > ;
347
+ } ) }
348
+ </ div >
349
+
350
+ < div className = "section-divider" />
351
+
352
+ { pageContext ?. subject && < >
353
+ < div className = "section-divider" />
354
+
355
+ < div className = "sidebar-help" >
356
+ < p > The concepts shown on this page have been filtered to only show those that are relevant to { getHumanContext ( pageContext ) } .</ p >
357
+ < p > If you want to explore broader concepts across multiple subjects or learning stages, you can use the main concept browser:</ p >
358
+ < AffixButton size = "md" color = "keyline" tag = { Link } to = "/concepts" affix = { {
359
+ affix : "icon-right" ,
360
+ position : "suffix" ,
361
+ type : "icon"
362
+ } } >
363
+ Browse concepts
364
+ </ AffixButton >
365
+ </ div >
366
+ </ > }
367
+ </ ContentSidebar > ;
278
368
} ;
279
369
280
370
interface QuestionFinderSidebarProps extends SidebarProps {
0 commit comments