Skip to content

Commit 8608e32

Browse files
authored
Plugin system for sidebar panels (#1251)
1 parent fffd3e9 commit 8608e32

File tree

8 files changed

+145
-8
lines changed

8 files changed

+145
-8
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
## Unreleased
88

9+
### Added
10+
- Sidebar plugin functionality (#1251)
11+
912
### Changed
1013

1114
- Changed `set_pixel` to quantise the colour before writing (#1247)

src/components/Editor/Project/Project.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const Project = (props) => {
2222
withProjectbar = true,
2323
withSidebar = true,
2424
sidebarOptions = [],
25+
sidebarPlugins = [],
2526
} = props;
2627
const saving = useSelector((state) => state.editor.saving);
2728
const autosave = useSelector((state) => state.editor.lastSaveAutosave);
@@ -59,7 +60,9 @@ const Project = (props) => {
5960
"proj-container--wc": webComponent,
6061
})}
6162
>
62-
{withSidebar && <Sidebar options={sidebarOptions} />}
63+
{withSidebar && (
64+
<Sidebar options={sidebarOptions} plugins={sidebarPlugins} />
65+
)}
6366
<div className="project-wrapper" ref={containerRef}>
6467
{withProjectbar && <ProjectBar nameEditable={nameEditable} />}
6568
{!loading && (

src/components/Menus/Sidebar/Sidebar.jsx

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ import { MOBILE_MEDIA_QUERY } from "../../../utils/mediaQueryBreakpoints";
2323
import FileIcon from "../../../utils/FileIcon";
2424
import DownloadPanel from "./DownloadPanel/DownloadPanel";
2525
import InstructionsPanel from "./InstructionsPanel/InstructionsPanel";
26+
import SidebarPanel from "./SidebarPanel";
2627

27-
const Sidebar = ({ options = [] }) => {
28+
const Sidebar = ({ options = [], plugins = [] }) => {
2829
const { t } = useTranslation();
2930

3031
let menuOptions = [
@@ -79,6 +80,22 @@ const Sidebar = ({ options = [] }) => {
7980
},
8081
].filter((option) => options.includes(option.name));
8182

83+
let pluginMenuOptions = plugins.map((plugin) => {
84+
return {
85+
name: plugin.name,
86+
icon: plugin.icon,
87+
title: plugin.title,
88+
position: plugin.position || "top",
89+
panel: () => (
90+
<SidebarPanel heading={plugin.heading} Button={plugin.button}>
91+
{plugin.panel()}
92+
</SidebarPanel>
93+
),
94+
};
95+
});
96+
97+
menuOptions = [...menuOptions, ...pluginMenuOptions];
98+
8299
const isMobile = useMediaQuery({ query: MOBILE_MEDIA_QUERY });
83100
const projectImages = useSelector((state) => state.editor.project.image_list);
84101
const instructionsSteps = useSelector(
@@ -103,17 +120,23 @@ const Sidebar = ({ options = [] }) => {
103120
removeOption("instructions", instructionsSteps);
104121
}
105122

123+
const autoOpenPlugin = plugins?.find((plugin) => plugin.autoOpen);
124+
106125
const [option, setOption] = useState(
107-
instructionsEditable || instructionsSteps ? "instructions" : "file",
126+
autoOpenPlugin
127+
? autoOpenPlugin.name
128+
: instructionsEditable || instructionsSteps
129+
? "instructions"
130+
: "file",
108131
);
109132

110133
const hasInstructions = instructionsSteps && instructionsSteps.length > 0;
111134

112135
useEffect(() => {
113-
if (instructionsEditable || hasInstructions) {
136+
if (!autoOpenPlugin && (instructionsEditable || hasInstructions)) {
114137
setOption("instructions");
115138
}
116-
}, [instructionsEditable, hasInstructions]);
139+
}, [autoOpenPlugin, instructionsEditable, hasInstructions]);
117140

118141
const toggleOption = (newOption) => {
119142
if (option !== newOption) {

src/components/Menus/Sidebar/Sidebar.test.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,3 +316,93 @@ describe("When the project has no instructions", () => {
316316
});
317317
});
318318
});
319+
320+
describe("When plugins are provided", () => {
321+
const initialState = {
322+
editor: {
323+
project: {
324+
components: [],
325+
image_list: [],
326+
},
327+
instructionsEditable: false,
328+
},
329+
instructions: {},
330+
};
331+
const mockStore = configureStore([]);
332+
const store = mockStore(initialState);
333+
const defaultPlugin = {
334+
name: "my-amazing-plugin",
335+
icon: () => {},
336+
heading: "My amazing plugin",
337+
title: "my amazing plugin",
338+
panel: () => <p>My amazing content</p>,
339+
};
340+
341+
describe("when plugin has autoOpen true", () => {
342+
beforeEach(() => {
343+
const plugins = [
344+
{
345+
...defaultPlugin,
346+
autoOpen: true,
347+
},
348+
];
349+
render(
350+
<Provider store={store}>
351+
<div id="app">
352+
<Sidebar options={options} plugins={plugins} />
353+
</div>
354+
</Provider>,
355+
);
356+
});
357+
358+
test("Shows plugin icon", () => {
359+
expect(screen.queryByTitle("my amazing plugin")).toBeInTheDocument();
360+
});
361+
362+
test("Render the plugin panel open by default", () => {
363+
expect(screen.queryByText("My amazing plugin")).toBeInTheDocument();
364+
});
365+
366+
test("Renders the plugin content", () => {
367+
expect(screen.queryByText("My amazing content")).toBeInTheDocument();
368+
});
369+
});
370+
371+
describe("when plugin has autoOpen false", () => {
372+
beforeEach(() => {
373+
const plugins = [
374+
{
375+
...defaultPlugin,
376+
autoOpen: false,
377+
},
378+
];
379+
render(
380+
<Provider store={store}>
381+
<div id="app">
382+
<Sidebar options={options} plugins={plugins} />
383+
</div>
384+
</Provider>,
385+
);
386+
});
387+
388+
test("Shows plugin icon", () => {
389+
expect(screen.queryByTitle("my amazing plugin")).toBeInTheDocument();
390+
});
391+
392+
test("Does not render the plugin panel open by default", () => {
393+
expect(screen.queryByText("My amazing plugin")).not.toBeInTheDocument();
394+
});
395+
396+
test("Opening the panel shows the plugin heading", () => {
397+
const pluginButton = screen.getByTitle("my amazing plugin");
398+
fireEvent.click(pluginButton);
399+
expect(screen.queryByText("My amazing plugin")).toBeInTheDocument();
400+
});
401+
402+
test("Opening the panel shows the plugin content", () => {
403+
const pluginButton = screen.getByTitle("my amazing plugin");
404+
fireEvent.click(pluginButton);
405+
expect(screen.queryByText("My amazing content")).toBeInTheDocument();
406+
});
407+
});
408+
});

src/components/Mobile/MobileProject/MobileProject.jsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ import { useTranslation } from "react-i18next";
1414
import Sidebar from "../../Menus/Sidebar/Sidebar";
1515
import { showSidebar } from "../../../redux/EditorSlice";
1616

17-
const MobileProject = ({ withSidebar, sidebarOptions = [] }) => {
17+
const MobileProject = ({
18+
withSidebar,
19+
sidebarOptions = [],
20+
sidebarPlugins = [],
21+
}) => {
1822
const projectType = useSelector((state) => state.editor.project.project_type);
1923
const sidebarShowing = useSelector((state) => state.editor.sidebarShowing);
2024
const codeRunTriggered = useSelector(
@@ -48,7 +52,7 @@ const MobileProject = ({ withSidebar, sidebarOptions = [] }) => {
4852
>
4953
{withSidebar && (
5054
<TabPanel>
51-
<Sidebar options={sidebarOptions} />
55+
<Sidebar options={sidebarOptions} plugins={sidebarPlugins} />
5256
</TabPanel>
5357
)}
5458
<TabPanel>

src/components/WebComponentProject/WebComponentProject.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const WebComponentProject = ({
3535
outputOnly = false,
3636
outputPanels = ["text", "visual"],
3737
outputSplitView = false,
38+
sidebarPlugins = [],
3839
}) => {
3940
const loading = useSelector((state) => state.editor.loading);
4041
const project = useSelector((state) => state.editor.project);
@@ -145,13 +146,15 @@ const WebComponentProject = ({
145146
<MobileProject
146147
withSidebar={withSidebar}
147148
sidebarOptions={sidebarOptions}
149+
sidebarPlugins={sidebarPlugins}
148150
/>
149151
) : (
150152
<Project
151153
nameEditable={nameEditable}
152154
withProjectbar={withProjectbar}
153155
withSidebar={withSidebar}
154156
sidebarOptions={sidebarOptions}
157+
sidebarPlugins={sidebarPlugins}
155158
/>
156159
))}
157160
{outputOnly && (

src/containers/WebComponentLoader.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const WebComponentLoader = (props) => {
4747
outputOnly = false,
4848
outputPanels = ["text", "visual"],
4949
outputSplitView = false,
50+
sidebarPlugins = [],
5051
projectNameEditable = false,
5152
reactAppApiEndpoint = process.env.REACT_APP_API_ENDPOINT,
5253
readOnly = false,
@@ -227,6 +228,7 @@ const WebComponentLoader = (props) => {
227228
outputPanels={outputPanels}
228229
outputSplitView={outputSplitView}
229230
editableInstructions={editableInstructions}
231+
sidebarPlugins={sidebarPlugins}
230232
/>
231233
{errorModalShowing && <ErrorModal />}
232234
{newFileModalShowing && <NewFileModal />}

src/web-component.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class WebComponent extends HTMLElement {
2727
mountPoint;
2828
componentAttributes = {};
2929
componentProperties = {};
30+
sidebarPlugins = [];
3031

3132
connectedCallback() {
3233
if (!this.shadowRoot) {
@@ -112,6 +113,11 @@ class WebComponent extends HTMLElement {
112113
this.mountReactApp();
113114
}
114115

116+
setSidebarPlugins(sidebarPlugins) {
117+
this.sidebarPlugins = sidebarPlugins;
118+
this.mountReactApp();
119+
}
120+
115121
get editorCode() {
116122
const state = store.getState();
117123
return state.editor.project.components[0]?.content;
@@ -177,7 +183,10 @@ class WebComponent extends HTMLElement {
177183
<React.StrictMode>
178184
<Provider store={store}>
179185
<BrowserRouter>
180-
<WebComponentLoader {...this.reactProps()} />
186+
<WebComponentLoader
187+
sidebarPlugins={this.sidebarPlugins}
188+
{...this.reactProps()}
189+
/>
181190
</BrowserRouter>
182191
</Provider>
183192
</React.StrictMode>,

0 commit comments

Comments
 (0)