|
1 |
| -import React, { ChangeEvent, RefObject, useEffect, useRef, useState } from "react"; |
| 1 | +import React, { ChangeEvent, Dispatch, RefObject, SetStateAction, useEffect, 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";
|
5 | 5 | import { AssignmentDTO, ContentSummaryDTO, GameboardDTO, GameboardItem, IsaacBookIndexPageDTO, IsaacConceptPageDTO, QuestionDTO, QuizAssignmentDTO, QuizAttemptDTO, RegisteredUserDTO, Stage } from "../../../../IsaacApiTypes";
|
6 |
| -import { above, ACCOUNT_TAB, ACCOUNT_TABS, AUDIENCE_DISPLAY_FIELDS, below, BOARD_ORDER_NAMES, BoardCompletions, BoardCreators, BoardLimit, BoardSubjects, BoardViews, confirmThen, determineAudienceViews, EventStageMap, EventStatusFilter, EventTypeFilter, filterAssignmentsByStatus, filterAudienceViewsByProperties, getDistinctAssignmentGroups, getDistinctAssignmentSetters, getHumanContext, getThemeFromContextAndTags, HUMAN_STAGES, ifKeyIsEnter, isAda, isDefined, PHY_NAV_SUBJECTS, isTeacherOrAbove, QuizStatus, siteSpecific, TAG_ID, tags, STAGE, useDeviceSize, LearningStage, HUMAN_SUBJECTS, ArrayElement, isFullyDefinedContext, isSingleStageContext, Item, stageLabelMap, extractTeacherName, determineGameboardSubjects, PATHS, getQuestionPlaceholder } from "../../../services"; |
| 6 | +import { above, ACCOUNT_TAB, ACCOUNT_TABS, AUDIENCE_DISPLAY_FIELDS, below, BOARD_ORDER_NAMES, BoardCompletions, BoardCreators, BoardLimit, BoardSubjects, BoardViews, confirmThen, determineAudienceViews, EventStageMap, |
| 7 | + EventStatusFilter, EventTypeFilter, filterAssignmentsByStatus, filterAudienceViewsByProperties, getDistinctAssignmentGroups, getDistinctAssignmentSetters, getHumanContext, getThemeFromContextAndTags, HUMAN_STAGES, |
| 8 | + ifKeyIsEnter, isAda, isDefined, PHY_NAV_SUBJECTS, isTeacherOrAbove, QuizStatus, siteSpecific, TAG_ID, tags, STAGE, useDeviceSize, LearningStage, HUMAN_SUBJECTS, ArrayElement, isFullyDefinedContext, isSingleStageContext, |
| 9 | + Item, stageLabelMap, extractTeacherName, determineGameboardSubjects, PATHS, getQuestionPlaceholder, getFilteredStageOptions } from "../../../services"; |
7 | 10 | import { StageAndDifficultySummaryIcons } from "../StageAndDifficultySummaryIcons";
|
8 | 11 | import { selectors, useAppSelector, useGetQuizAssignmentsAssignedToMeQuery } from "../../../state";
|
9 | 12 | import { Link, useHistory, useLocation } from "react-router-dom";
|
@@ -541,9 +544,132 @@ export const QuestionFinderSidebar = (props: QuestionFinderSidebarProps) => {
|
541 | 544 | </ContentSidebar>;
|
542 | 545 | };
|
543 | 546 |
|
544 |
| -export const PracticeQuizzesSidebar = (props: SidebarProps) => { |
545 |
| - // TODO |
546 |
| - return <ContentSidebar {...props}/>; |
| 547 | +interface PracticeQuizzesSidebarProps extends SidebarProps { |
| 548 | + filterText: string; |
| 549 | + setFilterText: Dispatch<SetStateAction<string>>; |
| 550 | + filterTags?: Tag[]; |
| 551 | + setFilterTags: Dispatch<SetStateAction<Tag[]>>; |
| 552 | + tagCounts: Record<string, number>; |
| 553 | + filterStages?: Stage[]; |
| 554 | + setFilterStages: Dispatch<SetStateAction<Stage[] | undefined>>; |
| 555 | + stageCounts: Record<string, number>; |
| 556 | +} |
| 557 | + |
| 558 | +export const PracticeQuizzesSidebar = (props: PracticeQuizzesSidebarProps) => { |
| 559 | + const { filterText, setFilterText, filterTags, setFilterTags, tagCounts, filterStages, setFilterStages, stageCounts, ...rest } = props; |
| 560 | + const pageContext = useAppSelector(selectors.pageContext.context); |
| 561 | + const fields = pageContext?.subject ? tags.getDirectDescendents(pageContext.subject as TAG_ID) : []; |
| 562 | + |
| 563 | + const updateFilterTags = (tag: Tag) => { |
| 564 | + if (filterTags?.includes(tag)) { |
| 565 | + setFilterTags(filterTags.filter(t => t !== tag)); |
| 566 | + } |
| 567 | + else { |
| 568 | + setFilterTags([...(filterTags ?? []), tag]); |
| 569 | + } |
| 570 | + }; |
| 571 | + |
| 572 | + const updateFilterStages = (stage: Stage) => { |
| 573 | + if (filterStages?.includes(stage)) { |
| 574 | + setFilterStages(filterStages.filter(s => s !== stage)); |
| 575 | + } else { |
| 576 | + setFilterStages([...(filterStages ?? []), stage]); |
| 577 | + } |
| 578 | + }; |
| 579 | + |
| 580 | + // Clear stage filters on subject change, since previous stages may not be visible to deselect |
| 581 | + useEffect(() => { |
| 582 | + setFilterStages(undefined); |
| 583 | + }, [filterTags]); |
| 584 | + |
| 585 | + return <ContentSidebar {...rest}> |
| 586 | + <div className="section-divider"/> |
| 587 | + <h5>Search practice tests</h5> |
| 588 | + <Input type="search" placeholder="e.g. Challenge" value={filterText} className="search--filter-input my-3" |
| 589 | + onChange={(e: ChangeEvent<HTMLInputElement>) => setFilterText(e.target.value)} /> |
| 590 | + |
| 591 | + {!pageContext?.subject && Object.keys(PHY_NAV_SUBJECTS).filter(s => tagCounts[s] > 0).length > 0 && <> |
| 592 | + <div className="section-divider"/> |
| 593 | + <h5>Filter by subject</h5> |
| 594 | + <ul> |
| 595 | + {Object.keys(PHY_NAV_SUBJECTS).filter(s => tagCounts[s] > 0).map((subject, i) => { |
| 596 | + const subjectTag = tags.getById(subject as TAG_ID); |
| 597 | + const descendentTags = tags.getDirectDescendents(subjectTag.id); |
| 598 | + const isSelected = filterTags?.includes(subjectTag) || descendentTags.some(tag => filterTags?.includes(tag)); |
| 599 | + const isPartial = descendentTags.some(tag => filterTags?.includes(tag)) && descendentTags.some(tag => !filterTags?.includes(tag)); |
| 600 | + return <li key={i} className={classNames("ps-2", {"checkbox-region": isSelected})}> |
| 601 | + <FilterCheckbox |
| 602 | + checkboxStyle="button" color="theme" data-bs-theme={subject} tag={subjectTag} conceptFilters={filterTags as Tag[]} |
| 603 | + setConceptFilters={setFilterTags} tagCounts={tagCounts} dependentTags={descendentTags} incompatibleTags={descendentTags} |
| 604 | + partiallySelected={descendentTags.some(tag => filterTags?.includes(tag))} |
| 605 | + className={classNames({"icon-checkbox-off": !isSelected, "icon icon-checkbox-partial-alt": isSelected && isPartial, "icon-checkbox-selected": isSelected && !isPartial})} |
| 606 | + /> |
| 607 | + {isSelected && <ul className="ms-3 ps-2"> |
| 608 | + {descendentTags.filter(tag => tagCounts[tag.id] > 0) |
| 609 | + .map((tag, j) => <li key={j}> |
| 610 | + <FilterCheckbox |
| 611 | + checkboxStyle="button" color="theme" bsSize="sm" data-bs-theme={subject} tag={tag} conceptFilters={filterTags as Tag[]} |
| 612 | + setConceptFilters={setFilterTags} tagCounts={tagCounts} incompatibleTags={[subjectTag]} |
| 613 | + /> |
| 614 | + </li>)} |
| 615 | + </ul>} |
| 616 | + </li>; |
| 617 | + })} |
| 618 | + </ul> |
| 619 | + </>} |
| 620 | + |
| 621 | + {pageContext?.subject && fields.filter(tag => tagCounts[tag.id] > 0).length > 0 && <> |
| 622 | + <div className="section-divider"/> |
| 623 | + <h5>Filter by topic</h5> |
| 624 | + <ul> |
| 625 | + {fields.filter(tag => tagCounts[tag.id] > 0) |
| 626 | + .map((tag, j) => <li key={j} > |
| 627 | + <StyledTabPicker checkboxTitle={tag.title} checked={filterTags?.includes(tag)} |
| 628 | + count={tagCounts[tag.id]} onInputChange={() => updateFilterTags(tag)}/> |
| 629 | + </li>)} |
| 630 | + </ul> |
| 631 | + </>} |
| 632 | + |
| 633 | + {!isSingleStageContext(pageContext) && getFilteredStageOptions().filter(s => stageCounts[s.label] > 0).length > 0 && <> |
| 634 | + <div className="section-divider"/> |
| 635 | + <h5>Filter by stage</h5> |
| 636 | + <ul> |
| 637 | + {getFilteredStageOptions().filter(s => stageCounts[s.label] > 0).map((stage, i) => |
| 638 | + <li key={i}> |
| 639 | + <StyledCheckbox checked={filterStages?.includes(stage.value)} |
| 640 | + label={<>{stage.label} {tagCounts && <span className="text-muted">({stageCounts[stage.label]})</span>}</>} |
| 641 | + color="theme" data-bs-theme={filterTags?.length === 1 ? filterTags[0].id : undefined} |
| 642 | + onChange={() => {updateFilterStages(stage.value);}}/> |
| 643 | + </li>)} |
| 644 | + </ul> |
| 645 | + </>} |
| 646 | + |
| 647 | + {isFullyDefinedContext(pageContext) && <> |
| 648 | + <div className="section-divider"/> |
| 649 | + <div className="sidebar-help"> |
| 650 | + <p>The practice tests shown here have been filtered to only show those that are relevant to {getHumanContext(pageContext)}.</p> |
| 651 | + <p>If you want to explore our full range of practice tests, you can view the main practice tests page:</p> |
| 652 | + <AffixButton size="md" color="keyline" tag={Link} to="/practice_tests" affix={{ |
| 653 | + affix: "icon-right", |
| 654 | + position: "suffix", |
| 655 | + type: "icon" |
| 656 | + }}> |
| 657 | + Browse all practice tests |
| 658 | + </AffixButton> |
| 659 | + </div> |
| 660 | + </>} |
| 661 | + <div className="section-divider"/> |
| 662 | + <div className="sidebar-help"> |
| 663 | + <p>You can see all of the tests that you have in progress or have completed in your My Isaac:</p> |
| 664 | + <AffixButton size="md" color="keyline" tag={Link} to="/tests" affix={{ |
| 665 | + affix: "icon-right", |
| 666 | + position: "suffix", |
| 667 | + type: "icon" |
| 668 | + }}> |
| 669 | + My tests |
| 670 | + </AffixButton> |
| 671 | + </div> |
| 672 | + </ContentSidebar>; |
547 | 673 | };
|
548 | 674 |
|
549 | 675 | export const LessonsAndRevisionSidebar = (props: SidebarProps) => {
|
@@ -1188,10 +1314,12 @@ export const MyQuizzesSidebar = (props: MyQuizzesSidebarProps) => {
|
1188 | 1314 | {statusOptions.map(state => <QuizStatusCheckbox
|
1189 | 1315 | key={state} status={state} count={undefined} statusFilter={quizStatusFilter} setStatusFilter={setQuizStatusFilter}
|
1190 | 1316 | />)}
|
1191 |
| - <h5 className="my-3">Filter by assigner</h5> |
1192 |
| - <Input type="select" onChange={e => setQuizCreatorFilter(e.target.value)}> |
1193 |
| - {["All", ...getDistinctAssignmentSetters(quizzes)].map(setter => <option key={setter} value={setter}>{setter}</option>)} |
1194 |
| - </Input> |
| 1317 | + {activeTab === 1 && <> |
| 1318 | + <h5 className="my-3">Filter by assigner</h5> |
| 1319 | + <Input type="select" onChange={e => setQuizCreatorFilter(e.target.value)}> |
| 1320 | + {["All", ...getDistinctAssignmentSetters(quizzes)].map(setter => <option key={setter} value={setter}>{setter}</option>)} |
| 1321 | + </Input> |
| 1322 | + </>} |
1195 | 1323 | <div className="section-divider mt-4"/>
|
1196 | 1324 | <h5 className="mb-3">Display</h5>
|
1197 | 1325 | <StyledDropdown value={displayMode} onChange={() => setDisplayMode(d => d === "table" ? "cards" : "table")}>
|
|
0 commit comments