@@ -50,25 +36,12 @@ const ProjectBar = ({ nameEditable = true }) => {
type="tertiary"
/>
- {!isScratchProject && !projectOwner && !readOnly && (
+ {!projectOwner && !readOnly && (
)}
- {showScratchSaveButton && (
-
- }
- type="primary"
- disabled={isScratchSaving}
- />
-
- )}
- {lastSavedTime && user && !readOnly && !isScratchProject && (
+ {lastSavedTime && user && !readOnly && (
)}
diff --git a/src/components/ProjectBar/ProjectBar.test.js b/src/components/ProjectBar/ProjectBar.test.js
index d66088cbf..9a664c14b 100644
--- a/src/components/ProjectBar/ProjectBar.test.js
+++ b/src/components/ProjectBar/ProjectBar.test.js
@@ -1,15 +1,11 @@
import React from "react";
-import { act, fireEvent, render, screen } from "@testing-library/react";
+import { render, screen } from "@testing-library/react";
import { Provider } from "react-redux";
import configureStore from "redux-mock-store";
import { MemoryRouter } from "react-router-dom";
import ProjectBar from "./ProjectBar";
-import { postMessageToScratchIframe } from "../../utils/scratchIframe";
jest.mock("axios");
-jest.mock("../../utils/scratchIframe", () => ({
- postMessageToScratchIframe: jest.fn(),
-}));
jest.mock("react-router-dom", () => ({
...jest.requireActual("react-router-dom"),
@@ -31,11 +27,6 @@ const user = {
},
};
-const scratchProject = {
- ...project,
- project_type: "code_editor_scratch",
-};
-
const renderProjectBar = (state) => {
const middlewares = [];
const mockStore = configureStore(middlewares);
@@ -60,23 +51,6 @@ const renderProjectBar = (state) => {
);
};
-const getScratchOrigin = () => process.env.ASSETS_URL || window.location.origin;
-
-const dispatchScratchMessage = (type, origin = getScratchOrigin()) => {
- act(() => {
- window.dispatchEvent(
- new MessageEvent("message", {
- origin,
- data: { type },
- }),
- );
- });
-};
-
-beforeEach(() => {
- jest.clearAllMocks();
-});
-
describe("When logged in and user owns project", () => {
beforeEach(() => {
renderProjectBar({
@@ -102,6 +76,10 @@ describe("When logged in and user owns project", () => {
expect(screen.queryByText("header.download")).toBeInTheDocument();
});
+ test("Upload button is not shown", () => {
+ expect(screen.queryByText("header.upload")).not.toBeInTheDocument();
+ });
+
test("Save button is not shown", () => {
expect(screen.queryByText("header.save")).not.toBeInTheDocument();
});
@@ -129,6 +107,10 @@ describe("When logged in and no project identifier", () => {
expect(screen.queryByText("header.download")).toBeInTheDocument();
});
+ test("Upload button is not shown", () => {
+ expect(screen.queryByText("header.upload")).not.toBeInTheDocument();
+ });
+
test("Project name is shown", () => {
expect(screen.queryByText(project.name)).toBeInTheDocument();
});
@@ -155,6 +137,10 @@ describe("When not logged in", () => {
expect(screen.queryByText("header.download")).toBeInTheDocument();
});
+ test("Upload button is not shown", () => {
+ expect(screen.queryByText("header.upload")).not.toBeInTheDocument();
+ });
+
test("Project name is shown", () => {
expect(screen.queryByText(project.name)).toBeInTheDocument();
});
@@ -189,6 +175,10 @@ describe("When no project loaded", () => {
expect(screen.queryByText("header.download")).not.toBeInTheDocument();
});
+ test("No upload button", () => {
+ expect(screen.queryByText("header.upload")).not.toBeInTheDocument();
+ });
+
test("No save button", () => {
expect(screen.queryByText("header.save")).not.toBeInTheDocument();
});
@@ -212,6 +202,10 @@ describe("When read only", () => {
expect(screen.queryByTitle("header.renameProject")).not.toBeInTheDocument();
});
+ test("Upload button is not shown", () => {
+ expect(screen.queryByText("header.upload")).not.toBeInTheDocument();
+ });
+
test("Save button is not shown", () => {
expect(screen.queryByText("header.save")).not.toBeInTheDocument();
});
@@ -220,109 +214,3 @@ describe("When read only", () => {
expect(screen.queryByText(/saveStatus.saved/)).not.toBeInTheDocument();
});
});
-
-describe("When project is Scratch", () => {
- beforeEach(() => {
- postMessageToScratchIframe.mockClear();
- renderProjectBar({
- editor: {
- project: scratchProject,
- },
- auth: {
- user,
- },
- });
- });
-
- test("clicking Save sends scratch-gui-save message", () => {
- fireEvent.click(screen.getByRole("button", { name: "header.save" }));
-
- expect(postMessageToScratchIframe).toHaveBeenCalledTimes(1);
- expect(postMessageToScratchIframe).toHaveBeenCalledWith({
- type: "scratch-gui-save",
- });
- });
-});
-
-describe("Additional Scratch manual save states", () => {
- test("shows the saving state from the scratch save hook", () => {
- renderProjectBar({
- editor: {
- project: scratchProject,
- },
- auth: {
- user,
- },
- });
-
- dispatchScratchMessage("scratch-gui-saving-started");
-
- expect(
- screen.getByRole("button", { name: "saveStatus.saving" }),
- ).toBeDisabled();
- });
-
- test("shows the saved state from the scratch save hook", () => {
- renderProjectBar({
- editor: {
- project: scratchProject,
- },
- auth: {
- user,
- },
- });
-
- dispatchScratchMessage("scratch-gui-saving-succeeded");
-
- expect(
- screen.getByRole("button", { name: "saveStatus.saved" }),
- ).toBeInTheDocument();
- });
-
- test("does not show save for logged-out Scratch users", () => {
- renderProjectBar({
- editor: {
- project: scratchProject,
- },
- });
-
- expect(screen.queryByText("header.save")).not.toBeInTheDocument();
- expect(screen.queryByText("header.loginToSave")).not.toBeInTheDocument();
- });
-
- test("shows save for logged-in non-owners", () => {
- renderProjectBar({
- editor: {
- project: {
- ...scratchProject,
- user_id: "teacher-id",
- },
- },
- auth: {
- user,
- },
- });
-
- expect(
- screen.getByRole("button", { name: "header.save" }),
- ).toBeInTheDocument();
- });
-
- test("shows save for logged-in users without a Scratch project identifier", () => {
- renderProjectBar({
- editor: {
- project: {
- ...scratchProject,
- identifier: null,
- },
- },
- auth: {
- user,
- },
- });
-
- expect(
- screen.getByRole("button", { name: "header.save" }),
- ).toBeInTheDocument();
- });
-});
diff --git a/src/components/ProjectBar/ScratchProjectBar.jsx b/src/components/ProjectBar/ScratchProjectBar.jsx
new file mode 100644
index 000000000..30f9ec93d
--- /dev/null
+++ b/src/components/ProjectBar/ScratchProjectBar.jsx
@@ -0,0 +1,75 @@
+import React from "react";
+import { useSelector } from "react-redux";
+import { useTranslation } from "react-i18next";
+import DownloadIcon from "../../assets/icons/download.svg";
+import UploadIcon from "../../assets/icons/upload.svg";
+import SaveIcon from "../../assets/icons/save.svg";
+import ProjectName from "../ProjectName/ProjectName";
+import DownloadButton from "../DownloadButton/DownloadButton";
+import UploadButton from "../UploadButton/UploadButton";
+import DesignSystemButton from "../DesignSystemButton/DesignSystemButton";
+
+import "../../assets/stylesheets/ProjectBar.scss";
+import { useScratchSaveState } from "../../hooks/useScratchSaveState";
+
+const ScratchProjectBar = ({ nameEditable = true }) => {
+ const { t } = useTranslation();
+
+ const user = useSelector((state) => state.auth.user);
+ const loading = useSelector((state) => state.editor.loading);
+ const readOnly = useSelector((state) => state.editor.readOnly);
+ const showScratchSaveButton = Boolean(user && !readOnly);
+ const enableScratchSaveState = Boolean(
+ loading === "success" && showScratchSaveButton,
+ );
+ const { isScratchSaving, saveScratchProject, scratchSaveLabelKey } =
+ useScratchSaveState({
+ enabled: enableScratchSaveState,
+ });
+ const scratchSaveLabel = t(scratchSaveLabelKey);
+
+ if (loading !== "success") {
+ return null;
+ }
+
+ return (
+
+
+
+ {!readOnly && (
+
+
+
+ )}
+
+
+
+ {showScratchSaveButton && (
+
+ }
+ type="primary"
+ disabled={isScratchSaving}
+ />
+
+ )}
+
+
+ );
+};
+
+export default ScratchProjectBar;
diff --git a/src/components/ProjectBar/ScratchProjectBar.test.js b/src/components/ProjectBar/ScratchProjectBar.test.js
new file mode 100644
index 000000000..0fbf0f068
--- /dev/null
+++ b/src/components/ProjectBar/ScratchProjectBar.test.js
@@ -0,0 +1,188 @@
+import React from "react";
+import { act, fireEvent, render, screen } from "@testing-library/react";
+import { Provider } from "react-redux";
+import configureStore from "redux-mock-store";
+import { MemoryRouter } from "react-router-dom";
+import ScratchProjectBar from "./ScratchProjectBar";
+import { postMessageToScratchIframe } from "../../utils/scratchIframe";
+
+jest.mock("axios");
+jest.mock("../../utils/scratchIframe", () => ({
+ postMessageToScratchIframe: jest.fn(),
+}));
+
+jest.mock("react-router-dom", () => ({
+ ...jest.requireActual("react-router-dom"),
+ useNavigate: () => jest.fn(),
+}));
+
+const scratchProject = {
+ name: "Hello world",
+ identifier: "hello-world-project",
+ components: [],
+ image_list: [],
+ user_id: "b48e70e2-d9ed-4a59-aee5-fc7cf09dbfaf",
+ project_type: "code_editor_scratch",
+};
+
+const user = {
+ access_token: "39a09671-be55-4847-baf5-8919a0c24a25",
+ profile: {
+ user: "b48e70e2-d9ed-4a59-aee5-fc7cf09dbfaf",
+ },
+};
+
+const renderScratchProjectBar = (state) => {
+ const middlewares = [];
+ const mockStore = configureStore(middlewares);
+ const store = mockStore({
+ editor: {
+ loading: "success",
+ project: {},
+ ...state.editor,
+ },
+ auth: {
+ user: null,
+ ...state.auth,
+ },
+ });
+
+ render(
+
+
+
+
+ ,
+ );
+};
+
+const getScratchOrigin = () => process.env.ASSETS_URL || window.location.origin;
+
+const dispatchScratchMessage = (type, origin = getScratchOrigin()) => {
+ act(() => {
+ window.dispatchEvent(
+ new MessageEvent("message", {
+ origin,
+ data: { type },
+ }),
+ );
+ });
+};
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+describe("When project is Scratch", () => {
+ beforeEach(() => {
+ postMessageToScratchIframe.mockClear();
+ renderScratchProjectBar({
+ editor: {
+ project: scratchProject,
+ },
+ auth: {
+ user,
+ },
+ });
+ });
+
+ test("Upload button shown", () => {
+ expect(screen.queryByText("header.upload")).toBeInTheDocument();
+ });
+
+ test("Download button shown", () => {
+ expect(screen.queryByText("header.download")).toBeInTheDocument();
+ });
+
+ test("clicking Save sends scratch-gui-save message", () => {
+ fireEvent.click(screen.getByRole("button", { name: "header.save" }));
+
+ expect(postMessageToScratchIframe).toHaveBeenCalledTimes(1);
+ expect(postMessageToScratchIframe).toHaveBeenCalledWith({
+ type: "scratch-gui-save",
+ });
+ });
+});
+
+describe("Additional Scratch manual save states", () => {
+ test("shows the saving state from the scratch save hook", () => {
+ renderScratchProjectBar({
+ editor: {
+ project: scratchProject,
+ },
+ auth: {
+ user,
+ },
+ });
+
+ dispatchScratchMessage("scratch-gui-saving-started");
+
+ expect(
+ screen.getByRole("button", { name: "saveStatus.saving" }),
+ ).toBeDisabled();
+ });
+
+ test("shows the saved state from the scratch save hook", () => {
+ renderScratchProjectBar({
+ editor: {
+ project: scratchProject,
+ },
+ auth: {
+ user,
+ },
+ });
+
+ dispatchScratchMessage("scratch-gui-saving-succeeded");
+
+ expect(
+ screen.getByRole("button", { name: "saveStatus.saved" }),
+ ).toBeInTheDocument();
+ });
+
+ test("does not show save for logged-out Scratch users", () => {
+ renderScratchProjectBar({
+ editor: {
+ project: scratchProject,
+ },
+ });
+
+ expect(screen.queryByText("header.save")).not.toBeInTheDocument();
+ expect(screen.queryByText("header.loginToSave")).not.toBeInTheDocument();
+ });
+
+ test("shows save for logged-in non-owners", () => {
+ renderScratchProjectBar({
+ editor: {
+ project: {
+ ...scratchProject,
+ user_id: "teacher-id",
+ },
+ },
+ auth: {
+ user,
+ },
+ });
+
+ expect(
+ screen.getByRole("button", { name: "header.save" }),
+ ).toBeInTheDocument();
+ });
+
+ test("shows save for logged-in users without a Scratch project identifier", () => {
+ renderScratchProjectBar({
+ editor: {
+ project: {
+ ...scratchProject,
+ identifier: null,
+ },
+ },
+ auth: {
+ user,
+ },
+ });
+
+ expect(
+ screen.getByRole("button", { name: "header.save" }),
+ ).toBeInTheDocument();
+ });
+});