diff --git a/client/package.json b/client/package.json index 2b8b6e79c..83c8f9e08 100644 --- a/client/package.json +++ b/client/package.json @@ -15,9 +15,11 @@ "files": [ "src/**/*.ts", "src/**/*.js", + "src/**/*.tsx", + "src/**/*.jsx", + "src/**/*.css", "vite.config.ts", - "tsconfig.json", - ".env.production" + "tsconfig.json" ], "output": [ "dist/**/*.js", diff --git a/client/src/assets/graphicalAnswerExamples.doenet b/client/src/assets/graphicalAnswerExamples.doenet new file mode 100644 index 000000000..bf2a422db --- /dev/null +++ b/client/src/assets/graphicalAnswerExamples.doenet @@ -0,0 +1,243 @@ +
+ Illustrations of graphical answers + + + Examples showing different types of answer tags involving graphs + + + + Move point to quadrant + +

Move the point to the second quadrant.

+ + + A graph with point $P that is movable + (0,0) + + +

+ $P.x < 0 and $P.y > 0 + +

+
+ + + + Maxima of a function + + + + A graph of a function that increases until x=-7, + then decreases until x=-2 + then increases until x = 4 + and then decreases. + Two movable points $A and $B are shown. + + + + + (4,7) + + $f.maxima + $f.minima + + + + + (6,7) + + $f.maxima + $f.minima + + + + + +

Move the $A.styleDescriptionWithNouns to the maxima of the function

+ + + $A $B = $f.maxima + +
+ + + Draw function with prescribed maxima + +

+ Move the points so that the below function has a local maximum at + x = -2, + a local minimum of y= -5, + and no other local extrema. +

+ + A graph of a function through five movable points + + (-8,-5) + + x=$maxLocation + y=$minValue + + + + (-4,-1) + + + + (-1,1) + + + + (3,3) + + + + (7,4) + + + + + + + + + + + + $f.numMaxima = 1 and $f.maximumLocations[1]=$maxLocation + and + $f.numMinima = 1 and $f.minimumValues[1] = $minValue + + + + +

The function needs to have a local maximum

+
+ +

The function must have only one local maximum

+
+ +

The function correctly has one local maximum, but it is not in the correct location.

+
+ +

Good job. The function correctly has one local maximum in the correct location.

+
+ + + +

The function needs to have a local minimum

+
+ +

The function must have only one local minimum

+
+ +

The function correctly has one local minimum, but it does not have the correct value.

+
+ +

Looks good. The function correctly has one local minimum with the correct value.

+
+ + +
+ + + Sketch solution to differential equation + + + 8 + 1 + 0 + + + +

Let y(t) be the solution to the differential equation + + \frac{dy}{dt} &= $a - y + y(0) &= 1 + +

+ +

Move the points and adjust the slopes so that the $C.styleDescription curve is a qualitative sketch of the solution y(t).

+ + + + A graph of a function through two movable points, + where the slope of the function at each point can be adjusted + by changing a line segment at the point + + + t + y + + + (0, $y0) + + + + + + + + + + + y=$a + + + + + + y=0 + + + + + + (0.5 $P1slope.x, + 0.5 $P1slope.y) + + $P2slope + + + + + + + + + $P1.y = $y0 and + $P2.y = $a and + $P2slope.y = 0 and + $P1slope.y/$P1slope.x > 2 + + + + + The initial condition is not correct. + + + + The solution should start out increasing. + + + + The solution should start by increasing more rapidly. + + + + The solution's behavior for large values of t is incorrect. + + + +

Good job! The $C.styleDescription curve qualitatively represents the solution y(t).

+
    +
  1. It starts at the initial condition y(0)=$y0.
  2. +
  3. It begins by increasing rapidly.
  4. +
  5. It asymptotes to y=$a as t increases.
  6. +
+
+ +
+ + +
\ No newline at end of file diff --git a/client/src/assets/mathAnswerExamples.doenet b/client/src/assets/mathAnswerExamples.doenet new file mode 100644 index 000000000..490b7eba2 --- /dev/null +++ b/client/src/assets/mathAnswerExamples.doenet @@ -0,0 +1,183 @@ + +
+ Illustrations of mathematical answers + + + Examples showing different types of answer tags involving math + + + + The simplest answer + +

What is x+x? 2x

+ +
+ + + Adding a short description or label + +

The simplest answer triggers a warning, as there is no information for screen readers to give about the answer blank. You can fix that by adding either a shortDescription (accessed only by screen readers) or a label (both visible and accessed by screen readers).

+ +

+ + + 2x + +

+ +

What is x+x? + + x+x + 2x + +

+ +

This is equivalent to the last one, but just adds an award for clarity.

+ +

What is x+x? + + x+x + 2x + +

+ + + +
+ + + Symbolic equality + +

The above answers actually accept x+x + as a correct answer, as it is mathematical equivalent to 2x. To require exact answers, specify the symbolicEquality attribute.

+ +

What is x+x? + + x+x + 2x + +

+ +
+ + + Specifying partial credit options + +

+ Compute the integral \int x^2\, dx +

+

+ + integral of x-squared + x^3/3 + c + x^3/3 + C + x^3/3 + +

+ +
+ + + Partial credit for unordered list + +

Let f(x) = (x-2)(x+5). Solve f(x) = 0. If more than one answer, separate by commas.

+ +

x= + + solve f(x)=0 + 2, -5 + +

+ +
+ + + Allowing answers in a range + + + + + + 2 <= $n <= 7 + + + + + + Specifying an interval + + + + A graph of a line segment on the x-axis from the point -2 to the point 5. + At the point -2 is an open circle. + At the point 5 is a closed circle. + + x + + (-2,0) + (5,0) + + +

What is the interval shown above? + + + Enter interval + + + $interval = + (-2,5] + + +

+ +

The above answer will accept different ways to express the interval, including: +

+

+ +
+ + + Dynamic number of answers + +

Let f(x) = (x-2)(x+5). Solve f(x) = 0.

+ +

How many solutions are there? + + number of solutions + +

+ +

The solutions are + + x = + + answer number $i + + . +

+ + + + + + +

+ + + $values = 2 -5 + + +

+ +
+ + + +
\ No newline at end of file diff --git a/client/src/assets/multipleChoiceExamples.doenet b/client/src/assets/multipleChoiceExamples.doenet new file mode 100644 index 000000000..6b731d74e --- /dev/null +++ b/client/src/assets/multipleChoiceExamples.doenet @@ -0,0 +1,120 @@ + +
+ Illustrations of multiple-choice answers + + + Examples showing different types of multiple choice answer tags + available in Doenet + + + + Basic example + +

What's the heaviest dog breed?

+ + heaviest dog breed + English Mastiff + Irish Wolfhound + Great Dane + Saint Bernard + +
+ + + Shuffle order + +

What's the heaviest dog breed?

+ + heaviest dog breed + English Mastiff + Irish Wolfhound + Great Dane + Saint Bernard + + +
+ + + Inline + +

+ A mammal is a + + Describe a mammal + warm-blooded + cold-blooded + + animal. +

+ +
+ + + Select multiple + + + + 10 + 92 + 0 + -29 + 5.6 + + + + + + Select multiple with partial credit + + + + 10 + 92 + 0 + -29 + 5.6 + + + + + + + Limit number of attempts + + + + Mozart + Verdi + Puccini + Wagner + + + + + + Disable wrong choices, reduce credit with wrong choices + + + + Archduke Franz Ferdinand + Gavrilo Princip + Danilo Ilić + Nedeljko Čabrinović + + + + + + Disable after correct + + + + table salt + Sodium acetate + sugar + vinegar + + + + +
\ No newline at end of file diff --git a/client/src/assets/scratchPadDefault.doenet b/client/src/assets/scratchPadDefault.doenet new file mode 100644 index 000000000..b6d02573d --- /dev/null +++ b/client/src/assets/scratchPadDefault.doenet @@ -0,0 +1,61 @@ +Welcome to the DoenetML scratch pad + +

+ You can use this space to experiment with writing DoenetML. +

+

+ Your scratch pad might get overwritten, so be sure to save elsewhere anything you want to keep. +

+ +
+ Examples + +
+ Auto-graded answers + +

1+1 = 2

+ +

The point (5,-7) is in the + + first + second + third + fourth + + quadrant. +

+
+ +
+ References + +

What is your name?

+ +

Hello, $yourName!

+
+ +
+ A graph + +

Move the point. The change will be reflected here. The point is $P.

+ + + (3,4) + + +

Change the point by typing: $P

+ +
+ +
+ +
+ Need help? + +

For support, you can:

+ +
\ No newline at end of file diff --git a/client/src/index.tsx b/client/src/index.tsx index 5a80a13e1..ed7d5fea8 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -123,6 +123,7 @@ import { loader as sharedWithMeLoader, } from "./paths/SharedWithMe"; import { editorUrl } from "./utils/url"; +import { ScratchPad, loader as scratchPadLoader } from "./paths/ScratchPad"; const theme = extendTheme({ fonts: { @@ -437,6 +438,13 @@ const router = createBrowserRouter([ path: "loadShareStatus/:contentId", loader: loadShareStatus, }, + { + path: "scratchPad", + loader: scratchPadLoader, + action: genericAction, + errorElement: , + element: , + }, ], }, ]); diff --git a/client/src/paths/Home.tsx b/client/src/paths/Home.tsx index 0036e17bf..e799a9d35 100644 --- a/client/src/paths/Home.tsx +++ b/client/src/paths/Home.tsx @@ -249,6 +249,14 @@ export function Home() { > How to get involved with Doenet + . + + + To experiment with writing Doenet activities, visit the{" "} + + Scratch Pad + + . Events diff --git a/client/src/paths/ScratchPad.tsx b/client/src/paths/ScratchPad.tsx new file mode 100644 index 000000000..f1890a950 --- /dev/null +++ b/client/src/paths/ScratchPad.tsx @@ -0,0 +1,307 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { redirect, useLoaderData, useOutletContext } from "react-router"; +import { DoenetmlVersion } from "../types"; +import { DoenetEditor } from "@doenet/doenetml-iframe"; +import axios from "axios"; +import defaultSource from "../assets/scratchPadDefault.doenet?raw"; +import multipleChoice from "../assets/multipleChoiceExamples.doenet?raw"; +import mathAnswers from "../assets/mathAnswerExamples.doenet?raw"; +import graphicalAnswers from "../assets/graphicalAnswerExamples.doenet?raw"; + +import { + Alert, + AlertIcon, + AlertTitle, + AlertDescription, + Box, + Flex, + Button, + Menu, + MenuButton, + MenuItem, + MenuList, + Text, + useDisclosure, +} from "@chakra-ui/react"; +import { SiteContext } from "./SiteHeader"; +import { SaveDoenetmlAndReportFinish } from "../popups/SaveDoenetmlAndReportFinish"; + +export async function loader({ request }: { request: Request }) { + const url = new URL(request.url); + + const doenetML = url.searchParams.get("doenetml"); + if (doenetML) { + try { + // Save requested DoenetML in localStorage + // and then reload page without doenetml param + // so that reloading the page won't reset to the original DoenetML + localStorage.setItem("scratchPad", doenetML); + return redirect(`/scratchPad`); + } catch (e) { + console.error(e); + } + } + + const contentId = url.searchParams.get("contentId"); + if (contentId) { + try { + const { data } = await axios.get( + `/api/activityEditView/getContentSource/${contentId}`, + ); + + // Save DoenetML source from the contentId in localStorage + // and then reload page without contentId param + // so that reloading the page won't reset to the original DoenetML + localStorage.setItem("scratchPad", data.source); + return redirect(`/scratchPad`); + } catch (e) { + console.error(e); + } + } + + const { + data: { defaultDoenetmlVersion }, + } = await axios.get(`/api/info/getDefaultDoenetmlVersion`); + + let source = ""; + try { + source = localStorage.getItem("scratchPad") || defaultSource; + } catch (e) { + console.error(e); + } + + return { + doenetmlVersion: defaultDoenetmlVersion, + source, + }; +} + +/** + * This page allows you edit a scratch pad of DoenetML to explore the features of DoenetML. + */ +export function ScratchPad() { + const { doenetmlVersion, source } = useLoaderData() as { + doenetmlVersion: DoenetmlVersion; + source: string; + }; + + useEffect(() => { + document.title = `Scratch pad - Doenet`; + }, []); + + const [initialSource, setInitialSource] = useState(source); + const [resetNum, setResetNum] = useState(0); + + const { user } = useOutletContext(); + + const { + isOpen: saveDialogIsOpen, + onOpen: saveDialogOnOpen, + onClose: saveDialogOnClose, + } = useDisclosure(); + + const saveDocumentDialog = ( + + ); + + const loadButton = ( + + + Load + + + { + localStorage.removeItem("scratchPad"); + setInitialSource(defaultSource); + // We update reset num to make sure editor updates. + // If started with defaultSource and then we try to reset it back to defaultSource + // no change is detected in initialSource even though we want to reset + setResetNum((was) => was + 1); + }} + > + Scratch Pad Welcome + + { + try { + localStorage.setItem("scratchPad", multipleChoice); + } catch (e) { + console.error(e); + } + setInitialSource(multipleChoice); + // We update reset num to make sure editor updates. + setResetNum((was) => was + 1); + }} + > + Multiple Choice Examples + + { + try { + localStorage.setItem("scratchPad", mathAnswers); + } catch (e) { + console.error(e); + } + setInitialSource(mathAnswers); + // We update reset num to make sure editor updates. + setResetNum((was) => was + 1); + }} + > + Math Answer Examples + + { + try { + localStorage.setItem("scratchPad", graphicalAnswers); + } catch (e) { + console.error(e); + } + setInitialSource(graphicalAnswers); + // We update reset num to make sure editor updates. + setResetNum((was) => was + 1); + }} + > + Graphical Answer Examples + + + + ); + + const saveScratchPad = user && ( + + ); + + const scratchPadMessage = ( + + + Scratch Pad + + + Your changes are not permanently saved. + + + + ); + + return ( + <> + {saveDocumentDialog} + + + {scratchPadMessage} + + {loadButton} + {saveScratchPad} + + + + + + + ); +} + +function DocumentEditor({ + source, + doenetmlVersion, +}: { + source: string; + doenetmlVersion: DoenetmlVersion; +}) { + const textEditorDoenetML = useRef(source); + const savedDoenetML = useRef(source); + + const handleSaveDoc = useCallback(async () => { + if (savedDoenetML.current === textEditorDoenetML.current) { + return; + } + + const newDoenetML = textEditorDoenetML.current; + + try { + //Save in localStorage + localStorage.setItem("scratchPad", newDoenetML); + + savedDoenetML.current = newDoenetML; + } catch (e) { + console.error(e); + } + }, []); + + // save draft when leave page + useEffect(() => { + return () => { + handleSaveDoc(); + }; + }, [handleSaveDoc]); + + const baseUrl = window.location.protocol + "//" + window.location.host; + const doenetViewerUrl = `${baseUrl}/activityViewer`; + + return ( + { + handleSaveDoc(); + }} + immediateDoenetmlChangeCallback={(newDoenetML: string) => { + textEditorDoenetML.current = newDoenetML; + }} + doenetmlVersion={doenetmlVersion.fullVersion} + border="none" + doenetViewerUrl={doenetViewerUrl} + /> + ); +} diff --git a/client/src/paths/SiteHeader.tsx b/client/src/paths/SiteHeader.tsx index 21f8a8716..89a8ace20 100644 --- a/client/src/paths/SiteHeader.tsx +++ b/client/src/paths/SiteHeader.tsx @@ -399,7 +399,13 @@ export function SiteHeader() { > Update {user.isAnonymous ? "pseudonym" : "name"} - + { + localStorage.removeItem("scratchPad"); + }} + > {user.isAnonymous ? "Clear anonymous data" : "Log Out"} diff --git a/client/src/popups/AddContentToMenu.tsx b/client/src/popups/AddContentToMenu.tsx index ea0b08f17..b9d7214f1 100644 --- a/client/src/popups/AddContentToMenu.tsx +++ b/client/src/popups/AddContentToMenu.tsx @@ -30,7 +30,11 @@ import { import axios from "axios"; import { MoveCopyContent } from "../popups/MoveCopyContent"; import { CopyContentAndReportFinish } from "../popups/CopyContentAndReportFinish"; -import { FetcherWithComponents, useOutletContext } from "react-router"; +import { + FetcherWithComponents, + useNavigate, + useOutletContext, +} from "react-router"; import { SiteContext } from "../paths/SiteHeader"; import { getAllowedParentTypes, menuIcons } from "../utils/activity"; @@ -80,6 +84,8 @@ export function AddContentToMenu({ const [baseContains, setBaseContains] = useState([]); + const navigate = useNavigate(); + useEffect(() => { const allowedParents = getAllowedParentTypes( sourceContent.map((c) => c.type), @@ -175,6 +181,29 @@ export function AddContentToMenu({ ); } + const scratchPadDisabled = !sourceContent[0]?.doenetmlVersion?.default; + + const loadIntoScratchPad = sourceContent.length === 1 && ( + + { + navigate(`/scratchPad?contentId=${sourceContent[0].contentId}`); + }} + > + Load into Scratch Pad + + + ); + return ( <> {suggestCurationModal} @@ -290,6 +319,7 @@ export function AddContentToMenu({ > My Activities + {loadIntoScratchPad} {recentContent.length > 0 ? ( {recentContent.map((rc) => { diff --git a/client/src/popups/SaveDoenetmlAndReportFinish.tsx b/client/src/popups/SaveDoenetmlAndReportFinish.tsx new file mode 100644 index 000000000..a0bd9ba36 --- /dev/null +++ b/client/src/popups/SaveDoenetmlAndReportFinish.tsx @@ -0,0 +1,150 @@ +import { RefObject, useEffect, useState } from "react"; +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + ModalCloseButton, + ModalFooter, + Button, + HStack, + Text, + Spinner, +} from "@chakra-ui/react"; +import { useFetcher, useNavigate, useOutletContext } from "react-router"; +import { SiteContext } from "../paths/SiteHeader"; + +/** + * A modal that immediately upon opening saves the DoenetML into an new document in Activities + * + * When the save is finished, the modal allows the user to close it or navigate to Activities. + */ +export function SaveDoenetmlAndReportFinish({ + isOpen, + onClose, + finalFocusRef, + DoenetML, + documentName, +}: { + isOpen: boolean; + onClose: () => void; + finalFocusRef?: RefObject; + DoenetML: string; + documentName: string; +}) { + const [newContentId, setNewContentId] = useState(null); + + const navigate = useNavigate(); + + const [errMsg, setErrMsg] = useState(""); + + const fetcher = useFetcher(); + + const { user, setAddTo } = useOutletContext(); + + useEffect(() => { + if (fetcher.data) { + if (fetcher.data.status === 200) { + setNewContentId(fetcher.data.data.contentId); + } else { + const message = fetcher.data.message; + setErrMsg( + `An error occurred while saving${message ? ": " + message : ""}.`, + ); + } + + document.body.style.cursor = "default"; + } + }, [fetcher.data]); + + useEffect(() => { + if (isOpen) { + if (newContentId === null) { + document.body.style.cursor = "wait"; + + fetcher.submit( + { + path: "updateContent/createContent", + parentId: null, + contentType: "singleDoc", + doenetml: DoenetML, + name: documentName, + }, + { method: "post", encType: "application/json" }, + ); + } + } else { + setNewContentId(null); + setErrMsg(""); + } + // When we included all the dependencies here, it copied content several times + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOpen]); + + const destinationAction = "Go to My Activities"; + const destinationUrl = `/activities/${user?.userId}`; + + return ( + + + + + {newContentId === null + ? errMsg === "" + ? "Saving..." + : "An error occurred" + : `Save finished`} + + {newContentId !== null ? : null} + + {errMsg === "" ? ( + newContentId === null ? ( + + Saving... + + + ) : ( + <> + Successfully saved to new document “{documentName}” + in My Activities + + ) + ) : ( + <>{errMsg} + )} + + + + + + + + + ); +} diff --git a/client/src/types.ts b/client/src/types.ts index f2fe55dce..313c0a2cf 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -319,6 +319,7 @@ export type ContentDescription = { grandparentId?: Uuid | null; grandparentName?: string | null; + doenetmlVersion?: DoenetmlVersion; // TODO: remove this when fix bad assignment versions hasBadVersion?: boolean; }; diff --git a/server/src/query/activity.ts b/server/src/query/activity.ts index 20d62c8e9..e12eba163 100644 --- a/server/src/query/activity.ts +++ b/server/src/query/activity.ts @@ -30,12 +30,14 @@ export async function createContent({ parentId, inLibrary = false, name, + doenetml = "", }: { loggedInUserId: Uint8Array; contentType: ContentType; parentId: Uint8Array | null; inLibrary?: boolean; name?: string; + doenetml?: string; }) { // TODO: Eventually, when we are sure we do not want question banks, // we will remove them entirely from the codebase. For now, just @@ -54,11 +56,7 @@ export async function createContent({ const sortIndex = await getNextSortIndexForParent(ownerId, parentId); - const defaultDoenetmlVersion = await prisma.doenetmlVersions.findFirstOrThrow( - { - where: { default: true }, - }, - ); + const { defaultDoenetmlVersion } = await getDefaultDoenetmlVersion(); let isPublic = false; let licenseCode = undefined; @@ -134,7 +132,7 @@ export async function createContent({ isPublic, licenseCode, sortIndex, - source: contentType === "singleDoc" ? "" : null, + source: contentType === "singleDoc" ? doenetml : null, doenetmlVersionId: contentType === "singleDoc" ? defaultDoenetmlVersion.id : null, sharedWith: { @@ -617,6 +615,16 @@ export async function getAllDoenetmlVersions() { return { allDoenetmlVersions }; } +export async function getDefaultDoenetmlVersion() { + const defaultDoenetmlVersion = await prisma.doenetmlVersions.findFirstOrThrow( + { + where: { default: true }, + }, + ); + + return { defaultDoenetmlVersion }; +} + /** * Create a new record in `contentRevisions` corresponding to the current state of * `contentId` owned by `loggedInUserId`, if it doesn't exist already. diff --git a/server/src/routes/infoRoutes.ts b/server/src/routes/infoRoutes.ts index 52c3737bc..4897c6cf1 100644 --- a/server/src/routes/infoRoutes.ts +++ b/server/src/routes/infoRoutes.ts @@ -3,6 +3,7 @@ import { getAllCategories } from "../query/classification"; import { getAllDoenetmlVersions, getContentDescription, + getDefaultDoenetmlVersion, } from "../query/activity"; import { queryLoggedIn, @@ -26,6 +27,11 @@ infoRouter.get( queryOptionalLoggedInNoArguments(getAllDoenetmlVersions), ); +infoRouter.get( + "/getDefaultDoenetmlVersion", + queryOptionalLoggedInNoArguments(getDefaultDoenetmlVersion), +); + infoRouter.get( "/getAllLicenses", queryOptionalLoggedInNoArguments(getAllLicenses), diff --git a/server/src/schemas/contentSchema.ts b/server/src/schemas/contentSchema.ts index 841ae1171..95271da48 100644 --- a/server/src/schemas/contentSchema.ts +++ b/server/src/schemas/contentSchema.ts @@ -16,6 +16,7 @@ export const createContentSchema = z.object({ contentType: contentTypeSchema, name: z.string().optional(), parentId: uuidOrNullSchema, + doenetml: z.string().optional(), }); export const updateContentSettingsSchema = z.object({ diff --git a/server/src/test/activity.test.ts b/server/src/test/activity.test.ts index 218df4d2c..f336896da 100644 --- a/server/src/test/activity.test.ts +++ b/server/src/test/activity.test.ts @@ -22,6 +22,7 @@ import { restoreDeletedContent, getContentSource, getDescendantIds, + getDefaultDoenetmlVersion, } from "../query/activity"; import { getActivityViewerData, getContent } from "../query/activity_edit_view"; import { getMyContent, getMyTrash } from "../query/content_list"; @@ -61,6 +62,12 @@ const currentDoenetmlVersion = { deprecationMessage: "", }; +test("Get default DoenetML version", async () => { + const { defaultDoenetmlVersion } = await getDefaultDoenetmlVersion(); + + expect(defaultDoenetmlVersion).eqls(currentDoenetmlVersion); +}); + test("New activity starts out private, then delete it", async () => { const user = await createTestUser(); const userId = user.userId; @@ -2091,3 +2098,37 @@ test("getContent does not provide email", async () => { }); expect(result.owner).not.toHaveProperty("email"); }); + +test("Create new activity with DoenetML", async () => { + const user = await createTestUser(); + const userId = user.userId; + const { contentId: contentId } = await createContent({ + loggedInUserId: userId, + contentType: "singleDoc", + parentId: null, + doenetml: "My DoenetML source", + name: "The new document", + }); + + const data = await getMyContent({ + ownerId: userId, + loggedInUserId: userId, + parentId: null, + }); + + if (data.notMe) { + throw Error("shouldn't happen"); + } + expect(data.content).toBeDefined(); + expect(data.content.length).toBe(1); + + const newDoc = data.content[0]; + + if (newDoc.type !== "singleDoc") { + throw Error("shouldn't happen"); + } + + expect(newDoc.contentId).toStrictEqual(contentId); + expect(newDoc.name).toBe("The new document"); + expect(newDoc.doenetML).toBe("My DoenetML source"); +}); diff --git a/server/src/types.ts b/server/src/types.ts index f2fe55dce..313c0a2cf 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -319,6 +319,7 @@ export type ContentDescription = { grandparentId?: Uuid | null; grandparentName?: string | null; + doenetmlVersion?: DoenetmlVersion; // TODO: remove this when fix bad assignment versions hasBadVersion?: boolean; };