diff --git a/common-docs/teachertool/test/catalog-shared.json b/common-docs/teachertool/test/catalog-shared.json index 956d680e026f..449b6b55b1d2 100644 --- a/common-docs/teachertool/test/catalog-shared.json +++ b/common-docs/teachertool/test/catalog-shared.json @@ -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", @@ -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 } ] } ] -} \ No newline at end of file +} diff --git a/common-docs/teachertool/test/validator-plans-shared.json b/common-docs/teachertool/test/validator-plans-shared.json index 7f8975ae6c01..1cd2ffb53cc0 100644 --- a/common-docs/teachertool/test/validator-plans-shared.json +++ b/common-docs/teachertool/test/validator-plans-shared.json @@ -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 + } + ] } ] } diff --git a/react-common/components/controls/Input.tsx b/react-common/components/controls/Input.tsx index d043bc377cb0..5363f741bd93 100644 --- a/react-common/components/controls/Input.tsx +++ b/react-common/components/controls/Input.tsx @@ -220,4 +220,4 @@ export const Input = (props: InputProps) => { } ); -} \ No newline at end of file +} diff --git a/teachertool/package-lock.json b/teachertool/package-lock.json index d62ce04e6b39..f794682b07e0 100644 --- a/teachertool/package-lock.json +++ b/teachertool/package-lock.json @@ -8,9 +8,11 @@ "name": "makecode-arcade-teachertool", "version": "0.1.0", "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", @@ -3837,6 +3839,11 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, + "node_modules/@types/jsonpath": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@types/jsonpath/-/jsonpath-0.2.4.tgz", + "integrity": "sha512-K3hxB8Blw0qgW6ExKgMbXQv2UPZBoE2GqLpVY+yr7nMD2Pq86lsuIzyAaiQ7eMqFL5B6di6pxSkogLJEyEHoGA==" + }, "node_modules/@types/mime": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz", @@ -20998,6 +21005,11 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, + "@types/jsonpath": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@types/jsonpath/-/jsonpath-0.2.4.tgz", + "integrity": "sha512-K3hxB8Blw0qgW6ExKgMbXQv2UPZBoE2GqLpVY+yr7nMD2Pq86lsuIzyAaiQ7eMqFL5B6di6pxSkogLJEyEHoGA==" + }, "@types/mime": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz", diff --git a/teachertool/package.json b/teachertool/package.json index f8e2301008d6..2c25b2f16848 100644 --- a/teachertool/package.json +++ b/teachertool/package.json @@ -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", diff --git a/teachertool/src/components/CatalogModal.tsx b/teachertool/src/components/CatalogModal.tsx index 91553c81e475..a9b5ff03bf9e 100644 --- a/teachertool/src/components/CatalogModal.tsx +++ b/teachertool/src/components/CatalogModal.tsx @@ -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 = ({ criteria }) => { + const segments = useMemo(() => splitCriteriaTemplate(criteria.template), [criteria.template]); + return (
- {criteria.template &&
{criteria.template}
} + {criteria.template && ( +
+ {segments.map((segment, index) => { + return ( + + {segment.content} + + ); + })} +
+ )} {criteria.description &&
{criteria.description}
}
); diff --git a/teachertool/src/components/CriteriaInstanceDisplay.tsx b/teachertool/src/components/CriteriaInstanceDisplay.tsx new file mode 100644 index 000000000000..1b0fd7bb8541 --- /dev/null +++ b/teachertool/src/components/CriteriaInstanceDisplay.tsx @@ -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 = ({ + initialValue, + instance, + param, + shouldExpand, + numeric, +}) => { + function onChange(newValue: string) { + setParameterValue(instance.instanceId, param.name, newValue); + } + + return ( + + ); +}; + +interface CriteriaInstanceDisplayProps { + criteriaInstance: CriteriaInstance; +} + +export const CriteriaInstanceDisplay: React.FC = ({ 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 ( + + ); + } + } + + function getPlainTextSegmentDisplay(text: string): JSX.Element | null { + return text ?
{text}
: null; + } + + const templateSegments = splitCriteriaTemplate(catalogCriteria.template); + const display = templateSegments.map(s => + s.type === "plain-text" ? getPlainTextSegmentDisplay(s.content) : getParameterSegmentDisplay(s.content) + ); + + return catalogCriteria ? ( +
+
+ {display.map((part, i) => ( + + {part} + + ))} +
+
{catalogCriteria.description}
+
+ ) : null; +}; diff --git a/teachertool/src/components/CriteriaResultEntry.tsx b/teachertool/src/components/CriteriaResultEntry.tsx index 2cb08072777c..1e9ad2721387 100644 --- a/teachertool/src/components/CriteriaResultEntry.tsx +++ b/teachertool/src/components/CriteriaResultEntry.tsx @@ -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; @@ -68,22 +68,28 @@ interface CriteriaResultEntryProps { export const CriteriaResultEntry: React.FC = ({ criteriaId }) => { const { state: teacherTool } = useContext(AppStateContext); const [showInput, setShowInput] = useState(!!teacherTool.evalResults[criteriaId]?.notes); - const criteriaTemplateString = useRef(getTemplateStringFromCriteriaInstanceId(criteriaId)); + const criteriaDisplayString = useRef(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 && (
-

{criteriaTemplateString.current}

+

{criteriaDisplayString.current}

= ({ criteriaI } return catalogCriteria ? ( -
-
- {catalogCriteria.template} - {catalogCriteria.description && ( -
{catalogCriteria.description}
- )} +
+
+
= ({}) => {
-
+
{Strings.Criteria}
c.id === id); } +export function getCriteriaInstanceWithId(state: AppState, id: string): CriteriaInstance | undefined { + return state.rubric.criteria.find(c => c.instanceId === id); +} + export function verifyCriteriaInstanceIntegrity(instance: CriteriaInstance) { const catalogCriteria = getCatalogCriteriaWithId(instance.catalogCriteriaId); @@ -19,7 +23,7 @@ export function verifyCriteriaInstanceIntegrity(instance: CriteriaInstance) { } for (const param of instance.params ?? []) { - if (!catalogCriteria?.parameters?.find(p => p.name === param.name)) { + if (!catalogCriteria?.params?.find(p => p.name === param.name)) { throw new Error("Unrecognized parameter in criteria instance."); } } @@ -74,7 +78,7 @@ export function getSelectableCatalogCriteria(state: AppState): CatalogCriteria[] return ( state.catalog?.filter( catalogCriteria => - ((catalogCriteria.parameters && catalogCriteria.parameters.length > 0) || + ((catalogCriteria.params && catalogCriteria.params.length > 0) || !usedCatalogCriteria.includes(catalogCriteria.id)) && !catalogCriteria.hideInCatalog ) ?? [] diff --git a/teachertool/src/transforms/addCriteriaToRubric.ts b/teachertool/src/transforms/addCriteriaToRubric.ts index 341071f0af10..48ae4a540682 100644 --- a/teachertool/src/transforms/addCriteriaToRubric.ts +++ b/teachertool/src/transforms/addCriteriaToRubric.ts @@ -25,11 +25,11 @@ export function addCriteriaToRubric(catalogCriteriaIds: string[]) { continue; } - const params = catalogCriteria.parameters?.map( + const params = catalogCriteria.params?.map( param => ({ name: param.name, - value: undefined, + value: param.default, } as CriteriaParameterValue) ); diff --git a/teachertool/src/transforms/loadCatalogAsync.ts b/teachertool/src/transforms/loadCatalogAsync.ts index 3a668b59fe31..0deca85961ad 100644 --- a/teachertool/src/transforms/loadCatalogAsync.ts +++ b/teachertool/src/transforms/loadCatalogAsync.ts @@ -11,5 +11,13 @@ const prodFiles = [ 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 => { + c.params?.forEach(p => { + p.name = p.name.toLocaleLowerCase(); + }); + }); + dispatch(Actions.setCatalog(fullCatalog)); } diff --git a/teachertool/src/transforms/runEvaluateAsync.ts b/teachertool/src/transforms/runEvaluateAsync.ts index f4862a1b9096..c17ca88a928b 100644 --- a/teachertool/src/transforms/runEvaluateAsync.ts +++ b/teachertool/src/transforms/runEvaluateAsync.ts @@ -5,12 +5,16 @@ import * as Actions from "../state/actions"; import { getCatalogCriteriaWithId } from "../state/helpers"; import { EvaluationStatus, CriteriaInstance } from "../types/criteria"; import { ErrorCode } from "../types/errorCode"; -import { makeToast } from "../utils"; +import { getReadableCriteriaTemplate, makeToast } from "../utils"; import { showToast } from "./showToast"; import { setActiveTab } from "./setActiveTab"; import { setEvalResultOutcome } from "./setEvalResultOutcome"; +import jp from "jsonpath"; -function generateValidatorPlan(criteriaInstance: CriteriaInstance): pxt.blocks.ValidatorPlan | undefined { +function generateValidatorPlan( + criteriaInstance: CriteriaInstance, + showErrors: boolean +): pxt.blocks.ValidatorPlan | undefined { const { state: teacherTool } = stateAndDispatch(); const catalogCriteria = getCatalogCriteriaWithId(criteriaInstance.catalogCriteriaId); @@ -29,7 +33,42 @@ function generateValidatorPlan(criteriaInstance: CriteriaInstance): pxt.blocks.V return undefined; } - // TODO: Fill in any parameters. Error if parameters are missing. + // Fill in parameters. + for (const param of criteriaInstance.params ?? []) { + const catalogParam = catalogCriteria.params?.find(p => p.name === param.name); + if (!catalogParam) { + if (showErrors) { + logError( + ErrorCode.evalMissingCatalogParameter, + "Attempting to evaluate criteria with unrecognized parameter", + { catalogId: criteriaInstance.catalogCriteriaId, paramName: param.name } + ); + } + return undefined; + } + + if (!param.value) { + // User didn't set a value for the parameter. + if (showErrors) { + logError(ErrorCode.evalParameterUnset, "Attempting to evaluate criteria with unset parameter value", { + catalogId: criteriaInstance.catalogCriteriaId, + paramName: param.name, + }); + showToast( + makeToast( + "error", + // prettier-ignore + lf("Unable to evaluate criteria: missing '{0}' in '{1}'", param.name, getReadableCriteriaTemplate(catalogCriteria)) + ) + ); + } + return undefined; + } + + for (const path of catalogParam.paths) { + jp.apply(plan, path, () => param.value); + } + } return plan; } @@ -55,7 +94,7 @@ export async function runEvaluateAsync(fromUserInteraction: boolean) { return resolve(false); } - const plan = generateValidatorPlan(criteriaInstance); + const plan = generateValidatorPlan(criteriaInstance, fromUserInteraction); if (!plan) { dispatch(Actions.clearEvalResult(criteriaInstance.instanceId)); @@ -80,10 +119,12 @@ export async function runEvaluateAsync(fromUserInteraction: boolean) { } const results = await Promise.all(evalRequests); - const errorCount = results.filter(r => !r).length; - if (errorCount === teacherTool.rubric.criteria.length) { - showToast(makeToast("error", lf("Unable to run evaluation"))); - } else if (errorCount > 0) { - showToast(makeToast("error", lf("Unable to evaluate some criteria"))); + if (fromUserInteraction) { + const errorCount = results.filter(r => !r).length; + if (errorCount === teacherTool.rubric.criteria.length) { + showToast(makeToast("error", lf("Unable to run evaluation"))); + } else if (errorCount > 0) { + showToast(makeToast("error", lf("Unable to evaluate some criteria"))); + } } } diff --git a/teachertool/src/transforms/setParameterValue.ts b/teachertool/src/transforms/setParameterValue.ts new file mode 100644 index 000000000000..67d62ff27382 --- /dev/null +++ b/teachertool/src/transforms/setParameterValue.ts @@ -0,0 +1,37 @@ +import { logDebug, logError } from "../services/loggingService"; +import { stateAndDispatch } from "../state"; +import { getCriteriaInstanceWithId } from "../state/helpers"; +import { ErrorCode } from "../types/errorCode"; +import { setRubric } from "./setRubric"; + +export function setParameterValue(instanceId: string, paramName: string, newValue: any) { + const { state: teacherTool } = stateAndDispatch(); + logDebug(`Setting parameter '${paramName}' to '${newValue}' for criteria instance '${instanceId}'`); + + const oldCriteriaInstance = getCriteriaInstanceWithId(teacherTool, instanceId); + if (!oldCriteriaInstance) { + logError(ErrorCode.missingCriteriaInstance, `Unable to find criteria instance with id '${instanceId}'`); + return; + } + + const oldParam = oldCriteriaInstance.params?.find(p => p.name === paramName); + if (!oldParam) { + logError( + ErrorCode.missingParameter, + `Unable to find parameter with name '${paramName}' in criteria instance '${instanceId}'` + ); + return; + } + + const newParam = { ...oldParam, value: newValue }; + const newCriteriaInstance = { + ...oldCriteriaInstance, + params: oldCriteriaInstance.params?.map(p => (p.name === paramName ? newParam : p)), + }; + const newInstanceSet = teacherTool.rubric.criteria.map(c => + c.instanceId === instanceId ? newCriteriaInstance : c + ); + const newRubric = { ...teacherTool.rubric, criteria: newInstanceSet }; + + setRubric(newRubric); +} diff --git a/teachertool/src/types/criteria.ts b/teachertool/src/types/criteria.ts index a798b349e6bb..04401a7c97ea 100644 --- a/teachertool/src/types/criteria.ts +++ b/teachertool/src/types/criteria.ts @@ -5,7 +5,7 @@ export interface CatalogCriteria { 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 + params: CriteriaParameter[] | undefined; // Any parameters that affect the criteria hideInCatalog?: boolean; // Whether the criteria should be hidden in the user-facing catalog } @@ -17,10 +17,12 @@ export interface CriteriaInstance { } // Represents a parameter definition in a catalog criteria. +export type CriteriaParameterType = "string" | "longString" | "number" | "block"; export interface CriteriaParameter { name: string; - type: string; - path: string; // The json path of the parameter in the catalog criteria. + type: CriteriaParameterType; + default: string | undefined; + paths: string[]; // The json path(s) to update with the parameter value in the catalog criteria. } // Represents a parameter value in a criteria instance. diff --git a/teachertool/src/types/errorCode.ts b/teachertool/src/types/errorCode.ts index a38cf7d840a2..db15739c870c 100644 --- a/teachertool/src/types/errorCode.ts +++ b/teachertool/src/types/errorCode.ts @@ -6,6 +6,8 @@ export enum ErrorCode { downloadTargetConfigAsync = "downloadTargetConfigAsync", evalMissingCriteria = "evalMissingCriteria", evalMissingPlan = "evalMissingPlan", + evalParameterUnset = "evalParameterUnset", + evalMissingCatalogParameter = "evalMissingCatalogParameter", loadCollectionFileFailed = "loadCollectionFileFailed", unableToGetIndexedDbRecord = "unableToGetIndexedDbRecord", unableToSetIndexedDbRecord = "unableToSetIndexedDbRecord", @@ -15,7 +17,9 @@ export enum ErrorCode { unableToReadRubricFile = "unableToReadRubricFile", localStorageReadError = "localStorageReadError", localStorageWriteError = "localStorageWriteError", + missingCriteriaInstance = "missingCriteriaInstance", validatorPlansNotFound = "validatorPlansNotFound", fetchRequestFailed = "fetchRequestFailed", fetchJsonDocAsync = "fetchJsonDocAsync", + missingParameter = "missingParameter", } diff --git a/teachertool/src/types/index.ts b/teachertool/src/types/index.ts index 195ac6d1ca89..2473b7b399c4 100644 --- a/teachertool/src/types/index.ts +++ b/teachertool/src/types/index.ts @@ -50,9 +50,15 @@ export type RequestStatus = "init" | "loading" | "error" | "success"; export type ProjectData = pxt.Cloud.JsonScript & { inputText: string; }; + export type ConfirmationModalOptions = { title: string; message: string; onCancel: () => void; onContinue: () => void; }; + +export type CriteriaTemplateSegment = { + type: "plain-text" | "param"; + content: string; // plain text or parameter name +}; diff --git a/teachertool/src/utils/index.ts b/teachertool/src/utils/index.ts index 8be86a48645c..88322765f119 100644 --- a/teachertool/src/utils/index.ts +++ b/teachertool/src/utils/index.ts @@ -1,7 +1,8 @@ import { nanoid } from "nanoid"; -import { CarouselRubricResourceCard, ToastType, ToastWithId } from "../types"; +import { CarouselRubricResourceCard, CriteriaTemplateSegment, ToastType, ToastWithId } from "../types"; import { Rubric } from "../types/rubric"; import { classList } from "react-common/components/util"; +import { CatalogCriteria } from "../types/criteria"; export function makeToast(type: ToastType, text: string, timeoutMs: number = 5000): ToastWithId { return { @@ -44,3 +45,29 @@ export function getProjectLink(inputText: string): string { const hasMakeCode = inputText?.indexOf("makecode") !== -1; return hasMakeCode ? inputText : `https://makecode.com/${inputText}`; } + +export function splitCriteriaTemplate(template: string): CriteriaTemplateSegment[] { + // Split by the regex, which will give us an array where every other element is a parameter. + // If the template starts with a parameter, the first element will be an empty string. + const paramRegex = /\$\{([\w\s]+)\}/g; + const parts = template.split(paramRegex); + + const segments: CriteriaTemplateSegment[] = []; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + + if (part) { + if (i % 2 === 0) { + segments.push({ type: "plain-text", content: part.trim() }); + } else { + segments.push({ type: "param", content: part.toLocaleLowerCase().trim() }); + } + } + } + + return segments; +} + +export function getReadableCriteriaTemplate(criteria: CatalogCriteria): string { + return criteria.template.replaceAll("${", "").replaceAll("}", ""); +}