Skip to content

Commit

Permalink
Initial Parameterized Input Support (#9911)
Browse files Browse the repository at this point in the history
This adds support for string and number parameters in criteria. I've tried to make the UI passable, but it's not perfect (particularly, I think the text boxes should expand to fit contents, it lacks signal when there's an issue, like an empty parameter, and the results page should probably put boxes around the parameter values). I'd like to follow up with a separate change to polish all that off, since this one is a bit big already.

One notable adjustment, I made the parameter path field into a list so that one parameter can affect multiple values in a validator plan (useful if the thing you're validating requires multiple checks, like custom functions are created and called).
  • Loading branch information
thsparks authored Mar 8, 2024
1 parent 80c84d3 commit a3c462c
Show file tree
Hide file tree
Showing 22 changed files with 473 additions and 52 deletions.
59 changes: 51 additions & 8 deletions common-docs/teachertool/test/catalog-shared.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"criteria": [
{
"id": "7AE7EA2A-3AC8-42DC-89DB-65E3AE157156",
"use": "check_for_comments",
"use": "block_comment_used",
"template": "At least ${count} comments",
"description": "The project contains at least the specified number of comments.",
"docPath": "/teachertool",
Expand All @@ -11,30 +11,73 @@
"name": "count",
"type": "number",
"default": 1,
"path": "checks[0].count"
"paths": ["checks[0].count"]
}
]
},
{
"id": "59AAC5BA-B0B3-4389-AA90-1E767EFA8563",
"use": "block_used_n_times",
"template": "${block_id} used ${count} times",
"template": "${Block} used ${count} times",
"description": "This block was used the specified number of times in your project.",
"docPath": "/teachertool",
"params": [
{
"name": "block_id",
"name": "block",
"type": "string",
"picker": "blockSelector",
"path": "checks[0].blocks[:clear&:append]"
"paths": ["checks[0].blockCounts[0].blockId"]
},
{
"name": "count",
"type": "number",
"default": 1,
"path": "checks[0].count"
"paths": ["checks[0].blockCounts[0].count"]
}
]
},
{
"id": "499F3572-E655-4DEE-953B-5F26BF0191D7",
"use": "block_used_n_times",
"template": "Long String: ${question}",
"description": "This is just a test for long string inputs.",
"docPath": "/teachertool",
"params": [
{
"name": "question",
"type": "longString",
"paths": ["checks[0].blockCounts[0].blockId"]
}
]
},
{
"id": "B8987394-1531-4C71-8661-BE4086CE0C6E",
"use": "n_loops",
"template": "At least ${count} loops used",
"docPath": "/teachertool",
"description": "The program uses at least this many loops of any kind (for, repeat, while, or for-of).",
"params": [
{
"name": "count",
"type": "number",
"paths": ["checks[0].count"],
"default": 1
}
]
},
{
"id": "79D5DAF7-FED3-473F-81E2-E004922E5F55",
"use": "custom_function_called",
"template": "At least ${count} custom functions exist and get called",
"docPath": "/teachertool",
"description": "At least this many user-defined functions are created and called.",
"params": [
{
"name": "count",
"type": "number",
"paths": ["checks[0].count", "checks[1].count"],
"default": 1
}
]
}
]
}
}
12 changes: 12 additions & 0 deletions common-docs/teachertool/test/validator-plans-shared.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@
]
}
]
},
{
".desc": "Loops used n times (to be filled in by the user)",
"name": "n_loops",
"threshold": 1,
"checks": [
{
"validator": "blocksInSetExist",
"blocks": ["controls_repeat_ext", "device_while", "pxt_controls_for", "pxt_controls_for_of"],
"count": 0
}
]
}
]
}
2 changes: 1 addition & 1 deletion react-common/components/controls/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,4 +220,4 @@ export const Input = (props: InputProps) => {
}
</div>
);
}
}
12 changes: 12 additions & 0 deletions teachertool/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions teachertool/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@types/jsonpath": "^0.2.4",
"@types/node": "^16.11.33",
"framer-motion": "^6.5.1",
"idb": "^7.1.1",
"jsonpath": "^1.1.1",
"nanoid": "^4.0.2",
"qrcode.react": "^3.1.0",
"react-scripts": "5.0.1",
Expand Down
15 changes: 14 additions & 1 deletion teachertool/src/components/CatalogModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,27 @@ import { addCriteriaToRubric } from "../transforms/addCriteriaToRubric";
import { CatalogCriteria } from "../types/criteria";
import { getSelectableCatalogCriteria } from "../state/helpers";
import css from "./styling/CatalogModal.module.scss";
import { splitCriteriaTemplate } from "../utils";

interface CatalogCriteriaDisplayProps {
criteria: CatalogCriteria;
}
const CatalogCriteriaDisplay: React.FC<CatalogCriteriaDisplayProps> = ({ criteria }) => {
const segments = useMemo(() => splitCriteriaTemplate(criteria.template), [criteria.template]);

return (
<div className={css["criteria-display"]}>
{criteria.template && <div className={css["criteria-template"]}>{criteria.template}</div>}
{criteria.template && (
<div className={css["criteria-template"]}>
{segments.map((segment, index) => {
return (
<span key={`${criteria.id}-${index}`} className={css[`${segment.type}-segment`]}>
{segment.content}
</span>
);
})}
</div>
)}
{criteria.description && <div className={css["criteria-description"]}>{criteria.description}</div>}
</div>
);
Expand Down
106 changes: 106 additions & 0 deletions teachertool/src/components/CriteriaInstanceDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { getCatalogCriteriaWithId } from "../state/helpers";
import { CriteriaInstance, CriteriaParameterValue } from "../types/criteria";
import { DebouncedInput } from "./DebouncedInput";
import { logDebug } from "../services/loggingService";
import { setParameterValue } from "../transforms/setParameterValue";
import { classList } from "react-common/components/util";
import { splitCriteriaTemplate } from "../utils";
// eslint-disable-next-line import/no-internal-modules
import css from "./styling/CriteriaInstanceDisplay.module.scss";

interface InlineInputSegmentProps {
initialValue: string;
instance: CriteriaInstance;
param: CriteriaParameterValue;
shouldExpand: boolean;
numeric: boolean;
}
const InlineInputSegment: React.FC<InlineInputSegmentProps> = ({
initialValue,
instance,
param,
shouldExpand,
numeric,
}) => {
function onChange(newValue: string) {
setParameterValue(instance.instanceId, param.name, newValue);
}

return (
<DebouncedInput
className={classList(
css["inline-input"],
numeric ? css["number-input"] : css["string-input"],
shouldExpand ? css["long"] : undefined,
)}
initialValue={initialValue}
onChange={onChange}
preserveValueOnBlur={true}
placeholder={numeric ? "0" : param.name}
title={param.name}
autoComplete={false}
type = {numeric ? "number" : "text"}
/>
);
};

interface CriteriaInstanceDisplayProps {
criteriaInstance: CriteriaInstance;
}

export const CriteriaInstanceDisplay: React.FC<CriteriaInstanceDisplayProps> = ({ criteriaInstance }) => {
const catalogCriteria = getCatalogCriteriaWithId(criteriaInstance.catalogCriteriaId);
if (!catalogCriteria) {
return null;
}

function getParameterSegmentDisplay(paramName: string): JSX.Element | null {
if (!paramName) {
return null;
}

const paramDef = catalogCriteria?.params?.find(p => p.name === paramName);
const paramInstance = criteriaInstance?.params?.find(p => p.name === paramName);
if (!paramDef || !paramInstance) {
logDebug(`Missing info for '${paramName}': paramDef=${paramDef}, paramInstance=${paramInstance}`);
return null;
}

if (paramDef.type === "block") {
// TODO
return null;
} else {
return (
<InlineInputSegment
initialValue={paramInstance.value}
param={paramInstance}
instance={criteriaInstance}
shouldExpand={paramDef.type === "longString"}
numeric={paramDef.type === "number"}
/>
);
}
}

function getPlainTextSegmentDisplay(text: string): JSX.Element | null {
return text ? <div className={css["text-segment"]}>{text}</div> : null;
}

const templateSegments = splitCriteriaTemplate(catalogCriteria.template);
const display = templateSegments.map(s =>
s.type === "plain-text" ? getPlainTextSegmentDisplay(s.content) : getParameterSegmentDisplay(s.content)
);

return catalogCriteria ? (
<div className={css["criteria-instance-display"]}>
<div className={css["segment-container"]}>
{display.map((part, i) => (
<span className={css["segment"]} key={i}>
{part}
</span>
))}
</div>
<div className={css["criteria-description"]}>{catalogCriteria.description}</div>
</div>
) : null;
};
26 changes: 16 additions & 10 deletions teachertool/src/components/CriteriaResultEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Strings, Ticks } from "../constants";
import { setEvalResultNotes } from "../transforms/setEvalResultNotes";
import { CriteriaEvalResultDropdown } from "./CriteriaEvalResultDropdown";
import { DebouncedTextarea } from "./DebouncedTextarea";
import { getCatalogCriteriaWithId } from "../state/helpers";
import { getCatalogCriteriaWithId, getCriteriaInstanceWithId } from "../state/helpers";

interface AddNotesButtonProps {
criteriaId: string;
Expand Down Expand Up @@ -68,22 +68,28 @@ interface CriteriaResultEntryProps {
export const CriteriaResultEntry: React.FC<CriteriaResultEntryProps> = ({ criteriaId }) => {
const { state: teacherTool } = useContext(AppStateContext);
const [showInput, setShowInput] = useState(!!teacherTool.evalResults[criteriaId]?.notes);
const criteriaTemplateString = useRef<string>(getTemplateStringFromCriteriaInstanceId(criteriaId));
const criteriaDisplayString = useRef<string>(getDisplayStringFromCriteriaInstanceId(criteriaId));

function getTemplateStringFromCriteriaInstanceId(instanceId: string): string {
const catalogCriteriaId = teacherTool.rubric.criteria?.find(
criteria => criteria.instanceId === instanceId
)?.catalogCriteriaId;
if (!catalogCriteriaId) return "";
return getCatalogCriteriaWithId(catalogCriteriaId)?.template ?? "";
function getDisplayStringFromCriteriaInstanceId(instanceId: string): string {
const instance = getCriteriaInstanceWithId(teacherTool, instanceId);
if (!instance) {
return "";
}

let displayText = getCatalogCriteriaWithId(instance.catalogCriteriaId)?.template ?? "";
for (const param of instance.params ?? []) {
displayText = displayText.replace(new RegExp(`\\$\\{${param.name}}`, 'i'), param.value);
}

return displayText;
}

return (
<>
{criteriaTemplateString.current && (
{criteriaDisplayString.current && (
<div className={css["specific-criteria-result"]} key={criteriaId}>
<div className={css["result-details"]}>
<h4 className={css["block-id-label"]}>{criteriaTemplateString.current}</h4>
<h4 className={css["display-string"]}>{criteriaDisplayString.current}</h4>
<CriteriaEvalResultDropdown
result={teacherTool.evalResults[criteriaId].result}
criteriaId={criteriaId}
Expand Down
17 changes: 10 additions & 7 deletions teachertool/src/components/CriteriaTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { removeCriteriaFromRubric } from "../transforms/removeCriteriaFromRubric
import { CriteriaInstance } from "../types/criteria";
import { classList } from "react-common/components/util";
import { Button } from "react-common/components/controls/Button";
import { CriteriaInstanceDisplay } from "./CriteriaInstanceDisplay";
import { getReadableCriteriaTemplate } from "../utils";
import css from "./styling/CriteriaTable.module.scss";
import React from "react";

Expand All @@ -19,12 +21,13 @@ const CriteriaInstanceRow: React.FC<CriteriaInstanceDisplayProps> = ({ criteriaI
}

return catalogCriteria ? (
<div className={css["criteria-instance-display"]} role="row" title={catalogCriteria.template}>
<div className={classList(css["cell"], css["criteria-text-cell"])} role="cell">
{catalogCriteria.template}
{catalogCriteria.description && (
<div className={css["criteria-description"]}>{catalogCriteria.description}</div>
)}
<div
className={css["criteria-instance-display"]}
role="row"
title={getReadableCriteriaTemplate(catalogCriteria)}
>
<div className={classList(css["cell"], css["criteria-display-cell"])} role="cell">
<CriteriaInstanceDisplay criteriaInstance={criteriaInstance} />
</div>
<div
className={classList(css["cell"], css["criteria-action-menu-cell"])}
Expand All @@ -51,7 +54,7 @@ const CriteriaTableControl: React.FC<CriteriaTableProps> = ({}) => {
<div className={css["criteria-table"]} role="table" aria-label={Strings.Criteria}>
<div role="rowgroup">
<div className={css["criteria-header"]} role="row">
<div className={classList(css["cell"], css["criteria-text-cell"])} role="columnheader">
<div className={classList(css["cell"], css["criteria-display-cell"])} role="columnheader">
{Strings.Criteria}
</div>
<div
Expand Down
Loading

0 comments on commit a3c462c

Please sign in to comment.