From 8d0374677896950375da4a113e7c3a980e228550 Mon Sep 17 00:00:00 2001 From: Conor Date: Thu, 16 Jan 2025 14:34:50 +0000 Subject: [PATCH 01/12] initial tabs work --- .../InstructionsPanel/InstructionsPanel.jsx | 43 ++++++++++++++----- src/utils/i18n.js | 2 + 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx b/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx index 2ae12d5dc..f629de03d 100644 --- a/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx +++ b/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx @@ -2,6 +2,8 @@ import React, { useEffect, useRef, useMemo, useState } from "react"; import SidebarPanel from "../SidebarPanel"; import { useTranslation } from "react-i18next"; import { useSelector, useDispatch } from "react-redux"; +import { Tabs, TabList, Tab, TabPanel } from "react-tabs"; + import ProgressBar from "./ProgressBar/ProgressBar"; import "../../../../assets/stylesheets/Instructions.scss"; import "prismjs/plugins/highlight-keywords/prism-highlight-keywords.js"; @@ -11,6 +13,7 @@ import "prismjs/plugins/line-highlight/prism-line-highlight.css"; import { quizReadyEvent } from "../../../../events/WebComponentCustomEvents"; import { setCurrentStepPosition } from "../../../../redux/InstructionsSlice"; import { setProjectInstructions } from "../../../../redux/EditorSlice"; + const InstructionsPanel = () => { const instructionsEditable = useSelector( (state) => state.editor?.instructionsEditable, @@ -26,6 +29,7 @@ const InstructionsPanel = () => { const stepContent = useRef(); const [isQuiz, setIsQuiz] = useState(false); + const [instructionsTab, setInstructionsTab] = useState(0); const quizCompleted = useMemo(() => { return quiz?.currentQuestion === quiz?.questionCount; @@ -61,6 +65,8 @@ const InstructionsPanel = () => { useEffect(() => { const setStepContent = (content) => { + if (!stepContent?.current) return; + stepContent.current.parentElement.scrollTo({ top: 0 }); stepContent.current.innerHTML = content; applySyntaxHighlighting(stepContent.current); @@ -78,6 +84,7 @@ const InstructionsPanel = () => { quiz, quizCompleted, isQuiz, + instructionsTab, ]); useEffect(() => { @@ -100,16 +107,32 @@ const InstructionsPanel = () => { heading={t("instructionsPanel.projectSteps")} {...{ Footer: hasMultipleSteps && ProgressBar }} > -
- {instructionsEditable && ( - - )} -
-
+ {instructionsEditable ? ( + { + setInstructionsTab(index); + }} + > + + {t("instructionsPanel.edit")} + {t("instructionsPanel.view")} + + + + + + <> +
+ +
+
+ ) : ( +
+ )} ); }; diff --git a/src/utils/i18n.js b/src/utils/i18n.js index b61ab4d92..e3c3accdc 100644 --- a/src/utils/i18n.js +++ b/src/utils/i18n.js @@ -207,6 +207,8 @@ i18n nextStep: "Next step", previousStep: "Previous step", projectSteps: "Project steps", + edit: "Edit", + view: "View", }, projectsPanel: { projects: "Projects", From e8b547deccce3286d7a5e6e9178133f51a5aa27f Mon Sep 17 00:00:00 2001 From: Lois Wells <88904316+loiswells97@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:12:03 +0000 Subject: [PATCH 02/12] Downloading the instructions (#1160) ## What's Changed? - Includes instructions in the project download if there are any - Adds a list of reserved file names which includes `INSTRUCTIONS.md` to prevent clashes Closes https://github.com/RaspberryPiFoundation/digital-editor-issues/issues/374 --- CHANGELOG.md | 5 ++ .../DownloadButton/DownloadButton.jsx | 4 ++ .../DownloadButton/DownloadButton.test.js | 58 +++++++++++++++++++ src/components/Modals/NewFileModal.test.js | 11 ++++ src/components/Modals/RenameFileModal.test.js | 11 ++++ src/utils/componentNameValidation.js | 7 +++ src/utils/i18n.js | 2 + 7 files changed, 98 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf7c19a0b..83b799413 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Support for the `outputPanels` attribute in the `PyodideRunner` (#1157) - Show instructions option in sidebar if instructions are editable (#1164) - Open instructions panel by default if instructions are editable (#1164) +- Downloading project instructions (#1160) + +### Changed + +- Made `INSTRUCTIONS.md` a reserved file name (#1160) ## [0.28.14] - 2025-01-06 diff --git a/src/components/DownloadButton/DownloadButton.jsx b/src/components/DownloadButton/DownloadButton.jsx index eb9c15b8d..b37342633 100644 --- a/src/components/DownloadButton/DownloadButton.jsx +++ b/src/components/DownloadButton/DownloadButton.jsx @@ -39,6 +39,10 @@ const DownloadButton = (props) => { const zip = new JSZip(); + if (project.instructions) { + zip.file("INSTRUCTIONS.md", project.instructions); + } + project.components.forEach((file) => { zip.file(`${file.name}.${file.extension}`, file.content); }); diff --git a/src/components/DownloadButton/DownloadButton.test.js b/src/components/DownloadButton/DownloadButton.test.js index ef9e38253..1d8a5a655 100644 --- a/src/components/DownloadButton/DownloadButton.test.js +++ b/src/components/DownloadButton/DownloadButton.test.js @@ -25,6 +25,7 @@ describe("Downloading project with name set", () => { project: { name: "My epic project", identifier: "hello-world-project", + instructions: "print hello world to the console", components: [ { name: "main", @@ -53,6 +54,18 @@ describe("Downloading project with name set", () => { expect(downloadButton).toBeInTheDocument(); }); + test("Clicking download zips instructions", async () => { + fireEvent.click(downloadButton); + const JSZipInstance = JSZip.mock.instances[0]; + const mockFile = JSZipInstance.file; + await waitFor(() => + expect(mockFile).toHaveBeenCalledWith( + "INSTRUCTIONS.md", + "print hello world to the console", + ), + ); + }); + test("Clicking download zips project file content", async () => { fireEvent.click(downloadButton); const JSZipInstance = JSZip.mock.instances[0]; @@ -123,3 +136,48 @@ describe("Downloading project with no name set", () => { ); }); }); + +describe("Downloading project with no instructions set", () => { + let downloadButton; + + beforeEach(() => { + JSZip.mockClear(); + const middlewares = []; + const mockStore = configureStore(middlewares); + const initialState = { + editor: { + project: { + name: "My epic project", + identifier: "hello-world-project", + components: [ + { + name: "main", + extension: "py", + content: "", + }, + ], + image_list: [], + }, + }, + }; + const store = mockStore(initialState); + render( + + {}} /> + , + ); + downloadButton = screen.queryByText("Download").parentElement; + }); + + test("Clicking download button does not zip instructions", async () => { + fireEvent.click(downloadButton); + const JSZipInstance = JSZip.mock.instances[0]; + const mockFile = JSZipInstance.file; + await waitFor(() => + expect(mockFile).not.toHaveBeenCalledWith( + "INSTRUCTIONS.md", + expect.anything(), + ), + ); + }); +}); diff --git a/src/components/Modals/NewFileModal.test.js b/src/components/Modals/NewFileModal.test.js index 69eb5a39d..8b0035fbf 100644 --- a/src/components/Modals/NewFileModal.test.js +++ b/src/components/Modals/NewFileModal.test.js @@ -81,6 +81,17 @@ describe("Testing the new file modal", () => { expect(store.getActions()).toEqual(expectedActions); }); + test("Reserved file name throws error", () => { + fireEvent.change(inputBox, { target: { value: "INSTRUCTIONS.md" } }); + fireEvent.click(saveButton); + const expectedActions = [ + setNameError("filePanel.errors.reservedFileName", { + fileName: "INSTRUCTIONS.md", + }), + ]; + expect(store.getActions()).toEqual(expectedActions); + }); + test("Unsupported extension throws error", () => { fireEvent.change(inputBox, { target: { value: "file1.js" } }); fireEvent.click(saveButton); diff --git a/src/components/Modals/RenameFileModal.test.js b/src/components/Modals/RenameFileModal.test.js index be2df5ac6..112f9b16a 100644 --- a/src/components/Modals/RenameFileModal.test.js +++ b/src/components/Modals/RenameFileModal.test.js @@ -91,6 +91,17 @@ describe("Testing the rename file modal", () => { expect(store.getActions()).toEqual(expectedActions); }); + test("Reserved file name throws error", () => { + fireEvent.change(inputBox, { target: { value: "INSTRUCTIONS.md" } }); + fireEvent.click(saveButton); + const expectedActions = [ + setNameError("filePanel.errors.reservedFileName", { + fileName: "INSTRUCTIONS.md", + }), + ]; + expect(store.getActions()).toEqual(expectedActions); + }); + test("Unchanged file name does not throw error", () => { fireEvent.click(saveButton); const expectedActions = [ diff --git a/src/utils/componentNameValidation.js b/src/utils/componentNameValidation.js index 003c6c562..26ce6c689 100644 --- a/src/utils/componentNameValidation.js +++ b/src/utils/componentNameValidation.js @@ -5,6 +5,8 @@ const allowedExtensions = { html: ["html", "css", "js"], }; +const reservedFileNames = ["INSTRUCTIONS.md"]; + const allowedExtensionsString = (projectType, t) => { const extensionsList = allowedExtensions[projectType]; if (extensionsList.length === 1) { @@ -19,6 +21,7 @@ const allowedExtensionsString = (projectType, t) => { const isValidFileName = (fileName, projectType, componentNames) => { const extension = fileName.split(".").slice(1).join("."); if ( + !reservedFileNames.includes(fileName) && allowedExtensions[projectType].includes(extension) && !componentNames.includes(fileName) && fileName.split(" ").length === 1 @@ -44,6 +47,10 @@ export const validateFileName = ( (currentFileName && fileName === currentFileName) ) { callback(); + } else if (reservedFileNames.includes(fileName)) { + dispatch( + setNameError(t("filePanel.errors.reservedFileName", { fileName })), + ); } else if (componentNames.includes(fileName)) { dispatch(setNameError(t("filePanel.errors.notUnique"))); } else if (fileName.split(" ").length > 1) { diff --git a/src/utils/i18n.js b/src/utils/i18n.js index e3c3accdc..bacb95a3e 100644 --- a/src/utils/i18n.js +++ b/src/utils/i18n.js @@ -130,6 +130,8 @@ i18n }, filePanel: { errors: { + reservedFileName: + "{{fileName}} is a reserved file name. Please choose a different name.", containsSpaces: "File names must not contain spaces.", generalError: "Error", notUnique: "File names must be unique.", From ade61b233cf55ffdb1189d9b0d0587d57bb1cf42 Mon Sep 17 00:00:00 2001 From: Lois Wells <88904316+loiswells97@users.noreply.github.com> Date: Thu, 16 Jan 2025 15:14:00 +0000 Subject: [PATCH 03/12] Instructions empty state (#1165) --- CHANGELOG.md | 4 + package.json | 1 + src/assets/markdown/demoInstructions.md | 20 + src/assets/stylesheets/Instructions.scss | 13 + .../InstructionsPanel/InstructionsPanel.jsx | 126 +++-- .../InstructionsPanel.test.js | 449 ++++++++++-------- src/utils/i18n.js | 10 + src/utils/setupTests.js | 4 + webpack.config.js | 4 + yarn.lock | 13 + 10 files changed, 426 insertions(+), 218 deletions(-) create mode 100644 src/assets/markdown/demoInstructions.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 83b799413..584ed4283 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Show instructions option in sidebar if instructions are editable (#1164) - Open instructions panel by default if instructions are editable (#1164) - Downloading project instructions (#1160) +- Downloading project instructions (#1160) +- Show instructions option in sidebar if instructions are editable (#1164) +- Open instructions panel by default if instructions are editable (#1164) +- Instructions empty state to show when instructions are editable (#1165) ### Changed diff --git a/package.json b/package.json index 59042ccbf..a953c2772 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "prismjs": "^1.29.0", "prompts": "2.4.0", "prop-types": "^15.8.1", + "raw-loader": "^4.0.2", "rc-resize-observer": "^1.3.1", "re-resizable": "6.9.9", "react": "^18.1.0", diff --git a/src/assets/markdown/demoInstructions.md b/src/assets/markdown/demoInstructions.md new file mode 100644 index 000000000..0aaa55945 --- /dev/null +++ b/src/assets/markdown/demoInstructions.md @@ -0,0 +1,20 @@ +# How instructions work + +## Enabling instructions + +Any text written here will be visible to students in the sidebar + +If you decide you do not want instructions, simply leave this panel blank. + +## Writing instructions + +Write your instructions using [Markdown](https://www.markdownguide.org/) +### What you can do + +Lists: + +- Bullet points +- Bullet points + +1. numbered steps +2. numbered steps diff --git a/src/assets/stylesheets/Instructions.scss b/src/assets/stylesheets/Instructions.scss index e32b73253..109aa9e65 100644 --- a/src/assets/stylesheets/Instructions.scss +++ b/src/assets/stylesheets/Instructions.scss @@ -255,4 +255,17 @@ white-space: pre-wrap; } } + + .project-instructions__empty { + background-color: $rpf-teal-100; + border-radius: $space-0-5; + display: flex; + flex-direction: column; + gap: $space-1-5; + padding: $space-1; + } + + .project-instructions__empty-text { + margin: 0; + } } diff --git a/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx b/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx index f629de03d..4a6f74abd 100644 --- a/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx +++ b/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx @@ -1,8 +1,9 @@ import React, { useEffect, useRef, useMemo, useState } from "react"; import SidebarPanel from "../SidebarPanel"; -import { useTranslation } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import { useSelector, useDispatch } from "react-redux"; import { Tabs, TabList, Tab, TabPanel } from "react-tabs"; +import { Link } from "react-router-dom"; import ProgressBar from "./ProgressBar/ProgressBar"; import "../../../../assets/stylesheets/Instructions.scss"; @@ -12,7 +13,9 @@ import "prismjs/plugins/line-numbers/prism-line-numbers.css"; import "prismjs/plugins/line-highlight/prism-line-highlight.css"; import { quizReadyEvent } from "../../../../events/WebComponentCustomEvents"; import { setCurrentStepPosition } from "../../../../redux/InstructionsSlice"; +import DesignSystemButton from "../../../DesignSystemButton/DesignSystemButton"; import { setProjectInstructions } from "../../../../redux/EditorSlice"; +import demoInstructions from "../../../../assets/markdown/demoInstructions.md"; const InstructionsPanel = () => { const instructionsEditable = useSelector( @@ -65,11 +68,11 @@ const InstructionsPanel = () => { useEffect(() => { const setStepContent = (content) => { - if (!stepContent?.current) return; - - stepContent.current.parentElement.scrollTo({ top: 0 }); - stepContent.current.innerHTML = content; - applySyntaxHighlighting(stepContent.current); + if (stepContent.current) { + stepContent.current?.parentElement.scrollTo({ top: 0 }); + stepContent.current.innerHTML = content; + applySyntaxHighlighting(stepContent.current); + } }; if (isQuiz && !quizCompleted) { setStepContent(quiz.questions[quiz.currentQuestion]); @@ -97,6 +100,24 @@ const InstructionsPanel = () => { } }, [quizCompleted, currentStepPosition, numberOfSteps, dispatch, isQuiz]); + const addInstructions = () => { + dispatch(setProjectInstructions(demoInstructions)); + }; + + const AddInstructionsButton = () => { + return ( + + ); + }; + const onChange = (e) => { dispatch(setProjectInstructions(e.target.value)); }; @@ -105,34 +126,75 @@ const InstructionsPanel = () => { - {instructionsEditable ? ( - { - setInstructionsTab(index); - }} - > - - {t("instructionsPanel.edit")} - {t("instructionsPanel.view")} - - - - - - <> -
- -
-
- ) : ( -
- )} +
+ {instructionsEditable ? ( + hasInstructions ? ( +
+ {instructionsEditable && ( + { + setInstructionsTab(index); + }} + > + + {t("instructionsPanel.edit")} + {t("instructionsPanel.view")} + + + + + + <> +
+ +
+
+ )} +
+ ) : ( +
+

+ {t("instructionsPanel.emptyState.purpose")} +

+

+ {t("instructionsPanel.emptyState.location")} +

+

+ {() => ( + , + ]} + /> + )} +

+

+ {t("instructionsPanel.emptyState.edits")} +

+
+ ) + ) : ( +
+ )} +
); }; diff --git a/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.test.js b/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.test.js index ec9dbf53e..d21a4ca05 100644 --- a/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.test.js +++ b/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.test.js @@ -3,234 +3,311 @@ import InstructionsPanel from "./InstructionsPanel"; import { Provider } from "react-redux"; import configureStore from "redux-mock-store"; import { setProjectInstructions } from "../../../../redux/EditorSlice"; +import { act } from "react"; + window.HTMLElement.prototype.scrollTo = jest.fn(); window.Prism = { highlightElement: jest.fn(), }; -describe("It renders project steps when there is no quiz", () => { - beforeEach(() => { - const mockStore = configureStore([]); - const initialState = { - editor: { - project: {}, - instructionsEditable: false, - }, - instructions: { - project: { - steps: [ - { content: "

step 0

" }, - { - content: `

step 1

- print('hello') -

Hello world

- .hello { color: purple } - `, - }, - ], - }, - quiz: {}, - currentStepPosition: 1, - }, - }; - const store = mockStore(initialState); - render( - - - , - ); - }); +describe("When instructionsEditable is true", () => { + describe("When there are instructions", () => { + let store; - test("Renders with correct instruction step content", () => { - expect(screen.queryByText("step 1")).toBeInTheDocument(); - }); + beforeEach(() => { + const mockStore = configureStore([]); + const initialState = { + editor: { + project: {}, + instructionsEditable: true, + }, + instructions: { + project: { + steps: [{ content: "instructions" }], + }, + quiz: {}, + currentStepPosition: 1, + }, + }; + store = mockStore(initialState); + render( + + + , + ); + }); - test("Scrolls instructions to the top", () => { - expect(window.HTMLElement.prototype.scrollTo).toHaveBeenCalledWith({ - top: 0, + test("Renders the edit panel", () => { + expect(screen.getByTestId("instructionTextarea")).toBeInTheDocument(); }); - }); - test("Renders the progress bar", () => { - expect(screen.queryByRole("progressbar")).toBeInTheDocument(); - }); + test("saves content", async () => { + const textarea = screen.getByTestId("instructionTextarea"); + const testString = "SomeInstructions"; - test("Applies syntax highlighting to python code", () => { - const codeElement = document.getElementsByClassName("language-python")[0]; - expect(window.Prism.highlightElement).toHaveBeenCalledWith(codeElement); - }); + fireEvent.change(textarea, { target: { value: testString } }); - test("Applies syntax highlighting to HTML code", () => { - const codeElement = document.getElementsByClassName("language-html")[0]; - expect(window.Prism.highlightElement).toHaveBeenCalledWith(codeElement); - }); + await waitFor(() => { + expect(store.getActions()).toEqual( + expect.arrayContaining([setProjectInstructions(testString)]), + ); + }); + }); - test("Applies syntax highlighting to CSS code", () => { - const codeElement = document.getElementsByClassName("language-css")[0]; - expect(window.Prism.highlightElement).toHaveBeenCalledWith(codeElement); + test("Does not render the add instructions button", () => { + expect( + screen.queryByText("instructionsPanel.emptyState.addInstructions"), + ).not.toBeInTheDocument(); + }); }); -}); -describe("When instructionsEditable is true", () => { - let store; - - beforeEach(() => { - const mockStore = configureStore([]); - const initialState = { - editor: { - project: {}, - instructionsEditable: true, - }, - instructions: { - project: { - steps: [{ content: "instructions" }], - }, - quiz: {}, - currentStepPosition: 1, - }, - }; - store = mockStore(initialState); - render( - - - , - ); - }); + describe("When there are no instructions", () => { + let store; - test("Renders the edit panel", () => { - expect(screen.getByTestId("instructionTextarea")).toBeInTheDocument(); - }); + beforeEach(() => { + const mockStore = configureStore([]); + const initialState = { + editor: { + project: {}, + instructionsEditable: true, + }, + instructions: { + project: { + steps: [], + }, + quiz: {}, + currentStepPosition: 1, + }, + }; + store = mockStore(initialState); + render( + + + , + ); + }); - test("saves content", async () => { - const textarea = screen.getByTestId("instructionTextarea"); - const testString = "SomeInstructions"; + test("Renders the add instrucitons button", () => { + expect( + screen.queryByText("instructionsPanel.emptyState.addInstructions"), + ).toBeInTheDocument(); + }); - fireEvent.change(textarea, { target: { value: testString } }); + test("Clicking the add instructions button adds the demo instructions", () => { + const addInstructionsButton = screen.getByText( + "instructionsPanel.emptyState.addInstructions", + ); + act(() => { + fireEvent.click(addInstructionsButton); + }); - await waitFor(() => { expect(store.getActions()).toEqual( - expect.arrayContaining([setProjectInstructions(testString)]), + expect.arrayContaining([setProjectInstructions("demoInstructions.md")]), ); }); + + test("Renders the instructions explanation", () => { + expect( + screen.queryByText("instructionsPanel.emptyState.purpose"), + ).toBeInTheDocument(); + }); }); }); -describe("When there is only one step", () => { - beforeEach(() => { - const mockStore = configureStore([]); - const initialState = { - editor: { - instructionsEditable: false, - }, - instructions: { - project: { - steps: [{ content: "

step 0

" }], +describe("When instructions are not editable", () => { + describe("When there are no instructions", () => { + beforeEach(() => { + const mockStore = configureStore([]); + const initialState = { + editor: { + project: {}, + instructionsEditable: false, }, - quiz: {}, - currentStepPosition: 0, - }, - }; - const store = mockStore(initialState); - render( - - - , - ); - }); + instructions: { + project: { + steps: [], + }, + quiz: {}, + currentStepPosition: 1, + }, + }; + const store = mockStore(initialState); + render( + + + , + ); + }); + + test("Does not render the add instructions button", () => { + expect( + screen.queryByText("instructionsPanel.emptyState.addInstructions"), + ).not.toBeInTheDocument(); + }); + + test("Does not render the instructions explanation", () => { + expect( + screen.queryByText("instructionsPanel.emptyState.purpose"), + ).not.toBeInTheDocument(); + }); + + test("It renders without crashing", () => { + expect( + screen.queryByText("instructionsPanel.projectSteps"), + ).toBeInTheDocument(); + }); - test("Does not render the progress bar", () => { - expect(screen.queryByRole("progressbar")).not.toBeInTheDocument(); + test("Does not render the progress bar", () => { + expect(screen.queryByRole("progressbar")).not.toBeInTheDocument(); + }); }); -}); -describe("When there are no steps", () => { - beforeEach(() => { - const mockStore = configureStore([]); - const initialState = { - editor: { - instructionsEditable: false, - }, - instructions: { - project: { - steps: [], + describe("When there are instructions", () => { + beforeEach(() => { + const mockStore = configureStore([]); + const initialState = { + editor: { + project: {}, + instructionsEditable: false, }, - quiz: {}, - currentStepPosition: 0, - }, - }; - const store = mockStore(initialState); - render( - - - , - ); - }); + instructions: { + project: { + steps: [ + { content: "

step 0

" }, + { + content: `

step 1

+ print('hello') +

Hello world

+ .hello { color: purple } + `, + }, + ], + }, + quiz: {}, + currentStepPosition: 1, + }, + }; + const store = mockStore(initialState); + render( + + + , + ); + }); - test("It renders without crashing", () => { - expect( - screen.queryByText("instructionsPanel.projectSteps"), - ).toBeInTheDocument(); - }); + test("Renders with correct instruction step content", () => { + expect(screen.queryByText("step 1")).toBeInTheDocument(); + }); - test("Does not render the progress bar", () => { - expect(screen.queryByRole("progressbar")).not.toBeInTheDocument(); - }); -}); + test("Scrolls instructions to the top", () => { + expect(window.HTMLElement.prototype.scrollTo).toHaveBeenCalledWith({ + top: 0, + }); + }); -describe("It renders a quiz when it has one", () => { - const quizHandler = jest.fn(); + test("Renders the progress bar", () => { + expect(screen.queryByRole("progressbar")).toBeInTheDocument(); + }); + + test("Applies syntax highlighting to python code", () => { + const codeElement = document.getElementsByClassName("language-python")[0]; + expect(window.Prism.highlightElement).toHaveBeenCalledWith(codeElement); + }); - beforeAll(() => { - document.addEventListener("editor-quizReady", quizHandler); + test("Applies syntax highlighting to HTML code", () => { + const codeElement = document.getElementsByClassName("language-html")[0]; + expect(window.Prism.highlightElement).toHaveBeenCalledWith(codeElement); + }); + + test("Applies syntax highlighting to CSS code", () => { + const codeElement = document.getElementsByClassName("language-css")[0]; + expect(window.Prism.highlightElement).toHaveBeenCalledWith(codeElement); + }); }); - beforeEach(() => { - const mockStore = configureStore([]); - const initialState = { - instructions: { - project: { - steps: [ - { content: "

step 0

" }, - { content: "

step 1

", knowledgeQuiz: "quizPath" }, - ], + + describe("When there is only one step", () => { + beforeEach(() => { + const mockStore = configureStore([]); + const initialState = { + editor: { + instructionsEditable: false, }, - quiz: { - questions: [ - "

Test quiz

step 1

print('hello')", - ], - questionCount: 1, - currentQuestion: 0, + instructions: { + project: { + steps: [{ content: "

step 0

" }], + }, + quiz: {}, + currentStepPosition: 0, }, - currentStepPosition: 1, - }, - }; - const store = mockStore(initialState); - render( - - - , - ); - }); + }; + const store = mockStore(initialState); + render( + + + , + ); + }); - test("Renders the quiz content", () => { - expect(screen.queryByText("Test quiz")).toBeInTheDocument(); + test("Does not render the progress bar", () => { + expect(screen.queryByRole("progressbar")).not.toBeInTheDocument(); + }); }); - test("Scrolls instructions to the top", () => { - expect(window.HTMLElement.prototype.scrollTo).toHaveBeenCalledWith({ - top: 0, + describe("When there is a quiz", () => { + const quizHandler = jest.fn(); + + beforeAll(() => { + document.addEventListener("editor-quizReady", quizHandler); + }); + beforeEach(() => { + const mockStore = configureStore([]); + const initialState = { + instructions: { + project: { + steps: [ + { content: "

step 0

" }, + { content: "

step 1

", knowledgeQuiz: "quizPath" }, + ], + }, + quiz: { + questions: [ + "

Test quiz

step 1

print('hello')", + ], + questionCount: 1, + currentQuestion: 0, + }, + currentStepPosition: 1, + }, + }; + const store = mockStore(initialState); + render( + + + , + ); }); - }); - test("Retains the progress bar", () => { - expect(screen.queryByRole("progressbar")).toBeInTheDocument(); - }); + test("Renders the quiz content", () => { + expect(screen.queryByText("Test quiz")).toBeInTheDocument(); + }); - test("Applies syntax highlighting", () => { - const codeElement = document.getElementsByClassName("language-python")[0]; - expect(window.Prism.highlightElement).toHaveBeenCalledWith(codeElement); - }); + test("Scrolls instructions to the top", () => { + expect(window.HTMLElement.prototype.scrollTo).toHaveBeenCalledWith({ + top: 0, + }); + }); - test("Fires a quizIsReady event", () => { - expect(quizHandler).toHaveBeenCalled(); + test("Retains the progress bar", () => { + expect(screen.queryByRole("progressbar")).toBeInTheDocument(); + }); + + test("Applies syntax highlighting", () => { + const codeElement = document.getElementsByClassName("language-python")[0]; + expect(window.Prism.highlightElement).toHaveBeenCalledWith(codeElement); + }); + + test("Fires a quizIsReady event", () => { + expect(quizHandler).toHaveBeenCalled(); + }); }); }); diff --git a/src/utils/i18n.js b/src/utils/i18n.js index bacb95a3e..c69e5a40c 100644 --- a/src/utils/i18n.js +++ b/src/utils/i18n.js @@ -206,6 +206,16 @@ i18n info: "Information", }, instructionsPanel: { + emptyState: { + addInstructions: "Add instructions", + edits: + "Like project code, students will not see any edits you make to the instructions after they have saved their version of the project.", + location: + "These instructions will be shown to students in their project sidebar and will be view-only.", + markdown: "Instructions are written in <0>markdown.", + purpose: + "Instructions can be added to your project to guide students.", + }, nextStep: "Next step", previousStep: "Previous step", projectSteps: "Project steps", diff --git a/src/utils/setupTests.js b/src/utils/setupTests.js index d0025dcd8..485976317 100644 --- a/src/utils/setupTests.js +++ b/src/utils/setupTests.js @@ -42,6 +42,10 @@ jest.mock("./i18n", () => ({ t: (string) => string, })); +jest.mock("../assets/markdown/demoInstructions.md", () => { + return "demoInstructions.md"; +}); + global.Blob = jest.fn(); window.URL.createObjectURL = jest.fn(); window.Worker = PyodideWorker; diff --git a/webpack.config.js b/webpack.config.js index 0666b78f8..536abd2a4 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -43,6 +43,10 @@ module.exports = { }, ], }, + { + test: /\.md$/, + use: ["raw-loader"], + }, { test: /\/src\/assets\/icons\/.*\.svg$/, use: [ diff --git a/yarn.lock b/yarn.lock index c8beaed28..79885cf79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2836,6 +2836,7 @@ __metadata: prismjs: ^1.29.0 prompts: 2.4.0 prop-types: ^15.8.1 + raw-loader: ^4.0.2 rc-resize-observer: ^1.3.1 re-resizable: 6.9.9 react: ^18.1.0 @@ -15048,6 +15049,18 @@ __metadata: languageName: node linkType: hard +"raw-loader@npm:^4.0.2": + version: 4.0.2 + resolution: "raw-loader@npm:4.0.2" + dependencies: + loader-utils: ^2.0.0 + schema-utils: ^3.0.0 + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + checksum: 51cc1b0d0e8c37c4336b5318f3b2c9c51d6998ad6f56ea09612afcfefc9c1f596341309e934a744ae907177f28efc9f1654eacd62151e82853fcc6d37450e795 + languageName: node + linkType: hard + "rc-resize-observer@npm:^1.3.1": version: 1.4.0 resolution: "rc-resize-observer@npm:1.4.0" From bde878b6422615e2e7ab7ac200734838009dda3d Mon Sep 17 00:00:00 2001 From: Conor Date: Thu, 16 Jan 2025 17:52:44 +0000 Subject: [PATCH 04/12] styles --- .../stylesheets/DraggableTabs copy.scss | 26 ++++++++ src/assets/stylesheets/Instructions.scss | 65 ++++++++++++++++--- .../InstructionsPanel/InstructionsPanel.jsx | 4 +- 3 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 src/assets/stylesheets/DraggableTabs copy.scss diff --git a/src/assets/stylesheets/DraggableTabs copy.scss b/src/assets/stylesheets/DraggableTabs copy.scss new file mode 100644 index 000000000..614891272 --- /dev/null +++ b/src/assets/stylesheets/DraggableTabs copy.scss @@ -0,0 +1,26 @@ +@use './rpf_design_system/colours' as *; +@use './rpf_design_system/spacing' as *; + +.draggable-tab { + user-select: none; + + &:focus-visible { + outline: none; + } +} + +.--dark .draggable-tab:focus-visible .react-tabs__tab { + outline: 2px auto $rpf-white; + outline-offset: -1px; +} + +.--light .draggable-tab:focus-visible .react-tabs__tab { + outline: 2px auto Highlight; + outline: 2px auto -webkit-focus-ring-color; + outline-offset: -1px; +} + +.droppable-tab-list { + display: flex; + inline-size: 100%; +} diff --git a/src/assets/stylesheets/Instructions.scss b/src/assets/stylesheets/Instructions.scss index 109aa9e65..ddd19ead8 100644 --- a/src/assets/stylesheets/Instructions.scss +++ b/src/assets/stylesheets/Instructions.scss @@ -1,10 +1,14 @@ -@use "../../../node_modules/@raspberrypifoundation/design-system-core/scss/components/squiggle.scss" as *; +@use "../../../node_modules/@raspberrypifoundation/design-system-core/scss/components/squiggle.scss" + as *; +@use "../../../node_modules/@raspberrypifoundation/design-system-core/scss/mixins/typography"; @use "./rpf_design_system/colours" as *; @use "./rpf_design_system/spacing" as *; -@use './rpf_design_system/font-size' as *; +@use "./rpf_design_system/font-size" as *; @use "./rpf_design_system/font-weight" as *; .project-instructions { + height: 100%; + h2 { @include font-size-1-25(bold); margin: 0; @@ -72,14 +76,11 @@ border-block-end: 1px solid $rpf-grey-600; } - - .line-numbers { padding-inline-start: $space-3; padding-inline-end: $space-1; } - .line-numbers-rows { border-color: $rpf-text-secondary-dark; @@ -97,13 +98,16 @@ } .language-python { - .number, .boolean, .function { + .number, + .boolean, + .function { color: $rpf-syntax-1; } .keyword { color: $rpf-syntax-4; } - .string, .char { + .string, + .char { color: $rpf-syntax-2; } .comment { @@ -129,7 +133,8 @@ color: $rpf-syntax-4; } - .property, .punctuation { + .property, + .punctuation { color: $rpf-white; } } @@ -137,7 +142,8 @@ .language-html { .tag { color: $rpf-syntax-4; - .punctuation, .attr-name { + .punctuation, + .attr-name { color: $rpf-white; } @@ -269,3 +275,44 @@ margin: 0; } } + +#app, +#wc { + .c-instruction-tabs { + display: flex; + flex-direction: column; + height: 100%; + + .react-tabs { + border: 1px solid var(--editor-color-outline); + + .react-tabs__tab-list { + border-bottom: 1px solid var(--editor-color-outline); + } + } + + .react-tabs__tab { + background: var(--rpf-off-white); + padding-inline: var(--space-1-5); + + &--selected { + background: var(--rpf-white); + } + } + + .react-tabs__tab-panel { + .project-instructions { + padding-inline: var(--space-1); + } + } + + textarea { + @include typography.style-1(); + + border: none; + height: 100%; + overflow-y: scroll; + padding: var(--space-1); + } + } +} diff --git a/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx b/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx index 4a6f74abd..ac369610d 100644 --- a/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx +++ b/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx @@ -132,7 +132,7 @@ const InstructionsPanel = () => {
{instructionsEditable ? ( hasInstructions ? ( -
+
{instructionsEditable && ( { @@ -140,7 +140,7 @@ const InstructionsPanel = () => { }} > - {t("instructionsPanel.edit")} + {t("instructionsPanel.edit")} {t("instructionsPanel.view")} From 63db4d3eac647ae2589a01e9a117c5f80a4dce0f Mon Sep 17 00:00:00 2001 From: Conor Date: Thu, 16 Jan 2025 17:55:31 +0000 Subject: [PATCH 05/12] changelog --- CHANGELOG.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 584ed4283..bf7c19a0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,15 +13,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Support for the `outputPanels` attribute in the `PyodideRunner` (#1157) - Show instructions option in sidebar if instructions are editable (#1164) - Open instructions panel by default if instructions are editable (#1164) -- Downloading project instructions (#1160) -- Downloading project instructions (#1160) -- Show instructions option in sidebar if instructions are editable (#1164) -- Open instructions panel by default if instructions are editable (#1164) -- Instructions empty state to show when instructions are editable (#1165) - -### Changed - -- Made `INSTRUCTIONS.md` a reserved file name (#1160) ## [0.28.14] - 2025-01-06 From 18984f8c848e7a99f67f4b49c197c4db39a395c2 Mon Sep 17 00:00:00 2001 From: Lois Wells <88904316+loiswells97@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:12:03 +0000 Subject: [PATCH 06/12] Downloading the instructions (#1160) ## What's Changed? - Includes instructions in the project download if there are any - Adds a list of reserved file names which includes `INSTRUCTIONS.md` to prevent clashes Closes https://github.com/RaspberryPiFoundation/digital-editor-issues/issues/374 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf7c19a0b..83b799413 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Support for the `outputPanels` attribute in the `PyodideRunner` (#1157) - Show instructions option in sidebar if instructions are editable (#1164) - Open instructions panel by default if instructions are editable (#1164) +- Downloading project instructions (#1160) + +### Changed + +- Made `INSTRUCTIONS.md` a reserved file name (#1160) ## [0.28.14] - 2025-01-06 From 73f35fd3b10a843ae705c9e5d65f8f6c89de2022 Mon Sep 17 00:00:00 2001 From: Lois Wells <88904316+loiswells97@users.noreply.github.com> Date: Thu, 16 Jan 2025 15:14:00 +0000 Subject: [PATCH 07/12] Instructions empty state (#1165) --- CHANGELOG.md | 4 ++++ .../Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83b799413..584ed4283 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Show instructions option in sidebar if instructions are editable (#1164) - Open instructions panel by default if instructions are editable (#1164) - Downloading project instructions (#1160) +- Downloading project instructions (#1160) +- Show instructions option in sidebar if instructions are editable (#1164) +- Open instructions panel by default if instructions are editable (#1164) +- Instructions empty state to show when instructions are editable (#1165) ### Changed diff --git a/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx b/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx index ac369610d..80c749186 100644 --- a/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx +++ b/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx @@ -140,7 +140,9 @@ const InstructionsPanel = () => { }} > - {t("instructionsPanel.edit")} + + {t("instructionsPanel.edit")} + {t("instructionsPanel.view")} From ff0105a919a7a4be64ac640c7211a8d9c0ff5eb2 Mon Sep 17 00:00:00 2001 From: Lois Wells <88904316+loiswells97@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:12:03 +0000 Subject: [PATCH 08/12] Downloading the instructions (#1160) ## What's Changed? - Includes instructions in the project download if there are any - Adds a list of reserved file names which includes `INSTRUCTIONS.md` to prevent clashes Closes https://github.com/RaspberryPiFoundation/digital-editor-issues/issues/374 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 584ed4283..7a052b2da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Show instructions option in sidebar if instructions are editable (#1164) - Open instructions panel by default if instructions are editable (#1164) - Instructions empty state to show when instructions are editable (#1165) +- Downloading project instructions (#1160) ### Changed From 057d579b50f1973355ce85faefb32c1b1d204dc8 Mon Sep 17 00:00:00 2001 From: Lois Wells <88904316+loiswells97@users.noreply.github.com> Date: Thu, 16 Jan 2025 15:14:00 +0000 Subject: [PATCH 09/12] Instructions empty state (#1165) --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a052b2da..b1b3e70ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Open instructions panel by default if instructions are editable (#1164) - Instructions empty state to show when instructions are editable (#1165) - Downloading project instructions (#1160) +- Downloading project instructions (#1160) +- Show instructions option in sidebar if instructions are editable (#1164) +- Open instructions panel by default if instructions are editable (#1164) +- Instructions empty state to show when instructions are editable (#1165) ### Changed From 93aa8e2f2000873a84644affa0dc55229c676d7d Mon Sep 17 00:00:00 2001 From: Conor Date: Thu, 16 Jan 2025 18:02:02 +0000 Subject: [PATCH 10/12] tidy --- .../stylesheets/DraggableTabs copy.scss | 26 ------------------- .../InstructionsPanel/InstructionsPanel.jsx | 16 +++++------- 2 files changed, 7 insertions(+), 35 deletions(-) delete mode 100644 src/assets/stylesheets/DraggableTabs copy.scss diff --git a/src/assets/stylesheets/DraggableTabs copy.scss b/src/assets/stylesheets/DraggableTabs copy.scss deleted file mode 100644 index 614891272..000000000 --- a/src/assets/stylesheets/DraggableTabs copy.scss +++ /dev/null @@ -1,26 +0,0 @@ -@use './rpf_design_system/colours' as *; -@use './rpf_design_system/spacing' as *; - -.draggable-tab { - user-select: none; - - &:focus-visible { - outline: none; - } -} - -.--dark .draggable-tab:focus-visible .react-tabs__tab { - outline: 2px auto $rpf-white; - outline-offset: -1px; -} - -.--light .draggable-tab:focus-visible .react-tabs__tab { - outline: 2px auto Highlight; - outline: 2px auto -webkit-focus-ring-color; - outline-offset: -1px; -} - -.droppable-tab-list { - display: flex; - inline-size: 100%; -} diff --git a/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx b/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx index 80c749186..b95cce7c9 100644 --- a/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx +++ b/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx @@ -19,14 +19,14 @@ import demoInstructions from "../../../../assets/markdown/demoInstructions.md"; const InstructionsPanel = () => { const instructionsEditable = useSelector( - (state) => state.editor?.instructionsEditable, + (state) => state.editor?.instructionsEditable ); const project = useSelector((state) => state.editor?.project); const steps = useSelector((state) => state.instructions.project?.steps); const quiz = useSelector((state) => state.instructions?.quiz); const dispatch = useDispatch(); const currentStepPosition = useSelector( - (state) => state.instructions.currentStepPosition, + (state) => state.instructions.currentStepPosition ); const { t } = useTranslation(); const stepContent = useRef(); @@ -39,7 +39,7 @@ const InstructionsPanel = () => { }, [quiz]); const numberOfSteps = useSelector( - (state) => state.instructions.project?.steps?.length || 0, + (state) => state.instructions.project?.steps?.length || 0 ); const hasInstructions = steps && steps.length > 0; @@ -47,7 +47,7 @@ const InstructionsPanel = () => { const applySyntaxHighlighting = (container) => { const codeElements = container.querySelectorAll( - ".language-python, .language-html, .language-css", + ".language-python, .language-html, .language-css" ); codeElements.forEach((element) => { @@ -94,8 +94,8 @@ const InstructionsPanel = () => { if (quizCompleted && isQuiz) { dispatch( setCurrentStepPosition( - Math.min(currentStepPosition + 1, numberOfSteps - 1), - ), + Math.min(currentStepPosition + 1, numberOfSteps - 1) + ) ); } }, [quizCompleted, currentStepPosition, numberOfSteps, dispatch, isQuiz]); @@ -140,9 +140,7 @@ const InstructionsPanel = () => { }} > - - {t("instructionsPanel.edit")} - + {t("instructionsPanel.edit")} {t("instructionsPanel.view")} From b1e7aa3e7d9ac01160a0b4f2d491024d5ec72915 Mon Sep 17 00:00:00 2001 From: Conor Date: Thu, 16 Jan 2025 18:09:23 +0000 Subject: [PATCH 11/12] changelog --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14f7cfb59..b4f9750f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added -- Autosave instructions -- Editable instructions +- Autosave instructions (#1163) +- Editable instructions (#1161) - Support for the `outputPanels` attribute in the `PyodideRunner` (#1157) - Downloading project instructions (#1160) - Show instructions option in sidebar if instructions are editable (#1164) - Open instructions panel by default if instructions are editable (#1164) - Instructions empty state to show when instructions are editable (#1165) +- Instructions tabs for edit and viewing (#1167) ### Changed From 408e232e3bf44f7014cb7a7638f67dc4b4849e05 Mon Sep 17 00:00:00 2001 From: Conor Date: Thu, 16 Jan 2025 18:09:59 +0000 Subject: [PATCH 12/12] lint --- .../Sidebar/InstructionsPanel/InstructionsPanel.jsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx b/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx index b95cce7c9..8165fffaf 100644 --- a/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx +++ b/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx @@ -19,14 +19,14 @@ import demoInstructions from "../../../../assets/markdown/demoInstructions.md"; const InstructionsPanel = () => { const instructionsEditable = useSelector( - (state) => state.editor?.instructionsEditable + (state) => state.editor?.instructionsEditable, ); const project = useSelector((state) => state.editor?.project); const steps = useSelector((state) => state.instructions.project?.steps); const quiz = useSelector((state) => state.instructions?.quiz); const dispatch = useDispatch(); const currentStepPosition = useSelector( - (state) => state.instructions.currentStepPosition + (state) => state.instructions.currentStepPosition, ); const { t } = useTranslation(); const stepContent = useRef(); @@ -39,7 +39,7 @@ const InstructionsPanel = () => { }, [quiz]); const numberOfSteps = useSelector( - (state) => state.instructions.project?.steps?.length || 0 + (state) => state.instructions.project?.steps?.length || 0, ); const hasInstructions = steps && steps.length > 0; @@ -47,7 +47,7 @@ const InstructionsPanel = () => { const applySyntaxHighlighting = (container) => { const codeElements = container.querySelectorAll( - ".language-python, .language-html, .language-css" + ".language-python, .language-html, .language-css", ); codeElements.forEach((element) => { @@ -94,8 +94,8 @@ const InstructionsPanel = () => { if (quizCompleted && isQuiz) { dispatch( setCurrentStepPosition( - Math.min(currentStepPosition + 1, numberOfSteps - 1) - ) + Math.min(currentStepPosition + 1, numberOfSteps - 1), + ), ); } }, [quizCompleted, currentStepPosition, numberOfSteps, dispatch, isQuiz]);