Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | 2x 12x 46x 46x 5x 5x 4x 4x 4x 4x 4x 5x 20x 5x 5x 5x 5x 5x 1x 46x 2x 2x 2x 46x 1x 1x 1x 46x | import React from "react";
import { Button, Form } from "react-bootstrap";
import { useForm } from "react-hook-form";
import { toast } from "react-toastify";
import axios from "axios";
import { useBackendMutation } from "main/utils/useBackend";
/**
* Sanitizes a value for use in a downloaded filename: trims surrounding
* whitespace, replaces any run of characters that are not letters or digits
* with a single dash, and strips any leading/trailing dashes left behind.
* Returns "" for null/undefined input.
*
* Examples: "CMPSC 8" -> "CMPSC-8"; " Fall 2026! " -> "Fall-2026"; null -> "".
*
* @param {*} value the value to sanitize (coerced to a string)
* @returns {string} the sanitized, filename-safe string
*/
const sanitizeForFilename = (value) =>
String(value ?? "")
.trim()
.replace(/[^a-zA-Z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
export default function ScaffoldTabComponent({
courseId,
courseName,
term,
school,
testIdPrefix,
}) {
const {
register,
formState: { errors },
handleSubmit,
reset,
} = useForm();
const downloadYaml = async () => {
try {
const response = await axios({
url: "/api/concepts/yaml/download",
method: "GET",
params: { courseId },
});
const blob = new Blob([response.data], { type: "application/x-yaml" });
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
const schoolKey = school?.key ?? school;
const filename = [
"Scaffold",
sanitizeForFilename(courseName),
sanitizeForFilename(term),
sanitizeForFilename(schoolKey),
courseId,
]
.filter((part) => part !== "" && part !== undefined && part !== null)
.join("-");
link.setAttribute("download", `${filename}.yml`);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
} catch (error) {
toast.error(`Error downloading concepts YAML: ${error.message}`);
}
};
const objectToAxiosParamsUpload = (formData) => {
const file = new FormData();
file.append("file", formData.upload[0]);
return {
url: "/api/concepts/yaml/upload",
method: "POST",
data: file,
params: {
courseId,
},
};
};
const uploadMutation = useBackendMutation(objectToAxiosParamsUpload, {
onSuccess: (report) => {
toast(
`Concepts replaced: ${report.conceptsCreated} concepts, ` +
`${report.subconceptsCreated} subconcepts, ${report.edgesCreated} edges, ` +
`${report.practiceProblemsCreated} practice problems. ` +
`Saved scaffold state was cleared for ${report.userStatesCleared} user(s).`,
);
reset();
},
onError: (error) => {
toast.error(
`Error uploading concepts YAML: ${JSON.stringify(error.response.data, null, 2)}`,
);
},
});
return (
<div className="tabComponent" data-testid={`${testIdPrefix}-scaffoldTab`}>
<h2>Scaffold</h2>
<p>
Download this course's concepts, subconcepts, prerequisite edges,
and practice problems as an editable YAML file, or replace them by
uploading one. See <code>docs/yaml-format.md</code> for the file format.
</p>
<Button
onClick={downloadYaml}
data-testid={`${testIdPrefix}-download-yaml-button`}
>
Download Concepts YAML
</Button>
<Form onSubmit={handleSubmit(uploadMutation.mutate)} className="mt-4">
<Form.Group className="mb-2">
<Form.Label htmlFor="concepts-yaml-upload">
Upload Concepts YAML
</Form.Label>
<Form.Control
data-testid={`${testIdPrefix}-upload-yaml-input`}
id="concepts-yaml-upload"
type="file"
accept=".yaml,.yml"
isInvalid={Boolean(errors.upload)}
{...register("upload", { required: true })}
/>
<Form.Control.Feedback type="invalid">
{errors.upload && "Concepts YAML file is required."}
</Form.Control.Feedback>
<Form.Text muted>
Warning: uploading replaces ALL concepts, subconcepts, edges, and
practice problems for this course, and clears every student's
saved scaffold state.
</Form.Text>
</Form.Group>
<Button
type="submit"
data-testid={`${testIdPrefix}-upload-yaml-button`}
className="mt-3"
>
Upload
</Button>
</Form>
</div>
);
}
|