Skip to content

Commit

Permalink
Load Teacher Tools Catalog from Docs, Store in State (#9829)
Browse files Browse the repository at this point in the history
This change includes the following:
1. Loading the catalog from docs files. (Shared and target-specific files are possible, with "live" and "test" files in each. Test catalog entries are only loaded if the URL contains testcatalog=1 or tc=1.)
2. Storing catalog in state once loaded
3. Selecting catalog criteria items to add to your rubric
4. Storing selected criteria instances in state
5. Basic UI for selecting criteria & displaying selected criteria
6. A few new types needed to read/parse criteria json

Notably, it does not include:
1. Any linking between the selected criteria and what gets sent to the editor for validation
2. Generating validator plans from the selected criteria (would be a prerequisite to sending to the editor)
3. Any target-specific catalogs (those will need to be checked into their respective pxt-target repos)
  • Loading branch information
thsparks authored Jan 25, 2024
1 parent 27109ad commit cd7ed80
Show file tree
Hide file tree
Showing 23 changed files with 523 additions and 22 deletions.
19 changes: 19 additions & 0 deletions docs/teachertool/catalog-shared-test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"criteria": [
{
"id": "7AE7EA2A-3AC8-42DC-89DB-65E3AE157156",
"use": "check_for_comments",
"template": "At least ${count} comments",
"description": "The project contains at least the specified number of comments.",
"docPath": "/teachertool",
"params": [
{
"name": "count",
"type": "number",
"default": 1,
"path": "checks[0].count"
}
]
}
]
}
30 changes: 30 additions & 0 deletions docs/teachertool/catalog-shared.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"criteria": [
{
"id": "A7C78825-51B5-45B0-9E42-2AAC767D4E02",
"use": "processes_and_displays_array_in_loop",
"template": "One kind of loop to process/display the information in an array",
"docPath": "/teachertool"
},
{
"id": "59AAC5BA-B0B3-4389-AA90-1E767EFA8563",
"use": "block_used_n_times",
"template": "${block_id} used ${count} times",
"description": "This block was used the specified number of times in your project.",
"docPath": "/teachertool",
"params": [
{
"name": "block_id",
"type": "block_id",
"path": "checks[0].blocks[:clear&:append]"
},
{
"name": "count",
"type": "number",
"default": 1,
"path": "checks[0].count"
}
]
}
]
}
24 changes: 18 additions & 6 deletions teachertool/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,23 @@ import { useEffect, useContext, useState } from "react";
import "./teacherTool.css";
import { AppStateContext, AppStateReady } from "./state/appStateContext";
import { usePromise } from "./hooks";
import { makeNotification } from "./utils";
import * as Actions from "./state/actions";
import * as NotificationService from "./services/notificationService";
import { downloadTargetConfigAsync } from "./services/ackendRequests";
import { logDebug } from "./services/loggingService";

import HeaderBar from "./components/HeaderBar";
import Notifications from "./components/Notifications";
import * as NotificationService from "./services/notificationService";
import { postNotification } from "./transforms/postNotification";
import { makeNotification } from "./utils";
import DebugInput from "./components/DebugInput";
import { MakeCodeFrame } from "./components/MakecodeFrame";
import EvalResultDisplay from "./components/EvalResultDisplay";
import { downloadTargetConfigAsync } from "./services/ackendRequests";
import * as Actions from "./state/actions";
import { logDebug } from "./services/loggingService";
import ActiveRubricDisplay from "./components/ActiveRubricDisplay";
import CatalogModal from "./components/CatalogModal";

import { postNotification } from "./transforms/postNotification";
import { loadCatalogAsync } from "./transforms/loadCatalogAsync";


function App() {
const { state, dispatch } = useContext(AppStateContext);
Expand All @@ -28,6 +34,10 @@ function App() {
const cfg = await downloadTargetConfigAsync();
dispatch(Actions.setTargetConfig(cfg || {}));
pxt.BrowserUtils.initTheme();

// Load criteria catalog
await loadCatalogAsync();

// TODO: Remove this. Delay app init to expose any startup race conditions.
setTimeout(() => {
// Test notification
Expand All @@ -49,9 +59,11 @@ function App() {
<HeaderBar />
<div className="inner-app-container">
<DebugInput />
<ActiveRubricDisplay />
<EvalResultDisplay />
<MakeCodeFrame />
</div>
<CatalogModal />
<Notifications />
</div>
);
Expand Down
39 changes: 39 additions & 0 deletions teachertool/src/components/ActiveRubricDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/// <reference path="../../../built/pxtblocks.d.ts"/>

import { useContext } from "react";
import { AppStateContext } from "../state/appStateContext";
import { getCatalogCriteriaWithId } from "../state/helpers";
import { Button } from "react-common/components/controls/Button";
import { removeCriteriaFromRubric } from "../transforms/removeCriteriaFromRubric";
import { showCatalogModal } from "../transforms/showCatalogModal";


interface IProps {}

const ActiveRubricDisplay: React.FC<IProps> = ({}) => {
const { state: teacherTool, dispatch } = useContext(AppStateContext);

return (
<div className="rubric-display">
<h3>{lf("Rubric")}</h3>
{teacherTool.selectedCriteria?.map(criteriaInstance => {
if (!criteriaInstance) return null;

const catalogCriteria = getCatalogCriteriaWithId(criteriaInstance.catalogCriteriaId);
return criteriaInstance.catalogCriteriaId && (
<div className="criteria-instance-display" key={criteriaInstance.instanceId}>
{catalogCriteria?.template}
<Button
className="criteria-btn-remove"
label={lf("X")}
onClick={() => removeCriteriaFromRubric(criteriaInstance)}
title={lf("Remove")} />
</div>
);
})}
<Button className="add-criteria secondary" label={lf("+ Add Criteria")} onClick={showCatalogModal} title={lf("Add Criteria")} />
</div>
);
};

export default ActiveRubricDisplay;
73 changes: 73 additions & 0 deletions teachertool/src/components/CatalogModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/// <reference path="../../../built/pxtblocks.d.ts"/>

import { useContext, useState } from "react";
import { AppStateContext } from "../state/appStateContext";
import { Checkbox } from "react-common/components/controls/Checkbox";
import { Modal } from "react-common/components/controls/Modal";
import { hideModal } from "../transforms/hideModal";
import { addCriteriaToRubric } from "../transforms/addCriteriaToRubric";
import { CatalogCriteria } from "../types/criteria";

interface IProps {}

const CatalogModal: React.FC<IProps> = ({}) => {
const { state: teacherTool } = useContext(AppStateContext);
const [ checkedCriteriaIds, setCheckedCriteria ] = useState<Set<string>>(new Set<string>());

function handleCriteriaSelectedChange(criteria: CatalogCriteria, newValue: boolean) {
const newSet = new Set(checkedCriteriaIds);
if (newValue) {
newSet.add(criteria.id);
} else {
newSet.delete(criteria.id); // Returns false if criteria.id is not in the set, can be safely ignored.
}
setCheckedCriteria(newSet);
}

function isCriteriaSelected(criteriaId: string): boolean {
return checkedCriteriaIds.has(criteriaId);
}

function handleAddSelectedClicked() {
addCriteriaToRubric([...checkedCriteriaIds]);
closeModal();
}

function closeModal() {
hideModal("catalog-display");

// Clear for next open.
setCheckedCriteria(new Set<string>());
}

const modalActions = [
{
label: lf("Cancel"),
className: "secondary",
onClick: closeModal,
},
{
label: lf("Add Selected"),
className: "primary",
onClick: handleAddSelectedClicked,
},
]

return teacherTool.modal === "catalog-display" ? (
<Modal className="catalog-modal" title={lf("Select the criteria you'd like to include")} onClose={closeModal} actions={modalActions}>
{teacherTool.catalog?.map(criteria => {
return criteria?.template && (
<Checkbox
id={`checkbox_${criteria.id}`}
key={criteria.id}
className="catalog-item"
label={criteria.template}
onChange={(newValue) => handleCriteriaSelectedChange(criteria, newValue)}
isChecked={isCriteriaSelected(criteria.id)} />
);
})}
</Modal>
) : null;
};

export default CatalogModal;
2 changes: 1 addition & 1 deletion teachertool/src/components/DebugInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { runEvaluateAsync } from "../transforms/runEvaluateAsync";
interface IProps {}

const DebugInput: React.FC<IProps> = ({}) => {
const [shareLink, setShareLink] = useState("https://arcade.makecode.com/S70821-26848-68192-30094");
const [shareLink, setShareLink] = useState("https://makecode.microbit.org/S95591-52406-50965-65671");
const [rubric, setRubric] = useState("");

const evaluate = async () => {
Expand Down
7 changes: 4 additions & 3 deletions teachertool/src/services/ackendRequests.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ErrorCode } from "../types/errorCode";
import { logError } from "./loggingService";

export async function getProjectTextAsync(
Expand All @@ -13,7 +14,7 @@ export async function getProjectTextAsync(
return projectText;
}
} catch (e) {
logError("getProjectTextAsync", e);
logError(ErrorCode.getProjectTextAsync, e);
}
}

Expand All @@ -30,7 +31,7 @@ export async function getProjectMetaAsync(
return projectMeta;
}
} catch (e) {
logError("getProjectMetaAsync", e);
logError(ErrorCode.getProjectMetaAsync, e);
}
}

Expand All @@ -40,6 +41,6 @@ export async function downloadTargetConfigAsync(): Promise<
try {
return await pxt.targetConfigAsync();
} catch (e) {
logError("downloadTargetConfigAsync", e);
logError(ErrorCode.downloadTargetConfigAsync, e);
}
}
9 changes: 3 additions & 6 deletions teachertool/src/services/loggingService.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import { ErrorCode } from "../types/errorCode";

const timestamp = () => {
const time = new Date();
return `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}]`;
};

const formatName = (name: string) => {
return name.toLowerCase().replace(/ /g, "_");
};

export const logError = (
errorCode: string,
errorCode: ErrorCode,
message?: any,
data: pxt.Map<string | number> = {}
) => {
errorCode = formatName(errorCode);
let dataObj = { ...data };
if (message) {
if (typeof message === "object") {
Expand Down
3 changes: 2 additions & 1 deletion teachertool/src/services/makecodeEditorService.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ErrorCode } from "../types/errorCode";
import { logDebug, logError } from "./loggingService";

interface PendingMessage {
Expand Down Expand Up @@ -96,7 +97,7 @@ export async function runEvalInEditorAsync(serializedRubric: string): Promise<px
validateResponse(result, true); // Throws on failure
evalResults = result.resp as pxt.blocks.EvaluationResult;
} catch (e: any) {
logError("runeval_error", e);
logError(ErrorCode.runEval, e);
}

return evalResults;
Expand Down
63 changes: 61 additions & 2 deletions teachertool/src/state/actions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NotificationWithId } from "../types";
import { ModalType, NotificationWithId } from "../types";
import { CatalogCriteria, CriteriaInstance } from "../types/criteria";

// Changes to app state are performed by dispatching actions to the reducer
type ActionBase = {
Expand Down Expand Up @@ -33,6 +34,30 @@ type SetTargetConfig = ActionBase & {
config: pxt.TargetConfig;
};

type SetCatalog = ActionBase & {
type: "SET_CATALOG";
catalog: CatalogCriteria[] | undefined;
};

type SetSelectedCriteria = ActionBase & {
type: "SET_SELECTED_CRITERIA"
criteria: CriteriaInstance[];
};

type RemoveCriteriaInstance = ActionBase & {
type: "REMOVE_CRITERIA_INSTANCE";
instanceId: string;
};

type ShowModal = ActionBase & {
type: "SHOW_MODAL";
modal: ModalType;
};

type HideModal = ActionBase & {
type: "HIDE_MODAL";
};

/**
* Union of all actions
*/
Expand All @@ -42,7 +67,12 @@ export type Action =
| RemoveNotification
| SetProjectMetadata
| SetEvalResult
| SetTargetConfig;
| SetTargetConfig
| SetCatalog
| SetSelectedCriteria
| RemoveCriteriaInstance
| ShowModal
| HideModal;

/**
* Action creators
Expand Down Expand Up @@ -78,10 +108,39 @@ const setTargetConfig = (config: pxt.TargetConfig): SetTargetConfig => ({
config,
});

const setCatalog = (catalog: CatalogCriteria[] | undefined): SetCatalog => ({
type: "SET_CATALOG",
catalog,
});

const setSelectedCriteria = (criteria: CriteriaInstance[]): SetSelectedCriteria => ({
type: "SET_SELECTED_CRITERIA",
criteria,
});

const removeCriteriaInstance = (instanceId: string): RemoveCriteriaInstance => ({
type: "REMOVE_CRITERIA_INSTANCE",
instanceId,
});

const showModal = (modal: ModalType): ShowModal => ({
type: "SHOW_MODAL",
modal,
});

const hideModal = (): HideModal => ({
type: "HIDE_MODAL",
});

export {
postNotification,
removeNotification,
setProjectMetadata,
setEvalResult,
setTargetConfig,
setCatalog,
setSelectedCriteria,
removeCriteriaInstance,
showModal,
hideModal
};
Loading

0 comments on commit cd7ed80

Please sign in to comment.