diff --git a/docs/teachertool/catalog-shared-test.json b/docs/teachertool/catalog-shared-test.json
new file mode 100644
index 000000000000..fd1bd6b52308
--- /dev/null
+++ b/docs/teachertool/catalog-shared-test.json
@@ -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"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/docs/teachertool/catalog-shared.json b/docs/teachertool/catalog-shared.json
new file mode 100644
index 000000000000..9df4abc6afb0
--- /dev/null
+++ b/docs/teachertool/catalog-shared.json
@@ -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"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/teachertool/src/App.tsx b/teachertool/src/App.tsx
index 4ccba477196d..a1d134577511 100644
--- a/teachertool/src/App.tsx
+++ b/teachertool/src/App.tsx
@@ -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);
@@ -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
@@ -49,9 +59,11 @@ function App() {
+
);
diff --git a/teachertool/src/components/ActiveRubricDisplay.tsx b/teachertool/src/components/ActiveRubricDisplay.tsx
new file mode 100644
index 000000000000..23014a837dee
--- /dev/null
+++ b/teachertool/src/components/ActiveRubricDisplay.tsx
@@ -0,0 +1,39 @@
+///
+
+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 = ({}) => {
+ const { state: teacherTool, dispatch } = useContext(AppStateContext);
+
+ return (
+
+
{lf("Rubric")}
+ {teacherTool.selectedCriteria?.map(criteriaInstance => {
+ if (!criteriaInstance) return null;
+
+ const catalogCriteria = getCatalogCriteriaWithId(criteriaInstance.catalogCriteriaId);
+ return criteriaInstance.catalogCriteriaId && (
+
+ {catalogCriteria?.template}
+
+ );
+ })}
+
+
+ );
+};
+
+export default ActiveRubricDisplay;
diff --git a/teachertool/src/components/CatalogModal.tsx b/teachertool/src/components/CatalogModal.tsx
new file mode 100644
index 000000000000..6db5c102fb80
--- /dev/null
+++ b/teachertool/src/components/CatalogModal.tsx
@@ -0,0 +1,73 @@
+///
+
+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 = ({}) => {
+ const { state: teacherTool } = useContext(AppStateContext);
+ const [ checkedCriteriaIds, setCheckedCriteria ] = useState>(new Set());
+
+ 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());
+ }
+
+ const modalActions = [
+ {
+ label: lf("Cancel"),
+ className: "secondary",
+ onClick: closeModal,
+ },
+ {
+ label: lf("Add Selected"),
+ className: "primary",
+ onClick: handleAddSelectedClicked,
+ },
+ ]
+
+ return teacherTool.modal === "catalog-display" ? (
+
+ {teacherTool.catalog?.map(criteria => {
+ return criteria?.template && (
+ handleCriteriaSelectedChange(criteria, newValue)}
+ isChecked={isCriteriaSelected(criteria.id)} />
+ );
+ })}
+
+ ) : null;
+};
+
+export default CatalogModal;
diff --git a/teachertool/src/components/DebugInput.tsx b/teachertool/src/components/DebugInput.tsx
index 22d590730def..ba9527b34ef4 100644
--- a/teachertool/src/components/DebugInput.tsx
+++ b/teachertool/src/components/DebugInput.tsx
@@ -10,7 +10,7 @@ import { runEvaluateAsync } from "../transforms/runEvaluateAsync";
interface IProps {}
const DebugInput: React.FC = ({}) => {
- 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 () => {
diff --git a/teachertool/src/services/ackendRequests.ts b/teachertool/src/services/ackendRequests.ts
index 69a30bb318b9..e86b42514563 100644
--- a/teachertool/src/services/ackendRequests.ts
+++ b/teachertool/src/services/ackendRequests.ts
@@ -1,3 +1,4 @@
+import { ErrorCode } from "../types/errorCode";
import { logError } from "./loggingService";
export async function getProjectTextAsync(
@@ -13,7 +14,7 @@ export async function getProjectTextAsync(
return projectText;
}
} catch (e) {
- logError("getProjectTextAsync", e);
+ logError(ErrorCode.getProjectTextAsync, e);
}
}
@@ -30,7 +31,7 @@ export async function getProjectMetaAsync(
return projectMeta;
}
} catch (e) {
- logError("getProjectMetaAsync", e);
+ logError(ErrorCode.getProjectMetaAsync, e);
}
}
@@ -40,6 +41,6 @@ export async function downloadTargetConfigAsync(): Promise<
try {
return await pxt.targetConfigAsync();
} catch (e) {
- logError("downloadTargetConfigAsync", e);
+ logError(ErrorCode.downloadTargetConfigAsync, e);
}
}
diff --git a/teachertool/src/services/loggingService.ts b/teachertool/src/services/loggingService.ts
index 24e02ff6a7ac..5c2df766a7a7 100644
--- a/teachertool/src/services/loggingService.ts
+++ b/teachertool/src/services/loggingService.ts
@@ -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 = {}
) => {
- errorCode = formatName(errorCode);
let dataObj = { ...data };
if (message) {
if (typeof message === "object") {
diff --git a/teachertool/src/services/makecodeEditorService.ts b/teachertool/src/services/makecodeEditorService.ts
index d1aa7cd9f4e9..a8e3d8465194 100644
--- a/teachertool/src/services/makecodeEditorService.ts
+++ b/teachertool/src/services/makecodeEditorService.ts
@@ -1,3 +1,4 @@
+import { ErrorCode } from "../types/errorCode";
import { logDebug, logError } from "./loggingService";
interface PendingMessage {
@@ -96,7 +97,7 @@ export async function runEvalInEditorAsync(serializedRubric: string): Promise ({
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
};
diff --git a/teachertool/src/state/appStateContext.tsx b/teachertool/src/state/appStateContext.tsx
index b04c99c4d256..1274e2f28f5f 100644
--- a/teachertool/src/state/appStateContext.tsx
+++ b/teachertool/src/state/appStateContext.tsx
@@ -40,9 +40,16 @@ export function AppStateProvider(
): React.ReactElement {
// Read the URL parameters and set the initial state accordingly
const url = window.location.href;
+ const testCatalog = !!/testcatalog(?:[:=])1/.test(url) || !!/tc(?:[:=])1/.test(url);
// Create the application state and state change mechanism (dispatch)
- const [state_, dispatch_] = useReducer(reducer, {...initialAppState});
+ const [state_, dispatch_] = useReducer(reducer, {
+ ...initialAppState,
+ flags: {
+ ...initialAppState.flags,
+ testCatalog
+ }
+ });
// Make state and dispatch available outside the React context
useEffect(() => {
diff --git a/teachertool/src/state/helpers.ts b/teachertool/src/state/helpers.ts
new file mode 100644
index 000000000000..75c4852d929b
--- /dev/null
+++ b/teachertool/src/state/helpers.ts
@@ -0,0 +1,7 @@
+import { CatalogCriteria } from "../types/criteria";
+import { stateAndDispatch } from "./appStateContext";
+
+export function getCatalogCriteriaWithId(id: string): CatalogCriteria | undefined {
+ const { state } = stateAndDispatch();
+ return state.catalog?.find(c => c.id === id);
+}
\ No newline at end of file
diff --git a/teachertool/src/state/reducer.ts b/teachertool/src/state/reducer.ts
index f6aea646d39b..004230443e12 100644
--- a/teachertool/src/state/reducer.ts
+++ b/teachertool/src/state/reducer.ts
@@ -43,6 +43,36 @@ export default function reducer(state: AppState, action: Action): AppState {
currentEvalResult: action.result,
};
}
+ case "SET_CATALOG": {
+ return {
+ ...state,
+ catalog: action.catalog,
+ };
+ }
+ case "SET_SELECTED_CRITERIA": {
+ return {
+ ...state,
+ selectedCriteria: [...action.criteria],
+ };
+ }
+ case "REMOVE_CRITERIA_INSTANCE": {
+ return {
+ ...state,
+ selectedCriteria: state.selectedCriteria.filter(c => c.instanceId !== action.instanceId)
+ };
+ }
+ case "SHOW_MODAL": {
+ return {
+ ...state,
+ modal: action.modal,
+ };
+ }
+ case "HIDE_MODAL": {
+ return {
+ ...state,
+ modal: undefined,
+ };
+ }
case "SET_TARGET_CONFIG": {
return {
...state,
diff --git a/teachertool/src/state/state.ts b/teachertool/src/state/state.ts
index a08bddee54dc..8e3abde58a02 100644
--- a/teachertool/src/state/state.ts
+++ b/teachertool/src/state/state.ts
@@ -1,14 +1,27 @@
-import { Notifications } from "../types";
+import { ModalType, Notifications } from "../types";
+import { CatalogCriteria, CriteriaInstance } from "../types/criteria";
export type AppState = {
targetConfig?: pxt.TargetConfig;
notifications: Notifications;
currentEvalResult: pxt.blocks.EvaluationResult | undefined;
projectMetadata: pxt.Cloud.JsonScript | undefined;
+ catalog: CatalogCriteria[] | undefined;
+ selectedCriteria: CriteriaInstance[];
+ modal: ModalType | undefined;
+ flags: {
+ testCatalog: boolean;
+ }
};
export const initialAppState: AppState = {
notifications: [],
currentEvalResult: undefined,
projectMetadata: undefined,
+ catalog: undefined,
+ selectedCriteria: [],
+ modal: undefined,
+ flags: {
+ testCatalog: false
+ }
};
diff --git a/teachertool/src/teacherTool.css b/teachertool/src/teacherTool.css
index 96336e45c7de..36377f94b263 100644
--- a/teachertool/src/teacherTool.css
+++ b/teachertool/src/teacherTool.css
@@ -285,6 +285,64 @@ code {
display: none;
}
+/*******************************/
+/******* CATALOG STYLING ******/
+/*******************************/
+
+.catalog-modal .common-modal-footer {
+ display: flex;
+ justify-content: flex-end;
+}
+
+.catalog-done {
+ align-self: flex-end;
+}
+
+.catalog-item {
+ padding: 0.5rem;
+}
+
+/*******************************/
+/******* RUBRIC STYLING *******/
+/*******************************/
+
+.rubric-display {
+ border-radius: 0;
+ margin: 1rem 1rem;
+}
+
+.rubric-display h3 {
+ border-bottom: 1px solid #ddd;
+ padding-bottom: 0.1rem;
+ margin-bottom: 0.5rem;
+}
+
+.criteria-instance-display {
+ background-color: var(--white);
+ border: 1px solid #ddd;
+ border-radius: 0;
+ padding: 0.1rem 0.5rem;
+ margin-bottom: 0.5rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.criteria-btn-remove {
+ background-color: #f55c51;
+ color: var(--white);
+ border: none;
+ display: inline-block;
+ margin: 0.5rem;
+ padding: 0.5rem;
+ border-radius: 0;
+ transition: background-color 0.2s ease;
+}
+
+.criteria-btn-remove:hover {
+ background-color: #da4949;
+}
+
/*******************************/
/***** HIGH CONTRAST *****/
/*******************************/
diff --git a/teachertool/src/transforms/addCriteriaToRubric.ts b/teachertool/src/transforms/addCriteriaToRubric.ts
new file mode 100644
index 000000000000..3c1e99fe9d01
--- /dev/null
+++ b/teachertool/src/transforms/addCriteriaToRubric.ts
@@ -0,0 +1,44 @@
+import { stateAndDispatch } from "../state";
+import * as Actions from "../state/actions";
+import { getCatalogCriteriaWithId } from "../state/helpers";
+import { logDebug, logError } from "../services/loggingService";
+import { CriteriaInstance, CriteriaParameterValue } from "../types/criteria";
+import { nanoid } from "nanoid";
+import { ErrorCode } from "../types/errorCode";
+
+export function addCriteriaToRubric(catalogCriteriaIds: string[]) {
+ const { state: teacherTool, dispatch } = stateAndDispatch();
+
+ // Create instances for each of the catalog criteria.
+ const newSelectedCriteria = [...teacherTool.selectedCriteria ?? []]
+ for(const catalogCriteriaId of catalogCriteriaIds) {
+ const catalogCriteria = getCatalogCriteriaWithId(catalogCriteriaId);
+ if (!catalogCriteria) {
+ logError(ErrorCode.addingMissingCriteria, "Attempting to add criteria with unrecognized id", { id: catalogCriteriaId });
+ continue;
+ }
+
+ const params = catalogCriteria.parameters?.map(
+ param =>
+ ({
+ name: param.name,
+ value: undefined,
+ } as CriteriaParameterValue)
+ );
+
+ const instanceId = nanoid();
+
+ logDebug(`Adding criteria with Catalog ID '${catalogCriteriaId}' and Instance ID '${instanceId}'`);
+ const criteriaInstance = {
+ catalogCriteriaId,
+ instanceId,
+ params
+ } as CriteriaInstance;
+
+ newSelectedCriteria.push(criteriaInstance);
+ }
+
+ dispatch(Actions.setSelectedCriteria(newSelectedCriteria));
+
+ pxt.tickEvent("teachertool.addcriteria", { ids: JSON.stringify(catalogCriteriaIds) });
+}
\ No newline at end of file
diff --git a/teachertool/src/transforms/hideModal.ts b/teachertool/src/transforms/hideModal.ts
new file mode 100644
index 000000000000..ce5c6d958e30
--- /dev/null
+++ b/teachertool/src/transforms/hideModal.ts
@@ -0,0 +1,14 @@
+import { logDebug } from "../services/loggingService";
+import { stateAndDispatch } from "../state";
+import * as Actions from "../state/actions";
+import { ModalType } from "../types";
+
+export function hideModal(modal: ModalType) {
+ const { state: teacherTool, dispatch } = stateAndDispatch();
+
+ if (teacherTool.modal === modal) {
+ dispatch(Actions.hideModal());
+ } else {
+ logDebug(`Trying to hide '${modal}' model when it was not active`);
+ }
+}
\ No newline at end of file
diff --git a/teachertool/src/transforms/loadCatalogAsync.ts b/teachertool/src/transforms/loadCatalogAsync.ts
new file mode 100644
index 000000000000..67fc996a8583
--- /dev/null
+++ b/teachertool/src/transforms/loadCatalogAsync.ts
@@ -0,0 +1,39 @@
+import { stateAndDispatch } from "../state";
+import * as Actions from "../state/actions";
+import { logError } from "../services/loggingService";
+import { CatalogCriteria } from "../types/criteria";
+import { ErrorCode } from "../types/errorCode";
+
+const prodFiles = [
+ "/teachertool/catalog.json", // target-specific catalog
+ "/teachertool/catalog-shared.json" // shared across all targets
+];
+
+// Catalog entries still being tested, will only appear when in debug mode (?dbg=1)
+const testFiles = [
+ "/teachertool/catalog-test.json",
+ "/teachertool/catalog-shared-test.json"
+]
+
+interface CatalogInfo {
+ criteria: CatalogCriteria[];
+}
+
+export async function loadCatalogAsync() {
+ const { state: teacherTool, dispatch } = stateAndDispatch();
+ const catalogFiles = teacherTool.flags.testCatalog ? prodFiles.concat(testFiles) : prodFiles;
+
+ let fullCatalog: CatalogCriteria[] = [];
+ for (const catalogFile of catalogFiles) {
+ try {
+ const catalogResponse = await fetch(catalogFile);
+ const catalogContent = await catalogResponse.json() as CatalogInfo;
+ fullCatalog = fullCatalog.concat(catalogContent.criteria ?? []);
+ } catch (e) {
+ logError(ErrorCode.loadCatalogFailed, e, { catalogFile });
+ continue;
+ }
+ }
+
+ dispatch(Actions.setCatalog(fullCatalog));
+}
\ No newline at end of file
diff --git a/teachertool/src/transforms/removeCriteriaFromRubric.ts b/teachertool/src/transforms/removeCriteriaFromRubric.ts
new file mode 100644
index 000000000000..285509f7f47b
--- /dev/null
+++ b/teachertool/src/transforms/removeCriteriaFromRubric.ts
@@ -0,0 +1,12 @@
+import { stateAndDispatch } from "../state";
+import * as Actions from "../state/actions";
+import { logDebug } from "../services/loggingService";
+import { CriteriaInstance } from "../types/criteria";
+
+export function removeCriteriaFromRubric(instance: CriteriaInstance) {
+ logDebug(`Removing criteria with id: ${instance.instanceId}`);
+
+ const { dispatch } = stateAndDispatch();
+ dispatch(Actions.removeCriteriaInstance(instance.instanceId));
+ pxt.tickEvent("teachertool.removecriteria", { catalogCriteriaId: instance.catalogCriteriaId });
+}
\ No newline at end of file
diff --git a/teachertool/src/transforms/showCatalogModal.ts b/teachertool/src/transforms/showCatalogModal.ts
new file mode 100644
index 000000000000..bbf53e20ac41
--- /dev/null
+++ b/teachertool/src/transforms/showCatalogModal.ts
@@ -0,0 +1,7 @@
+import { stateAndDispatch } from "../state";
+import * as Actions from "../state/actions";
+
+export function showCatalogModal() {
+ const { dispatch } = stateAndDispatch();
+ dispatch(Actions.showModal("catalog-display"));
+}
\ No newline at end of file
diff --git a/teachertool/src/types/criteria.ts b/teachertool/src/types/criteria.ts
new file mode 100644
index 000000000000..afddd662a209
--- /dev/null
+++ b/teachertool/src/types/criteria.ts
@@ -0,0 +1,29 @@
+// A criteria defined in the catalog of all possible criteria for the user to choose from when creating a rubric.
+export interface CatalogCriteria {
+ id: string; // A unique id (GUID) for the catalog criteria
+ use: string; // Refers to the validator plan this criteria relies upon
+ template: string; // A (mostly) human-readable string describing the criteria. May contain parameters
+ description: string | undefined; // More detailed description
+ docPath: string | undefined; // Path to documentation
+ parameters: CriteriaParameter[] | undefined; // Any parameters that affect the criteria
+}
+
+// An instance of a criteria in a rubric.
+export interface CriteriaInstance {
+ catalogCriteriaId: string;
+ instanceId: string;
+ params: CriteriaParameterValue[] | undefined;
+}
+
+// Represents a parameter definition in a catalog criteria.
+export interface CriteriaParameter {
+ name: string;
+ type: string;
+ path: string; // The json path of the parameter in the catalog criteria.
+}
+
+// Represents a parameter value in a criteria instance.
+export interface CriteriaParameterValue {
+ name: string;
+ value: any; // Undefined if no value has been selected.
+}
\ No newline at end of file
diff --git a/teachertool/src/types/errorCode.ts b/teachertool/src/types/errorCode.ts
new file mode 100644
index 000000000000..bd9ff3732ee3
--- /dev/null
+++ b/teachertool/src/types/errorCode.ts
@@ -0,0 +1,8 @@
+export enum ErrorCode {
+ runEval = "runEval",
+ addingMissingCriteria = "addingMissingCriteria",
+ loadCatalogFailed = "loadCatalogFailed",
+ getProjectTextAsync = "getProjectTextAsync",
+ getProjectMetaAsync = "getProjectMetaAsync",
+ downloadTargetConfigAsync = "downloadTargetConfigAsync"
+}
\ No newline at end of file
diff --git a/teachertool/src/types/index.ts b/teachertool/src/types/index.ts
index 3be0d9b0e009..76dff186f9ce 100644
--- a/teachertool/src/types/index.ts
+++ b/teachertool/src/types/index.ts
@@ -8,4 +8,6 @@ export type NotificationWithId = Notification & {
expiration: number; // Date.now() + duration
};
-export type Notifications = NotificationWithId[];
\ No newline at end of file
+export type Notifications = NotificationWithId[];
+
+export type ModalType = "catalog-display";
\ No newline at end of file