@@ -24,58 +79,6 @@ const VisualOutputPane = ({ visuals, setVisuals }) => {
);
};
-const showVisuals = (visuals, output) =>
- visuals.map((v) => (v.showing ? v : showVisual(v, output)));
-
-const showVisual = (visual, output) => {
- switch (visual.origin) {
- case "sense_hat":
- output.current.textContent = JSON.stringify(visual.content);
- break;
- case "pygal":
- const chartContent = {
- ...visual.content,
- chart: {
- ...visual.content.chart,
- events: {
- ...visual.content.chart.events,
- load: function () {
- this.renderTo.style.overflow = "visible";
- },
- },
- },
- tooltip: {
- ...visual.content.tooltip,
- formatter:
- visual.content.chart.type === "pie"
- ? function () {
- return this.key + ": " + this.y;
- }
- : null,
- },
- };
- Highcharts.chart(output.current, chartContent);
- break;
- case "turtle":
- output.current.innerHTML = elementFromProps(visual.content).outerHTML;
- break;
- case "matplotlib":
- // convert visual.content from Uint8Array to jpg
- const img = document.createElement("img");
- img.style = "max-width: 100%; max-height: 100%;";
- img.src = `data:image/jpg;base64,${window.btoa(
- String.fromCharCode(...new Uint8Array(visual.content)),
- )}`;
- output.current.innerHTML = img.outerHTML;
- break;
- default:
- throw new Error(`Unsupported origin: ${visual.origin}`);
- }
-
- visual.showing = true;
- return visual;
-};
-
const elementFromProps = (map) => {
const tag = map.get("tag");
if (!tag) {
diff --git a/src/redux/EditorSlice.js b/src/redux/EditorSlice.js
index ec5119c87..624ef3dd2 100644
--- a/src/redux/EditorSlice.js
+++ b/src/redux/EditorSlice.js
@@ -95,6 +95,7 @@ export const loadProjectList = createAsyncThunk(
const initialState = {
project: {},
+ cascadeUpdate: false,
readOnly: false,
saveTriggered: false,
saving: "idle",
@@ -197,7 +198,7 @@ export const EditorSlice = createSlice({
state.project.components.push({
name: action.payload.name,
extension: action.payload.extension,
- content: "",
+ content: action.payload.content || "",
});
state.saving = "idle";
},
@@ -262,18 +263,22 @@ export const EditorSlice = createSlice({
state.saveTriggered = true;
},
updateProjectComponent: (state, action) => {
- const extension = action.payload.extension;
- const fileName = action.payload.name;
- const code = action.payload.code;
+ const {
+ extension,
+ name: fileName,
+ content,
+ cascadeUpdate,
+ } = action.payload;
const mapped = state.project.components.map((item) => {
if (item.extension !== extension || item.name !== fileName) {
return item;
}
- return { ...item, ...{ content: code } };
+ return { ...item, ...{ content } };
});
state.project.components = mapped;
+ state.cascadeUpdate = cascadeUpdate;
},
updateProjectName: (state, action) => {
state.project.name = action.payload;
@@ -295,6 +300,9 @@ export const EditorSlice = createSlice({
}
state.saving = "idle";
},
+ setCascadeUpdate: (state, action) => {
+ state.cascadeUpdate = action.payload;
+ },
setError: (state, action) => {
state.error = action.payload;
},
@@ -454,6 +462,7 @@ export const {
setEmbedded,
setIsOutputOnly,
setBrowserPreview,
+ setCascadeUpdate,
setError,
setIsSplitView,
setNameError,
diff --git a/src/redux/EditorSlice.test.js b/src/redux/EditorSlice.test.js
index 186dac7a8..7c7baf6e2 100644
--- a/src/redux/EditorSlice.test.js
+++ b/src/redux/EditorSlice.test.js
@@ -11,6 +11,9 @@ import reducer, {
setIsOutputOnly,
setErrorDetails,
setReadOnly,
+ addProjectComponent,
+ updateProjectComponent,
+ setCascadeUpdate,
} from "./EditorSlice";
const mockCreateRemix = jest.fn();
@@ -104,6 +107,82 @@ test("Action setReadOnly correctly sets readOnly", () => {
expect(reducer(previousState, setReadOnly(true))).toEqual(expectedState);
});
+test("Action addProjectComponent adds component to project with correct content", () => {
+ const previousState = {
+ project: {
+ components: [],
+ },
+ };
+ const expectedState = {
+ project: {
+ components: [
+ {
+ name: "main",
+ extension: "py",
+ content: "print('hello world')",
+ },
+ ],
+ },
+ saving: "idle",
+ };
+ expect(
+ reducer(
+ previousState,
+ addProjectComponent({
+ name: "main",
+ extension: "py",
+ content: "print('hello world')",
+ }),
+ ),
+ ).toEqual(expectedState);
+});
+
+test("Action updateProjectComponent updates component in project with correct content", () => {
+ const previousState = {
+ project: {
+ components: [
+ {
+ name: "main",
+ extension: "py",
+ content: "print('hello world')",
+ },
+ ],
+ },
+ cascadeUpdate: false,
+ };
+ const expectedState = {
+ project: {
+ components: [
+ {
+ name: "main",
+ extension: "py",
+ content: "print('hello there world!')",
+ },
+ ],
+ },
+ cascadeUpdate: true,
+ };
+ expect(
+ reducer(
+ previousState,
+ updateProjectComponent({
+ name: "main",
+ extension: "py",
+ content: "print('hello there world!')",
+ cascadeUpdate: true,
+ }),
+ ),
+ ).toEqual(expectedState);
+});
+
+test("Action setCascadeUpdate sets cascadeUpdate correctly", () => {
+ const previousState = { cascadeUpdate: true };
+ const expectedState = { cascadeUpdate: false };
+ expect(reducer(previousState, setCascadeUpdate(false))).toEqual(
+ expectedState,
+ );
+});
+
test("Showing rename modal sets file state and showing status", () => {
const previousState = {
renameFileModalShowing: false,
From 5d0c69caf145ffe5a9587ac2851a720b3a405f83 Mon Sep 17 00:00:00 2001
From: Dan Halson
Date: Wed, 29 Jan 2025 09:13:22 +0000
Subject: [PATCH 04/18] Try pinning previous ubuntu version to avoid cloudflare
issue (#1178)
Test deployment to try out a potential workaround for cloudflare issue:
```
Update - We are continuing to work on a fix for native CRC32 checksum handling in R2 APIs. If you run into an issue, please see the documentation for workarounds based on the SDK that you are using: https://developers.cloudflare.com/r2/examples/aws/
Jan 22, 2025 - 16:33 UTC
Update - We are continuing to work on a fix for this issue.
Jan 17, 2025 - 11:23 UTC
Identified - AWS recently updated their SDKs to enable CRC32 checksums on multiple object operations by default.
R2 does not currently support CRC32 checksums, and the default configurations will return header related errors such as Header 'x-amz-checksum-algorithm' with value 'CRC32' not implemented.
Impacted users can either pin AWS SDKs to a prior version or modify the configuration to restore the prior default behavior of not checking checksums on upload.
For more details, see the examples section of the R2 Docs for the relevant SDK:
https://developers.cloudflare.com/r2/examples/aws/
Jan 17, 2025 - 03:20 UTC
```
---------
Co-authored-by: Conor
---
.github/workflows/changelog.yml | 34 ++++++++++++++++-----------------
.github/workflows/deploy.yml | 6 ++++++
CHANGELOG.md | 4 ++++
3 files changed, 27 insertions(+), 17 deletions(-)
diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml
index 82567be5c..6f34a5ca5 100644
--- a/.github/workflows/changelog.yml
+++ b/.github/workflows/changelog.yml
@@ -3,26 +3,26 @@ name: Changelog
on:
pull_request:
branches:
- - '**'
+ - "**"
jobs:
changelog-updated:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- with:
- fetch-depth: 0
- - name: Check if changelog updated
- id: changed-files-specific
- uses: tj-actions/changed-files@v29.0.4
- with:
- base_sha: ${{ github.event.pull_request.base.sha }}
- files: |
- CHANGELOG.md
- env:
- GITHUB_TOKEN: ${{ github.token }}
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Check if changelog updated
+ id: changed-files-specific
+ uses: tj-actions/changed-files@v29.0.4
+ with:
+ base_sha: ${{ github.event.pull_request.base.sha }}
+ files: |
+ CHANGELOG.md
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
- - name: Fail job if changelog not updated
- if: steps.changed-files-specific.outputs.any_changed == 'false'
- run: |
- exit 1
+ - name: Fail job if changelog not updated
+ if: steps.changed-files-specific.outputs.any_changed == 'false'
+ run: |
+ exit 1
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index de3b9fe93..db09ef1b3 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -123,6 +123,12 @@ jobs:
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Install AWS CLI v2.22.35
+ run: |
+ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64-2.22.35.zip" -o "awscliv2.zip"
+ unzip awscliv2.zip
+ sudo ./aws/install --update
+
- name: Build WC bundle
run: |
# TODO: Reinitialise when storybook build is fixed
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6795ef2bb..43cbee96f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
## Unreleased
+### Fixed
+
+- Fix AWS CLI in deploy script to 2.22.35 to workaround cloudflare issue (See https://developers.cloudflare.com/r2/examples/aws/aws-cli/) (#1158)
+
### Added
- Ability to write to files in `python` (#1146)
From d554021a89f008ca01f80a5a2b61b2af2fdc5400 Mon Sep 17 00:00:00 2001
From: "create-issue-branch[bot]"
<53036503+create-issue-branch[bot]@users.noreply.github.com>
Date: Wed, 29 Jan 2025 10:00:28 +0000
Subject: [PATCH 05/18] Integration: Save / Login functionality not as expected
(#1162)
'Login to save' button now automatically saves after logging in.
Closes #843
---------
Co-authored-by: create-issue-branch[bot] <53036503+create-issue-branch[bot]@users.noreply.github.com>
Co-authored-by: Dan Halson
Co-authored-by: Dan Halson
Co-authored-by: Conor
---
.devcontainer/devcontainer.json | 8 +-------
CHANGELOG.md | 3 ++-
src/hooks/useProjectPersistence.js | 18 +++++++++++++-----
src/hooks/useProjectPersistence.test.js | 20 ++++++++++++++++++++
4 files changed, 36 insertions(+), 13 deletions(-)
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index e8f310d6f..a4ce7d334 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -39,8 +39,6 @@
"vscode": {
"extensions": [
"ms-azuretools.vscode-docker",
- "ninoseki.vscode-gem-lens",
- "rebornix.ruby",
"eamodio.gitlens",
"github.vscode-pull-request-github",
"wmaurer.change-case",
@@ -53,7 +51,6 @@
"pkosta2005.heroku-command",
"yzhang.markdown-all-in-one",
"mikestead.dotenv",
- "wingrunr21.vscode-ruby",
"ms-vscode.remote-repositories",
"github.remotehub",
"circleci.circleci",
@@ -68,10 +65,7 @@
],
"settings": {
"terminal.integrated.defaultProfile.linux": "zsh",
- "jest.autoRun": {
- "watch": false,
- "onSave": "test-src-file"
- }
+ "jest.jestCommandLine": "yarn test"
}
}
},
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 43cbee96f..c6145d4c6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,7 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Fixed
-- Fix AWS CLI in deploy script to 2.22.35 to workaround cloudflare issue (See https://developers.cloudflare.com/r2/examples/aws/aws-cli/) (#1158)
+- Fix AWS CLI in deploy script to 2.22.35 to workaround cloudflare issue (See https://developers.cloudflare.com/r2/examples/aws/aws-cli/) (#1178)
### Added
@@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Changed
- Made `INSTRUCTIONS.md` a reserved file name (#1160)
+- Login to save now logs in and automatically saves (#1162)
## [0.28.14] - 2025-01-06
diff --git a/src/hooks/useProjectPersistence.js b/src/hooks/useProjectPersistence.js
index 115d9a0f2..f5bc2ca65 100644
--- a/src/hooks/useProjectPersistence.js
+++ b/src/hooks/useProjectPersistence.js
@@ -42,17 +42,26 @@ export const useProjectPersistence = ({
const accessToken = user?.access_token;
const params = { reactAppApiEndpoint, accessToken };
- if (saveTriggered) {
+ if (saveTriggered || localStorage.getItem("awaitingSave")) {
if (isOwner(user, project)) {
await dispatch(
syncProject("save")({ ...params, project, autosave: false }),
);
+ localStorage.removeItem("awaitingSave");
} else if (user && identifier) {
- await dispatch(syncProject("remix")({ ...params, project }));
+ await dispatch(
+ syncProject("remix")({
+ ...params,
+ project,
+ }),
+ );
if (loadRemix) {
// Ensure the remixed project is loaded, otherwise we'll get in a mess
await dispatch(
- syncProject("loadRemix")({ ...params, identifier }),
+ syncProject("loadRemix")({
+ ...params,
+ identifier,
+ }),
);
}
}
@@ -81,13 +90,12 @@ export const useProjectPersistence = ({
if (justLoaded) {
dispatch(expireJustLoaded());
} else {
- saveToLocalStorage(project);
-
if (!hasShownSavePrompt) {
user ? showSavePrompt() : showLoginPrompt();
dispatch(setHasShownSavePrompt());
}
}
+ saveToLocalStorage(project);
}
}
}, autoSaveInterval);
diff --git a/src/hooks/useProjectPersistence.test.js b/src/hooks/useProjectPersistence.test.js
index 0ec078cbc..212710f2c 100644
--- a/src/hooks/useProjectPersistence.test.js
+++ b/src/hooks/useProjectPersistence.test.js
@@ -342,5 +342,25 @@ describe("When logged in", () => {
autosave: false,
});
});
+
+ test("Saves project to database if awaitingSave is set", async () => {
+ localStorage.setItem("awaitingSave", "true");
+
+ renderHook(() =>
+ useProjectPersistence({
+ user: user1,
+ project: project,
+ saveTriggered: false,
+ }),
+ );
+ jest.runAllTimers();
+ expect(saveProject).toHaveBeenCalledWith({
+ project,
+ accessToken: user1.access_token,
+ autosave: false,
+ });
+
+ localStorage.removeItem("awaitingSave");
+ });
});
});
From 784e86ab7a36a800622ae716576d2399716ddf89 Mon Sep 17 00:00:00 2001
From: Lois Wells <88904316+loiswells97@users.noreply.github.com>
Date: Thu, 30 Jan 2025 09:15:52 +0000
Subject: [PATCH 06/18] HTML audio video support (#1179)
## What's Changed?
- `HtmlRunner` now looks for media in `editor.project.videos` and
`editor.project.audio` as well as `editor.project.image_list`
- Upgraded React Testing Library to reduce test warnings
---
CHANGELOG.md | 1 +
package.json | 2 +-
.../Editor/Runners/HtmlRunner/HtmlRunner.jsx | 34 +++++----
.../Runners/HtmlRunner/HtmlRunner.test.js | 71 +++++++++++++++++++
yarn.lock | 50 ++++++++-----
5 files changed, 127 insertions(+), 31 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c6145d4c6..391380a3a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- Ability to write to files in `python` (#1146)
- Support for the `outputPanels` attribute in the `PyodideRunner` (#1157)
- Downloading project instructions (#1160)
+- Support for audio and video files in HTML projects (#1179)
### Changed
diff --git a/package.json b/package.json
index bbda89f88..9ddc62e48 100644
--- a/package.json
+++ b/package.json
@@ -111,7 +111,7 @@
"@react-three/test-renderer": "8.2.1",
"@svgr/webpack": "5.5.0",
"@testing-library/jest-dom": "^5.16.5",
- "@testing-library/react": "^13.2.0",
+ "@testing-library/react": "14.3.1",
"@testing-library/user-event": "^12.1.10",
"@typescript-eslint/eslint-plugin": "^4.5.0",
"@typescript-eslint/parser": "^4.5.0",
diff --git a/src/components/Editor/Runners/HtmlRunner/HtmlRunner.jsx b/src/components/Editor/Runners/HtmlRunner/HtmlRunner.jsx
index f5bcbd13e..ae8a47720 100644
--- a/src/components/Editor/Runners/HtmlRunner/HtmlRunner.jsx
+++ b/src/components/Editor/Runners/HtmlRunner/HtmlRunner.jsx
@@ -31,7 +31,11 @@ import { MOBILE_MEDIA_QUERY } from "../../../../utils/mediaQueryBreakpoints";
function HtmlRunner() {
const project = useSelector((state) => state.editor.project);
const projectCode = project.components;
- const projectImages = project.image_list;
+ const projectMedia = [
+ ...(project.image_list || []),
+ ...(project.audio || []),
+ ...(project.videos || []),
+ ];
const firstPanelIndex = 0;
const focussedFileIndex = useSelector(
@@ -114,9 +118,9 @@ function HtmlRunner() {
const cssProjectImgs = (projectFile) => {
var updatedProjectFile = { ...projectFile };
if (projectFile.extension === "css") {
- projectImages.forEach((image) => {
- const find = new RegExp(`['"]${image.filename}['"]`, "g"); // prevent substring matches
- const replace = `"${image.url}"`;
+ projectMedia.forEach((media_file) => {
+ const find = new RegExp(`['"]${media_file.filename}['"]`, "g"); // prevent substring matches
+ const replace = `"${media_file.url}"`;
updatedProjectFile.content = updatedProjectFile.content.replaceAll(
find,
replace,
@@ -264,27 +268,29 @@ function HtmlRunner() {
const replaceSrcNodes = (
indexPage,
- projectImages,
+ projectMedia,
projectCode,
attr = "src",
) => {
const srcNodes = indexPage.querySelectorAll(`[${attr}]`);
srcNodes.forEach((srcNode) => {
- const projectImage = projectImages.find(
+ const projectMediaFile = projectMedia.find(
(component) => component.filename === srcNode.attrs[attr],
);
- const projectFile = projectCode.find(
+ const projectTextFile = projectCode.find(
(file) => `${file.name}.${file.extension}` === srcNode.attrs[attr],
);
let src = "";
- if (!!projectImage) {
- src = projectImage.url;
- } else if (!!projectFile) {
+ if (!!projectMediaFile) {
+ src = projectMediaFile.url;
+ } else if (!!projectTextFile) {
src = getBlobURL(
- projectFile.content,
- mimeTypes.lookup(`${projectFile.name}.${projectFile.extension}`),
+ projectTextFile.content,
+ mimeTypes.lookup(
+ `${projectTextFile.name}.${projectTextFile.extension}`,
+ ),
);
} else if (matchingRegexes(allowedExternalLinks, srcNode.attrs[attr])) {
src = srcNode.attrs[attr];
@@ -351,8 +357,8 @@ function HtmlRunner() {
body.insertAdjacentHTML("afterbegin", disableLocalStorageScript);
replaceHrefNodes(indexPage, projectCode);
- replaceSrcNodes(indexPage, projectImages, projectCode);
- replaceSrcNodes(indexPage, projectImages, projectCode, "data-src");
+ replaceSrcNodes(indexPage, projectMedia, projectCode);
+ replaceSrcNodes(indexPage, projectMedia, projectCode, "data-src");
body.appendChild(parse(``));
diff --git a/src/components/Editor/Runners/HtmlRunner/HtmlRunner.test.js b/src/components/Editor/Runners/HtmlRunner/HtmlRunner.test.js
index 98fac8be5..40ad19de9 100644
--- a/src/components/Editor/Runners/HtmlRunner/HtmlRunner.test.js
+++ b/src/components/Editor/Runners/HtmlRunner/HtmlRunner.test.js
@@ -499,6 +499,77 @@ describe("When an allowed external link is rendered", () => {
});
});
+describe("When media is rendered", () => {
+ const mediaHTML =
+ '
';
+ let generatedHtml;
+ beforeEach(() => {
+ const middlewares = [];
+ const mockStore = configureStore(middlewares);
+ const initialState = {
+ editor: {
+ project: {
+ components: [
+ { name: "index", extension: "html", content: mediaHTML },
+ ],
+ image_list: [
+ {
+ filename: "image.jpeg",
+ url: "https://example.com/image.jpeg",
+ },
+ ],
+ videos: [
+ {
+ filename: "video.mp4",
+ url: "https://example.com/video.mp4",
+ },
+ ],
+ audio: [
+ {
+ filename: "audio.mp3",
+ url: "https://example.com/audio.mp3",
+ },
+ ],
+ },
+ focussedFileIndices: [0],
+ openFiles: [["index.html"]],
+ codeRunTriggered: true,
+ codeHasBeenRun: true,
+ errorModalShowing: false,
+ },
+ };
+ const store = mockStore(initialState);
+ render(
+
+
+
+
+
+
+ ,
+ );
+ [generatedHtml] = Blob.mock.calls[0][0];
+ });
+
+ test("Transforms image sources", () => {
+ expect(generatedHtml).toContain(
+ '
{
+ expect(generatedHtml).toContain(
+ '