Skip to content

Commit ba35c0b

Browse files
committed
Merge branch 'offline' of github.com:RaspberryPiFoundation/editor-ui into offline
2 parents cec38db + cef2125 commit ba35c0b

12 files changed

Lines changed: 721 additions & 208 deletions

cypress/e2e/spec-scratch.cy.js

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ describe("Scratch save integration", () => {
9797
cy.findByText("cool-scratch").click();
9898
});
9999

100-
it("remixes on the first save, keeps the iframe project loaded, and saves after the identifier update", () => {
100+
it("remixes on the first save, keeps the iframe project loaded, and auto-saves after the identifier update", () => {
101101
getEditorShadow()
102102
.find("iframe[title='Scratch']")
103103
.its("0.contentDocument.body")
@@ -131,6 +131,14 @@ describe("Scratch save integration", () => {
131131
},
132132
}),
133133
);
134+
win.dispatchEvent(
135+
new win.MessageEvent("message", {
136+
origin: win.location.origin,
137+
data: {
138+
type: "scratch-gui-remixing-succeeded",
139+
},
140+
}),
141+
);
134142
});
135143

136144
cy.get("#project-identifier").should("have.text", "student-remix");
@@ -146,11 +154,35 @@ describe("Scratch save integration", () => {
146154
postMessage.resetHistory();
147155
});
148156

149-
getEditorShadow().findByRole("button", { name: "Save" }).click();
157+
getEditorShadow()
158+
.find("button")
159+
.should(($buttons) => {
160+
const buttonText = [...$buttons].map((button) =>
161+
button.textContent.trim(),
162+
);
163+
expect(buttonText).not.to.include("Save");
164+
});
150165

151-
cy.get("@scratchPostMessage")
152-
.its("firstCall.args.0")
153-
.should("deep.include", { type: "scratch-gui-save" });
166+
cy.window().then((win) => {
167+
win.dispatchEvent(
168+
new win.MessageEvent("message", {
169+
origin: win.location.origin,
170+
data: {
171+
type: "scratch-gui-project-changed",
172+
},
173+
}),
174+
);
175+
});
176+
177+
cy.wait(2100);
178+
179+
cy.get("@scratchPostMessage").should((postMessage) => {
180+
const saveMessage = postMessage
181+
.getCalls()
182+
.some((call) => call.args[0]?.type === "scratch-gui-save");
183+
184+
expect(saveMessage).to.eq(true);
185+
});
154186
});
155187
});
156188

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"@szhsin/react-menu": "^3.2.0",
2828
"apollo-link-sentry": "^3.2.3",
2929
"assert": "^2.1.0",
30-
"axios": "^1.15.0",
30+
"axios": "^1.15.2",
3131
"classnames": "^2.3.2",
3232
"codemirror": "^6.0.1",
3333
"container-query-polyfill": "^1.0.2",

src/components/ProjectBar/ScratchProjectBar.jsx

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import React from "react";
2-
import { useSelector } from "react-redux";
1+
import React, { useEffect } from "react";
2+
import { useDispatch, useSelector } from "react-redux";
33
import { useTranslation } from "react-i18next";
44
import DownloadIcon from "../../assets/icons/download.svg";
55
import UploadIcon from "../../assets/icons/upload.svg";
@@ -8,26 +8,53 @@ import ProjectName from "../ProjectName/ProjectName";
88
import DownloadButton from "../DownloadButton/DownloadButton";
99
import UploadButton from "../UploadButton/UploadButton";
1010
import DesignSystemButton from "../DesignSystemButton/DesignSystemButton";
11+
import SaveStatus from "../SaveStatus/SaveStatus";
1112

1213
import "../../assets/stylesheets/ProjectBar.scss";
14+
import { setScratchLastSavedTime } from "../../redux/EditorSlice";
1315
import { useScratchSave } from "../../hooks/useScratchSave";
1416

17+
const getProjectLastSavedTime = (updatedAt) => {
18+
const timestamp = Date.parse(updatedAt || "");
19+
return Number.isNaN(timestamp) ? null : timestamp;
20+
};
21+
1522
const ScratchProjectBar = ({ nameEditable = true }) => {
1623
const { t } = useTranslation();
24+
const dispatch = useDispatch();
1725

1826
const user = useSelector((state) => state.auth.user);
1927
const loading = useSelector((state) => state.editor.loading);
28+
const project = useSelector((state) => state.editor.project);
2029
const readOnly = useSelector((state) => state.editor.readOnly);
21-
const showScratchSaveButton = Boolean(user && !readOnly);
22-
const {
23-
isScratchSaving,
24-
saveScratchProject,
25-
scratchSaveLabelKey,
26-
shouldRemixOnSave,
27-
} = useScratchSave({
28-
enabled: showScratchSaveButton,
30+
const saving = useSelector((state) => state.editor.saving);
31+
const lastSavedTime = useSelector((state) => state.editor.lastSavedTime);
32+
const canSave = Boolean(user && !readOnly);
33+
const { saveScratchProject, shouldRemixOnSave } = useScratchSave({
34+
enabled: canSave,
2935
});
30-
const scratchSaveLabel = t(scratchSaveLabelKey);
36+
37+
const projectIdentifier = project?.identifier;
38+
const isScratchSaving = saving === "pending";
39+
const isScratchSaveFailed = saving === "failed";
40+
const isNewProject = !projectIdentifier;
41+
const canAutoSave = Boolean(projectIdentifier && !shouldRemixOnSave);
42+
const showSaveButton = canSave && (isNewProject || shouldRemixOnSave);
43+
const showSaveStatus =
44+
canSave && canAutoSave && Boolean(lastSavedTime) && !isScratchSaveFailed;
45+
const projectLastSavedTime = getProjectLastSavedTime(project?.updated_at);
46+
47+
useEffect(() => {
48+
if (!canSave || !canAutoSave || lastSavedTime || !projectLastSavedTime) {
49+
return;
50+
}
51+
52+
dispatch(
53+
setScratchLastSavedTime({
54+
lastSavedTime: projectLastSavedTime,
55+
}),
56+
);
57+
}, [dispatch, canAutoSave, canSave, lastSavedTime, projectLastSavedTime]);
3158

3259
if (loading !== "success") {
3360
return null;
@@ -55,19 +82,20 @@ const ScratchProjectBar = ({ nameEditable = true }) => {
5582
type="tertiary"
5683
/>
5784
</div>
58-
{showScratchSaveButton && (
85+
{showSaveButton && (
5986
<div className="project-bar__btn-wrapper">
6087
<DesignSystemButton
6188
className="project-bar__btn btn--save btn--primary"
6289
onClick={() => saveScratchProject({ shouldRemixOnSave })}
63-
text={scratchSaveLabel}
90+
text={t("header.save")}
6491
textAlways
6592
icon={<SaveIcon />}
6693
type="primary"
6794
disabled={isScratchSaving}
6895
/>
6996
</div>
7097
)}
98+
{showSaveStatus && <SaveStatus />}
7199
</div>
72100
</div>
73101
);

0 commit comments

Comments
 (0)