diff --git a/src/components/Menus/Sidebar/Sidebar.jsx b/src/components/Menus/Sidebar/Sidebar.jsx
index 994b5ed87..9dcdfde65 100644
--- a/src/components/Menus/Sidebar/Sidebar.jsx
+++ b/src/components/Menus/Sidebar/Sidebar.jsx
@@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import classNames from "classnames";
-import { useSelector } from "react-redux";
+import { useDispatch, useSelector } from "react-redux";
import { useMediaQuery } from "react-responsive";
import FilePanel from "./FilePanel/FilePanel";
@@ -24,9 +24,11 @@ import FileIcon from "../../../utils/FileIcon";
import DownloadPanel from "./DownloadPanel/DownloadPanel";
import InstructionsPanel from "./InstructionsPanel/InstructionsPanel";
import SidebarPanel from "./SidebarPanel";
+import { setSidebarOption } from "../../../redux/EditorSlice";
const Sidebar = ({ options = [], plugins = [], allowMobileView = true }) => {
const { t } = useTranslation();
+ const dispatch = useDispatch();
const projectType = useSelector((state) => state.editor.project.project_type);
const projectImages = useSelector((state) => state.editor.project.image_list);
const instructionsSteps = useSelector(
@@ -35,6 +37,9 @@ const Sidebar = ({ options = [], plugins = [], allowMobileView = true }) => {
const instructionsEditable = useSelector(
(state) => state.editor.instructionsEditable,
);
+ const selectedSidebarOption = useSelector(
+ (state) => state.editor.selectedSidebarOption,
+ );
const viewportIsMobile = useMediaQuery({ query: MOBILE_MEDIA_QUERY });
const isMobile = allowMobileView && viewportIsMobile;
@@ -134,28 +139,45 @@ const Sidebar = ({ options = [], plugins = [], allowMobileView = true }) => {
}
const autoOpenPlugin = plugins?.find((plugin) => plugin.autoOpen);
-
- const [option, setOption] = useState(
- autoOpenPlugin
- ? autoOpenPlugin.name
- : instructionsEditable || instructionsSteps
- ? "instructions"
- : "file",
- );
-
const hasInstructions = instructionsSteps && instructionsSteps.length > 0;
+ let defaultOption = "file";
+ if (autoOpenPlugin) {
+ defaultOption = autoOpenPlugin.name;
+ } else if (instructionsEditable || hasInstructions) {
+ defaultOption = "instructions";
+ }
+ const defaultOptionIsAvailable = menuOptions.some(
+ (menuOption) => menuOption.name === defaultOption,
+ );
+ const nextDefaultOption = defaultOptionIsAvailable ? defaultOption : null;
+ const initialOption =
+ selectedSidebarOption === undefined
+ ? nextDefaultOption
+ : selectedSidebarOption;
+ const [option, setOption] = useState(initialOption);
+ const optionIsAvailable =
+ option === null ||
+ menuOptions.some((menuOption) => menuOption.name === option);
useEffect(() => {
- if (!autoOpenPlugin && (instructionsEditable || hasInstructions)) {
- setOption("instructions");
+ if (selectedSidebarOption === undefined) {
+ setOption(nextDefaultOption);
+ } else if (!optionIsAvailable) {
+ setOption(nextDefaultOption);
+ dispatch(setSidebarOption(nextDefaultOption));
}
- }, [autoOpenPlugin, instructionsEditable, hasInstructions]);
+ }, [dispatch, nextDefaultOption, optionIsAvailable, selectedSidebarOption]);
+
+ const updateOption = (nextOption) => {
+ setOption(nextOption);
+ dispatch(setSidebarOption(nextOption));
+ };
const toggleOption = (newOption) => {
if (option !== newOption) {
- setOption(newOption);
+ updateOption(newOption);
} else if (!isMobile) {
- setOption(null);
+ updateOption(null);
}
};
diff --git a/src/components/Menus/Sidebar/Sidebar.test.js b/src/components/Menus/Sidebar/Sidebar.test.js
index 5e28ab817..7b26990d0 100644
--- a/src/components/Menus/Sidebar/Sidebar.test.js
+++ b/src/components/Menus/Sidebar/Sidebar.test.js
@@ -3,6 +3,7 @@ import { fireEvent, render, screen } from "@testing-library/react";
import Sidebar from "./Sidebar";
import configureStore from "redux-mock-store";
import { Provider } from "react-redux";
+import { setSidebarOption } from "../../../redux/EditorSlice";
let images = [
{
@@ -24,10 +25,27 @@ const optionsWithDownload = [
"info",
];
+const renderSidebarWithState = (initialState, props = {}) => {
+ const mockStore = configureStore([]);
+ const store = mockStore(initialState);
+
+ return {
+ store,
+ ...render(
+
+
+
+
+ ,
+ ),
+ };
+};
+
describe("When project has images", () => {
describe("and no instructions", () => {
+ let store;
+
beforeEach(() => {
- const mockStore = configureStore([]);
const initialState = {
editor: {
project: {
@@ -37,14 +55,7 @@ describe("When project has images", () => {
},
instructions: {},
};
- const store = mockStore(initialState);
- render(
-
-
-
-
- ,
- );
+ ({ store } = renderSidebarWithState(initialState));
});
test("Clicking expand opens the file pane", () => {
@@ -104,6 +115,7 @@ describe("When project has images", () => {
const imageButton = screen.getByTitle("sidebar.images");
fireEvent.click(imageButton);
expect(screen.queryByText("imagePanel.gallery")).toBeInTheDocument();
+ expect(store.getActions()).toEqual([setSidebarOption("images")]);
});
});
@@ -324,6 +336,86 @@ describe("When the project has no instructions", () => {
});
});
+describe("When sidebar state is persisted", () => {
+ test("Renders the stored sidebar state", () => {
+ const initialState = {
+ editor: {
+ project: {
+ components: [],
+ image_list: images,
+ },
+ selectedSidebarOption: "images",
+ },
+ instructions: {},
+ };
+ const { unmount } = renderSidebarWithState(initialState);
+
+ expect(screen.queryByText("imagePanel.gallery")).toBeInTheDocument();
+
+ unmount();
+
+ renderSidebarWithState({
+ editor: {
+ project: {
+ components: [],
+ image_list: images,
+ },
+ selectedSidebarOption: null,
+ },
+ instructions: {},
+ });
+
+ expect(screen.queryByTitle("sidebar.expand")).toBeInTheDocument();
+ expect(screen.queryByText("filePanel.files")).not.toBeInTheDocument();
+ });
+});
+
+describe("When no sidebar option is stored", () => {
+ const mockStore = configureStore([]);
+
+ test("Switches to a new default option after mount", () => {
+ const initialState = {
+ editor: {
+ project: {
+ components: [],
+ image_list: [],
+ },
+ instructionsEditable: false,
+ },
+ instructions: {},
+ };
+ const nextState = {
+ editor: {
+ project: {
+ components: [],
+ image_list: [],
+ },
+ instructionsEditable: true,
+ },
+ instructions: {},
+ };
+
+ const { rerender } = renderSidebarWithState(initialState, {
+ options: ["file", "instructions"],
+ });
+
+ expect(screen.queryByText("filePanel.files")).toBeInTheDocument();
+
+ rerender(
+
+
+
+
+ ,
+ );
+
+ expect(
+ screen.queryByText("instructionsPanel.projectSteps"),
+ ).toBeInTheDocument();
+ expect(screen.queryByText("filePanel.files")).not.toBeInTheDocument();
+ });
+});
+
describe("When plugins are provided", () => {
const initialState = {
editor: {
diff --git a/src/components/WebComponentProject/WebComponentProject.test.js b/src/components/WebComponentProject/WebComponentProject.test.js
index 3c7800a58..a673d6255 100644
--- a/src/components/WebComponentProject/WebComponentProject.test.js
+++ b/src/components/WebComponentProject/WebComponentProject.test.js
@@ -23,6 +23,7 @@ let store;
const renderWebComponentProject = ({
projectType,
instructions,
+ imageList = [],
permitOverride = true,
loading,
codeRunTriggered = false,
@@ -38,7 +39,7 @@ const renderWebComponentProject = ({
components: [
{ name: "main", extension: "py", content: "print('hello')" },
],
- image_list: [],
+ image_list: imageList,
instructions,
},
loading,
@@ -55,7 +56,7 @@ const renderWebComponentProject = ({
};
store = mockStore(initialState);
- render(
+ return render(
,
diff --git a/src/redux/EditorSlice.js b/src/redux/EditorSlice.js
index c3ac922cd..096189265 100644
--- a/src/redux/EditorSlice.js
+++ b/src/redux/EditorSlice.js
@@ -119,6 +119,7 @@ export const editorInitialState = {
newFileModalShowing: false,
renameFileModalShowing: false,
sidebarShowing: true,
+ selectedSidebarOption: undefined,
modals: {},
errorDetails: {},
runnerBeingLoaded: null | "pyodide" | "skulpt",
@@ -374,6 +375,9 @@ export const EditorSlice = createSlice({
hideSidebar: (state) => {
state.sidebarShowing = false;
},
+ setSidebarOption: (state, action) => {
+ state.selectedSidebarOption = action.payload;
+ },
disableTheming: (state) => {
state.isThemeable = false;
},
@@ -495,6 +499,7 @@ export const {
closeRenameFileModal,
showSidebar,
hideSidebar,
+ setSidebarOption,
disableTheming,
setErrorDetails,
} = EditorSlice.actions;