Trigger Scratch remix on first save of educator projects#1397
Trigger Scratch remix on first save of educator projects#1397abcampo-iry wants to merge 3 commits intomainfrom
Conversation
Handle Scratch remix start, success, and failure events in the shared save-state hook, and forward Scratch remix creation failures from the iframe host page.
Store the original Scratch iframe project identifier, update the parent project id when Scratch posts a remixed id, and use that state to remix only on the first save without reloading the iframe.
Move iframe-specific message handling into shared Scratch helpers and centralize the Scratch save button wiring in a hook so the first-save remix flow reads closer to the regular project save flow.
There was a problem hiding this comment.
Pull request overview
Updates the Scratch editor save flow so that when a user saves a Scratch project they don’t own, the first save triggers a Scratch “remix” instead of a normal save, while keeping the UI save state consistent and preventing the Scratch iframe from reloading after the remix identifier is issued.
Changes:
- Adds remix-aware save state handling (start/success/failure) and a shared Scratch save/remix pathway used by both the generic Save button and the Scratch project bar.
- Tracks the original Scratch iframe project identifier separately from the parent project identifier, and updates the parent identifier when Scratch reports a new remixed id.
- Subscribes to Scratch GUI “project id updated” messages and pins the iframe
project_idto the original identifier to avoid iframe reloads.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/scratchIframe.js | Adds remix decision helper and message subscription; updates postMessage target origin handling. |
| src/utils/scratchIframe.test.js | Adds tests for new scratch iframe helpers and identifier update subscription. |
| src/scratch.jsx | Emits remix-failed event for Scratch GUI “creatingError” alerts. |
| src/redux/reducers/loadProjectReducers.js | Initializes and sets scratchIframeProjectIdentifier during project load lifecycle. |
| src/redux/reducers/loadProjectReducers.test.js | Updates reducer expectations for new scratch iframe identifier state. |
| src/redux/EditorSlice.js | Stores original Scratch iframe identifier on project set; adds action to update only parent identifier. |
| src/redux/EditorSlice.test.js | Tests scratch iframe identifier initialization and parent-id-only updates. |
| src/hooks/useScratchSaveState.js | Extends save-state lifecycle to include remix events; supports remix command. |
| src/hooks/useScratchSaveState.test.js | Updates tests for remix command behavior and remix lifecycle. |
| src/hooks/useScratchSave.js | New hook encapsulating Scratch save/remix eligibility + save state wiring. |
| src/components/WebComponentProject/WebComponentProject.integration.test.js | Ensures identifier-changed event fires when remix updates the parent identifier. |
| src/components/SaveButton/SaveButton.jsx | Routes Scratch projects through shared Scratch save/remix hook; disables during Scratch saving/remixing. |
| src/components/SaveButton/SaveButton.test.js | Adds coverage that first Scratch save triggers remix via postMessage instead of triggerSave. |
| src/components/ProjectBar/ScratchProjectBar.jsx | Uses shared Scratch save/remix hook and passes remix intent on click. |
| src/components/ProjectBar/ScratchProjectBar.test.js | Adds coverage for remix-on-first-save and remix “saving” UI state. |
| src/components/Editor/Project/ScratchContainer.jsx | Subscribes to Scratch id updates and pins iframe to original identifier while updating parent identifier. |
| src/components/Editor/Project/ScratchContainer.test.js | Tests parent identifier updates without changing iframe project_id. |
Comments suppressed due to low confidence (1)
src/components/Editor/Project/ScratchContainer.jsx:38
iframeSrcUrlis built by string-concatenatingprocess.env.ASSETS_URL. IfASSETS_URLis configured with a trailing slash, this produces a double-slash path; if it's ever configured as a path (e.g. "/") it can produce a protocol-relative URL like//scratch.htmlpointing at the wrong host. Building this with theURLconstructor (and ensuring the base is an absolute URL) avoids malformed iframe URLs and keeps the iframe origin aligned with the postMessage origin checks.
const iframeSrcUrl = `${
process.env.ASSETS_URL
}/scratch.html?${queryParams.toString()}`;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const allowedOrigin = process.env.ASSETS_URL || window.location.origin; | ||
| getScratchIframeContentWindow().postMessage(message, allowedOrigin); | ||
| }; |
There was a problem hiding this comment.
postMessage targetOrigin must be an origin (scheme+host+port) or "*". Using process.env.ASSETS_URL directly can be invalid if it contains a path or trailing slash (or is set to something like "/" via CRA publicUrl), which will throw at runtime and/or fail origin checks. Consider normalizing via new URL(process.env.ASSETS_URL, window.location.href).origin (with a safe fallback) and reusing the same normalization anywhere you compare event.origin to the allowed origin (e.g. useScratchSaveState).
| const allowedOrigin = process.env.ASSETS_URL || window.location.origin; | ||
|
|
||
| const handleScratchMessage = ({ origin, data }) => { | ||
| if (origin !== allowedOrigin) return; | ||
| if (data?.type !== "scratch-gui-project-id-updated") return; | ||
| if (!data.projectId) return; | ||
|
|
There was a problem hiding this comment.
Same origin-normalization issue here: origin from a MessageEvent is always just the origin (no trailing slash/path). If allowedOrigin comes from ASSETS_URL with a trailing slash or path, origin !== allowedOrigin will incorrectly ignore valid Scratch messages. Normalize ASSETS_URL to an origin before comparing.
Closes: https://github.com/RaspberryPiFoundation/digital-editor-issues/issues/1238
Summary
This updates the Scratch flow so the first save of a saved Scratch project that the current user does not own sends a Scratch remix instead of a normal save.
It also keeps the save button in the Scratch saving state through remix events, updates the parent project identifier when Scratch reports the remixed id, and avoids reloading the Scratch iframe by keeping the iframe bound to its original project id.
What changed
Behaviour