diff --git a/common-docs/teachertool/catalog-shared.json b/common-docs/teachertool/catalog-shared.json index 883162e77ca0..af52b385028d 100644 --- a/common-docs/teachertool/catalog-shared.json +++ b/common-docs/teachertool/catalog-shared.json @@ -6,6 +6,7 @@ "template": "${Block} used ${count} times", "description": "This block was used the specified number of times in your project.", "docPath": "/teachertool", + "tags": ["General"], "params": [ { "name": "block", @@ -27,6 +28,7 @@ "description": "The project contains at least the specified number of comments.", "docPath": "/teachertool", "maxCount": 1, + "tags": ["General"], "params": [ { "name": "count", @@ -43,6 +45,7 @@ "docPath": "/teachertool", "description": "The program uses at least this many loops of any kind (for, repeat, while, or for-of).", "maxCount": 1, + "tags": ["Code Elements"], "params": [ { "name": "count", @@ -59,6 +62,7 @@ "docPath": "/teachertool", "description": "At least this many user-defined functions are created and called.", "maxCount": 1, + "tags": ["Code Elements"], "params": [ { "name": "count", @@ -75,6 +79,7 @@ "docPath": "/teachertool", "description": "The program creates and uses at least this many user-defined variables.", "maxCount": 1, + "tags": ["Code Elements"], "params": [ { "name": "count", diff --git a/common-docs/teachertool/test/catalog-shared.json b/common-docs/teachertool/test/catalog-shared.json index 73e98c28ce44..bd4db6dc9042 100644 --- a/common-docs/teachertool/test/catalog-shared.json +++ b/common-docs/teachertool/test/catalog-shared.json @@ -7,6 +7,7 @@ "description": "Experimental: AI outputs may not be accurate. Use with caution and always review responses.", "docPath": "/teachertool", "maxCount": 10, + "tags": ["General"], "params": [ { "name": "question", diff --git a/react-common/components/controls/Accordion/Accordion.tsx b/react-common/components/controls/Accordion/Accordion.tsx index 15ae70e29d33..a6c1e092545a 100644 --- a/react-common/components/controls/Accordion/Accordion.tsx +++ b/react-common/components/controls/Accordion/Accordion.tsx @@ -1,15 +1,19 @@ import * as React from "react"; import { ContainerProps, classList, fireClickOnEnter } from "../../util"; import { useId } from "../../../hooks/useId"; -import { AccordionProvider, clearExpanded, setExpanded, useAccordionDispatch, useAccordionState } from "./context"; +import { AccordionProvider, removeExpanded, setExpanded, useAccordionDispatch, useAccordionState } from "./context"; export interface AccordionProps extends ContainerProps { + multiExpand?: boolean; + defaultExpandedIds?: string[]; children?: React.ReactElement[] | React.ReactElement; } export interface AccordionItemProps extends ContainerProps { children?: [React.ReactElement, React.ReactElement]; noChevron?: boolean; + itemId?: string; + onExpandToggled?: (expanded: boolean) => void; } export interface AccordionHeaderProps extends ContainerProps { @@ -27,10 +31,12 @@ export const Accordion = (props: AccordionProps) => { ariaHidden, ariaDescribedBy, role, + multiExpand, + defaultExpandedIds } = props; return ( - +
{ ariaHidden, ariaDescribedBy, role, - noChevron + noChevron, + itemId, + onExpandToggled, } = props; const { expanded } = useAccordionState(); const dispatch = useAccordionDispatch(); - const panelId = useId(); + const panelId = itemId ?? useId(); const mappedChildren = React.Children.toArray(children); - const isExpanded = expanded === panelId; + const isExpanded = expanded.indexOf(panelId) !== -1; const onHeaderClick = React.useCallback(() => { if (isExpanded) { - dispatch(clearExpanded()); + dispatch(removeExpanded(panelId)); } else { dispatch(setExpanded(panelId)); } + onExpandToggled?.(!isExpanded); }, [isExpanded]); return ( @@ -150,4 +159,4 @@ export const AccordionPanel = (props: AccordionPanelProps) => { {children}
); -} \ No newline at end of file +} diff --git a/react-common/components/controls/Accordion/context.tsx b/react-common/components/controls/Accordion/context.tsx index 8bde33951a30..c2b591c6d70c 100644 --- a/react-common/components/controls/Accordion/context.tsx +++ b/react-common/components/controls/Accordion/context.tsx @@ -1,17 +1,22 @@ import * as React from "react"; interface AccordionState { - expanded?: string; + multiExpand?: boolean; + expanded: string[]; } const AccordionStateContext = React.createContext(null); const AccordionDispatchContext = React.createContext<(action: Action) => void>(null); -export const AccordionProvider = ({ children }: React.PropsWithChildren<{}>) => { - const [state, dispatch] = React.useReducer( - accordionReducer, - {} - ); +export const AccordionProvider = ({ + multiExpand, + defaultExpandedIds, + children, +}: React.PropsWithChildren<{ multiExpand?: boolean; defaultExpandedIds?: string[] }>) => { + const [state, dispatch] = React.useReducer(accordionReducer, { + expanded: defaultExpandedIds ?? [], + multiExpand, + }); return ( @@ -27,11 +32,12 @@ type SetExpanded = { id: string; }; -type ClearExpanded = { - type: "CLEAR_EXPANDED"; +type RemoveExpanded = { + type: "REMOVE_EXPANDED"; + id: string; }; -type Action = SetExpanded | ClearExpanded; +type Action = SetExpanded | RemoveExpanded; export const setExpanded = (id: string): SetExpanded => ( { @@ -40,14 +46,15 @@ export const setExpanded = (id: string): SetExpanded => ( } ); -export const clearExpanded = (): ClearExpanded => ( +export const removeExpanded = (id: string): RemoveExpanded => ( { - type: "CLEAR_EXPANDED" + type: "REMOVE_EXPANDED", + id } ); export function useAccordionState() { - return React.useContext(AccordionStateContext) + return React.useContext(AccordionStateContext); } export function useAccordionDispatch() { @@ -59,12 +66,12 @@ function accordionReducer(state: AccordionState, action: Action): AccordionState case "SET_EXPANDED": return { ...state, - expanded: action.id + expanded: state.multiExpand ? [...state.expanded, action.id] : [action.id], }; - case "CLEAR_EXPANDED": + case "REMOVE_EXPANDED": return { ...state, - expanded: undefined + expanded: state.expanded.filter((id) => id !== action.id), }; } -} \ No newline at end of file +} diff --git a/teachertool/src/components/CatalogOverlay.tsx b/teachertool/src/components/CatalogOverlay.tsx index f01a244aaf56..1c0f64fd0b62 100644 --- a/teachertool/src/components/CatalogOverlay.tsx +++ b/teachertool/src/components/CatalogOverlay.tsx @@ -6,11 +6,15 @@ import { getCatalogCriteria } from "../state/helpers"; import { ReadOnlyCriteriaDisplay } from "./ReadonlyCriteriaDisplay"; import { Strings } from "../constants"; import { Button } from "react-common/components/controls/Button"; -import { getReadableCriteriaTemplate, makeToast } from "../utils"; +import { Accordion } from "react-common/components/controls/Accordion"; +import { getReadableCriteriaTemplate } from "../utils"; import { setCatalogOpen } from "../transforms/setCatalogOpen"; import { classList } from "react-common/components/util"; import { announceToScreenReader } from "../transforms/announceToScreenReader"; import { FocusTrap } from "react-common/components/controls/FocusTrap"; +import { logError } from "../services/loggingService"; +import { ErrorCode } from "../types/errorCode"; +import { addExpandedCatalogTag, getExpandedCatalogTags, removeExpandedCatalogTag } from "../services/storageService"; import css from "./styling/CatalogOverlay.module.scss"; interface CatalogHeaderProps { @@ -67,16 +71,59 @@ const CatalogItemLabel: React.FC = ({ catalogCriteria, is ); }; +interface CatalogItemProps { + catalogCriteria: CatalogCriteria; + recentlyAddedIds: pxsim.Map; + onItemClicked: (c: CatalogCriteria) => void; +} +const CatalogItem: React.FC = ({ catalogCriteria, recentlyAddedIds, onItemClicked }) => { + const { state: teacherTool } = useContext(AppStateContext); + + const existingInstanceCount = teacherTool.checklist.criteria.filter( + i => i.catalogCriteriaId === catalogCriteria.id + ).length; + const isMaxed = catalogCriteria.maxCount !== undefined && existingInstanceCount >= catalogCriteria.maxCount; + return catalogCriteria.template ? ( +