Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 37 additions & 15 deletions src/components/Menus/Sidebar/Sidebar.jsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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(
Expand All @@ -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;

Expand Down Expand Up @@ -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);
}
};

Expand Down
110 changes: 101 additions & 9 deletions src/components/Menus/Sidebar/Sidebar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
{
Expand All @@ -24,10 +25,27 @@ const optionsWithDownload = [
"info",
];

const renderSidebarWithState = (initialState, props = {}) => {
const mockStore = configureStore([]);
const store = mockStore(initialState);

return {
store,
...render(
<Provider store={store}>
<div id="app">
<Sidebar options={options} {...props} />
</div>
</Provider>,
),
};
};

describe("When project has images", () => {
describe("and no instructions", () => {
let store;

beforeEach(() => {
const mockStore = configureStore([]);
const initialState = {
editor: {
project: {
Expand All @@ -37,14 +55,7 @@ describe("When project has images", () => {
},
instructions: {},
};
const store = mockStore(initialState);
render(
<Provider store={store}>
<div id="app">
<Sidebar options={options} />
</div>
</Provider>,
);
({ store } = renderSidebarWithState(initialState));
});

test("Clicking expand opens the file pane", () => {
Expand Down Expand Up @@ -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")]);
});
});

Expand Down Expand Up @@ -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(
<Provider store={mockStore(nextState)}>
<div id="app">
<Sidebar options={["file", "instructions"]} />
</div>
</Provider>,
);

expect(
screen.queryByText("instructionsPanel.projectSteps"),
).toBeInTheDocument();
expect(screen.queryByText("filePanel.files")).not.toBeInTheDocument();
});
});

describe("When plugins are provided", () => {
const initialState = {
editor: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ let store;
const renderWebComponentProject = ({
projectType,
instructions,
imageList = [],
permitOverride = true,
loading,
codeRunTriggered = false,
Expand All @@ -38,7 +39,7 @@ const renderWebComponentProject = ({
components: [
{ name: "main", extension: "py", content: "print('hello')" },
],
image_list: [],
image_list: imageList,
instructions,
},
loading,
Expand All @@ -55,7 +56,7 @@ const renderWebComponentProject = ({
};
store = mockStore(initialState);

render(
return render(
<Provider store={store}>
<WebComponentProject {...props} />
</Provider>,
Expand Down
5 changes: 5 additions & 0 deletions src/redux/EditorSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export const editorInitialState = {
newFileModalShowing: false,
renameFileModalShowing: false,
sidebarShowing: true,
selectedSidebarOption: undefined,
modals: {},
errorDetails: {},
runnerBeingLoaded: null | "pyodide" | "skulpt",
Expand Down Expand Up @@ -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;
},
Expand Down Expand Up @@ -495,6 +499,7 @@ export const {
closeRenameFileModal,
showSidebar,
hideSidebar,
setSidebarOption,
disableTheming,
setErrorDetails,
} = EditorSlice.actions;
Expand Down
Loading