From ecd3cb55b49b0dac918e62b37de0f48c6cf53492 Mon Sep 17 00:00:00 2001 From: Sarah Rietkerk <49178322+srietkerk@users.noreply.github.com> Date: Tue, 9 Jan 2024 11:29:46 -0800 Subject: [PATCH] Teacher tool: set up infrastructure to send messages to iframe (#9811) * makecodeframe only holds ui, message handling for iframe split into the makecodeeditor service, file made for the transform * successfully sending message to set high contrast to the iframe when the evaluate button is pressed * cleaned up a bit * do something with the results of sending the message * consolidated ref logic in the service, got rid of extra logic in editor container * got rid of the editor container, it doesn't do anything meaningful anymore * got rid of the load event listener * changed ref handling, some minor changes in the service * no longer exposing sendMessageAsync * pushing messages to the queue even if the iframe isn't ready yet * changed functions from anonymous to declarative * fixed set editor ref logic, added check to see if message is undefined * upgrade codeql analysis to v2 --- .github/workflows/codeql-analysis.yml | 2 +- teachertool/src/App.tsx | 6 +- teachertool/src/components/DebugInput.tsx | 4 + .../src/components/EditorContainer.tsx | 32 ----- teachertool/src/components/MakecodeFrame.tsx | 130 +----------------- .../src/services/makecodeEditorService.ts | 61 ++++++++ .../src/transforms/loadProjectAsync.ts | 12 ++ teachertool/src/utils/index.ts | 11 ++ 8 files changed, 97 insertions(+), 161 deletions(-) delete mode 100644 teachertool/src/components/EditorContainer.tsx create mode 100644 teachertool/src/services/makecodeEditorService.ts create mode 100644 teachertool/src/transforms/loadProjectAsync.ts diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e89cb61b2418..29d3d8b9507f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -66,4 +66,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/teachertool/src/App.tsx b/teachertool/src/App.tsx index 5e77758ecc63..c72dc6607b10 100644 --- a/teachertool/src/App.tsx +++ b/teachertool/src/App.tsx @@ -7,9 +7,9 @@ import HeaderBar from "./components/HeaderBar"; import Notifications from "./components/Notifications"; import * as NotificationService from "./services/notificationService"; import { postNotification } from "./transforms/postNotification"; -import { makeNotification } from "./utils"; +import { createIFrameUrl, makeNotification } from "./utils"; import DebugInput from "./components/DebugInput"; -import { EditorContainer } from "./components/EditorContainer"; +import { MakeCodeFrame } from "./components/MakecodeFrame"; function App() { @@ -36,7 +36,7 @@ function App() {
- +
diff --git a/teachertool/src/components/DebugInput.tsx b/teachertool/src/components/DebugInput.tsx index bc429f0f38ee..b3d5a43fd63d 100644 --- a/teachertool/src/components/DebugInput.tsx +++ b/teachertool/src/components/DebugInput.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import { Button } from "react-common/components/controls/Button"; import { Input } from "react-common/components/controls/Input"; import { Textarea } from "react-common/components/controls/Textarea"; +import { loadProjectAsync } from "../transforms/loadProjectAsync"; import { runEvaluateAsync } from "../transforms/runEvaluateAsync"; interface IProps {} @@ -11,9 +12,12 @@ interface IProps {} const DebugInput: React.FC = ({}) => { const [shareLink, setShareLink] = useState("https://arcade.makecode.com/S50644-45891-08403-36583"); const [rubric, setRubric] = useState(""); + const [bools, setBools] = useState(true); const evaluate = async () => { + setBools(!bools); await runEvaluateAsync(shareLink, rubric); + await loadProjectAsync("hi", bools); } return ( diff --git a/teachertool/src/components/EditorContainer.tsx b/teachertool/src/components/EditorContainer.tsx deleted file mode 100644 index 5a92c83d57ca..000000000000 --- a/teachertool/src/components/EditorContainer.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { MakeCodeFrame } from "./MakecodeFrame"; -import { isLocal, getEditorUrl } from "../utils"; - -const createIFrameUrl = (): string => { - const editorUrl: string = isLocal() ? "http://localhost:3232/index.html#editor" : getEditorUrl((window as any).pxtTargetBundle.appTheme.embedUrl); - - let url = editorUrl - if (editorUrl.charAt(editorUrl.length - 1) === "/" && !isLocal()) { - url = editorUrl.substr(0, editorUrl.length - 1); - } - url += `?controller=1&ws=browser&nocookiebanner=1`; - return url; -} - -export const EditorContainer: React.FC<{}> = () => { - const onIframeLoaded = () => { - console.log("iframe loaded"); - } - - const onIframeClosed = () => { - console.log("iframe closed"); - } - - return ( - - ) - -} - diff --git a/teachertool/src/components/MakecodeFrame.tsx b/teachertool/src/components/MakecodeFrame.tsx index d54be39ddb92..25e3faaf5b82 100644 --- a/teachertool/src/components/MakecodeFrame.tsx +++ b/teachertool/src/components/MakecodeFrame.tsx @@ -1,141 +1,21 @@ /// import * as React from "react"; -import { useState, useEffect, useRef } from "react"; -import { ProgressBar } from "react-common/components/controls/ProgressBar"; +import { setEditorRef } from "../services/makecodeEditorService"; interface MakeCodeFrameProps { pageSourceUrl: string; - highContrast?: boolean; - onFrameOpen: () => void; - onFrameClose: () => void; tutorialEventHandler?: (event: pxt.editor.EditorMessageTutorialEventRequest) => void; } -type FrameState = "loading" | "no-project" | "opening-project" | "project-open" | "closing-project"; - -interface PendingMessage { - original: pxt.editor.EditorMessageRequest; - handler: (response: any) => void; -} - export const MakeCodeFrame: React.FC = - ( { pageSourceUrl, - highContrast, - onFrameClose, - onFrameOpen - } ) => { - let ref: HTMLIFrameElement | undefined; - const messageQueue: pxt.editor.EditorMessageRequest[] = []; - let nextId: number = 0; - let pendingMessages: {[index: string]: PendingMessage} = {}; - const [frameState, setFrameState] = useState("loading"); - const [loadPercent, setLoadPercent] = useState(0); - const [workspaceReady, setWorkspaceReady] = useState(false); - - let frameRef = useRef(null); - - useEffect(() => { - // logic we want to do when the iframe is loaded - if (ref && ref.contentWindow) { - window.addEventListener("message", onMessageReceived); - ref.addEventListener("load", handleFrameReload) - - } - - // logic we want when the iframe unmounts - return () => { - window.removeEventListener("message", onMessageReceived); - if (ref && ref.contentWindow) { - ref.removeEventListener("load", handleFrameReload) - } - } - }, []) - - useEffect(() => { - sendMessageAsync({ - type: "pxteditor", - action: "sethighcontrast", - on: highContrast - } as pxt.editor.EditorMessageSetHighContrastRequest); - }, [highContrast]) - - - useEffect(() => { - if (frameState === "project-open") { - setFrameState("closing-project"); - onFrameClose(); - } - else if (frameState === "no-project") { - setFrameState("opening-project"); - onFrameOpen(); - } - }, [frameState]); - - - const handleFrameReload = () => { - setFrameState("loading") - } - - const onMessageReceived = (event: MessageEvent) => { - const data = event.data as pxt.editor.EditorMessageRequest; - if (frameState === "opening-project") setLoadPercent(Math.min((loadPercent || 0) + 7, 95)); - - if (data.type === "pxteditor" && data.id && pendingMessages[data.id]) { - const pending = pendingMessages[data.id]; - pending.handler(data); - delete pendingMessages[data.id]; - return; - } - - switch (data.action) { - case "newproject": - if (!workspaceReady) { - setWorkspaceReady(true); - sendMessageAsync(); // Flush message queue - } - if (frameState === "loading") { - setFrameState("no-project"); - } - break; - default: - console.log(JSON.stringify(data, null, 4)); - } - } - - const sendMessageAsync = (message?: any) => { - return new Promise(resolve => { - const sendMessageCore = (message: any) => { - message.response = true; - message.id = nextId++ + ""; - pendingMessages[message.id] = { - original: message, - handler: resolve - }; - ref!.contentWindow!.postMessage(message, "*"); - } + ( { pageSourceUrl} ) => { - if (ref) { - if (!workspaceReady) { - messageQueue.push(message); - } - else { - while (messageQueue.length) { - sendMessageCore(messageQueue.shift()); - } - if (message) sendMessageCore(message); - } - } - }); + const handleIFrameRef = (el: HTMLIFrameElement | null) => { + setEditorRef(el ?? undefined); } - const openingProject = frameState === "opening-project"; - const showLoader = openingProject || frameState === "closing-project"; /* eslint-disable @microsoft/sdl/react-iframe-missing-sandbox */ return
-
- {openingProject && } -
{lf("Loading...")}
-
- +
/* eslint-enable @microsoft/sdl/react-iframe-missing-sandbox */ } \ No newline at end of file diff --git a/teachertool/src/services/makecodeEditorService.ts b/teachertool/src/services/makecodeEditorService.ts new file mode 100644 index 000000000000..1e88091ef359 --- /dev/null +++ b/teachertool/src/services/makecodeEditorService.ts @@ -0,0 +1,61 @@ +interface PendingMessage { + original: pxt.editor.EditorMessageRequest; + handler: (response: any) => void; +} + +let makecodeEditorRef: HTMLIFrameElement | undefined; +const messageQueue: pxt.editor.EditorMessageRequest[] = []; +let nextId: number = 0; +let pendingMessages: {[index: string]: PendingMessage} = {}; + +function onMessageReceived(event: MessageEvent) { + const data = event.data as pxt.editor.EditorMessageRequest; + if (data.type === "pxteditor" && data.id && pendingMessages[data.id]) { + const pending = pendingMessages[data.id]; + pending.handler(data); + delete pendingMessages[data.id]; + return; + } + + console.log("Received message from iframe:", data); +} + +function sendMessageAsync(message?: any) { + return new Promise(resolve => { + const sendMessageCore = (message: any) => { + message.response = true; + message.id = nextId++ + ""; + pendingMessages[message.id] = { + original: message, + handler: resolve + }; + makecodeEditorRef!.contentWindow!.postMessage(message, "*"); + } + + if (message) messageQueue.push(message); + if (makecodeEditorRef) { + while (messageQueue.length) { + sendMessageCore(messageQueue.shift()); + } + } + }); +} + +export function setEditorRef(ref: HTMLIFrameElement | undefined) { + makecodeEditorRef = ref ?? undefined; + window.removeEventListener("message", onMessageReceived); + if (ref) { + window.addEventListener("message", onMessageReceived); + sendMessageAsync(); + } +} + +// an example of events that we want to/can send to the editor +export async function setHighContrastAsync(on: boolean) { + const result = await sendMessageAsync({ + type: "pxteditor", + action: "sethighcontrast", + on: on + }); + console.log(result); +} diff --git a/teachertool/src/transforms/loadProjectAsync.ts b/teachertool/src/transforms/loadProjectAsync.ts new file mode 100644 index 000000000000..272e5e4fccf7 --- /dev/null +++ b/teachertool/src/transforms/loadProjectAsync.ts @@ -0,0 +1,12 @@ +import { stateAndDispatch } from "../state"; +import * as Actions from "../state/actions"; +import { postNotification } from "./postNotification"; +import { makeNotification } from "../utils"; +import { setHighContrastAsync } from "../services/makecodeEditorService"; + +export async function loadProjectAsync(projectId: string, bool: boolean) { + const { dispatch } = stateAndDispatch(); + await setHighContrastAsync(bool); + postNotification(makeNotification(`project ${projectId} evaluated`, 2000)); + +} diff --git a/teachertool/src/utils/index.ts b/teachertool/src/utils/index.ts index 8623f94812c9..7a65a43e2afe 100644 --- a/teachertool/src/utils/index.ts +++ b/teachertool/src/utils/index.ts @@ -28,4 +28,15 @@ export const getEditorUrl = (embedUrl: string) => { // example: https://arcade.makecode.com/abc123 and this would get returned const path = /\/([\da-zA-Z\.]+)(?:--)?/i.exec(window.location.pathname); return `${embedUrl.replace(/\/$/, "")}/${path?.[1] || ""}`; +} + +export const createIFrameUrl = (): string => { + const editorUrl: string = isLocal() ? "http://localhost:3232/index.html#editor" : getEditorUrl((window as any).pxtTargetBundle.appTheme.embedUrl); + + let url = editorUrl + if (editorUrl.charAt(editorUrl.length - 1) === "/" && !isLocal()) { + url = editorUrl.substr(0, editorUrl.length - 1); + } + url += `?controller=1&ws=browser&nocookiebanner=1`; + return url; } \ No newline at end of file