From ea2a2af89a38b4b8542af8e2bf905f80fd8fa73e Mon Sep 17 00:00:00 2001 From: thsparks Date: Tue, 7 May 2024 10:31:25 -0700 Subject: [PATCH 01/23] Basic Accordion Implementation for Catalog, no styling --- common-docs/teachertool/catalog-shared.json | 5 ++ .../teachertool/test/catalog-shared.json | 1 + teachertool/src/components/CatalogOverlay.tsx | 81 ++++++++++++------- .../src/transforms/loadCatalogAsync.ts | 7 +- teachertool/src/types/criteria.ts | 1 + teachertool/src/types/errorCode.ts | 1 + 6 files changed, 67 insertions(+), 29 deletions(-) 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/teachertool/src/components/CatalogOverlay.tsx b/teachertool/src/components/CatalogOverlay.tsx index f01a244aaf56..7c414fa58e1e 100644 --- a/teachertool/src/components/CatalogOverlay.tsx +++ b/teachertool/src/components/CatalogOverlay.tsx @@ -6,12 +6,16 @@ import { getCatalogCriteria } from "../state/helpers"; import { ReadOnlyCriteriaDisplay } from "./ReadonlyCriteriaDisplay"; import { Strings } from "../constants"; import { Button } from "react-common/components/controls/Button"; +import { Accordion } from "react-common/components/controls/Accordion"; import { getReadableCriteriaTemplate, makeToast } 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 css from "./styling/CatalogOverlay.module.scss"; +import { logError } from "../services/loggingService"; +import { ErrorCode } from "../types/errorCode"; +import { AccordionHeader, AccordionItem, AccordionPanel } from "react-common/components/controls/Accordion/Accordion"; interface CatalogHeaderProps { onClose: () => void; @@ -73,10 +77,22 @@ const CatalogList: React.FC = () => { const recentlyAddedWindowMs = 500; const [recentlyAddedIds, setRecentlyAddedIds] = useState>({}); - const criteria = useMemo( - () => getCatalogCriteria(teacherTool), - [teacherTool.catalog, teacherTool.checklist] - ); + // For now, we only look at the first tag of each criteria. + const criteriaGroupedByTag = useMemo>(() => { + const grouped: pxt.Map = {}; + getCatalogCriteria(teacherTool)?.forEach(c => { + const tag = c.tags?.[0]; + if (!tag) { + logError(ErrorCode.missingTag, { message: "Catalog criteria missing tag", criteria: c }); + return; + } + if (!grouped[tag]) { + grouped[tag] = []; + } + grouped[tag].push(c); + }); + return grouped; + }, [teacherTool.catalog]); function updateRecentlyAddedValue(id: string, value: NodeJS.Timeout | undefined) { setRecentlyAddedIds(prevState => { @@ -107,33 +123,42 @@ const CatalogList: React.FC = () => { } return ( -
- {criteria.map(c => { - const existingInstanceCount = teacherTool.checklist.criteria.filter( - i => i.catalogCriteriaId === c.id - ).length; - const isMaxed = c.maxCount !== undefined && existingInstanceCount >= c.maxCount; + + {Object.keys(criteriaGroupedByTag).map(tag => { return ( - c.template && ( -
+ ); }; diff --git a/teachertool/src/transforms/loadCatalogAsync.ts b/teachertool/src/transforms/loadCatalogAsync.ts index 876704bde6f1..8fb5bc0cffae 100644 --- a/teachertool/src/transforms/loadCatalogAsync.ts +++ b/teachertool/src/transforms/loadCatalogAsync.ts @@ -12,11 +12,16 @@ export async function loadCatalogAsync() { const { dispatch } = stateAndDispatch(); const fullCatalog = await loadTestableCollectionFromDocsAsync(prodFiles, "criteria"); - // Convert parameter names to lower-case for case-insensitive matching fullCatalog.forEach(c => { + // Convert parameter names to lower-case for case-insensitive matching c.params?.forEach(p => { p.name = p.name.toLocaleLowerCase(); }); + + // Add default tag if none are present + if (!c.tags || c.tags.length === 0) { + c.tags = ["Other"]; + } }); dispatch(Actions.setCatalog(fullCatalog)); diff --git a/teachertool/src/types/criteria.ts b/teachertool/src/types/criteria.ts index 97f9ac6e68cb..4b91bd3737d2 100644 --- a/teachertool/src/types/criteria.ts +++ b/teachertool/src/types/criteria.ts @@ -8,6 +8,7 @@ export interface CatalogCriteria { params: CriteriaParameter[] | undefined; // Any parameters that affect the criteria hideInCatalog?: boolean; // Whether the criteria should be hidden in the user-facing catalog maxCount?: number; // The maximum number of instances allowed for this criteria within a single checklist. Unlimited if undefined. + tags?: string[]; // Tags to help categorize the criteria } // An instance of a criteria in a checklist. diff --git a/teachertool/src/types/errorCode.ts b/teachertool/src/types/errorCode.ts index f122a6b22bcc..31df406efa2a 100644 --- a/teachertool/src/types/errorCode.ts +++ b/teachertool/src/types/errorCode.ts @@ -26,4 +26,5 @@ export enum ErrorCode { unrecognizedSystemParameter = "unrecognizedSystemParameter", invalidValidatorPlan = "invalidValidatorPlan", askCopilotQuestion = "askCopilotQuestion", + missingTag = "missingTag", } From 1dd5a1acb4780d28a12e198362b5c3b7f7ae3ca5 Mon Sep 17 00:00:00 2001 From: thsparks Date: Tue, 7 May 2024 10:57:03 -0700 Subject: [PATCH 02/23] Initial styling --- .../components/styling/CatalogOverlay.module.scss | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/teachertool/src/components/styling/CatalogOverlay.module.scss b/teachertool/src/components/styling/CatalogOverlay.module.scss index 94c1db07038b..aba70c8dc52d 100644 --- a/teachertool/src/components/styling/CatalogOverlay.module.scss +++ b/teachertool/src/components/styling/CatalogOverlay.module.scss @@ -14,8 +14,8 @@ align-items: center; .catalog-content-container { - max-width: 95%; - max-height: 95%; + width: 95%; + height: 95%; background-color: var(--pxt-page-background); border-radius: .285rem; // Match modal display: flex; @@ -58,6 +58,15 @@ flex-direction: column; width: 100%; + + button[class*="common-accordion-header-outer"] { + background-color: var(--pxt-content-foreground); + color: var(--pxt-content-background); + border-bottom: 1px solid var(--pxt-content-accent); + font-size: 1.2rem; + padding: 0.5rem 0.95rem; + } + .catalog-item { width: 100%; padding: 1.25rem 1.5rem 1.25rem 1rem; From 13d306dd23bf156d5fe48f6594c2e59bd3944d23 Mon Sep 17 00:00:00 2001 From: thsparks Date: Tue, 7 May 2024 11:29:47 -0700 Subject: [PATCH 03/23] Support multiple expanded panels in the accordion --- .../controls/Accordion/Accordion.tsx | 12 ++++--- .../components/controls/Accordion/context.tsx | 33 +++++++++++++++---- teachertool/src/components/CatalogOverlay.tsx | 2 +- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/react-common/components/controls/Accordion/Accordion.tsx b/react-common/components/controls/Accordion/Accordion.tsx index 15ae70e29d33..28a840e7f1cc 100644 --- a/react-common/components/controls/Accordion/Accordion.tsx +++ b/react-common/components/controls/Accordion/Accordion.tsx @@ -1,9 +1,10 @@ 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; children?: React.ReactElement[] | React.ReactElement; } @@ -27,10 +28,11 @@ export const Accordion = (props: AccordionProps) => { ariaHidden, ariaDescribedBy, role, + multiExpand } = props; return ( - +
{ const panelId = 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)); @@ -150,4 +152,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..4444c017cb98 100644 --- a/react-common/components/controls/Accordion/context.tsx +++ b/react-common/components/controls/Accordion/context.tsx @@ -1,16 +1,20 @@ 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<{}>) => { +export const AccordionProvider = ({ multiExpand, children }: React.PropsWithChildren<{multiExpand?: boolean}>) => { const [state, dispatch] = React.useReducer( accordionReducer, - {} + { + expanded: [], + multiExpand + } ); return ( @@ -27,11 +31,16 @@ type SetExpanded = { id: string; }; +type RemoveExpanded = { + type: "REMOVE_EXPANDED"; + id: string; +}; + type ClearExpanded = { type: "CLEAR_EXPANDED"; }; -type Action = SetExpanded | ClearExpanded; +type Action = SetExpanded | RemoveExpanded | ClearExpanded; export const setExpanded = (id: string): SetExpanded => ( { @@ -40,6 +49,13 @@ export const setExpanded = (id: string): SetExpanded => ( } ); +export const removeExpanded = (id: string): RemoveExpanded => ( + { + type: "REMOVE_EXPANDED", + id + } +); + export const clearExpanded = (): ClearExpanded => ( { type: "CLEAR_EXPANDED" @@ -59,7 +75,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 "REMOVE_EXPANDED": + return { + ...state, + expanded: state.expanded.filter(id => id !== action.id) }; case "CLEAR_EXPANDED": return { @@ -67,4 +88,4 @@ function accordionReducer(state: AccordionState, action: Action): AccordionState expanded: undefined }; } -} \ No newline at end of file +} diff --git a/teachertool/src/components/CatalogOverlay.tsx b/teachertool/src/components/CatalogOverlay.tsx index 7c414fa58e1e..70f7e70b3a15 100644 --- a/teachertool/src/components/CatalogOverlay.tsx +++ b/teachertool/src/components/CatalogOverlay.tsx @@ -123,7 +123,7 @@ const CatalogList: React.FC = () => { } return ( - + {Object.keys(criteriaGroupedByTag).map(tag => { return ( From add5f0670b1b5e39ce39333ccc64a4eb48329ceb Mon Sep 17 00:00:00 2001 From: thsparks Date: Tue, 7 May 2024 12:28:46 -0700 Subject: [PATCH 04/23] Default open initial category --- .../components/controls/Accordion/Accordion.tsx | 12 ++++++++---- .../components/controls/Accordion/context.tsx | 4 ++-- teachertool/src/components/CatalogOverlay.tsx | 11 ++++++++--- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/react-common/components/controls/Accordion/Accordion.tsx b/react-common/components/controls/Accordion/Accordion.tsx index 28a840e7f1cc..5bb41ac52536 100644 --- a/react-common/components/controls/Accordion/Accordion.tsx +++ b/react-common/components/controls/Accordion/Accordion.tsx @@ -5,12 +5,14 @@ import { AccordionProvider, removeExpanded, setExpanded, useAccordionDispatch, u 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; } export interface AccordionHeaderProps extends ContainerProps { @@ -28,11 +30,12 @@ export const Accordion = (props: AccordionProps) => { ariaHidden, ariaDescribedBy, role, - multiExpand + multiExpand, + defaultExpandedIds } = props; return ( - +
{ ariaHidden, ariaDescribedBy, role, - noChevron + noChevron, + itemId, } = props; const { expanded } = useAccordionState(); const dispatch = useAccordionDispatch(); - const panelId = useId(); + const panelId = itemId ?? useId(); const mappedChildren = React.Children.toArray(children); const isExpanded = expanded.indexOf(panelId) != -1; diff --git a/react-common/components/controls/Accordion/context.tsx b/react-common/components/controls/Accordion/context.tsx index 4444c017cb98..dc5bbe20815a 100644 --- a/react-common/components/controls/Accordion/context.tsx +++ b/react-common/components/controls/Accordion/context.tsx @@ -8,11 +8,11 @@ interface AccordionState { const AccordionStateContext = React.createContext(null); const AccordionDispatchContext = React.createContext<(action: Action) => void>(null); -export const AccordionProvider = ({ multiExpand, children }: React.PropsWithChildren<{multiExpand?: boolean}>) => { +export const AccordionProvider = ({ multiExpand, defaultExpandedIds, children }: React.PropsWithChildren<{multiExpand?: boolean, defaultExpandedIds?: string[]}>) => { const [state, dispatch] = React.useReducer( accordionReducer, { - expanded: [], + expanded: defaultExpandedIds ?? [], multiExpand } ); diff --git a/teachertool/src/components/CatalogOverlay.tsx b/teachertool/src/components/CatalogOverlay.tsx index 70f7e70b3a15..0a58fc72e509 100644 --- a/teachertool/src/components/CatalogOverlay.tsx +++ b/teachertool/src/components/CatalogOverlay.tsx @@ -122,11 +122,16 @@ const CatalogList: React.FC = () => { announceToScreenReader(lf("Added '{0}' to checklist.", getReadableCriteriaTemplate(c))); } + function getItemIdForTag(tag: string) { + return `accordion-item-${tag}`; + } + + const tags = Object.keys(criteriaGroupedByTag); return ( - - {Object.keys(criteriaGroupedByTag).map(tag => { + + {tags.map(tag => { return ( - + {tag} {criteriaGroupedByTag[tag].map(c => { From afc485019bc25490d34579bcbee700c639fbcd21 Mon Sep 17 00:00:00 2001 From: thsparks Date: Tue, 7 May 2024 12:29:16 -0700 Subject: [PATCH 05/23] Remove unused imports --- teachertool/src/components/CatalogOverlay.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/teachertool/src/components/CatalogOverlay.tsx b/teachertool/src/components/CatalogOverlay.tsx index 0a58fc72e509..8aaf37d18a71 100644 --- a/teachertool/src/components/CatalogOverlay.tsx +++ b/teachertool/src/components/CatalogOverlay.tsx @@ -7,7 +7,7 @@ import { ReadOnlyCriteriaDisplay } from "./ReadonlyCriteriaDisplay"; import { Strings } from "../constants"; import { Button } from "react-common/components/controls/Button"; import { Accordion } from "react-common/components/controls/Accordion"; -import { getReadableCriteriaTemplate, makeToast } from "../utils"; +import { getReadableCriteriaTemplate } from "../utils"; import { setCatalogOpen } from "../transforms/setCatalogOpen"; import { classList } from "react-common/components/util"; import { announceToScreenReader } from "../transforms/announceToScreenReader"; @@ -15,7 +15,6 @@ import { FocusTrap } from "react-common/components/controls/FocusTrap"; import css from "./styling/CatalogOverlay.module.scss"; import { logError } from "../services/loggingService"; import { ErrorCode } from "../types/errorCode"; -import { AccordionHeader, AccordionItem, AccordionPanel } from "react-common/components/controls/Accordion/Accordion"; interface CatalogHeaderProps { onClose: () => void; From 5d8408af0310bde95482ec9c07651db2d24f54c2 Mon Sep 17 00:00:00 2001 From: thsparks Date: Tue, 7 May 2024 12:29:39 -0700 Subject: [PATCH 06/23] Reorder imports --- teachertool/src/components/CatalogOverlay.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teachertool/src/components/CatalogOverlay.tsx b/teachertool/src/components/CatalogOverlay.tsx index 8aaf37d18a71..18544cfab28b 100644 --- a/teachertool/src/components/CatalogOverlay.tsx +++ b/teachertool/src/components/CatalogOverlay.tsx @@ -12,9 +12,9 @@ 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 css from "./styling/CatalogOverlay.module.scss"; import { logError } from "../services/loggingService"; import { ErrorCode } from "../types/errorCode"; +import css from "./styling/CatalogOverlay.module.scss"; interface CatalogHeaderProps { onClose: () => void; From 8dbd82c625bce1ee4f7a637994c2ecc632ec1eb8 Mon Sep 17 00:00:00 2001 From: thsparks Date: Tue, 7 May 2024 12:32:08 -0700 Subject: [PATCH 07/23] Formatting --- .../components/controls/Accordion/context.tsx | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/react-common/components/controls/Accordion/context.tsx b/react-common/components/controls/Accordion/context.tsx index dc5bbe20815a..556b95e3c737 100644 --- a/react-common/components/controls/Accordion/context.tsx +++ b/react-common/components/controls/Accordion/context.tsx @@ -8,14 +8,15 @@ interface AccordionState { const AccordionStateContext = React.createContext(null); const AccordionDispatchContext = React.createContext<(action: Action) => void>(null); -export const AccordionProvider = ({ multiExpand, defaultExpandedIds, children }: React.PropsWithChildren<{multiExpand?: boolean, defaultExpandedIds?: string[]}>) => { - const [state, dispatch] = React.useReducer( - accordionReducer, - { - expanded: defaultExpandedIds ?? [], - multiExpand - } - ); +export const AccordionProvider = ({ + multiExpand, + defaultExpandedIds, + children, +}: React.PropsWithChildren<{ multiExpand?: boolean; defaultExpandedIds?: string[] }>) => { + const [state, dispatch] = React.useReducer(accordionReducer, { + expanded: defaultExpandedIds ?? [], + multiExpand, + }); return ( @@ -63,7 +64,7 @@ export const clearExpanded = (): ClearExpanded => ( ); export function useAccordionState() { - return React.useContext(AccordionStateContext) + return React.useContext(AccordionStateContext); } export function useAccordionDispatch() { @@ -75,17 +76,17 @@ function accordionReducer(state: AccordionState, action: Action): AccordionState case "SET_EXPANDED": return { ...state, - expanded: state.multiExpand ? [...state.expanded, action.id] : [action.id] + expanded: state.multiExpand ? [...state.expanded, action.id] : [action.id], }; case "REMOVE_EXPANDED": return { ...state, - expanded: state.expanded.filter(id => id !== action.id) + expanded: state.expanded.filter((id) => id !== action.id), }; case "CLEAR_EXPANDED": return { ...state, - expanded: undefined + expanded: undefined, }; } } From 0ae19f3f955d8e796f6c4de95f3c1b4542bea028 Mon Sep 17 00:00:00 2001 From: thsparks Date: Tue, 7 May 2024 12:50:20 -0700 Subject: [PATCH 08/23] Save expanded categories --- .../controls/Accordion/Accordion.tsx | 3 ++ teachertool/src/components/CatalogOverlay.tsx | 27 ++++++++++++-- teachertool/src/services/storageService.ts | 35 +++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/react-common/components/controls/Accordion/Accordion.tsx b/react-common/components/controls/Accordion/Accordion.tsx index 5bb41ac52536..a3c402eb16e9 100644 --- a/react-common/components/controls/Accordion/Accordion.tsx +++ b/react-common/components/controls/Accordion/Accordion.tsx @@ -13,6 +13,7 @@ export interface AccordionItemProps extends ContainerProps { children?: [React.ReactElement, React.ReactElement]; noChevron?: boolean; itemId?: string; + onExpandToggled?: (expanded: boolean) => void; } export interface AccordionHeaderProps extends ContainerProps { @@ -61,6 +62,7 @@ export const AccordionItem = (props: AccordionItemProps) => { role, noChevron, itemId, + onExpandToggled, } = props; const { expanded } = useAccordionState(); @@ -77,6 +79,7 @@ export const AccordionItem = (props: AccordionItemProps) => { else { dispatch(setExpanded(panelId)); } + onExpandToggled?.(!isExpanded); }, [isExpanded]); return ( diff --git a/teachertool/src/components/CatalogOverlay.tsx b/teachertool/src/components/CatalogOverlay.tsx index 18544cfab28b..81624ca46273 100644 --- a/teachertool/src/components/CatalogOverlay.tsx +++ b/teachertool/src/components/CatalogOverlay.tsx @@ -15,6 +15,8 @@ import { FocusTrap } from "react-common/components/controls/FocusTrap"; import { logError } from "../services/loggingService"; import { ErrorCode } from "../types/errorCode"; import css from "./styling/CatalogOverlay.module.scss"; +import { addExandedCatalogTagAsync, getExpandedCatalogTags, removeExpandedCatalogTagAsync } from "../services/storageService"; +import exp from "constants"; interface CatalogHeaderProps { onClose: () => void; @@ -125,12 +127,33 @@ const CatalogList: React.FC = () => { return `accordion-item-${tag}`; } + function onTagExpandToggled(tag: string, expanded: boolean) { + if (expanded) { + /* await */ addExandedCatalogTagAsync(tag); + } else { + /* await */ removeExpandedCatalogTagAsync(tag); + } + } + const tags = Object.keys(criteriaGroupedByTag); + if (tags.length === 0) { + return null; + } + + const expandedTags = getExpandedCatalogTags(); + + // If no tags are expanded, expand the first one. + if (expandedTags.length === 0) { + addExandedCatalogTagAsync(tags[0]); + expandedTags.push(tags[0]); + } + + const expandedIds = expandedTags.map(t => getItemIdForTag(t)); return ( - + {tags.map(tag => { return ( - + onTagExpandToggled(tag, expanded)} key={getItemIdForTag(tag)}> {tag} {criteriaGroupedByTag[tag].map(c => { diff --git a/teachertool/src/services/storageService.ts b/teachertool/src/services/storageService.ts index 34f729e20937..d7567f851ce3 100644 --- a/teachertool/src/services/storageService.ts +++ b/teachertool/src/services/storageService.ts @@ -11,6 +11,7 @@ const KEY_PREFIX = "teachertool"; const AUTORUN_KEY = [KEY_PREFIX, "autorun"].join("/"); const LAST_ACTIVE_CHECKLIST_KEY = [KEY_PREFIX, "lastActiveChecklist"].join("/"); const SPLIT_POSITION_KEY = [KEY_PREFIX, "splitPosition"].join("/"); +const EXPANDED_CATALOG_TAGS_KEY = [KEY_PREFIX, "expandedCatalogTags"].join("/"); function getValue(key: string, defaultValue?: string): string | undefined { return localStorage.getItem(key) || defaultValue; @@ -192,3 +193,37 @@ export async function deleteChecklistAsync(name: string) { setLastActiveChecklistName(""); } } + +export function getExpandedCatalogTags(): string[] { + try { + const tags = getValue(EXPANDED_CATALOG_TAGS_KEY); + return tags ? JSON.parse(tags) : []; + } catch (e) { + logError(ErrorCode.localStorageReadError, e); + return []; + } +} + +export async function setExpandedCatalogTags(tags: string[]) { + try { + setValue(EXPANDED_CATALOG_TAGS_KEY, JSON.stringify(tags)); + } catch (e) { + logError(ErrorCode.localStorageWriteError, e); + } +} + +export async function addExandedCatalogTagAsync(tag: string) { + const expandedTags = getExpandedCatalogTags(); + expandedTags.push(tag); + await setExpandedCatalogTags(expandedTags); +} + +export async function removeExpandedCatalogTagAsync(tag: string) { + const expandedTags = getExpandedCatalogTags(); + const index = expandedTags.indexOf(tag); + if (index !== -1) { + expandedTags.splice(index, 1); + await setExpandedCatalogTags(expandedTags); + } +} + From a2183f5d9da05953e4610cf421c69f5535390523 Mon Sep 17 00:00:00 2001 From: thsparks Date: Tue, 7 May 2024 12:54:19 -0700 Subject: [PATCH 09/23] Persist all tags collapsed --- teachertool/src/components/CatalogOverlay.tsx | 9 +++---- teachertool/src/services/storageService.ts | 27 ++++++++++++------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/teachertool/src/components/CatalogOverlay.tsx b/teachertool/src/components/CatalogOverlay.tsx index 81624ca46273..ac5331133eb3 100644 --- a/teachertool/src/components/CatalogOverlay.tsx +++ b/teachertool/src/components/CatalogOverlay.tsx @@ -140,12 +140,11 @@ const CatalogList: React.FC = () => { return null; } - const expandedTags = getExpandedCatalogTags(); - - // If no tags are expanded, expand the first one. - if (expandedTags.length === 0) { + let expandedTags = getExpandedCatalogTags(); + if (!expandedTags) { + // If we haven't saved an expanded set, default expand the first one. addExandedCatalogTagAsync(tags[0]); - expandedTags.push(tags[0]); + expandedTags = [tags[0]]; } const expandedIds = expandedTags.map(t => getItemIdForTag(t)); diff --git a/teachertool/src/services/storageService.ts b/teachertool/src/services/storageService.ts index d7567f851ce3..31c49986e057 100644 --- a/teachertool/src/services/storageService.ts +++ b/teachertool/src/services/storageService.ts @@ -194,13 +194,15 @@ export async function deleteChecklistAsync(name: string) { } } -export function getExpandedCatalogTags(): string[] { +// Returns undefined if it has not been set or if there was an issue. +// Empty list means it was explicitly set to empty. +export function getExpandedCatalogTags(): string[] | undefined { try { const tags = getValue(EXPANDED_CATALOG_TAGS_KEY); - return tags ? JSON.parse(tags) : []; + return tags ? JSON.parse(tags) : undefined; } catch (e) { logError(ErrorCode.localStorageReadError, e); - return []; + return undefined; } } @@ -213,17 +215,24 @@ export async function setExpandedCatalogTags(tags: string[]) { } export async function addExandedCatalogTagAsync(tag: string) { - const expandedTags = getExpandedCatalogTags(); + let expandedTags = getExpandedCatalogTags(); + if (!expandedTags) { + expandedTags = []; + } expandedTags.push(tag); await setExpandedCatalogTags(expandedTags); } export async function removeExpandedCatalogTagAsync(tag: string) { - const expandedTags = getExpandedCatalogTags(); - const index = expandedTags.indexOf(tag); - if (index !== -1) { - expandedTags.splice(index, 1); - await setExpandedCatalogTags(expandedTags); + let expandedTags = getExpandedCatalogTags(); + if (!expandedTags) { + await setExpandedCatalogTags([]); + } else { + const index = expandedTags.indexOf(tag); + if (index !== -1) { + expandedTags.splice(index, 1); + await setExpandedCatalogTags(expandedTags); + } } } From 40446558a47c76e7159de10cbb5bca7b10078d6d Mon Sep 17 00:00:00 2001 From: thsparks Date: Tue, 7 May 2024 13:54:59 -0700 Subject: [PATCH 10/23] Formatting and tidy up imports --- teachertool/src/components/CatalogOverlay.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/teachertool/src/components/CatalogOverlay.tsx b/teachertool/src/components/CatalogOverlay.tsx index ac5331133eb3..fcba6de97c0b 100644 --- a/teachertool/src/components/CatalogOverlay.tsx +++ b/teachertool/src/components/CatalogOverlay.tsx @@ -14,9 +14,12 @@ import { announceToScreenReader } from "../transforms/announceToScreenReader"; import { FocusTrap } from "react-common/components/controls/FocusTrap"; import { logError } from "../services/loggingService"; import { ErrorCode } from "../types/errorCode"; +import { + addExandedCatalogTagAsync, + getExpandedCatalogTags, + removeExpandedCatalogTagAsync, +} from "../services/storageService"; import css from "./styling/CatalogOverlay.module.scss"; -import { addExandedCatalogTagAsync, getExpandedCatalogTags, removeExpandedCatalogTagAsync } from "../services/storageService"; -import exp from "constants"; interface CatalogHeaderProps { onClose: () => void; @@ -152,7 +155,11 @@ const CatalogList: React.FC = () => { {tags.map(tag => { return ( - onTagExpandToggled(tag, expanded)} key={getItemIdForTag(tag)}> + onTagExpandToggled(tag, expanded)} + key={getItemIdForTag(tag)} + > {tag} {criteriaGroupedByTag[tag].map(c => { From f981ff388fb720dc7c14ca4c2e78a0a14adb5cc0 Mon Sep 17 00:00:00 2001 From: thsparks Date: Tue, 7 May 2024 13:56:23 -0700 Subject: [PATCH 11/23] Prettier --- teachertool/src/services/storageService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/teachertool/src/services/storageService.ts b/teachertool/src/services/storageService.ts index 31c49986e057..5c9e060baa6e 100644 --- a/teachertool/src/services/storageService.ts +++ b/teachertool/src/services/storageService.ts @@ -235,4 +235,3 @@ export async function removeExpandedCatalogTagAsync(tag: string) { } } } - From 280b4167e5123136f736b6a679bbf5f5bebb1430 Mon Sep 17 00:00:00 2001 From: thsparks Date: Tue, 7 May 2024 14:00:43 -0700 Subject: [PATCH 12/23] Use lf and constant for default category --- teachertool/src/constants.ts | 1 + teachertool/src/transforms/loadCatalogAsync.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/teachertool/src/constants.ts b/teachertool/src/constants.ts index ea7a9f264721..cc502c2a006f 100644 --- a/teachertool/src/constants.ts +++ b/teachertool/src/constants.ts @@ -33,6 +33,7 @@ export namespace Strings { export const Checklist = lf("Checklist"); export const Home = lf("Home"); export const CreateEmptyChecklist = lf("Create Empty Checklist"); + export const Other = lf("Other"); } export namespace Ticks { diff --git a/teachertool/src/transforms/loadCatalogAsync.ts b/teachertool/src/transforms/loadCatalogAsync.ts index 8fb5bc0cffae..607f73e72452 100644 --- a/teachertool/src/transforms/loadCatalogAsync.ts +++ b/teachertool/src/transforms/loadCatalogAsync.ts @@ -1,3 +1,4 @@ +import { Strings } from "../constants"; import { loadTestableCollectionFromDocsAsync } from "../services/backendRequests"; import { stateAndDispatch } from "../state"; import * as Actions from "../state/actions"; @@ -20,7 +21,7 @@ export async function loadCatalogAsync() { // Add default tag if none are present if (!c.tags || c.tags.length === 0) { - c.tags = ["Other"]; + c.tags = [Strings.Other]; } }); From 4a6be94fa1c47ddc57bca288eaa24a84a2c0368f Mon Sep 17 00:00:00 2001 From: thsparks Date: Tue, 7 May 2024 14:38:40 -0700 Subject: [PATCH 13/23] Fix horizontal wrapping --- teachertool/src/components/styling/CatalogOverlay.module.scss | 2 +- .../src/components/styling/ReadonlyCriteriaDisplay.module.scss | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/teachertool/src/components/styling/CatalogOverlay.module.scss b/teachertool/src/components/styling/CatalogOverlay.module.scss index aba70c8dc52d..7057d901d3f7 100644 --- a/teachertool/src/components/styling/CatalogOverlay.module.scss +++ b/teachertool/src/components/styling/CatalogOverlay.module.scss @@ -57,7 +57,7 @@ display: flex; flex-direction: column; width: 100%; - + height: 100%; button[class*="common-accordion-header-outer"] { background-color: var(--pxt-content-foreground); diff --git a/teachertool/src/components/styling/ReadonlyCriteriaDisplay.module.scss b/teachertool/src/components/styling/ReadonlyCriteriaDisplay.module.scss index 6cef3907b25c..766b19832629 100644 --- a/teachertool/src/components/styling/ReadonlyCriteriaDisplay.module.scss +++ b/teachertool/src/components/styling/ReadonlyCriteriaDisplay.module.scss @@ -2,6 +2,8 @@ display: flex; flex-direction: column; align-items: flex-start; + text-wrap: wrap; + text-align: start; .criteria-template { margin-bottom: 0.4rem; From 3da27ac959e07cb076cc8c6839a23838bc95f93c Mon Sep 17 00:00:00 2001 From: thsparks Date: Tue, 7 May 2024 14:38:49 -0700 Subject: [PATCH 14/23] Adjust error handling in case tags list is empty --- teachertool/src/components/CatalogOverlay.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/teachertool/src/components/CatalogOverlay.tsx b/teachertool/src/components/CatalogOverlay.tsx index fcba6de97c0b..8283d816af95 100644 --- a/teachertool/src/components/CatalogOverlay.tsx +++ b/teachertool/src/components/CatalogOverlay.tsx @@ -85,11 +85,12 @@ const CatalogList: React.FC = () => { const criteriaGroupedByTag = useMemo>(() => { const grouped: pxt.Map = {}; getCatalogCriteria(teacherTool)?.forEach(c => { - const tag = c.tags?.[0]; - if (!tag) { + if (!c.tags || c.tags.length === 0) { logError(ErrorCode.missingTag, { message: "Catalog criteria missing tag", criteria: c }); return; } + + const tag = c.tags[0]; if (!grouped[tag]) { grouped[tag] = []; } From 8ba07dc55a82aaed9415861bf1b0908362746b05 Mon Sep 17 00:00:00 2001 From: thsparks Date: Tue, 7 May 2024 15:34:40 -0700 Subject: [PATCH 15/23] Make text align in headers and in criteria --- .../src/components/styling/CatalogOverlay.module.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/teachertool/src/components/styling/CatalogOverlay.module.scss b/teachertool/src/components/styling/CatalogOverlay.module.scss index 7057d901d3f7..66c5b8ff6acf 100644 --- a/teachertool/src/components/styling/CatalogOverlay.module.scss +++ b/teachertool/src/components/styling/CatalogOverlay.module.scss @@ -59,6 +59,10 @@ width: 100%; height: 100%; + div[class*="common-accordion-chevron"] { + width: 3rem; // Match action-indicators + } + button[class*="common-accordion-header-outer"] { background-color: var(--pxt-content-foreground); color: var(--pxt-content-background); @@ -86,7 +90,7 @@ .action-indicators { position: relative; padding: 0 1rem 0 0; - width: 3rem; + width: 3rem; // Match common-accordion-chevron display: flex; align-items: center; justify-content: center; From ee69a7d93c3e01b109b8f45b8c27046dcc0f5010 Mon Sep 17 00:00:00 2001 From: thsparks Date: Wed, 8 May 2024 16:32:52 -0700 Subject: [PATCH 16/23] !== instead of != --- react-common/components/controls/Accordion/Accordion.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react-common/components/controls/Accordion/Accordion.tsx b/react-common/components/controls/Accordion/Accordion.tsx index a3c402eb16e9..a6c1e092545a 100644 --- a/react-common/components/controls/Accordion/Accordion.tsx +++ b/react-common/components/controls/Accordion/Accordion.tsx @@ -70,7 +70,7 @@ export const AccordionItem = (props: AccordionItemProps) => { const panelId = itemId ?? useId(); const mappedChildren = React.Children.toArray(children); - const isExpanded = expanded.indexOf(panelId) != -1; + const isExpanded = expanded.indexOf(panelId) !== -1; const onHeaderClick = React.useCallback(() => { if (isExpanded) { From ad42cbf5344673a286b609450efad76699efef5e Mon Sep 17 00:00:00 2001 From: thsparks Date: Wed, 8 May 2024 16:32:59 -0700 Subject: [PATCH 17/23] Remove clear expanded --- .../components/controls/Accordion/context.tsx | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/react-common/components/controls/Accordion/context.tsx b/react-common/components/controls/Accordion/context.tsx index 556b95e3c737..c2b591c6d70c 100644 --- a/react-common/components/controls/Accordion/context.tsx +++ b/react-common/components/controls/Accordion/context.tsx @@ -37,11 +37,7 @@ type RemoveExpanded = { id: string; }; -type ClearExpanded = { - type: "CLEAR_EXPANDED"; -}; - -type Action = SetExpanded | RemoveExpanded | ClearExpanded; +type Action = SetExpanded | RemoveExpanded; export const setExpanded = (id: string): SetExpanded => ( { @@ -57,12 +53,6 @@ export const removeExpanded = (id: string): RemoveExpanded => ( } ); -export const clearExpanded = (): ClearExpanded => ( - { - type: "CLEAR_EXPANDED" - } -); - export function useAccordionState() { return React.useContext(AccordionStateContext); } @@ -83,10 +73,5 @@ function accordionReducer(state: AccordionState, action: Action): AccordionState ...state, expanded: state.expanded.filter((id) => id !== action.id), }; - case "CLEAR_EXPANDED": - return { - ...state, - expanded: undefined, - }; } } From 28fdf3a29ea0f83921c7e8b747d22f13cec00b7c Mon Sep 17 00:00:00 2001 From: thsparks Date: Wed, 8 May 2024 16:41:10 -0700 Subject: [PATCH 18/23] Fix typo in function name --- teachertool/src/components/CatalogOverlay.tsx | 6 +++--- teachertool/src/services/storageService.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/teachertool/src/components/CatalogOverlay.tsx b/teachertool/src/components/CatalogOverlay.tsx index 8283d816af95..e88d3ca83f95 100644 --- a/teachertool/src/components/CatalogOverlay.tsx +++ b/teachertool/src/components/CatalogOverlay.tsx @@ -15,7 +15,7 @@ import { FocusTrap } from "react-common/components/controls/FocusTrap"; import { logError } from "../services/loggingService"; import { ErrorCode } from "../types/errorCode"; import { - addExandedCatalogTagAsync, + addExpandedCatalogTagAsync, getExpandedCatalogTags, removeExpandedCatalogTagAsync, } from "../services/storageService"; @@ -133,7 +133,7 @@ const CatalogList: React.FC = () => { function onTagExpandToggled(tag: string, expanded: boolean) { if (expanded) { - /* await */ addExandedCatalogTagAsync(tag); + /* await */ addExpandedCatalogTagAsync(tag); } else { /* await */ removeExpandedCatalogTagAsync(tag); } @@ -147,7 +147,7 @@ const CatalogList: React.FC = () => { let expandedTags = getExpandedCatalogTags(); if (!expandedTags) { // If we haven't saved an expanded set, default expand the first one. - addExandedCatalogTagAsync(tags[0]); + addExpandedCatalogTagAsync(tags[0]); expandedTags = [tags[0]]; } diff --git a/teachertool/src/services/storageService.ts b/teachertool/src/services/storageService.ts index 5c9e060baa6e..630378ab1850 100644 --- a/teachertool/src/services/storageService.ts +++ b/teachertool/src/services/storageService.ts @@ -214,7 +214,7 @@ export async function setExpandedCatalogTags(tags: string[]) { } } -export async function addExandedCatalogTagAsync(tag: string) { +export async function addExpandedCatalogTagAsync(tag: string) { let expandedTags = getExpandedCatalogTags(); if (!expandedTags) { expandedTags = []; From c10516b8dd95852798cc50f3177a32c1814f237a Mon Sep 17 00:00:00 2001 From: thsparks Date: Wed, 8 May 2024 17:05:01 -0700 Subject: [PATCH 19/23] Create CatalogItem to encapsulate single entry in accordion panel --- teachertool/src/components/CatalogOverlay.tsx | 63 +++++++++++-------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/teachertool/src/components/CatalogOverlay.tsx b/teachertool/src/components/CatalogOverlay.tsx index e88d3ca83f95..01cf2aa80ec7 100644 --- a/teachertool/src/components/CatalogOverlay.tsx +++ b/teachertool/src/components/CatalogOverlay.tsx @@ -75,6 +75,37 @@ 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 ? ( +