diff --git a/.env.example b/.env.example
index 85d2e38ad..4d4e2433a 100644
--- a/.env.example
+++ b/.env.example
@@ -1,10 +1,9 @@
REACT_APP_AUTHENTICATION_CLIENT_ID='editor-dev'
-REACT_APP_AUTHENTICATION_URL='http://localhost:9001'
REACT_APP_SENTRY_DSN=''
REACT_APP_SENTRY_ENV='local'
PUBLIC_URL='http://localhost:3011'
ASSETS_URL='http://localhost:3011'
-REACT_APP_API_ENDPOINT='http://localhost:3009'
REACT_APP_GOOGLE_TAG_MANAGER_ID=''
+REACT_APP_API_ENDPOINT='http://localhost:3009'
REACT_APP_PLAUSIBLE_DATA_DOMAIN=''
REACT_APP_PLAUSIBLE_SOURCE=''
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 8f0a5f4b1..8b7134c1b 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -61,7 +61,7 @@ jobs:
run: yarn run test --coverage --maxWorkers=4 --workerThreads=true --reporters=default --reporters=jest-junit --reporters=jest-github-actions-reporter
env:
JEST_JUNIT_OUTPUT_DIR: ./coverage/
-
+ REACT_APP_API_ENDPOINT: http://localhost:3009
- name: Record coverage
run: ./.github/workflows/record_coverage
env:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e5aa1e0a..a6e9617b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
## Unreleased
+## [0.28.8] - 2024-11-04
+
+### Changed
+- REACT_APP_API_ENDPOINT env var is now only a default for the editor-wc prop, which can be overridden (#1124)
+
+### Removed
+- REACT_APP_AUTHENTICATION_URL env var no longer used and is instead a editor-wc prop (#1124)
+
## [0.28.5] - 2024-11-01
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 000000000..1dbfdf1f4
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,4 @@
+
You may be looking for...
+
diff --git a/src/app/store.js b/src/app/store.js
index aa1691a78..ec4e800b2 100644
--- a/src/app/store.js
+++ b/src/app/store.js
@@ -2,8 +2,11 @@ import { configureStore } from "@reduxjs/toolkit";
import EditorReducer from "../redux/EditorSlice";
import InstructionsReducer from "../redux/InstructionsSlice";
import { reducer, loadUser } from "redux-oidc";
-import userManager from "../utils/userManager";
+import UserManager from "../utils/userManager";
+// TODO - not used but keeping this in preparation for using
+// src/components/Editor/ImageUploadButton/ImageUploadButton.jsx
+const userManager = UserManager({ reactAppAuthenticationUrl: "TODO" });
const store = configureStore({
reducer: {
editor: EditorReducer,
diff --git a/src/components/DownloadButton/DownloadButton.jsx b/src/components/DownloadButton/DownloadButton.jsx
index 4016ae54c..eb9c15b8d 100644
--- a/src/components/DownloadButton/DownloadButton.jsx
+++ b/src/components/DownloadButton/DownloadButton.jsx
@@ -4,11 +4,10 @@ import { toSnakeCase } from "js-convert-case";
import JSZip from "jszip";
import JSZipUtils from "jszip-utils";
import { useTranslation } from "react-i18next";
-import { useDispatch, useSelector } from "react-redux";
+import { useSelector } from "react-redux";
import PropTypes from "prop-types";
import DesignSystemButton from "../DesignSystemButton/DesignSystemButton";
-import { closeLoginToSaveModal } from "../../redux/EditorSlice";
const DownloadButton = (props) => {
const {
@@ -20,10 +19,6 @@ const DownloadButton = (props) => {
} = props;
const { t } = useTranslation();
const project = useSelector((state) => state.editor.project);
- const loginToSaveModalShowing = useSelector(
- (state) => state.editor.loginToSaveModalShowing,
- );
- const dispatch = useDispatch();
const urlToPromise = (url) => {
return new Promise(function (resolve, reject) {
@@ -42,9 +37,6 @@ const DownloadButton = (props) => {
window.plausible("Download");
}
- if (loginToSaveModalShowing) {
- dispatch(closeLoginToSaveModal());
- }
const zip = new JSZip();
project.components.forEach((file) => {
diff --git a/src/components/DownloadButton/DownloadButton.test.js b/src/components/DownloadButton/DownloadButton.test.js
index 7c2c50d35..ef9e38253 100644
--- a/src/components/DownloadButton/DownloadButton.test.js
+++ b/src/components/DownloadButton/DownloadButton.test.js
@@ -6,7 +6,6 @@ import DownloadButton from "./DownloadButton";
import FileSaver from "file-saver";
import JSZip from "jszip";
import JSZipUtils from "jszip-utils";
-import { closeLoginToSaveModal } from "../../redux/EditorSlice";
jest.mock("file-saver");
jest.mock("jszip");
@@ -124,27 +123,3 @@ describe("Downloading project with no name set", () => {
);
});
});
-
-test("If login to save modal open, closes it when download clicked", () => {
- JSZip.mockClear();
- const middlewares = [];
- const mockStore = configureStore(middlewares);
- const initialState = {
- editor: {
- project: {
- components: [],
- image_list: [],
- },
- loginToSaveModalShowing: true,
- },
- };
- const store = mockStore(initialState);
- render(
-
- {}} />
- ,
- );
- const downloadButton = screen.queryByText("Download").parentElement;
- fireEvent.click(downloadButton);
- expect(store.getActions()).toEqual([closeLoginToSaveModal()]);
-});
diff --git a/src/components/Editor/ImageUploadButton/ImageUploadButton.jsx b/src/components/Editor/ImageUploadButton/ImageUploadButton.jsx
index 11ac2fdf4..ecb3b144d 100644
--- a/src/components/Editor/ImageUploadButton/ImageUploadButton.jsx
+++ b/src/components/Editor/ImageUploadButton/ImageUploadButton.jsx
@@ -9,7 +9,7 @@ import { updateImages, setNameError } from "../../../redux/EditorSlice";
import Button from "../../Button/Button";
import NameErrorMessage from "../ErrorMessage/NameErrorMessage";
import store from "../../../app/store";
-import { uploadImages } from "../../../utils/apiCallHandler";
+import ApiCallHandler from "../../../utils/apiCallHandler";
const allowedExtensions = {
python: ["jpg", "jpeg", "png", "gif"],
@@ -30,7 +30,7 @@ const allowedExtensionsString = (projectType) => {
}
};
-const ImageUploadButton = () => {
+const ImageUploadButton = ({ reactAppApiEndpoint }) => {
const [modalIsOpen, setIsOpen] = useState(false);
const [files, setFiles] = useState([]);
const dispatch = useDispatch();
@@ -51,6 +51,10 @@ const ImageUploadButton = () => {
setIsOpen(true);
};
const saveImages = async () => {
+ const { uploadImages } = ApiCallHandler({
+ reactAppApiEndpoint,
+ });
+
files.every((file) => {
const fileName = file.name;
const extension = fileName.split(".").slice(1).join(".").toLowerCase();
diff --git a/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx b/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx
index 8bb232feb..19a6e71a5 100644
--- a/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx
+++ b/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx
@@ -12,7 +12,7 @@ import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
import { useMediaQuery } from "react-responsive";
import { MOBILE_MEDIA_QUERY } from "../../../../../utils/mediaQueryBreakpoints";
import ErrorMessage from "../../../ErrorMessage/ErrorMessage";
-import { createError } from "../../../../../utils/apiCallHandler";
+import ApiCallHandler from "../../../../../utils/apiCallHandler";
import VisualOutputPane from "./VisualOutputPane";
import OutputViewToggle from "../OutputViewToggle";
import { SettingsContext } from "../../../../../utils/settings";
@@ -47,6 +47,7 @@ const PyodideRunner = (props) => {
const userId = user?.profile?.user;
const isSplitView = useSelector((s) => s.editor.isSplitView);
const isEmbedded = useSelector((s) => s.editor.isEmbedded);
+ const reactAppApiEndpoint = useSelector((s) => s.editor.reactAppApiEndpoint);
const codeRunTriggered = useSelector((s) => s.editor.codeRunTriggered);
const codeRunStopped = useSelector((s) => s.editor.codeRunStopped);
const output = useRef();
@@ -180,6 +181,9 @@ const PyodideRunner = (props) => {
errorMessage += `:\n${mistake}`;
}
+ const { createError } = ApiCallHandler({
+ reactAppApiEndpoint,
+ });
createError(projectIdentifier, userId, { errorType: type, errorMessage });
}
diff --git a/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx b/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx
index dc852989c..4e835f4bf 100644
--- a/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx
+++ b/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx
@@ -15,7 +15,7 @@ import {
triggerDraw,
} from "../../../../../redux/EditorSlice";
import ErrorMessage from "../../../ErrorMessage/ErrorMessage";
-import { createError } from "../../../../../utils/apiCallHandler";
+import ApiCallHandler from "../../../../../utils/apiCallHandler";
import store from "../../../../../redux/stores/WebComponentStore";
import VisualOutputPane from "../VisualOutputPane";
import OutputViewToggle from "../OutputViewToggle";
@@ -71,6 +71,7 @@ const SkulptRunner = ({ active, outputPanels = ["text", "visual"] }) => {
);
const codeRunStopped = useSelector((state) => state.editor.codeRunStopped);
const drawTriggered = useSelector((state) => state.editor.drawTriggered);
+ const reactAppApiEndpoint = useSelector((s) => s.editor.reactAppApiEndpoint);
const output = useRef();
const dispatch = useDispatch();
const { t } = useTranslation();
@@ -314,6 +315,8 @@ const SkulptRunner = ({ active, outputPanels = ["text", "visual"] }) => {
userId = user.profile?.user;
}
+ const { createError } = ApiCallHandler({ reactAppApiEndpoint });
+
errorMessage = `${errorType}: ${errorDescription} on line ${lineNumber} of ${fileName}${
explanation ? `. ${explanation}` : ""
}`;
diff --git a/src/components/Login/LoginButton.jsx b/src/components/Login/LoginButton.jsx
deleted file mode 100644
index c4a703e8b..000000000
--- a/src/components/Login/LoginButton.jsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import React from "react";
-import { useLocation } from "react-router-dom";
-import { useSelector } from "react-redux";
-import Button from "../Button/Button";
-import { login } from "../../utils/login";
-import PropTypes from "prop-types";
-
-const LoginButton = ({ buttonText, className, triggerSave, loginRedirect }) => {
- const location = useLocation();
- const project = useSelector((state) => state.editor.project);
- const accessDeniedData = useSelector(
- (state) => state.editor.modals?.accessDenied || null,
- );
-
- const onLoginButtonClick = (event) => {
- event.preventDefault();
- login({
- project,
- location,
- triggerSave,
- accessDeniedData,
- loginRedirect,
- });
- };
-
- return (
-
- );
-};
-
-LoginButton.propTypes = {
- buttonText: PropTypes.string,
- className: PropTypes.string,
- triggerSave: PropTypes.bool,
- loginRedirect: PropTypes.string,
-};
-
-export default LoginButton;
diff --git a/src/components/Login/LoginButton.test.js b/src/components/Login/LoginButton.test.js
deleted file mode 100644
index c10b4d605..000000000
--- a/src/components/Login/LoginButton.test.js
+++ /dev/null
@@ -1,229 +0,0 @@
-import React from "react";
-import { MemoryRouter } from "react-router-dom";
-import configureStore from "redux-mock-store";
-import { fireEvent, render, screen } from "@testing-library/react";
-import { Provider } from "react-redux";
-import userManager from "../../utils/userManager";
-import LoginButton from "./LoginButton";
-
-jest.mock("../../utils/userManager", () => ({
- signinRedirect: jest.fn(),
-}));
-
-const project = {
- components: [
- {
- name: "main",
- extension: "py",
- content: 'print("hello world")',
- },
- ],
-};
-let loginButton;
-
-describe("When accessDeniedData is false", () => {
- beforeEach(() => {
- const middlewares = [];
- const mockStore = configureStore(middlewares);
- const initialState = {
- editor: {
- project: project,
- modals: {},
- },
- auth: {
- user: null,
- },
- };
- const store = mockStore(initialState);
- render(
-
-
-
-
- ,
- );
- loginButton = screen.queryByText("Login");
- });
-
- test("Login button shown", () => {
- expect(loginButton).toBeInTheDocument();
- });
-
- test("Clicking login button signs the user in", () => {
- fireEvent.click(loginButton);
- expect(userManager.signinRedirect).toHaveBeenCalled();
- });
-
- test("Clicking login button saves the user's project content in local storage", () => {
- fireEvent.click(loginButton);
- expect(localStorage.getItem("project")).toBe(JSON.stringify(project));
- });
-
- test("Clicking login button saves user's location to local storage", () => {
- fireEvent.click(loginButton);
- expect(localStorage.getItem("location")).toBe("/my_project");
- });
-});
-
-describe("When accessDeniedData is true", () => {
- beforeEach(() => {
- project.identifier = "hello-world-project";
- project.projectType = "python";
-
- const middlewares = [];
- const mockStore = configureStore(middlewares);
- const initialState = {
- editor: {
- project: project,
- modals: {
- accessDenied: {
- identifier: project.identifier,
- projectType: project.projectType,
- },
- },
- },
- auth: {
- user: null,
- },
- };
- const store = mockStore(initialState);
- render(
-
-
-
-
- ,
- );
- loginButton = screen.queryByText("Login");
- });
-
- test("Clicking the login button saves user's location to local storage", () => {
- fireEvent.click(loginButton);
- expect(localStorage.getItem("location")).toBe(
- "/projects/hello-world-project",
- );
- });
-});
-
-describe("When loginRedirect is set", () => {
- beforeEach(() => {
- const middlewares = [];
- const mockStore = configureStore(middlewares);
- const initialState = {
- editor: {
- project: {},
- modals: {},
- },
- auth: {
- user: null,
- },
- };
- const store = mockStore(initialState);
- render(
-
-
-
-
- ,
- );
- loginButton = screen.queryByText("Login");
- });
-
- test("Clicking the login button saves loginRedirect location to local storage", () => {
- fireEvent.click(loginButton);
- expect(localStorage.getItem("location")).toBe("/some-other-page");
- });
-});
-
-describe("When project is not set", () => {
- beforeEach(() => {
- const middlewares = [];
- const mockStore = configureStore(middlewares);
- const initialState = {
- editor: {
- modals: {},
- },
- auth: {
- user: null,
- },
- };
- const store = mockStore(initialState);
- render(
-
-
-
-
- ,
- );
- loginButton = screen.queryByText("Login");
- });
-
- test("Clicking the login button doesn't save project to local storage", () => {
- fireEvent.click(loginButton);
- expect(localStorage.getItem("project")).toBeNull();
- });
-});
-
-describe("When login button has triggerSave set", () => {
- beforeEach(() => {
- const middlewares = [];
- const mockStore = configureStore(middlewares);
- const initialState = {
- editor: {
- project: project,
- modals: {},
- },
- auth: {
- user: null,
- },
- };
- const store = mockStore(initialState);
- render(
-
-
-
-
- ,
- );
- loginButton = screen.queryByText("Login");
- });
-
- test("Clicking login button sets 'awaitingSave' in local storage", () => {
- fireEvent.click(loginButton);
- expect(localStorage.getItem("awaitingSave")).toBe("true");
- });
-});
-
-describe("When login button does not have triggerSave set", () => {
- beforeEach(() => {
- const middlewares = [];
- const mockStore = configureStore(middlewares);
- const initialState = {
- editor: {
- project: project,
- modals: {},
- },
- auth: {
- user: null,
- },
- };
- const store = mockStore(initialState);
- render(
-
-
-
-
- ,
- );
- loginButton = screen.queryByText("Login");
- });
-
- test("Clicking login button does not set 'awaitingSave' in local storage", () => {
- fireEvent.click(loginButton);
- expect(localStorage.getItem("awaitingSave")).toBeNull();
- });
-});
-
-afterEach(() => {
- localStorage.clear();
-});
diff --git a/src/components/Login/LoginMenu.jsx b/src/components/Login/LoginMenu.jsx
deleted file mode 100644
index 4742593bf..000000000
--- a/src/components/Login/LoginMenu.jsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import React from "react";
-import { useSelector } from "react-redux";
-import { useTranslation } from "react-i18next";
-import LogoutButton from "./LogoutButton";
-import LoginButton from "./LoginButton";
-import "../../assets/stylesheets/LoginMenu.scss";
-import { Link } from "react-router-dom";
-
-const LoginMenu = () => {
- const { t, i18n } = useTranslation();
- const locale = i18n.language;
- const user = useSelector((state) => state.auth.user);
-
- return (
-
- );
-};
-
-export default LoginMenu;
diff --git a/src/components/Login/LoginMenu.test.js b/src/components/Login/LoginMenu.test.js
deleted file mode 100644
index d22ac9692..000000000
--- a/src/components/Login/LoginMenu.test.js
+++ /dev/null
@@ -1,94 +0,0 @@
-import React from "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 LoginMenu from "./LoginMenu";
-
-describe("When not logged in", () => {
- beforeEach(() => {
- const middlewares = [];
- const mockStore = configureStore(middlewares);
- const initialState = {
- editor: {
- project: {},
- modals: {},
- },
- auth: {
- user: null,
- },
- };
- const store = mockStore(initialState);
- render(
-
-
-
-
- ,
- );
- });
-
- test("Login button renders", () => {
- expect(
- screen.queryByText("globalNav.accountMenu.login"),
- ).toBeInTheDocument();
- });
-
- test("My profile does not render", () => {
- expect(
- screen.queryByText("globalNav.accountMenu.profile"),
- ).not.toBeInTheDocument();
- });
-
- test("My projects does not render", () => {
- expect(
- screen.queryByText("globalNav.accountMenu.projects"),
- ).not.toBeInTheDocument();
- });
-});
-
-describe("When logged in", () => {
- beforeEach(() => {
- const middlewares = [];
- const mockStore = configureStore(middlewares);
- const initialState = {
- editor: {
- project: {},
- },
- auth: {
- user: {
- profile: {
- profile: "profile_url",
- },
- },
- },
- };
- const store = mockStore(initialState);
- render(
-
-
-
-
- ,
- );
- });
-
- test("Logout button renders", () => {
- expect(
- screen.queryByText("globalNav.accountMenu.logout"),
- ).toBeInTheDocument();
- });
-
- test("My profile renders with correct link", () => {
- expect(screen.queryByText("globalNav.accountMenu.profile")).toHaveAttribute(
- "href",
- "profile_url/edit",
- );
- });
-
- test("My projects renders with correct link", () => {
- expect(
- screen.queryByText("globalNav.accountMenu.projects"),
- ).toHaveAttribute("href", "/ja-JP/projects");
- });
-});
diff --git a/src/components/Login/LogoutButton.jsx b/src/components/Login/LogoutButton.jsx
deleted file mode 100644
index 8eab9d1b5..000000000
--- a/src/components/Login/LogoutButton.jsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from "react";
-import userManager from "../../utils/userManager";
-import { useTranslation } from "react-i18next";
-import Button from "../Button/Button";
-import PropTypes from "prop-types";
-
-const LogoutButton = ({ className, user }) => {
- const { t } = useTranslation();
-
- const onLogoutButtonClick = async (event) => {
- event.preventDefault();
- userManager.signoutRedirect({ id_token_hint: user?.id_token });
- await userManager.removeUser();
- localStorage.clear();
- };
-
- return (
-
- );
-};
-
-LogoutButton.propTypes = {
- className: PropTypes.string,
- user: PropTypes.shape({
- id_token: PropTypes.string.isRequired,
- }).isRequired,
-};
-
-export default LogoutButton;
diff --git a/src/components/Login/LogoutButton.test.js b/src/components/Login/LogoutButton.test.js
deleted file mode 100644
index a7f72681b..000000000
--- a/src/components/Login/LogoutButton.test.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import React from "react";
-import { MemoryRouter } from "react-router-dom";
-import configureStore from "redux-mock-store";
-import { fireEvent, render, screen } from "@testing-library/react";
-import { Provider } from "react-redux";
-import userManager from "../../utils/userManager";
-import LogoutButton from "./LogoutButton";
-
-jest.mock("../../utils/userManager", () => ({
- signoutRedirect: jest.fn(),
- removeUser: jest.fn(),
-}));
-
-let logoutButton;
-
-const user = {
- id_token: "1234",
-};
-
-beforeEach(() => {
- const middlewares = [];
- const mockStore = configureStore(middlewares);
- const initialState = {};
- const store = mockStore(initialState);
- render(
-
-
-
-
- ,
- );
- logoutButton = screen.queryByText("globalNav.accountMenu.logout");
-});
-
-test("Log out button shown", () => {
- expect(logoutButton).toBeInTheDocument();
-});
-
-test("Clicking log out button signs the user out", () => {
- fireEvent.click(logoutButton);
- expect(userManager.signoutRedirect).toBeCalledWith({
- id_token_hint: user.id_token,
- });
- expect(userManager.removeUser).toHaveBeenCalled();
-});
diff --git a/src/components/Menus/ProjectActionsMenu/ProjectActionsMenu.jsx b/src/components/Menus/ProjectActionsMenu/ProjectActionsMenu.jsx
deleted file mode 100644
index a64b3e417..000000000
--- a/src/components/Menus/ProjectActionsMenu/ProjectActionsMenu.jsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import React from "react";
-import { useTranslation } from "react-i18next";
-import { useDispatch } from "react-redux";
-import BinIcon from "../../../assets/icons/bin.svg";
-import EllipsisVerticalIcon from "../../../assets/icons/ellipsis_vertical.svg";
-import PencilIcon from "../../../assets/icons/pencil.svg";
-
-import {
- showDeleteProjectModal,
- showRenameProjectModal,
-} from "../../../redux/EditorSlice";
-import ContextMenu from "../ContextMenu/ContextMenu";
-
-const ProjectActionsMenu = (props) => {
- const { project } = props;
- const { t } = useTranslation();
- const dispatch = useDispatch();
-
- const openRenameProjectModal = () => {
- dispatch(showRenameProjectModal(project));
- };
-
- const openDeleteProjectModal = () => {
- dispatch(showDeleteProjectModal(project));
- };
-
- return (
-
- );
-};
-
-export default ProjectActionsMenu;
diff --git a/src/components/Menus/ProjectActionsMenu/ProjectActionsMenu.test.js b/src/components/Menus/ProjectActionsMenu/ProjectActionsMenu.test.js
deleted file mode 100644
index 02d8c0bb4..000000000
--- a/src/components/Menus/ProjectActionsMenu/ProjectActionsMenu.test.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import { fireEvent, render, screen } from "@testing-library/react";
-import React from "react";
-import { Provider } from "react-redux";
-import ProjectActionsMenu from "./ProjectActionsMenu";
-import configureStore from "redux-mock-store";
-
-let store;
-
-beforeEach(() => {
- const mockStore = configureStore([]);
- const initialState = {};
- store = mockStore(initialState);
-
- render(
-
-
- ,
- );
-});
-
-test("Menu is not visible initially", () => {
- expect(screen.queryByRole("menu")).toBeNull();
-});
-
-test("Clicking button makes menu content appear", () => {
- const button = screen.getByRole("button");
- fireEvent.click(button);
- expect(screen.queryByRole("menu")).not.toBeNull();
-});
-
-test("Clicking rename option opens the rename project modal", () => {
- const button = screen.getByRole("button");
- fireEvent.click(button);
- const renameOption = screen.getByText("projectList.rename");
- fireEvent.click(renameOption);
- expect(store.getActions()).toEqual([
- {
- type: "editor/showRenameProjectModal",
- payload: { name: "my amazing project" },
- },
- ]);
-});
-
-test("Clicking delete option opens the delete project modal", () => {
- const button = screen.getByRole("button");
- fireEvent.click(button);
- const deleteOption = screen.getByText("projectList.delete");
- fireEvent.click(deleteOption);
- expect(store.getActions()).toEqual([
- {
- type: "editor/showDeleteProjectModal",
- payload: { name: "my amazing project" },
- },
- ]);
-});
diff --git a/src/components/Modals/AccessDeniedNoAuthModal.jsx b/src/components/Modals/AccessDeniedNoAuthModal.jsx
deleted file mode 100644
index 8dc548bb1..000000000
--- a/src/components/Modals/AccessDeniedNoAuthModal.jsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import React from "react";
-import { useDispatch, useSelector } from "react-redux";
-import { useTranslation } from "react-i18next";
-
-import Button from "../Button/Button";
-import "../../assets/stylesheets/Modal.scss";
-import { closeAccessDeniedNoAuthModal } from "../../redux/EditorSlice";
-import LoginButton from "../Login/LoginButton";
-import GeneralModal from "./GeneralModal";
-import { login } from "../../utils/login";
-
-const AccessDeniedNoAuthModal = (props) => {
- const {
- buttons = null,
- text = null,
- withCloseButton = true,
- withClickToClose = true,
- } = props;
- const dispatch = useDispatch();
- const { t } = useTranslation();
-
- const isModalOpen = useSelector(
- (state) => state.editor.accessDeniedNoAuthModalShowing,
- );
- const accessDeniedData = useSelector(
- (state) => state.editor.modals.accessDenied,
- );
-
- const closeModal = () => dispatch(closeAccessDeniedNoAuthModal());
-
- const defaultCallback = () => {
- login({ accessDeniedData });
- };
-
- return (
- ,
-
- {t("project.accessDeniedNoAuthModal.projectsSiteLinkText")}
- ,
- ,
- ]
- }
- defaultCallback={defaultCallback}
- />
- );
-};
-
-export default AccessDeniedNoAuthModal;
diff --git a/src/components/Modals/AccessDeniedNoAuthModal.test.js b/src/components/Modals/AccessDeniedNoAuthModal.test.js
deleted file mode 100644
index fa09dfb45..000000000
--- a/src/components/Modals/AccessDeniedNoAuthModal.test.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import React from "react";
-import { fireEvent, render, screen } from "@testing-library/react";
-import { Provider } from "react-redux";
-import configureStore from "redux-mock-store";
-import AccessDeniedNoAuthModal from "./AccessDeniedNoAuthModal";
-
-jest.mock("react-router-dom", () => ({
- useLocation: jest.fn(),
-}));
-
-const middlewares = [];
-const mockStore = configureStore(middlewares);
-
-describe("When accessDeniedNoAuthModalShowing is true", () => {
- let store;
-
- beforeEach(() => {
- const initialState = {
- editor: {
- accessDeniedNoAuthModalShowing: true,
- modals: {
- accessDenied: {
- identifer: "my-amazing-project",
- projectType: "python",
- },
- },
- },
- };
- store = mockStore(initialState);
- render(
-
-
- ,
- );
- });
-
- test("Modal rendered", () => {
- expect(
- screen.queryByText("project.accessDeniedNoAuthModal.heading"),
- ).toBeInTheDocument();
- });
-
- test("Clicking new project dispatches close modal action", () => {
- const newProjectLink = screen.queryByText(
- "project.accessDeniedNoAuthModal.newProject",
- );
- fireEvent.click(newProjectLink);
- expect(store.getActions()).toEqual([
- { type: "editor/closeAccessDeniedNoAuthModal" },
- ]);
- });
-});
diff --git a/src/components/Modals/AccessDeniedNoAuthModalEmbedded.jsx b/src/components/Modals/AccessDeniedNoAuthModalEmbedded.jsx
deleted file mode 100644
index 8ee9d164e..000000000
--- a/src/components/Modals/AccessDeniedNoAuthModalEmbedded.jsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import React from "react";
-import { useTranslation } from "react-i18next";
-
-import AccessDeniedNoAuthModal from "./AccessDeniedNoAuthModal";
-
-const AccessDeniedNoAuthModalEmbedded = (props) => {
- const { t } = useTranslation();
-
- return (
-
- {t("project.accessDeniedNoAuthModal.projectsSiteLinkText")}
- ,
- ]}
- text={[
- {
- type: "paragraph",
- content: t("project.accessDeniedNoAuthModal.embedded.text"),
- },
- ]}
- />
- );
-};
-
-export default AccessDeniedNoAuthModalEmbedded;
diff --git a/src/components/Modals/DeleteProjectModal.jsx b/src/components/Modals/DeleteProjectModal.jsx
deleted file mode 100644
index cbf808acd..000000000
--- a/src/components/Modals/DeleteProjectModal.jsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import React from "react";
-import { gql, useMutation } from "@apollo/client";
-import { useTranslation } from "react-i18next";
-import { useDispatch, useSelector } from "react-redux";
-import { closeDeleteProjectModal } from "../../redux/EditorSlice";
-import Button from "../Button/Button";
-import GeneralModal from "./GeneralModal";
-
-// Define mutation
-export const DELETE_PROJECT_MUTATION = gql`
- mutation DeleteProject($id: String!) {
- deleteProject(input: { id: $id }) {
- id
- }
- }
-`;
-
-export const DeleteProjectModal = () => {
- const dispatch = useDispatch();
- const { t } = useTranslation();
- const isModalOpen = useSelector(
- (state) => state.editor.deleteProjectModalShowing,
- );
- const project = useSelector((state) => state.editor.modals.deleteProject);
-
- const closeModal = () => dispatch(closeDeleteProjectModal());
-
- // This can capture data, error, loading as per normal queries, but we're not
- // using them yet.
- const [deleteProjectMutation] = useMutation(DELETE_PROJECT_MUTATION, {
- refetchQueries: ["ProjectIndexQuery"],
- });
-
- const onClickDelete = async () => {
- deleteProjectMutation({
- variables: { id: project.id },
- onCompleted: closeModal,
- });
- };
-
- return (
- ,
- ,
- ]}
- defaultCallback={onClickDelete}
- />
- );
-};
-
-export default DeleteProjectModal;
diff --git a/src/components/Modals/DeleteProjectModal.test.js b/src/components/Modals/DeleteProjectModal.test.js
deleted file mode 100644
index 62889cc5a..000000000
--- a/src/components/Modals/DeleteProjectModal.test.js
+++ /dev/null
@@ -1,94 +0,0 @@
-import React from "react";
-import { fireEvent, render, screen, waitFor } from "@testing-library/react";
-import { Provider } from "react-redux";
-import configureStore from "redux-mock-store";
-import { MockedProvider } from "@apollo/client/testing";
-
-import {
- DeleteProjectModal,
- DELETE_PROJECT_MUTATION,
-} from "./DeleteProjectModal";
-
-describe("Testing the delete project modal", () => {
- let store;
- let mocks;
- let project = { id: "abc", name: "my first project" };
-
- beforeEach(() => {
- const middlewares = [];
- const mockStore = configureStore(middlewares);
- const initialState = {
- editor: {
- modals: {
- deleteProject: project,
- },
- deleteProjectModalShowing: true,
- },
- };
-
- mocks = [
- {
- request: {
- query: DELETE_PROJECT_MUTATION,
- variables: { id: project.id },
- },
- result: jest.fn(() => ({
- data: {
- deleteProject: {
- id: project.id,
- },
- },
- })),
- },
- ];
-
- store = mockStore(initialState);
-
- render(
-
-
-
-
-
-
- ,
- );
- });
-
- test("Modal renders", () => {
- expect(
- screen.queryByText("projectList.deleteProjectModal.heading"),
- ).toBeInTheDocument();
- });
-
- test("Clicking cancel button closes modal and does not save", () => {
- const cancelButton = screen.queryByText(
- "projectList.deleteProjectModal.cancel",
- );
- fireEvent.click(cancelButton);
- expect(store.getActions()).toEqual([
- { type: "editor/closeDeleteProjectModal" },
- ]);
- });
-
- test("Clicking delete button (eventually) closes the modal", async () => {
- const deleteButton = screen.getByText(
- "projectList.deleteProjectModal.delete",
- );
- fireEvent.click(deleteButton);
- await waitFor(() =>
- expect(store.getActions()).toEqual([
- { type: "editor/closeDeleteProjectModal" },
- ]),
- );
- });
-
- test("Clicking delete button calls the mutation", async () => {
- const deleteButton = screen.getByText(
- "projectList.deleteProjectModal.delete",
- );
- const deleteProjectMutationMock = mocks[0].result;
- fireEvent.click(deleteButton);
- await waitFor(() => expect(deleteProjectMutationMock).toHaveBeenCalled());
- });
-});
diff --git a/src/components/Modals/LoginToSaveModal.jsx b/src/components/Modals/LoginToSaveModal.jsx
deleted file mode 100644
index 91df69f07..000000000
--- a/src/components/Modals/LoginToSaveModal.jsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import React from "react";
-import { useDispatch, useSelector } from "react-redux";
-import { useTranslation } from "react-i18next";
-
-import { closeLoginToSaveModal } from "../../redux/EditorSlice";
-import DownloadButton from "../DownloadButton/DownloadButton";
-import LoginButton from "../Login/LoginButton";
-import "../../assets/stylesheets/Modal.scss";
-import Button from "../Button/Button";
-import GeneralModal from "./GeneralModal";
-import { login } from "../../utils/login";
-import { useLocation } from "react-router-dom";
-
-const LoginToSaveModal = () => {
- const dispatch = useDispatch();
- const { t } = useTranslation();
- const location = useLocation();
- const project = useSelector((state) => state.editor.project);
- const isModalOpen = useSelector(
- (state) => state.editor.loginToSaveModalShowing,
- );
- const closeModal = () => dispatch(closeLoginToSaveModal());
-
- const defaultCallback = () => {
- login({ project, location, triggerSave: true });
- };
-
- return (
- ,
- ,
- ,
- ]}
- defaultCallback={defaultCallback}
- />
- );
-};
-
-export default LoginToSaveModal;
diff --git a/src/components/Modals/LoginToSaveModal.test.js b/src/components/Modals/LoginToSaveModal.test.js
deleted file mode 100644
index c16fe133b..000000000
--- a/src/components/Modals/LoginToSaveModal.test.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import React from "react";
-import { fireEvent, render, screen } from "@testing-library/react";
-import { Provider } from "react-redux";
-import configureStore from "redux-mock-store";
-import LoginToSaveModal from "./LoginToSaveModal";
-
-jest.mock("react-router-dom", () => ({
- useLocation: jest.fn(),
-}));
-
-const middlewares = [];
-const mockStore = configureStore(middlewares);
-
-describe("When loginToSaveModalShowing is true", () => {
- let store;
-
- beforeEach(() => {
- const initialState = {
- editor: {
- loginToSaveModalShowing: true,
- modals: {},
- },
- };
- store = mockStore(initialState);
- render(
-
-
-
-
- ,
- );
- });
-
- test("Modal rendered", () => {
- expect(screen.queryByText("loginToSaveModal.heading")).toBeInTheDocument();
- });
-
- test("Clicking cancel dispatches close modal action", () => {
- const cancelLink = screen.queryByText("loginToSaveModal.cancel");
- fireEvent.click(cancelLink);
- expect(store.getActions()).toEqual([
- { type: "editor/closeLoginToSaveModal" },
- ]);
- });
-});
diff --git a/src/components/Modals/NewProjectModal.jsx b/src/components/Modals/NewProjectModal.jsx
deleted file mode 100644
index c80ddc64f..000000000
--- a/src/components/Modals/NewProjectModal.jsx
+++ /dev/null
@@ -1,94 +0,0 @@
-import React, { useState } from "react";
-
-import Button from "../Button/Button";
-import { closeNewProjectModal } from "../../redux/EditorSlice";
-import { useDispatch, useSelector } from "react-redux";
-import { useTranslation } from "react-i18next";
-import InputModal from "./InputModal";
-import { createOrUpdateProject } from "../../utils/apiCallHandler";
-import { useNavigate } from "react-router-dom";
-import { DEFAULT_PROJECTS } from "../../utils/defaultProjects";
-import HTMLIcon from "../../assets/icons/html.svg";
-import PythonIcon from "../../assets/icons/python.svg";
-
-const NewProjectModal = () => {
- const { t, i18n } = useTranslation();
- const dispatch = useDispatch();
- const user = useSelector((state) => state.auth.user);
-
- const isModalOpen = useSelector(
- (state) => state.editor.newProjectModalShowing,
- );
- const closeModal = () => dispatch(closeNewProjectModal());
-
- const [projectName, setProjectName] = useState(
- t("newProjectModal.projectName.default"),
- );
- const [projectType, setProjectType] = useState();
-
- const navigate = useNavigate();
-
- const createProject = async () => {
- const response = await createOrUpdateProject(
- { ...DEFAULT_PROJECTS[projectType], name: projectName },
- user.access_token,
- );
- const identifier = response.data.identifier;
- const locale = i18n.language;
- closeModal();
- navigate(`/${locale}/projects/${identifier}`);
- };
-
- return (
- ,
- ,
- ]}
- />
- );
-};
-
-export default NewProjectModal;
diff --git a/src/components/Modals/NewProjectModal.test.js b/src/components/Modals/NewProjectModal.test.js
deleted file mode 100644
index 22dfb8ac8..000000000
--- a/src/components/Modals/NewProjectModal.test.js
+++ /dev/null
@@ -1,117 +0,0 @@
-import React from "react";
-import { Provider } from "react-redux";
-import configureStore from "redux-mock-store";
-import NewProjectModal from "./NewProjectModal";
-import { fireEvent, render, screen, waitFor } from "@testing-library/react";
-import { MemoryRouter } from "react-router-dom";
-import { createOrUpdateProject } from "../../utils/apiCallHandler";
-import {
- defaultHtmlProject,
- defaultPythonProject,
-} from "../../utils/defaultProjects";
-
-const mockNavigate = jest.fn();
-
-jest.mock("react-router", () => ({
- ...jest.requireActual("react-router"),
- useParams: jest.fn(),
- useNavigate: () => mockNavigate,
- useLocation: jest.fn(),
-}));
-
-jest.mock("../../utils/apiCallHandler");
-
-let store;
-let inputBox;
-let pythonOption;
-let htmlOption;
-let saveButton;
-
-beforeEach(() => {
- createOrUpdateProject.mockImplementationOnce(() =>
- Promise.resolve({
- status: 200,
- data: { identifier: "my-amazing-project" },
- }),
- );
- const middlewares = [];
- const mockStore = configureStore(middlewares);
- const initialState = {
- editor: {
- project: {
- components: [
- {
- name: "main",
- extension: "py",
- },
- ],
- project_type: "python",
- },
- nameError: "",
- newProjectModalShowing: true,
- },
- auth: {
- user: {
- access_token: "my_token",
- },
- },
- };
- store = mockStore(initialState);
- render(
-
-
-
-
-
-
- ,
- );
- inputBox = screen.getByRole("textbox");
- pythonOption = screen.getByText("projectTypes.python");
- htmlOption = screen.getByText("projectTypes.html");
- saveButton = screen.getByText("newProjectModal.createProject");
-});
-
-test("Renders", () => {
- expect(screen.queryByText("newProjectModal.heading")).toBeInTheDocument();
-});
-
-test("Creates python project correctly", async () => {
- fireEvent.change(inputBox, { target: { value: "My amazing project" } });
- fireEvent.click(pythonOption);
- await waitFor(() => fireEvent.click(saveButton));
- expect(createOrUpdateProject).toHaveBeenCalledWith(
- { ...defaultPythonProject, name: "My amazing project" },
- "my_token",
- );
-});
-
-test("Creates HTML project correctly", async () => {
- fireEvent.change(inputBox, { target: { value: "My amazing project" } });
- fireEvent.click(htmlOption);
- await waitFor(() => fireEvent.click(saveButton));
- expect(createOrUpdateProject).toHaveBeenCalledWith(
- { ...defaultHtmlProject, name: "My amazing project" },
- "my_token",
- );
-});
-
-test("Pressing Enter creates new project", async () => {
- fireEvent.change(inputBox, { target: { value: "My amazing project" } });
- fireEvent.click(htmlOption);
- const modal = screen.getByRole("dialog");
- await waitFor(() => fireEvent.keyDown(modal, { key: "Enter" }));
- expect(createOrUpdateProject).toHaveBeenCalledWith(
- { ...defaultHtmlProject, name: "My amazing project" },
- "my_token",
- );
-});
-
-test("Navigates to new project", async () => {
- fireEvent.change(inputBox, { target: { value: "My amazing project" } });
- fireEvent.click(pythonOption);
- await waitFor(() => fireEvent.click(saveButton));
- expect(mockNavigate).toHaveBeenCalledWith(
- "/ja-JP/projects/my-amazing-project",
- );
-});
diff --git a/src/components/ProjectIndex/ProjectIndex.jsx b/src/components/ProjectIndex/ProjectIndex.jsx
deleted file mode 100644
index c3e0c2c22..000000000
--- a/src/components/ProjectIndex/ProjectIndex.jsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import { useSelector, connect, useDispatch } from "react-redux";
-import { useTranslation } from "react-i18next";
-import { gql, useQuery } from "@apollo/client";
-import { useRequiresUser } from "../../hooks/useRequiresUser";
-import ProjectIndexHeader from "../ProjectIndexHeader/ProjectIndexHeader";
-import {
- ProjectListTable,
- PROJECT_LIST_TABLE_FRAGMENT,
-} from "../ProjectListTable/ProjectListTable";
-import Button from "../Button/Button";
-import PlusIcon from "../../assets/icons/plus.svg";
-import RenameProjectModal from "../Modals/RenameProjectModal";
-import DeleteProjectModal from "../Modals/DeleteProjectModal";
-import {
- ProjectIndexPagination,
- PROJECT_INDEX_PAGINATION_FRAGMENT,
-} from "./ProjectIndexPagination";
-import { showNewProjectModal } from "../../redux/EditorSlice";
-import NewProjectModal from "../Modals/NewProjectModal";
-
-export const PROJECT_INDEX_QUERY = gql`
- query ProjectIndexQuery(
- $userId: String
- $first: Int
- $last: Int
- $before: String
- $after: String
- ) {
- projects(
- userId: $userId
- first: $first
- last: $last
- before: $before
- after: $after
- ) {
- ...ProjectListTableFragment
- ...ProjectIndexPaginationFragment
- }
- }
- ${PROJECT_LIST_TABLE_FRAGMENT}
- ${PROJECT_INDEX_PAGINATION_FRAGMENT}
-`;
-
-const ProjectIndex = (props) => {
- const { isLoading, user } = props;
- const { t } = useTranslation();
- const pageSize = 8;
-
- useRequiresUser(isLoading, user);
-
- const newProjectModalShowing = useSelector(
- (state) => state.editor.newProjectModalShowing,
- );
- const renameProjectModalShowing = useSelector(
- (state) => state.editor.renameProjectModalShowing,
- );
- const deleteProjectModalShowing = useSelector(
- (state) => state.editor.deleteProjectModalShowing,
- );
-
- const dispatch = useDispatch();
- const onCreateProject = async () => {
- dispatch(showNewProjectModal());
- };
-
- const { loading, error, data, fetchMore } = useQuery(PROJECT_INDEX_QUERY, {
- fetchPolicy: "network-only",
- nextFetchPolicy: "cache-first",
- variables: { userId: user?.profile?.user, first: pageSize },
- skip: user === undefined,
- });
-
- return (
- <>
-
-
-
- {!loading && data ? (
- <>
-
-
- >
- ) : null}
- {loading ? {t("projectList.loading")}
: null}
- {error ? {t("projectList.loadingFailed")}
: null}
- {newProjectModalShowing ? : null}
- {renameProjectModalShowing ? : null}
- {deleteProjectModalShowing ? : null}
- >
- );
-};
-
-function mapStateToProps(state) {
- return {
- isLoading: state.auth.isLoadingUser,
- user: state.auth.user,
- };
-}
-
-export default connect(mapStateToProps)(ProjectIndex);
diff --git a/src/components/ProjectIndex/ProjectIndex.test.js b/src/components/ProjectIndex/ProjectIndex.test.js
deleted file mode 100644
index 339a1996e..000000000
--- a/src/components/ProjectIndex/ProjectIndex.test.js
+++ /dev/null
@@ -1,130 +0,0 @@
-import { render, screen } from "@testing-library/react";
-import React from "react";
-import { Provider } from "react-redux";
-import { MemoryRouter } from "react-router-dom";
-import configureStore from "redux-mock-store";
-import { MockedProvider } from "@apollo/client/testing";
-
-import { default as ProjectIndex, PROJECT_INDEX_QUERY } from "./ProjectIndex";
-
-jest.mock("date-fns");
-
-const mockedUseNavigate = jest.fn();
-
-jest.mock("react-router-dom", () => ({
- ...jest.requireActual("react-router-dom"),
- useNavigate: () => mockedUseNavigate,
-}));
-
-const user = {
- access_token: "myAccessToken",
- profile: {
- user: "b48e70e2-d9ed-4a59-aee5-fc7cf09dbfaf",
- },
-};
-
-describe("When authenticated", () => {
- const auth = { user: user };
-
- describe("When user has projects", () => {
- // Because we use fragments, we have to specify __typename for each entry
- // in the result
- const mocks = [
- {
- request: {
- query: PROJECT_INDEX_QUERY,
- variables: { userId: user.profile.user, first: 8 },
- },
- result: {
- data: {
- projects: {
- __typename: "ProjectConnection",
- edges: [
- {
- __typename: "ProjectEdge",
- cursor: "MQ",
- node: {
- __typename: "Project",
- id: "abc",
- name: "my project 1",
- identifier: "amazing-1",
- locale: "null",
- updatedAt: "2023-02-21T17:03:53Z",
- },
- },
- {
- __typename: "ProjectEdge",
- cursor: "Mg",
- node: {
- __typename: "Project",
- id: "def",
- name: "my project 2",
- identifier: "amazing-2",
- locale: "null",
- updatedAt: "2023-02-20T21:04:42Z",
- },
- },
- ],
- totalCount: 13,
- pageInfo: {
- __typename: "PageInfo",
- hasPreviousPage: false,
- startCursor: "MQ",
- endCursor: "Mg",
- hasNextPage: false,
- },
- },
- },
- },
- },
- ];
-
- beforeEach(() => {
- const initialState = {
- editor: {},
- auth: auth,
- };
-
- const mockStore = configureStore([]);
- const store = mockStore(initialState);
- render(
-
-
-
-
-
-
- ,
- );
- });
-
- it("Displays project titles", async () => {
- expect(await screen.findByText("my project 1")).toBeInTheDocument();
- expect(await screen.findByText("my project 2")).toBeInTheDocument();
- });
- }); // User has projects
-}); // Authenticated
-
-describe("When unauthenticated", () => {
- const auth = {};
-
- beforeEach(() => {
- const mockStore = configureStore([]);
- const initialState = {
- editor: {},
- auth: auth,
- };
- const store = mockStore(initialState);
- render(
-
-
-
-
- ,
- );
- });
-
- it("navigates back to /", () => {
- expect(mockedUseNavigate).toHaveBeenCalledWith("/");
- });
-});
diff --git a/src/components/ProjectIndex/ProjectIndexPagination.jsx b/src/components/ProjectIndex/ProjectIndexPagination.jsx
deleted file mode 100644
index 7cfa58db3..000000000
--- a/src/components/ProjectIndex/ProjectIndexPagination.jsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import React from "react";
-import { useTranslation } from "react-i18next";
-import { gql } from "@apollo/client";
-
-import Button from "../Button/Button";
-import "../../assets/stylesheets/ProjectIndexPagination.scss";
-
-export const PROJECT_INDEX_PAGINATION_FRAGMENT = gql`
- fragment ProjectIndexPaginationFragment on ProjectConnection {
- totalCount
- pageInfo {
- hasPreviousPage
- startCursor
- endCursor
- hasNextPage
- }
- }
-`;
-
-export const ProjectIndexPagination = (props) => {
- const { t } = useTranslation();
- const { paginationData, pageSize, fetchMore } = props;
-
- const totalCount = paginationData.totalCount || 0;
- if (totalCount === 0) return null;
-
- const pageInfo = paginationData.pageInfo || {};
- if (Object.keys(pageInfo).length === 0) return null;
-
- return (
-
-
- {pageInfo.hasNextPage ? (
- <>
-
-
- );
-};
diff --git a/src/components/ProjectIndex/ProjectIndexPagination.test.js b/src/components/ProjectIndex/ProjectIndexPagination.test.js
deleted file mode 100644
index e37ad480c..000000000
--- a/src/components/ProjectIndex/ProjectIndexPagination.test.js
+++ /dev/null
@@ -1,166 +0,0 @@
-import { fireEvent, render, screen } from "@testing-library/react";
-import React from "react";
-
-import { ProjectIndexPagination } from "./ProjectIndexPagination";
-
-const fetchMore = jest.fn();
-const pageSize = 2;
-
-describe("When pageInfo is missing", () => {
- const paginationData = {
- totalCount: 2,
- };
-
- beforeEach(() => {
- render(
- ,
- );
- });
-
- test("It doesn't show the navigation", () => {
- expect(
- screen.queryByTestId("projectIndexPagination"),
- ).not.toBeInTheDocument();
- });
-});
-
-describe("When totalCount is missing", () => {
- const paginationData = {
- pageInfo: {},
- };
-
- beforeEach(() => {
- render(
- ,
- );
- });
-
- test("It doesn't show the navigation", () => {
- expect(
- screen.queryByTestId("projectIndexPagination"),
- ).not.toBeInTheDocument();
- });
-});
-
-describe("When on the first page of projects", () => {
- const paginationData = {
- totalCount: 7,
- pageInfo: {
- startCursor: btoa(1),
- endCursor: btoa(2),
- hasNextPage: true,
- hasPreviousPage: false,
- },
- };
- beforeEach(() => {
- render(
- ,
- );
- });
-
- test("More buttons shown", () => {
- expect(
- screen.queryByTitle("projectList.pagination.more"),
- ).toBeInTheDocument();
- });
-});
-
-describe("When the endCursor is missing", () => {
- const paginationData = {
- totalCount: 7,
- pageInfo: {
- hasNextPage: true,
- hasPreviousPage: false,
- },
- };
- beforeEach(() => {
- render(
- ,
- );
- });
-
- test("Assume there is more to load", () => {
- expect(
- screen.queryByTitle("projectList.pagination.more"),
- ).toBeInTheDocument();
- });
-});
-
-describe("When on a middle page of projects", () => {
- const paginationData = {
- totalCount: 7,
- pageInfo: {
- startCursor: btoa(3),
- endCursor: btoa(4),
- hasNextPage: true,
- hasPreviousPage: true,
- },
- };
-
- beforeEach(() => {
- render(
- ,
- );
- });
-
- test("More buttons shown", () => {
- expect(
- screen.queryByTitle("projectList.pagination.more"),
- ).toBeInTheDocument();
- });
-
- test("Clicking the more button requests next page", () => {
- const nextButton = screen.queryByTitle("projectList.pagination.more");
- fireEvent.click(nextButton);
- expect(fetchMore).toHaveBeenCalledWith({
- variables: { first: pageSize, after: paginationData.pageInfo.endCursor },
- });
- });
-});
-
-describe("When on the last page of projects", () => {
- const paginationData = {
- totalCount: 7,
- pageInfo: {
- startCursor: btoa(7),
- endCursor: btoa(7),
- hasNextPage: false,
- hasPreviousPage: true,
- },
- };
- beforeEach(() => {
- render(
- ,
- );
- });
-
- test("More button not shown", () => {
- expect(
- screen.queryByTitle("projectList.pagination.more"),
- ).not.toBeInTheDocument();
- });
-});
diff --git a/src/components/ProjectIndexHeader/ProjectIndexHeader.jsx b/src/components/ProjectIndexHeader/ProjectIndexHeader.jsx
deleted file mode 100644
index ca760fbe5..000000000
--- a/src/components/ProjectIndexHeader/ProjectIndexHeader.jsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { useTranslation } from "react-i18next";
-import "../../assets/stylesheets/ProjectIndexHeader.scss";
-
-const ProjectIndexHeader = (props) => {
- const { t } = useTranslation();
-
- return (
-
- );
-};
-
-export default ProjectIndexHeader;
diff --git a/src/components/ProjectListItem/ProjectListItem.jsx b/src/components/ProjectListItem/ProjectListItem.jsx
deleted file mode 100644
index 1018ed77f..000000000
--- a/src/components/ProjectListItem/ProjectListItem.jsx
+++ /dev/null
@@ -1,94 +0,0 @@
-import { intlFormatDistance } from "date-fns";
-import { useDispatch } from "react-redux";
-import { useTranslation } from "react-i18next";
-import {
- showDeleteProjectModal,
- showRenameProjectModal,
-} from "../../redux/EditorSlice";
-import Button from "../Button/Button";
-import python_logo from "../../assets/python_icon.svg";
-import html_logo from "../../assets/html_icon.svg";
-import "../../assets/stylesheets/ProjectListItem.scss";
-import BinIcon from "../../assets/icons/bin.svg";
-import PencilIcon from "../../assets/icons/pencil.svg";
-import ProjectActionsMenu from "../Menus/ProjectActionsMenu/ProjectActionsMenu";
-import { Link } from "react-router-dom";
-import { gql } from "@apollo/client";
-
-export const PROJECT_LIST_ITEM_FRAGMENT = gql`
- fragment ProjectListItemFragment on Project {
- name
- identifier
- locale
- updatedAt
- projectType
- }
-`;
-
-export const ProjectListItem = (props) => {
- const project = props.project;
- const { t, i18n } = useTranslation();
- const locale = i18n.language;
- const dispatch = useDispatch();
- const lastSaved = intlFormatDistance(
- new Date(project.updatedAt),
- Date.now(),
- { style: "short" },
- );
- const projectType = props.project.projectType;
-
- const openRenameProjectModal = () => {
- dispatch(showRenameProjectModal(project));
- };
-
- const openDeleteProjectModal = () => {
- dispatch(showDeleteProjectModal(project));
- };
-
- return (
-
-
-
-
-
-
{project.name}
-
- {projectType === "html"
- ? t("projectList.html_type")
- : t("projectList.python_type")}
-
-
- {t("projectList.updated")} {lastSaved}
-
-
-
-
-
-
-
-
-
-
- );
-};
diff --git a/src/components/ProjectListItem/ProjectListItem.test.js b/src/components/ProjectListItem/ProjectListItem.test.js
deleted file mode 100644
index 710ac5276..000000000
--- a/src/components/ProjectListItem/ProjectListItem.test.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import { fireEvent, render, screen } from "@testing-library/react";
-import React from "react";
-import { Provider } from "react-redux";
-import { MemoryRouter } from "react-router-dom";
-import configureStore from "redux-mock-store";
-import { ProjectListItem } from "./ProjectListItem";
-
-jest.mock("date-fns");
-
-let store;
-let project = {
- identifier: "hello-world-project",
- name: "my amazing project",
- updatedAt: Date.now(),
-};
-
-beforeEach(() => {
- const mockStore = configureStore([]);
- const initialState = {};
- store = mockStore(initialState);
-
- render(
-
-
-
-
- ,
- );
-});
-
-test("Renders project name", () => {
- expect(screen.queryByText(project.name)).toBeInTheDocument();
-});
-
-test("Clicking rename button opens rename project modal", () => {
- const renameButtons = screen.queryAllByText("projectList.rename");
- fireEvent.click(renameButtons[0]);
- expect(store.getActions()).toEqual([
- { type: "editor/showRenameProjectModal", payload: project },
- ]);
-});
-
-test("Clicking delete button opens delete project modal", () => {
- const deleteButtons = screen.queryAllByText("projectList.delete");
- fireEvent.click(deleteButtons[0]);
- expect(store.getActions()).toEqual([
- { type: "editor/showDeleteProjectModal", payload: project },
- ]);
-});
diff --git a/src/components/ProjectListTable/ProjectListTable.jsx b/src/components/ProjectListTable/ProjectListTable.jsx
deleted file mode 100644
index 36f5f50f8..000000000
--- a/src/components/ProjectListTable/ProjectListTable.jsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { useTranslation } from "react-i18next";
-import {
- ProjectListItem,
- PROJECT_LIST_ITEM_FRAGMENT,
-} from "../ProjectListItem/ProjectListItem";
-import "../../assets/stylesheets/ProjectListTable.scss";
-import { gql } from "@apollo/client";
-
-export const PROJECT_LIST_TABLE_FRAGMENT = gql`
- fragment ProjectListTableFragment on ProjectConnection {
- edges {
- cursor
- node {
- id
- ...ProjectListItemFragment
- }
- }
- }
- ${PROJECT_LIST_ITEM_FRAGMENT}
-`;
-
-export const ProjectListTable = (props) => {
- const { t } = useTranslation();
- const { projectData } = props;
-
- const projectList = projectData?.edges?.map((edge) => edge.node);
-
- return (
-
-
- {projectList && projectList.length > 0 ? (
- <>
- {projectList.map((project, i) => (
-
- ))}
- >
- ) : (
-
-
{t("projectList.empty")}
-
- )}
-
-
- );
-};
diff --git a/src/components/ProjectListTable/ProjectListTable.test.js b/src/components/ProjectListTable/ProjectListTable.test.js
deleted file mode 100644
index 811828a22..000000000
--- a/src/components/ProjectListTable/ProjectListTable.test.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import { render, screen } from "@testing-library/react";
-import { Provider } from "react-redux";
-import { MemoryRouter } from "react-router-dom";
-import configureStore from "redux-mock-store";
-import { ProjectListTable } from "./ProjectListTable";
-
-describe("When the logged in user has projects", () => {
- const project = {
- name: "hello world",
- project_type: "python",
- identifier: "hello-world-project",
- updatedAt: Date.now(),
- };
-
- const projectData = {
- edges: [
- {
- cursor: "Mq",
- node: { ...project },
- },
- ],
- };
-
- beforeEach(() => {
- const mockStore = configureStore([]);
- const initialState = {};
- const store = mockStore(initialState);
-
- render(
-
-
-
-
- ,
- );
- });
-
- test("The projects page show a list of projects", () => {
- expect(screen.queryByText(project.name)).toBeInTheDocument();
- });
-});
-
-describe("When the logged in user has no projects", () => {
- const projectData = {
- edges: [],
- };
-
- beforeEach(() => {
- const mockStore = configureStore([]);
- const initialState = {};
- const store = mockStore(initialState);
- render(
-
-
-
-
- ,
- );
- });
-
- test("The projects page show an empty state message", () => {
- expect(screen.queryByText("projectList.empty")).toBeInTheDocument();
- });
-});
diff --git a/src/containers/WebComponentLoader.jsx b/src/containers/WebComponentLoader.jsx
index 7964ec7b5..e576a64cc 100644
--- a/src/containers/WebComponentLoader.jsx
+++ b/src/containers/WebComponentLoader.jsx
@@ -4,6 +4,7 @@ import {
disableTheming,
setSenseHatAlwaysEnabled,
setLoadRemixDisabled,
+ setReactAppApiEndpoint,
setReadOnly,
} from "../redux/EditorSlice";
import WebComponentProject from "../components/WebComponentProject/WebComponentProject";
@@ -31,24 +32,25 @@ const WebComponentLoader = (props) => {
const {
assetsIdentifier,
authKey,
- identifier,
code,
- senseHatAlwaysEnabled = false,
- instructions,
- withProjectbar = false,
- projectNameEditable = false,
- withSidebar = false,
- sidebarOptions = [],
- theme,
- outputPanels = ["text", "visual"],
embedded = false,
hostStyles, // Pass in styles from the host
- showSavePrompt = false,
+ identifier,
+ instructions,
loadRemixDisabled = false,
- readOnly = false,
outputOnly = false,
+ outputPanels = ["text", "visual"],
outputSplitView = false,
+ projectNameEditable = false,
+ reactAppApiEndpoint = process.env.REACT_APP_API_ENDPOINT,
+ readOnly = false,
+ senseHatAlwaysEnabled = false,
+ showSavePrompt = false,
+ sidebarOptions = [],
+ theme,
useEditorStyles = false, // If true use the standard editor styling for the web component
+ withProjectbar = false,
+ withSidebar = false,
} = props;
const dispatch = useDispatch();
const { t } = useTranslation();
@@ -118,6 +120,7 @@ const WebComponentLoader = (props) => {
}, [projectOwner, justLoaded]);
useProject({
+ reactAppApiEndpoint,
projectIdentifier: projectIdentifier,
assetsIdentifier: assetsIdentifier,
code,
@@ -135,6 +138,10 @@ const WebComponentLoader = (props) => {
saveTriggered,
});
+ useEffect(() => {
+ dispatch(setReactAppApiEndpoint(reactAppApiEndpoint));
+ }, [reactAppApiEndpoint, dispatch]);
+
useEffect(() => {
dispatch(setSenseHatAlwaysEnabled(senseHatAlwaysEnabled));
}, [senseHatAlwaysEnabled, dispatch]);
diff --git a/src/containers/WebComponentLoader.test.js b/src/containers/WebComponentLoader.test.js
index 645bf420d..a6c15cd83 100644
--- a/src/containers/WebComponentLoader.test.js
+++ b/src/containers/WebComponentLoader.test.js
@@ -7,6 +7,7 @@ import {
disableTheming,
setReadOnly,
setSenseHatAlwaysEnabled,
+ setReactAppApiEndpoint,
} from "../redux/EditorSlice";
import { setInstructions } from "../redux/InstructionsSlice";
import { setUser } from "../redux/WebComponentAuthSlice";
@@ -79,6 +80,63 @@ describe("When initially rendered", () => {
}),
);
});
+
+ describe("react app API endpoint", () => {
+ describe("when react app API endpoint isn't set", () => {
+ beforeEach(() => {
+ render(
+
+
+
+
+ ,
+ );
+ });
+
+ test("it defaults", () => {
+ expect(store.getActions()).toEqual(
+ expect.arrayContaining([
+ setReactAppApiEndpoint("http://localhost:3009"),
+ ]),
+ );
+ });
+ });
+
+ describe("when react app API endpoint is set", () => {
+ beforeEach(() => {
+ render(
+
+
+
+
+ ,
+ );
+ });
+
+ test("it uses the specified prop", () => {
+ expect(store.getActions()).toEqual(
+ expect.arrayContaining([setReactAppApiEndpoint("http://local.dev")]),
+ );
+ });
+ });
+ });
});
describe("When no user is in state", () => {
@@ -126,12 +184,14 @@ describe("When no user is in state", () => {
test("Calls useProject hook with correct attributes", () => {
expect(useProject).toHaveBeenCalledWith({
+ assetsIdentifier: undefined,
projectIdentifier: identifier,
code,
accessToken: undefined,
loadRemix: false,
loadCache: true,
remixLoadFailed: false,
+ reactAppApiEndpoint: "http://localhost:3009",
});
});
@@ -234,6 +294,7 @@ describe("When no user is in state", () => {
loadRemix: false,
loadCache: true,
remixLoadFailed: false,
+ reactAppApiEndpoint: "http://localhost:3009",
});
});
});
@@ -260,12 +321,14 @@ describe("When no user is in state", () => {
test("Calls useProject hook with correct attributes", () => {
expect(useProject).toHaveBeenCalledWith({
+ assetsIdentifier: undefined,
projectIdentifier: identifier,
code,
accessToken: "my_token",
loadRemix: true,
loadCache: false,
remixLoadFailed: false,
+ reactAppApiEndpoint: "http://localhost:3009",
});
});
@@ -384,12 +447,14 @@ describe("When user is in state", () => {
test("Calls useProject hook with correct attributes", () => {
expect(useProject).toHaveBeenCalledWith({
+ assetsIdentifier: undefined,
projectIdentifier: identifier,
code: undefined,
accessToken: "my_token",
loadRemix: true,
loadCache: false,
remixLoadFailed: false,
+ reactAppApiEndpoint: "http://localhost:3009",
});
});
@@ -412,12 +477,14 @@ describe("When user is in state", () => {
test("Calls useProject hook with loadRemix set to false, i.e. it is overidden", () => {
expect(useProject).toHaveBeenCalledWith({
+ assetsIdentifier: undefined,
projectIdentifier: identifier,
code: undefined,
accessToken: "my_token",
loadRemix: false,
loadCache: false,
remixLoadFailed: false,
+ reactAppApiEndpoint: "http://localhost:3009",
});
});
});
@@ -488,12 +555,14 @@ describe("When user is in state", () => {
test("Calls useProject hook with correct attributes", () => {
expect(useProject).toHaveBeenCalledWith({
+ assetsIdentifier: undefined,
projectIdentifier: identifier,
code: undefined,
accessToken: "my_token",
loadRemix: false,
loadCache: true,
remixLoadFailed: true,
+ reactAppApiEndpoint: "http://localhost:3009",
});
});
});
diff --git a/src/hooks/useProject.js b/src/hooks/useProject.js
index 20ba93fa5..3174735f2 100644
--- a/src/hooks/useProject.js
+++ b/src/hooks/useProject.js
@@ -6,6 +6,7 @@ import { defaultPythonProject } from "../utils/defaultProjects";
import { useTranslation } from "react-i18next";
export const useProject = ({
+ reactAppApiEndpoint = null,
assetsIdentifier = null,
projectIdentifier = null,
code = null,
@@ -54,6 +55,7 @@ export const useProject = ({
if (assetsIdentifier) {
dispatch(
syncProject("load")({
+ reactAppApiEndpoint,
identifier: assetsIdentifier,
locale: i18n.language,
accessToken,
@@ -66,6 +68,7 @@ export const useProject = ({
if (projectIdentifier) {
dispatch(
syncProject("load")({
+ reactAppApiEndpoint,
identifier: projectIdentifier,
locale: i18n.language,
accessToken: accessToken,
@@ -103,6 +106,7 @@ export const useProject = ({
if (!remixLoadFailed && !loadDispatched.current) {
dispatch(
syncProject("loadRemix")({
+ reactAppApiEndpoint,
identifier: projectIdentifier,
accessToken: accessToken,
}),
@@ -119,6 +123,7 @@ export const useProject = ({
if (remixLoadFailed && !loadDispatched.current) {
dispatch(
syncProject("load")({
+ reactAppApiEndpoint,
identifier: projectIdentifier,
locale: i18n.language,
accessToken: accessToken,
diff --git a/src/hooks/useProject.test.js b/src/hooks/useProject.test.js
index 3e32e506b..ed4b88d63 100644
--- a/src/hooks/useProject.test.js
+++ b/src/hooks/useProject.test.js
@@ -12,10 +12,11 @@ jest.mock("react-redux", () => ({
}));
const loadProject = jest.fn();
+const reactAppApiEndpoint = "localhost";
jest.mock("../redux/EditorSlice");
-jest.mock("../utils/apiCallHandler", () => ({
+jest.mock("../utils/apiCallHandler", () => () => ({
readProject: async (identifier, projectType) =>
Promise.resolve({
data: { identifier: identifier, project_type: projectType },
@@ -100,7 +101,12 @@ describe("When not embedded", () => {
syncProject.mockImplementationOnce(jest.fn((_) => loadProject));
localStorage.setItem("project", JSON.stringify(cachedProject));
renderHook(
- () => useProject({ projectIdentifier: project1.identifier, accessToken }),
+ () =>
+ useProject({
+ projectIdentifier: project1.identifier,
+ accessToken,
+ reactAppApiEndpoint,
+ }),
{ wrapper },
);
expect(syncProject).toHaveBeenCalledWith("load");
@@ -109,6 +115,7 @@ describe("When not embedded", () => {
identifier: project1.identifier,
locale: "ja-JP",
accessToken,
+ reactAppApiEndpoint,
}),
);
});
@@ -122,6 +129,7 @@ describe("When not embedded", () => {
projectIdentifier: project1.identifier,
accessToken,
loadCache: false,
+ reactAppApiEndpoint,
}),
{ wrapper },
);
@@ -131,6 +139,7 @@ describe("When not embedded", () => {
identifier: project1.identifier,
locale: "ja-JP",
accessToken,
+ reactAppApiEndpoint,
}),
);
});
@@ -139,7 +148,11 @@ describe("When not embedded", () => {
syncProject.mockImplementationOnce(jest.fn((_) => loadProject));
renderHook(
() =>
- useProject({ projectIdentifier: "hello-world-project", accessToken }),
+ useProject({
+ projectIdentifier: "hello-world-project",
+ accessToken,
+ reactAppApiEndpoint,
+ }),
{ wrapper },
);
expect(syncProject).toHaveBeenCalledWith("load");
@@ -148,6 +161,7 @@ describe("When not embedded", () => {
identifier: "hello-world-project",
locale: "ja-JP",
accessToken,
+ reactAppApiEndpoint,
}),
);
});
@@ -205,6 +219,7 @@ describe("When not embedded", () => {
projectIdentifier: project1.identifier,
accessToken,
loadRemix: true,
+ reactAppApiEndpoint,
}),
{ wrapper },
);
@@ -213,6 +228,7 @@ describe("When not embedded", () => {
expect(loadProject).toHaveBeenCalledWith({
identifier: project1.identifier,
accessToken,
+ reactAppApiEndpoint,
}),
);
});
@@ -226,6 +242,7 @@ describe("When not embedded", () => {
accessToken,
loadRemix: true,
remixLoadFailed: true,
+ reactAppApiEndpoint,
}),
{ wrapper },
);
@@ -235,6 +252,7 @@ describe("When not embedded", () => {
identifier: project1.identifier,
locale: "ja-JP",
accessToken,
+ reactAppApiEndpoint,
}),
);
});
@@ -243,7 +261,11 @@ describe("When not embedded", () => {
syncProject.mockImplementationOnce(jest.fn((_) => loadProject));
renderHook(
() =>
- useProject({ assetsIdentifier: "hello-world-project", accessToken }),
+ useProject({
+ assetsIdentifier: "hello-world-project",
+ accessToken,
+ reactAppApiEndpoint,
+ }),
{ wrapper },
);
expect(syncProject).toHaveBeenCalledWith("load");
@@ -253,6 +275,7 @@ describe("When not embedded", () => {
locale: "ja-JP",
accessToken,
assetsOnly: true,
+ reactAppApiEndpoint,
}),
);
});
@@ -468,6 +491,7 @@ describe("When embedded", () => {
projectIdentifier: "hello-world-project",
accessToken,
isEmbedded: true,
+ reactAppApiEndpoint,
}),
{ wrapper },
);
@@ -477,6 +501,7 @@ describe("When embedded", () => {
identifier: "hello-world-project",
locale: "ja-JP",
accessToken,
+ reactAppApiEndpoint,
}),
);
});
diff --git a/src/hooks/useProjectPersistence.js b/src/hooks/useProjectPersistence.js
index 32d36ed51..51b0f4797 100644
--- a/src/hooks/useProjectPersistence.js
+++ b/src/hooks/useProjectPersistence.js
@@ -4,7 +4,6 @@ import { isOwner } from "../utils/projectHelpers";
import {
expireJustLoaded,
setHasShownSavePrompt,
- showLoginToSaveModal,
syncProject,
} from "../redux/EditorSlice";
import { showLoginPrompt, showSavePrompt } from "../utils/Notifications";
@@ -48,8 +47,6 @@ export const useProjectPersistence = ({
accessToken: user.access_token,
}),
);
- } else {
- dispatch(showLoginToSaveModal());
}
localStorage.removeItem("awaitingSave");
}
diff --git a/src/hooks/useProjectPersistence.test.js b/src/hooks/useProjectPersistence.test.js
index 227918326..b1b69efac 100644
--- a/src/hooks/useProjectPersistence.test.js
+++ b/src/hooks/useProjectPersistence.test.js
@@ -3,7 +3,6 @@ import { useProjectPersistence } from "./useProjectPersistence";
import {
expireJustLoaded,
setHasShownSavePrompt,
- showLoginToSaveModal,
syncProject,
} from "../redux/EditorSlice";
import { showLoginPrompt, showSavePrompt } from "../utils/Notifications";
@@ -18,7 +17,6 @@ jest.mock("../redux/EditorSlice", () => ({
syncProject: jest.fn((_) => jest.fn()),
expireJustLoaded: jest.fn(),
setHasShownSavePrompt: jest.fn(),
- showLoginToSaveModal: jest.fn(),
}));
jest.mock("../utils/Notifications");
@@ -125,23 +123,6 @@ describe("When not logged in", () => {
);
});
});
-
- describe("When save has been triggered", () => {
- beforeEach(() => {
- renderHook(() =>
- useProjectPersistence({
- user: null,
- project: project,
- saveTriggered: true,
- }),
- );
- jest.runAllTimers();
- });
-
- test("Login to save modal is shown", () => {
- expect(showLoginToSaveModal).toHaveBeenCalled();
- });
- });
});
describe("When logged in", () => {
diff --git a/src/redux/EditorSlice.js b/src/redux/EditorSlice.js
index 8a73f4cc5..9d113727d 100644
--- a/src/redux/EditorSlice.js
+++ b/src/redux/EditorSlice.js
@@ -5,23 +5,32 @@ import {
loadProjectFulfilled,
loadProjectRejected,
} from "./reducers/loadProjectReducers";
-import {
- createOrUpdateProject,
- readProject,
- loadRemix,
- createRemix,
- deleteProject,
- readProjectList,
- loadAssets,
-} from "../utils/apiCallHandler";
+import ApiCallHandler from "../utils/apiCallHandler";
export const syncProject = (actionName) =>
createAsyncThunk(
`editor/${actionName}Project`,
async (
- { project, identifier, locale, accessToken, autosave, assetsOnly },
+ {
+ reactAppApiEndpoint,
+ project,
+ identifier,
+ locale,
+ accessToken,
+ autosave,
+ assetsOnly,
+ },
{ rejectWithValue },
) => {
+ const {
+ createOrUpdateProject,
+ readProject,
+ loadRemix,
+ createRemix,
+ deleteProject,
+ loadAssets,
+ } = ApiCallHandler({ reactAppApiEndpoint });
+
let response;
switch (actionName) {
case "load":
@@ -71,7 +80,10 @@ export const syncProject = (actionName) =>
export const loadProjectList = createAsyncThunk(
`editor/loadProjectList`,
- async ({ page, accessToken }) => {
+ async ({ reactAppApiEndpoint, page, accessToken }) => {
+ const { readProjectList } = ApiCallHandler({
+ reactAppApiEndpoint,
+ });
const response = await readProjectList(page, accessToken);
return {
projects: response.data,
@@ -110,24 +122,18 @@ const initialState = {
codeRunStopped: false,
projectList: [],
projectListLoaded: "idle",
- projectIndexCurrentPage: 1,
- projectIndexTotalPages: 1,
lastSaveAutosave: false,
lastSavedTime: null,
senseHatAlwaysEnabled: false,
senseHatEnabled: false,
loadRemixDisabled: false,
- accessDeniedNoAuthModalShowing: false,
accessDeniedWithAuthModalShowing: false,
betaModalShowing: false,
errorModalShowing: false,
- loginToSaveModalShowing: false,
notFoundModalShowing: false,
newFileModalShowing: false,
renameFileModalShowing: false,
- newProjectModalShowing: false,
renameProjectModalShowing: false,
- deleteProjectModalShowing: false,
sidebarShowing: true,
modals: {},
errorDetails: {},
@@ -243,6 +249,9 @@ export const EditorSlice = createSlice({
setLoadRemixDisabled: (state, action) => {
state.loadRemixDisabled = action.payload;
},
+ setReactAppApiEndpoint: (state, action) => {
+ state.reactAppApiEndpoint = action.payload;
+ },
triggerDraw: (state) => {
state.drawTriggered = true;
},
@@ -304,10 +313,6 @@ export const EditorSlice = createSlice({
state.codeRunTriggered = false;
state.codeRunStopped = false;
},
- closeAccessDeniedNoAuthModal: (state) => {
- state.accessDeniedNoAuthModalShowing = false;
- state.modals = {};
- },
closeAccessDeniedWithAuthModal: (state) => {
state.accessDeniedWithAuthModalShowing = false;
},
@@ -323,13 +328,6 @@ export const EditorSlice = createSlice({
closeErrorModal: (state) => {
state.errorModalShowing = false;
},
- showLoginToSaveModal: (state) => {
- state.loginToSaveModalShowing = true;
- },
- closeLoginToSaveModal: (state) => {
- state.loginToSaveModalShowing = false;
- state.saveTriggered = false;
- },
closeNotFoundModal: (state) => {
state.notFoundModalShowing = false;
},
@@ -348,12 +346,6 @@ export const EditorSlice = createSlice({
state.renameFileModalShowing = false;
state.nameError = "";
},
- showNewProjectModal: (state) => {
- state.newProjectModalShowing = true;
- },
- closeNewProjectModal: (state) => {
- state.newProjectModalShowing = false;
- },
showRenameProjectModal: (state, action) => {
state.modals.renameProject = action.payload;
state.renameProjectModalShowing = true;
@@ -362,18 +354,6 @@ export const EditorSlice = createSlice({
state.modals.renameProject = null;
state.renameProjectModalShowing = false;
},
- showDeleteProjectModal: (state, action) => {
- state.modals.deleteProject = action.payload;
- state.deleteProjectModalShowing = true;
- },
- closeDeleteProjectModal: (state) => {
- state.modals.deleteProject = null;
- state.deleteProjectModalShowing = false;
- },
- setProjectIndexPage: (state, action) => {
- state.projectIndexCurrentPage = action.payload;
- state.projectListLoaded = "idle";
- },
showSidebar: (state) => {
state.sidebarShowing = true;
},
@@ -439,25 +419,6 @@ export const EditorSlice = createSlice({
builder.addCase("editor/deleteProject/fulfilled", (state) => {
state.projectListLoaded = "idle";
state.modals.deleteProject = null;
- state.deleteProjectModalShowing = false;
- });
- builder.addCase("editor/loadProjectList/pending", (state) => {
- state.projectListLoaded = "pending";
- });
- builder.addCase("editor/loadProjectList/fulfilled", (state, action) => {
- if (action.payload.projects.length > 0 || action.payload.page === 1) {
- state.projectListLoaded = "success";
- state.projectList = action.payload.projects;
- const links = action.payload.links;
- state.projectIndexTotalPages =
- links && links.last ? parseInt(links.last.page) : action.payload.page;
- } else {
- state.projectIndexCurrentPage = state.projectIndexCurrentPage - 1;
- state.projectListLoaded = "idle";
- }
- });
- builder.addCase("editor/loadProjectList/rejected", (state) => {
- state.projectListLoaded = "failed";
});
},
});
@@ -487,6 +448,7 @@ export const {
setSenseHatAlwaysEnabled,
setSenseHatEnabled,
setLoadRemixDisabled,
+ setReactAppApiEndpoint,
stopCodeRun,
stopDraw,
triggerCodeRun,
@@ -496,26 +458,18 @@ export const {
updateImages,
updateProjectComponent,
updateProjectName,
- closeAccessDeniedNoAuthModal,
closeAccessDeniedWithAuthModal,
showBetaModal,
closeBetaModal,
showErrorModal,
closeErrorModal,
- showLoginToSaveModal,
- closeLoginToSaveModal,
closeNotFoundModal,
showNewFileModal,
closeNewFileModal,
showRenameFileModal,
closeRenameFileModal,
- showNewProjectModal,
- closeNewProjectModal,
showRenameProjectModal,
closeRenameProjectModal,
- showDeleteProjectModal,
- closeDeleteProjectModal,
- setProjectIndexPage,
showSidebar,
hideSidebar,
disableTheming,
diff --git a/src/redux/EditorSlice.test.js b/src/redux/EditorSlice.test.js
index 4d92af974..186dac7a8 100644
--- a/src/redux/EditorSlice.test.js
+++ b/src/redux/EditorSlice.test.js
@@ -1,12 +1,3 @@
-import {
- createOrUpdateProject,
- createRemix,
- deleteProject,
- loadAssets,
- readProject,
- readProjectList,
-} from "../utils/apiCallHandler";
-
import reducer, {
syncProject,
stopCodeRun,
@@ -16,14 +7,25 @@ import reducer, {
closeFile,
setFocussedFileIndex,
updateComponentName,
- loadProjectList,
setLoadRemixDisabled,
setIsOutputOnly,
setErrorDetails,
setReadOnly,
} from "./EditorSlice";
-jest.mock("../utils/apiCallHandler");
+const mockCreateRemix = jest.fn();
+const mockDeleteProject = jest.fn();
+const mockLoadAssets = jest.fn();
+const mockReadProject = jest.fn();
+const mockCreateOrUpdateProject = jest.fn();
+
+jest.mock("../utils/apiCallHandler", () => () => ({
+ createRemix: jest.fn(mockCreateRemix),
+ deleteProject: jest.fn(mockDeleteProject),
+ loadAssets: jest.fn(mockLoadAssets),
+ readProject: jest.fn(mockReadProject),
+ createOrUpdateProject: jest.fn(mockCreateOrUpdateProject),
+}));
test("Action stopCodeRun sets codeRunStopped to true", () => {
const previousState = {
@@ -191,11 +193,14 @@ describe("When project has no identifier", () => {
test("Saving creates new project", async () => {
await saveAction(dispatch, () => initialState);
- expect(createOrUpdateProject).toHaveBeenCalledWith(project, access_token);
+ expect(mockCreateOrUpdateProject).toHaveBeenCalledWith(
+ project,
+ access_token,
+ );
});
test("Successfully creating project triggers fulfilled action", async () => {
- createOrUpdateProject.mockImplementationOnce(() =>
+ mockCreateOrUpdateProject.mockImplementationOnce(() =>
Promise.resolve({ status: 200 }),
);
await saveAction(dispatch, () => initialState);
@@ -284,11 +289,14 @@ describe("When project has an identifier", () => {
test("Saving updates existing project", async () => {
await saveAction(dispatch, () => initialState);
- expect(createOrUpdateProject).toHaveBeenCalledWith(project, access_token);
+ expect(mockCreateOrUpdateProject).toHaveBeenCalledWith(
+ project,
+ access_token,
+ );
});
test("Successfully updating project triggers fulfilled action", async () => {
- createOrUpdateProject.mockImplementationOnce(() =>
+ mockCreateOrUpdateProject.mockImplementationOnce(() =>
Promise.resolve({ status: 200 }),
);
await saveAction(dispatch, () => initialState);
@@ -316,11 +324,13 @@ describe("When project has an identifier", () => {
test("Remixing triggers createRemix API call", async () => {
await remixAction(dispatch, () => initialState);
- expect(createRemix).toHaveBeenCalledWith(project, access_token);
+ expect(mockCreateRemix).toHaveBeenCalledWith(project, access_token);
});
test("Successfully remixing project triggers fulfilled action", async () => {
- createRemix.mockImplementationOnce(() => Promise.resolve({ status: 200 }));
+ mockCreateRemix.mockImplementationOnce(() =>
+ Promise.resolve({ status: 200 }),
+ );
await remixAction(dispatch, () => initialState);
expect(dispatch.mock.calls[1][0].type).toBe(
"editor/remixProject/fulfilled",
@@ -396,7 +406,6 @@ describe("When deleting a project", () => {
editor: {
project: {},
modals: { deleteProject: project },
- deleteProjectModalShowing: true,
projectListLoaded: "success",
},
auth: { user: { access_token } },
@@ -415,14 +424,14 @@ describe("When deleting a project", () => {
test("Deleting a project triggers deleteProject API call", async () => {
await deleteAction(dispatch, () => initialState);
- expect(deleteProject).toHaveBeenCalledWith(
+ expect(mockDeleteProject).toHaveBeenCalledWith(
project.identifier,
access_token,
);
});
test("Successfully deleting project triggers fulfilled action", async () => {
- deleteProject.mockImplementationOnce(() =>
+ mockDeleteProject.mockImplementationOnce(() =>
Promise.resolve({ status: 200 }),
);
await deleteAction(dispatch, () => initialState);
@@ -435,7 +444,6 @@ describe("When deleting a project", () => {
const expectedState = {
project: {},
modals: { deleteProject: null },
- deleteProjectModalShowing: false,
projectListLoaded: "idle",
};
expect(reducer(initialState.editor, deleteThunk.fulfilled({}))).toEqual(
@@ -444,87 +452,6 @@ describe("When deleting a project", () => {
});
});
-describe("When requesting project list", () => {
- const dispatch = jest.fn();
- const projects = [{ name: "project1" }, { name: "project2" }];
- const initialState = {
- projectList: [],
- projectListLoaded: "pending",
- projectIndexCurrentPage: 4,
- };
- let loadProjectListThunk;
-
- beforeEach(() => {
- loadProjectListThunk = loadProjectList({
- page: 12,
- accessToken: "access_token",
- });
- });
-
- test("Loading project list triggers loadProjectList API call", async () => {
- await loadProjectListThunk(dispatch, () => initialState);
- expect(readProjectList).toHaveBeenCalledWith(12, "access_token");
- });
-
- test("Successfully loading project list triggers fulfilled action", async () => {
- readProjectList.mockImplementationOnce(() =>
- Promise.resolve({ status: 200, headers: {} }),
- );
- await loadProjectListThunk(dispatch, () => initialState);
- expect(dispatch.mock.calls[1][0].type).toBe(
- "editor/loadProjectList/fulfilled",
- );
- });
-
- test("The loadProjectList/fulfilled action with projects returned sets the projectList and total pages", () => {
- const expectedState = {
- projectList: projects,
- projectListLoaded: "success",
- projectIndexCurrentPage: 4,
- projectIndexTotalPages: 12,
- };
- expect(
- reducer(
- initialState,
- loadProjectList.fulfilled({
- projects,
- page: 4,
- links: { last: { page: 12 } },
- }),
- ),
- ).toEqual(expectedState);
- });
-
- test("The loadProjectList/fulfilled action with no projects loads previous page", () => {
- const expectedState = {
- projectList: [],
- projectListLoaded: "idle",
- projectIndexCurrentPage: 3,
- };
- expect(
- reducer(
- initialState,
- loadProjectList.fulfilled({ projects: [], page: 4 }),
- ),
- ).toEqual(expectedState);
- });
-
- test("The loadProjectList/fulfilled action with no projects on page 1 sets loading to success", () => {
- const expectedState = {
- projectList: [],
- projectListLoaded: "success",
- projectIndexCurrentPage: 1,
- projectIndexTotalPages: 1,
- };
- expect(
- reducer(
- { ...initialState, projectIndexCurrentPage: 1 },
- loadProjectList.fulfilled({ projects: [], page: 1 }),
- ),
- ).toEqual(expectedState);
- });
-});
-
describe("Opening files", () => {
const initialState = {
openFiles: [["main.py", "file1.py"]],
@@ -689,7 +616,11 @@ describe("Loading a project", () => {
test("readProject is called", async () => {
await loadAction(dispatch, () => initialState);
- expect(readProject).toHaveBeenCalledWith(identifier, locale, accessToken);
+ expect(mockReadProject).toHaveBeenCalledWith(
+ identifier,
+ locale,
+ accessToken,
+ );
});
});
@@ -707,7 +638,11 @@ describe("Loading a project", () => {
test("loadAssets is called", async () => {
await loadAction(dispatch, () => initialState);
- expect(loadAssets).toHaveBeenCalledWith(identifier, locale, accessToken);
+ expect(mockLoadAssets).toHaveBeenCalledWith(
+ identifier,
+ locale,
+ accessToken,
+ );
});
});
});
diff --git a/src/redux/reducers/loadProjectReducers.js b/src/redux/reducers/loadProjectReducers.js
index dbfdcf36e..5bf49d54e 100644
--- a/src/redux/reducers/loadProjectReducers.js
+++ b/src/redux/reducers/loadProjectReducers.js
@@ -1,7 +1,6 @@
const loadProjectPending = (state, action) => {
state.loading = "pending";
state.remixLoadFailed = false; // We need to reset this at the start of any project load
- state.accessDeniedNoAuthModalShowing = false;
state.modals = {};
state.currentLoadingRequestId = action.meta.requestId;
state.lastSavedTime = null;
@@ -45,7 +44,6 @@ const loadProjectRejected = (state, action) => {
} else if (accessDeniedCodes.includes(errorCode) && accessToken) {
state.accessDeniedWithAuthModalShowing = true;
} else if (accessDeniedCodes.includes(errorCode) && !accessToken) {
- state.accessDeniedNoAuthModalShowing = true;
state.modals.accessDenied = {
identifier: action.meta.arg.identifier,
projectType: action.meta.arg.projectType,
diff --git a/src/redux/reducers/loadProjectReducers.test.js b/src/redux/reducers/loadProjectReducers.test.js
index 62ba8ad68..9b1a08b39 100644
--- a/src/redux/reducers/loadProjectReducers.test.js
+++ b/src/redux/reducers/loadProjectReducers.test.js
@@ -1,10 +1,12 @@
import produce from "immer";
-import { readProject } from "../../utils/apiCallHandler";
import reducer, { syncProject } from "../../redux/EditorSlice";
import { loadProjectRejected } from "./loadProjectReducers";
-jest.mock("../../utils/apiCallHandler");
+const mockReadProject = jest.fn();
+jest.mock("../../utils/apiCallHandler", () => () => ({
+ readProject: jest.fn(mockReadProject),
+}));
const requestingAProject = function (project, projectFile) {
const dispatch = jest.fn();
@@ -40,7 +42,7 @@ const requestingAProject = function (project, projectFile) {
test("Reads project from database", async () => {
await loadAction(dispatch, () => initialState);
- expect(readProject).toHaveBeenCalledWith(
+ expect(mockReadProject).toHaveBeenCalledWith(
"my-project-identifier",
"ja-JP",
"my_token",
@@ -241,7 +243,6 @@ describe("EditorSliceReducers::loadProjectRejectedReducer", () => {
let expectedState = {
loading: "failed",
saving: "idle",
- accessDeniedNoAuthModalShowing: true,
currentLoadingRequestId: undefined,
modals: {
accessDenied: {
diff --git a/src/utils/apiCallHandler.js b/src/utils/apiCallHandler.js
index 3294ab673..ead3394db 100644
--- a/src/utils/apiCallHandler.js
+++ b/src/utils/apiCallHandler.js
@@ -1,124 +1,144 @@
import axios from "axios";
import omit from "lodash/omit";
-const host = process.env.REACT_APP_API_ENDPOINT;
-
-const get = async (url, headers) => {
- return await axios.get(url, headers);
-};
-
-const post = async (url, body, headers) => {
- return await axios.post(url, body, headers);
-};
+const ApiCallHandler = ({ reactAppApiEndpoint }) => {
+ const host = reactAppApiEndpoint;
+
+ const get = async (url, headers) => {
+ return await axios.get(url, headers);
+ };
+
+ const post = async (url, body, headers) => {
+ return await axios.post(url, body, headers);
+ };
+
+ const put = async (url, body, headers) => {
+ return await axios.put(url, body, headers);
+ };
+
+ const headers = (accessToken) => {
+ let headersHash;
+ if (accessToken) {
+ headersHash = { Accept: "application/json", Authorization: accessToken };
+ } else {
+ headersHash = { Accept: "application/json" };
+ }
+ return { headers: headersHash };
+ };
+
+ const createOrUpdateProject = async (projectWithUserId, accessToken) => {
+ const project = omit(projectWithUserId, ["user_id"]);
+ if (!project.identifier) {
+ return await post(
+ `${host}/api/projects`,
+ { project },
+ headers(accessToken),
+ );
+ } else {
+ return await put(
+ `${host}/api/projects/${project.identifier}`,
+ { project },
+ headers(accessToken),
+ );
+ }
+ };
+
+ const deleteProject = async (identifier, accessToken) => {
+ return await axios.delete(
+ `${host}/api/projects/${identifier}`,
+ headers(accessToken),
+ );
+ };
-const put = async (url, body, headers) => {
- return await axios.put(url, body, headers);
-};
+ const getImage = async (url) => {
+ return await get(url, headers());
+ };
-const headers = (accessToken) => {
- let headersHash;
- if (accessToken) {
- headersHash = { Accept: "application/json", Authorization: accessToken };
- } else {
- headersHash = { Accept: "application/json" };
- }
- return { headers: headersHash };
-};
+ const loadRemix = async (projectIdentifier, accessToken) => {
+ return await get(
+ `${host}/api/projects/${projectIdentifier}/remix`,
+ headers(accessToken),
+ );
+ };
-export const createOrUpdateProject = async (projectWithUserId, accessToken) => {
- const project = omit(projectWithUserId, ["user_id"]);
- if (!project.identifier) {
+ const createRemix = async (project, accessToken) => {
return await post(
- `${host}/api/projects`,
+ `${host}/api/projects/${project.identifier}/remix`,
{ project },
headers(accessToken),
);
- } else {
- return await put(
- `${host}/api/projects/${project.identifier}`,
- { project },
+ };
+
+ const readProject = async (projectIdentifier, locale, accessToken) => {
+ const queryString = locale ? `?locale=${locale}` : "";
+ return await get(
+ `${host}/api/projects/${projectIdentifier}${queryString}`,
headers(accessToken),
);
- }
-};
-
-export const deleteProject = async (identifier, accessToken) => {
- return await axios.delete(
- `${host}/api/projects/${identifier}`,
- headers(accessToken),
- );
-};
+ };
-export const getImage = async (url) => {
- return await get(url, headers());
-};
-
-export const loadRemix = async (projectIdentifier, accessToken) => {
- return await get(
- `${host}/api/projects/${projectIdentifier}/remix`,
- headers(accessToken),
- );
-};
-
-export const createRemix = async (project, accessToken) => {
- return await post(
- `${host}/api/projects/${project.identifier}/remix`,
- { project },
- headers(accessToken),
- );
-};
-
-export const readProject = async (projectIdentifier, locale, accessToken) => {
- const queryString = locale ? `?locale=${locale}` : "";
- return await get(
- `${host}/api/projects/${projectIdentifier}${queryString}`,
- headers(accessToken),
- );
-};
-
-export const loadAssets = async (assetsIdentifier, locale, accessToken) => {
- const queryString = locale ? `?locale=${locale}` : "";
- return await get(
- `${host}/api/projects/${assetsIdentifier}/images${queryString}`,
- headers(accessToken),
- );
-};
+ const loadAssets = async (assetsIdentifier, locale, accessToken) => {
+ const queryString = locale ? `?locale=${locale}` : "";
+ return await get(
+ `${host}/api/projects/${assetsIdentifier}/images${queryString}`,
+ headers(accessToken),
+ );
+ };
-export const readProjectList = async (page, accessToken) => {
- return await get(`${host}/api/projects`, {
- params: { page },
- ...headers(accessToken),
- });
-};
+ const readProjectList = async (page, accessToken) => {
+ return await get(`${host}/api/projects`, {
+ params: { page },
+ ...headers(accessToken),
+ });
+ };
-export const uploadImages = async (projectIdentifier, accessToken, images) => {
- var formData = new FormData();
+ const uploadImages = async (projectIdentifier, accessToken, images) => {
+ var formData = new FormData();
- images.forEach((image) => {
- formData.append("images[]", image, image.name);
- });
+ images.forEach((image) => {
+ formData.append("images[]", image, image.name);
+ });
- return await post(
- `${host}/api/projects/${projectIdentifier}/images`,
- formData,
- { ...headers(accessToken), "Content-Type": "multipart/form-data" },
- );
+ return await post(
+ `${host}/api/projects/${projectIdentifier}/images`,
+ formData,
+ { ...headers(accessToken), "Content-Type": "multipart/form-data" },
+ );
+ };
+
+ const createError = async (
+ projectIdentifier,
+ userId,
+ error,
+ sendError = false,
+ ) => {
+ if (!sendError) {
+ return;
+ }
+ const { errorMessage, errorType } = error;
+ return await post(`${host}/api/project_errors`, {
+ error: errorMessage,
+ error_type: errorType,
+ project_id: projectIdentifier,
+ user_id: userId,
+ });
+ };
+
+ return {
+ get,
+ post,
+ put,
+ createOrUpdateProject,
+ deleteProject,
+ getImage,
+ loadRemix,
+ createRemix,
+ readProject,
+ loadAssets,
+ readProjectList,
+ uploadImages,
+ createError,
+ };
};
-export const createError = async (
- projectIdentifier,
- userId,
- error,
- sendError = false,
-) => {
- if (!sendError) {
- return;
- }
- const { errorMessage, errorType } = error;
- return await post(`${host}/api/project_errors`, {
- error: errorMessage,
- error_type: errorType,
- project_id: projectIdentifier,
- user_id: userId,
- });
-};
+export default ApiCallHandler;
diff --git a/src/utils/apiCallHandler.test.js b/src/utils/apiCallHandler.test.js
index 5b0206ea2..f05b2992d 100644
--- a/src/utils/apiCallHandler.test.js
+++ b/src/utils/apiCallHandler.test.js
@@ -1,6 +1,16 @@
import axios from "axios";
-import {
+import ApiCallHandler from "./apiCallHandler";
+
+jest.mock("axios");
+const host = "http://localhost:3009";
+const defaultHeaders = { headers: { Accept: "application/json" } };
+const accessToken = "39a09671-be55-4847-baf5-8919a0c24a25";
+const authHeaders = {
+ headers: { Accept: "application/json", Authorization: accessToken },
+};
+
+const {
getImage,
createOrUpdateProject,
readProject,
@@ -9,15 +19,7 @@ import {
uploadImages,
readProjectList,
createError,
-} from "./apiCallHandler";
-
-jest.mock("axios");
-const host = process.env.REACT_APP_API_ENDPOINT;
-const defaultHeaders = { headers: { Accept: "application/json" } };
-const accessToken = "39a09671-be55-4847-baf5-8919a0c24a25";
-const authHeaders = {
- headers: { Accept: "application/json", Authorization: accessToken },
-};
+} = ApiCallHandler({ reactAppApiEndpoint: host });
describe("Testing project API calls", () => {
test("Creating project", async () => {
diff --git a/src/utils/i18n.js b/src/utils/i18n.js
index d020738ce..c044d0f05 100644
--- a/src/utils/i18n.js
+++ b/src/utils/i18n.js
@@ -218,16 +218,6 @@ i18n
py5: "Py5: imported mode",
},
},
- loginToSaveModal: {
- cancel: "Cancel",
- downloadButtonText: "Download",
- downloadText:
- "Or you can download your project and save it on your computer.",
- heading: "Save your project",
- loginButtonText: "Log in to save",
- loginText:
- "Log in to your Raspberry Pi account to save your work, and you'll be able to access and edit your project whenever you need to.",
- },
mobile: {
code: "Code",
menu: "Menu",
@@ -238,21 +228,6 @@ i18n
modals: {
close: "Close",
},
- newProjectModal: {
- cancel: "Cancel",
- createProject: "Create project",
- heading: "Create a new project",
- projectName: {
- default: "Untitled",
- helpText: "You can always rename your project later",
- inputLabel: "Project name",
- },
- projectType: {
- html: "HTML",
- inputLabel: "What kind of project do you want to make?",
- python: "Python",
- },
- },
notifications: {
close: "close",
loginPrompt:
@@ -299,16 +274,6 @@ i18n
buttonSplitTitle: "Split view",
},
project: {
- accessDeniedNoAuthModal: {
- embedded: {
- text: "Visit the Projects site for cool project ideas",
- },
- heading: "You are not able to see this project",
- loginButtonText: "Log in to your account",
- newProject: "Create a new code project",
- projectsSiteLinkText: "Explore Projects site",
- text: "If this is your project, log in to see it. If this is not your project you can visit the Projects site for cool project ideas or to start coding in a new project.",
- },
accessDeniedWithAuthModal: {
embedded: {
text: "Visit the Projects site for cool project ideas",
@@ -338,12 +303,6 @@ i18n
projectList: {
delete: "Delete",
deleteLabel: "Delete project",
- deleteProjectModal: {
- cancel: "Cancel",
- delete: "Delete",
- heading: "Delete project",
- text: "Are you sure you want to delete this project?",
- },
empty: "No projects created yet",
label: "Open project menu",
loading: "Loading",
diff --git a/src/utils/login.js b/src/utils/login.js
deleted file mode 100644
index aec78c36a..000000000
--- a/src/utils/login.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import userManager from "./userManager";
-
-export const login = ({
- project,
- location,
- triggerSave,
- accessDeniedData,
- loginRedirect,
-} = {}) => {
- if (window.plausible) {
- window.plausible("Login button");
- }
-
- if (accessDeniedData) {
- localStorage.setItem(
- "location",
- `/projects/${accessDeniedData.identifier}`,
- );
- } else {
- localStorage.setItem("location", loginRedirect || location.pathname);
- if (project) {
- localStorage.setItem(
- project.identifier || "project",
- JSON.stringify(project),
- );
- }
- }
- if (triggerSave) {
- localStorage.setItem("awaitingSave", "true");
- }
- userManager.signinRedirect();
-};
diff --git a/src/utils/sentry.js b/src/utils/sentry.js
index 04c51570c..e9f23505c 100644
--- a/src/utils/sentry.js
+++ b/src/utils/sentry.js
@@ -21,7 +21,7 @@ Sentry.init({
createRoutesFromChildren,
matchRoutes,
),
- tracePropagationTargets: [process.env.REACT_APP_API_ENDPOINT, /\//],
+ tracePropagationTargets: [/\//],
}),
],
environment: process.env.REACT_APP_SENTRY_ENV,
diff --git a/src/utils/userManager.js b/src/utils/userManager.js
index 59eefc9cd..210a56764 100644
--- a/src/utils/userManager.js
+++ b/src/utils/userManager.js
@@ -5,13 +5,13 @@ const host = `${window.location.protocol}//${window.location.hostname}${
window.location.port ? `:${window.location.port}` : ""
}`;
-const userManagerConfig = {
+const userManagerConfig = ({ reactAppAuthenticationUrl }) => ({
client_id: process.env.REACT_APP_AUTHENTICATION_CLIENT_ID,
redirect_uri: `${host}/auth/callback`,
post_logout_redirect_uri: host,
response_type: "code",
scope: "openid email profile force-consent allow-u13-login roles",
- authority: process.env.REACT_APP_AUTHENTICATION_URL,
+ authority: reactAppAuthenticationUrl,
silent_redirect_uri: `${host}/auth/silent_renew`,
automaticSilentRenew: true,
filterProtocolClaims: false,
@@ -22,13 +22,21 @@ const userManagerConfig = {
brand: "editor",
login_options: "v1_signup",
},
-};
+});
-const userManager = createUserManager(userManagerConfig);
+const UserManager = ({ reactAppAuthenticationUrl }) => {
+ const mgr = createUserManager(
+ userManagerConfig({
+ reactAppAuthenticationUrl,
+ }),
+ );
-userManager.events.addAccessTokenExpired(() => {
- // If the token has expired, trigger the silent renew process
- userManager.signinSilent();
-});
+ mgr.events.addAccessTokenExpired(() => {
+ // If the token has expired, trigger the silent renew process
+ UserManager.signinSilent();
+ });
+
+ return mgr;
+};
-export default userManager;
+export default UserManager;
diff --git a/src/web-component.js b/src/web-component.js
index 8ad70f5f1..9a8006752 100644
--- a/src/web-component.js
+++ b/src/web-component.js
@@ -46,26 +46,27 @@ class WebComponent extends HTMLElement {
static get observedAttributes() {
return [
- "host_styles",
"assets_identifier",
"auth_key",
- "identifier",
"code",
- "sense_hat_always_enabled",
+ "embedded",
+ "host_styles",
+ "identifier",
"instructions",
- "with_projectbar",
- "project_name_editable",
- "with_sidebar",
- "read_only",
+ "load_remix_disabled",
"output_only",
"output_panels",
+ "output_split_view",
+ "project_name_editable",
+ "react_app_api_endpoint",
+ "read_only",
+ "sense_hat_always_enabled",
+ "show_save_prompt",
"sidebar_options",
"theme",
- "embedded",
- "show_save_prompt",
- "load_remix_disabled",
- "output_split_view",
"use_editor_styles",
+ "with_projectbar",
+ "with_sidebar",
];
}
@@ -74,17 +75,17 @@ class WebComponent extends HTMLElement {
if (
[
- "sense_hat_always_enabled",
- "with_sidebar",
- "with_projectbar",
- "project_name_editable",
- "show_save_prompt",
+ "embedded",
"load_remix_disabled",
"output_only",
- "embedded",
"output_split_view",
- "use_editor_styles",
+ "project_name_editable",
"read_only",
+ "sense_hat_always_enabled",
+ "show_save_prompt",
+ "use_editor_styles",
+ "with_projectbar",
+ "with_sidebar",
].includes(name)
) {
value = newVal !== "false";