Skip to content

Trigger Scratch remix on first save of educator projects#1397

Open
abcampo-iry wants to merge 3 commits intomainfrom
issues/1238-trigger-remix-when-saving-for-the-first-time
Open

Trigger Scratch remix on first save of educator projects#1397
abcampo-iry wants to merge 3 commits intomainfrom
issues/1238-trigger-remix-when-saving-for-the-first-time

Conversation

@abcampo-iry
Copy link
Contributor

@abcampo-iry abcampo-iry commented Mar 20, 2026

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

  • Track Scratch remix start, success, and failure in the shared save-state hook.
  • Store the original Scratch iframe project id separately from the parent project id.
  • Update the parent project id from scratch-gui-project-id-updated.
  • Use the updated parent id to make remix happen only on the first save.
  • Keep the iframe src pinned to the original Scratch project id so the iframe does not reload after remix.
  • Extract Scratch save helpers so the generic save button and Scratch project bar share the same save/remix.

Behaviour

  • Pressing save for the first time when viewing an educator project should trigger a scratch remix
  • The save button should be in it's 'saving' state until the remix is completed.
  • The page or Scratch iframe shouldn't reload (this could be split out if complex)

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.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_id to 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

  • iframeSrcUrl is built by string-concatenating process.env.ASSETS_URL. If ASSETS_URL is 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.html pointing at the wrong host. Building this with the URL constructor (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.

Comment on lines +8 to +10
const allowedOrigin = process.env.ASSETS_URL || window.location.origin;
getScratchIframeContentWindow().postMessage(message, allowedOrigin);
};
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +33
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;

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants