Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Load Teacher Tools Catalog from Docs, Store in State #9829

Merged
merged 41 commits into from
Jan 25, 2024

Conversation

thsparks
Copy link
Contributor

This change includes the following:

  1. Loading the catalog from docs files (shared and target-specific, with "live" and "test" files in each).
  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)

A few notes:

  1. Test catalog entries are only loaded if the URL contains dbg=1. We could also show this in localhost builds, but I decided not to since I thought that could lead to confusion if someone saw a new catalog entry in localhost and didn't realize it needed to be moved to be visible in live.
  2. In order to generate GUIDS, I added the uuid package: https://www.npmjs.com/package/uuid

Unfortunately upload targets don't work well with docs, but here are some screenshots:
image

image image

@thsparks thsparks requested a review from a team January 23, 2024 20:42
Copy link
Contributor

@srietkerk srietkerk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have lots of questions and comments, but this is super exciting stuff. Great work!!

}

// Possible values for CriteriaParameterPicker, these are different ways a user may enter parameter values.
export type CriteriaParameterPicker = "blocksPicker" | "numericInput";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Styling nit but also a question. I was thinking that it would be nice to have types defined before the interfaces, but maybe it makes more sense to have this close to CriteriaParameter since that's where it's used. Are there standards around this area?

@@ -9,11 +9,13 @@
"version": "0.1.0",
"dependencies": {
"@types/node": "^16.11.33",
"@types/uuid": "^9.0.7",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you look into if we have this somewhere common? I'm wondering if there is somewhere client-side where we make unique ids already and we can thus avoid adding this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked and wasn't able to find one. I was also surprised that we hadn't already needed to do this elsewhere, but searching for guid/uuid didn't turn up much at all.


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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: is the <string[]> needed when using useState? I don't think I usually include it when the types are more definitive (list, string, number, bool) and want to make sure that I'm following convention.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to make this into a set? I think we could make handleCheckboxChange simpler if so.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I add it when the type is not easily inferred from the default value. So it's not really needed for a number or a string, but in this case it's not clear what type is expected in the list, so I think it's helpful to include.

Set might work well. I'll take a look.

hideCatalogModal();

// Clear for next open.
setCheckedCriteria([]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something we want to do? I thought we wanted to keep criteria that have already been added to the rubric "checked" so we don't have duplicates.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By design, we want to enable duplicates of some criteria. We should probably hide criteria from this list if they've already been added and duplicates aren't allowed (I forgot to do that, but I can add it...). The checks are specifically for which new criteria we're adding in this "modal-session" (for lack of a better term).

Copy link
Contributor

@srietkerk srietkerk Jan 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense! I don't think the "don't allow duplicates" case is something that needs to be included in this PR.

export const logError = (name: string, details: string) => {
pxt.tickEvent("teachertool.error", { name: formatName(name), message: details });
console.error(formatMessageForConsole(`${name}: ${details}`));
export const logError = (name: string, details: string, props: pxt.Map<string | number> = {}) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: rename props to data or something similar. When I think of props, I think of React props..

const { state: teacherTool, dispatch } = useContext(AppStateContext);
const [ checkedCriteriaIds, setCheckedCriteria ] = useState<string[]>([]);

function handleCheckboxChange(criteria: pxt.blocks.CatalogCriteria, newValue: boolean) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something I was thinking about.. What if we added a selected field to the Catalog Criteria Schema? Since we parse the catalog from docs into state, we can manage whether or not something is checked from the catalog directly. The look-up for an object like that would be faster, right? And then when the checkbox is changed, we can just toggle that field. The done and close modal would have to be modified, though. Also, I'm not a huge fan of the implications that this has on state.. it feels kinda icky that a modal would have so much power over the state of the catalog, but I also feel like this handler should be simpler than having to do a look-up or filter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want the user to be able to have multiple instances of the same catalog criteria, so just having selected on catalog criteria doesn't quite work (if it did, there'd be no real need for criteria instances at all).

I'm not sure I quite understand the concern around the handler. Could you elaborate on that a bit?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, my idea would be that the selected state on the catalog would be reset every time the modal is closed so when the user re-opens the modal, they have a fresh state. This might be more work than it's worth.

In regards to the handler, I'm a bit worried about a list operation every time the box is checked or unchecked. To be fair, they shouldn't be very computationally taxing, but the list operations plus conditions when we want to toggle a condition for something seems like a lot of logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per our discussion in person, I think something like that might work (we'd need a new component like a CriteriaCheckbox to store selected, since I don't think we want UI-related fields to leak out into non-UI-specific classes), but it would probably just move the complexity elsewhere rather than actually simplifying things, so I'm inclined to leave this as-is at the moment.

const { state: teacherTool, dispatch } = stateAndDispatch();

// Create instances for each of the catalog criteria.
const instances = catalogCriteriaIds.reduce((accumulator, catalogCriteriaId) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reduce

I should probably just use a for...of loop here instead of reduce. Would be cleaner & easier to read.

Copy link
Collaborator

@eanders-ms eanders-ms left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still reviewing, but wanted to send out my comments so far.

@@ -37,9 +45,11 @@ function App() {
<HeaderBar />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note that we need to put all this rendering behind the ready flag. Otherwise stateAndDispatch may not be ready in code triggered by the first UI render.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave this be since you've added one in #9830

<Checkbox
id={`chk${criteria.id}`}
className="catalog-item"
label={criteria.template}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Figure out how to send this through localization.

criteria: pxt.blocks.CatalogCriteria[];
}

export async function loadCatalog() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export async function loadCatalog() {
export async function loadCatalogAsync() {

Update filename to match.

import * as Actions from "../state/actions";
import { logDebug, logError } from "../services/loggingService";

export async function addCriteriaToRubric(catalogCriteriaIds: string[]) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export async function addCriteriaToRubric(catalogCriteriaIds: string[]) {
export async function addCriteriaToRubricAsync(catalogCriteriaIds: string[]) {

import { stateAndDispatch } from "../state";
import * as Actions from "../state/actions";

export async function hideCatalogModal() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be async?

import * as Actions from "../state/actions";
import { logDebug } from "../services/loggingService";

export async function removeCriteriaFromRubric(instance: pxt.blocks.CriteriaInstance) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be async?

@@ -1,5 +1,6 @@
import { nanoid } from "nanoid";
import { NotificationWithId } from "../types";
import { stateAndDispatch } from "../state";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep app state out of the utils file. I suggest moving getCatalogCriteriaWithId to /src/state/helpers.ts. We'll probably accumulate more of these helpers, and I'd like to keep utils uncomplicated.

const catalogInfo = catalogInfoParsed as CatalogInfo;
fullCatalog = fullCatalog.concat(catalogInfo.criteria ?? []);
} catch (e) {
logError("parse_catalog_failed", e as string, {catalogFile});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be nice to organize these error codes in one place and refer to them here like Errors.parse_catalog_failed. This would simplify looking up error codes when building a Kusto query.

}

// Possible values for CriteriaParameterPicker, these are different ways a user may enter parameter values.
export type CriteriaParameterPicker = "blocksPicker" | "numericInput";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"numericInput"

I don't think we need a picker for numbers. We can make the param value directly editable.

export interface CriteriaParameter {
name: string;
type: string;
picker: CriteriaParameterPicker;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I'm thinking we don't need a picker field at all. We can use the type field to infer the picker. type: "block_id" for the block picker.


export async function loadCatalog() {
const { dispatch } = stateAndDispatch();
const catalogFiles = pxt.options.debug ? prodFiles.concat(testFiles) : prodFiles;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pxt.options.debug

Use a different flag or beta?

NotificationService.initialize();

// Load criteria catalog
loadCatalog();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadCatalog

Additional flag for when all this is done?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate PR :)

let catalogContent = "";
try {
const catalogResponse = await fetch(catalogFile);
catalogContent = await catalogResponse.text();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

text

json accessor?

},
]

return teacherTool.modal ? (
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

teacherTool.modal

teacherTool.modal == "catalog-modal"


return teacherTool.modal ? (
<Modal className="catalog-modal" title={lf("Select the criteria you'd like to include")} onClose={closeModal} actions={modalActions}>
<div className="catalog-container" title={lf("Select the criteria you'd like to include")}>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[](http://example.com/codeflow?start=12&length=91)

redundant now?

@@ -0,0 +1,35 @@
namespace pxt.blocks {
// A criteria defined in the catalog of all possible criteria for the user to choose from when creating a rubric.
export interface CatalogCriteria {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CatalogCriteria

move criteria types to teacher tool

catalog: action.catalog,
};
}
case "ADD_CRITERIA_INSTANCES": {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ADD_CRITERIA_INSTANCES

Fully set, not append

const catalogCriteria = getCatalogCriteriaWithId(criteriaInstance.catalogCriteriaId);

return criteriaInstance?.catalogCriteriaId && (
<div className="criteria-instance-display" id={`criteriaInstance${criteriaInstance.instanceId}`}>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

div

key here

import { stateAndDispatch } from "../state";
import * as Actions from "../state/actions";

export async function hideCatalogModal() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hideCatalogModal

hideModal(ModalType)

Copy link
Contributor

@kimprice kimprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the walkthrough you did yesterday, I have no concerns about this PR. LGTM! :)

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

// Load criteria catalog
await loadCatalogAsync();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eanders-ms I wasn't totally sure how to plug into this initialization code. Is this suitable?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, looks good! If we end up with a lot of async tasks here we should try to parallelize some of them. Nothing to worry about yet.

Copy link
Collaborator

@eanders-ms eanders-ms left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

@@ -0,0 +1,8 @@
export enum ErrorCode {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh nice. We might want to strongly type logError to this. logError(errorCode: ErrorCode,

@thsparks thsparks merged commit cd7ed80 into master Jan 25, 2024
6 checks passed
@thsparks thsparks deleted the thsparks/add_catalog branch January 25, 2024 21:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants