Skip to content

Feat/editable instructions #1166

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jan 30, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ jobs:
REACT_APP_PLAUSIBLE_SOURCE: ""

- name: Archive cypress artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4.6.0
if: failure()
with:
name: cypress-artifacts
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

### Added

- Autosave instructions (#1163)
- Editable instructions (#1161)
- Ability to write to files in `python` (#1146)
- Support for the `outputPanels` attribute in the `PyodideRunner` (#1157)
- Downloading project instructions (#1160)
- Support for audio and video files in HTML projects (#1179)
- 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, #1168)
- Allow `instructions` attribute to override instructions attached to the project (#1169)
- Instructions tabs for edit and viewing (#1167)


### Changed

- Made `INSTRUCTIONS.md` a reserved file name (#1160)
- Clear the redux store when the component unmounts (#1169)
- Login to save now logs in and automatically saves (#1162)

## [0.28.14] - 2025-01-06
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ The `editor-wc` tag accepts the following attributes, which must be provided as
- `assets_identifier`: Load assets (not code) from this project identifier
- `auth_key`: Authenticate the user to allow them to make API requests such as saving their work
- `code`: A preset blob of code to show in the editor pane (overrides content of `main.py`/`index.html`)
- `editable_instructions`: Boolean whether to show edit panel for instructions
- `embedded`: Enable embedded mode which hides some functionality (defaults to `false`)
- `host_styles`: Styles passed into the web component from the host page
- `identifier`: Load the project with this identifier from the database
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"js-convert-case": "^4.2.0",
"jszip": "^3.10.1",
"jszip-utils": "^0.1.0",
"marked": "^15.0.6",
"material-symbols": "^0.27.0",
"mime-types": "^2.1.35",
"node-html-parser": "^6.1.5",
Expand All @@ -53,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",
Expand Down
20 changes: 20 additions & 0 deletions src/assets/markdown/demoInstructions.md
Original file line number Diff line number Diff line change
@@ -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
78 changes: 69 additions & 9 deletions src/assets/stylesheets/Instructions.scss
Original file line number Diff line number Diff line change
@@ -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 {
block-size: 100%;

h2 {
@include font-size-1-25(bold);
margin: 0;
Expand Down Expand Up @@ -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;

Expand All @@ -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 {
Expand All @@ -129,15 +133,17 @@
color: $rpf-syntax-4;
}

.property, .punctuation {
.property,
.punctuation {
color: $rpf-white;
}
}

.language-html {
.tag {
color: $rpf-syntax-4;
.punctuation, .attr-name {
.punctuation,
.attr-name {
color: $rpf-white;
}

Expand Down Expand Up @@ -255,4 +261,58 @@
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;
}
}

#app,
#wc {
.c-instruction-tabs {
display: flex;
flex-direction: column;
block-size: 100%;

.react-tabs {
border: 1px solid var(--editor-color-outline);

.react-tabs__tab-list {
border-block-end: 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;
block-size: 100%;
overflow-block: scroll;
padding: var(--space-1);
}
}
}
126 changes: 117 additions & 9 deletions src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
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";
import "prismjs/plugins/highlight-keywords/prism-highlight-keywords.js";
Expand All @@ -10,8 +13,15 @@ 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(
(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();
Expand All @@ -22,15 +32,19 @@ const InstructionsPanel = () => {
const stepContent = useRef();

const [isQuiz, setIsQuiz] = useState(false);
const [instructionsTab, setInstructionsTab] = useState(0);

const quizCompleted = useMemo(() => {
return quiz?.currentQuestion === quiz?.questionCount;
}, [quiz]);

const numberOfSteps = useSelector(
(state) => state.instructions.project.steps.length,
(state) => state.instructions.project?.steps?.length || 0,
);

const hasInstructions = steps && steps.length > 0;
const hasMultipleSteps = numberOfSteps > 1;

const applySyntaxHighlighting = (container) => {
const codeElements = container.querySelectorAll(
".language-python, .language-html, .language-css",
Expand All @@ -54,17 +68,27 @@ const InstructionsPanel = () => {

useEffect(() => {
const setStepContent = (content) => {
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]);
document.dispatchEvent(quizReadyEvent);
} else if (steps[currentStepPosition]) {
} else if (hasInstructions && steps[currentStepPosition]) {
setStepContent(steps[currentStepPosition].content);
}
}, [steps, currentStepPosition, quiz, quizCompleted, isQuiz]);
}, [
hasInstructions,
steps,
currentStepPosition,
quiz,
quizCompleted,
isQuiz,
instructionsTab,
]);

useEffect(() => {
if (quizCompleted && isQuiz) {
Expand All @@ -76,13 +100,97 @@ const InstructionsPanel = () => {
}
}, [quizCompleted, currentStepPosition, numberOfSteps, dispatch, isQuiz]);

const addInstructions = () => {
dispatch(setProjectInstructions(demoInstructions));
};

const AddInstructionsButton = () => {
return (
<DesignSystemButton
className="btn--primary"
icon="add"
text={t("instructionsPanel.emptyState.addInstructions")}
onClick={addInstructions}
fill
textAlways
small
/>
);
};

const onChange = (e) => {
dispatch(setProjectInstructions(e.target.value));
};

return (
<SidebarPanel
defaultWidth="30vw"
heading={t("instructionsPanel.projectSteps")}
Footer={ProgressBar}
Button={instructionsEditable && !hasInstructions && AddInstructionsButton}
{...{ Footer: hasMultipleSteps && ProgressBar }}
>
<div className="project-instructions" ref={stepContent}></div>
<div className="project-instructions">
{instructionsEditable ? (
hasInstructions ? (
<div className="c-instruction-tabs">
<Tabs
onSelect={(index) => {
setInstructionsTab(index);
}}
>
<TabList>
<Tab>{t("instructionsPanel.edit")}</Tab>
<Tab>{t("instructionsPanel.view")}</Tab>
</TabList>
<TabPanel>
<textarea
data-testid="instructionTextarea"
value={project.instructions}
onChange={onChange}
></textarea>
</TabPanel>
<TabPanel>
<>
<div
className="project-instructions"
ref={stepContent}
></div>
</>
</TabPanel>
</Tabs>
</div>
) : (
<div className="project-instructions__empty">
<p className="project-instructions__empty-text">
{t("instructionsPanel.emptyState.purpose")}
</p>
<p className="project-instructions__empty-text">
{t("instructionsPanel.emptyState.location")}
</p>
<p className="project-instructions__empty-text">
<Trans
i18nKey="instructionsPanel.emptyState.markdown"
components={[
<Link
href="https://commonmark.org/help/"
target="_blank"
rel="noreferrer"
/>,
]}
/>
</p>
<p className="project-instructions__empty-text">
{t("instructionsPanel.emptyState.edits")}
</p>
</div>
)
) : (
<div
className="project-instructions__content"
ref={stepContent}
></div>
)}
</div>
</SidebarPanel>
);
};
Expand Down
Loading
Loading