Skip to content

Edit multiple steps - #1582

Draft
zetter-rpf wants to merge 11 commits into
mainfrom
edit-multiple-steps
Draft

Edit multiple steps#1582
zetter-rpf wants to merge 11 commits into
mainfrom
edit-multiple-steps

Conversation

@zetter-rpf

Copy link
Copy Markdown
Contributor

No description provided.

The markdown string format is what code classroom uses, the array with content is used by the projects site and has pre-rendered html
At the moment these instructions can only be passed as a web component attribute rather than as project props.

Decouple the type of instruction from where it came from.
Previously WebComponentProject converted project instructions to
HTML with marked.parse before storing them in the instructions
slice, so redux only ever held pre-rendered markup. That made the
raw source harder to work with for editing since it led to a more
complex chain of state updates.

This change stores the unconverted markdown string in the
instructions slice instead, and moves the marked.parse call (along
with its custom target=_blank link renderer) into InstructionsPanel,
which now converts to HTML immediately before rendering each step.
Pre-rendered HTML content from authored lesson JSON continues to
pass through marked.parse unchanged, so no other call sites needed
updating.

Updated WebComponentProject and InstructionsPanel tests to match:
the dispatched payload now carries raw markdown, and the target=_blank
link conversion is verified where the conversion now happens.
Previously every instruction step went through marked.parse in
InstructionsPanel, relying on markdown parsing being a safe no-op
passthrough for pre-rendered HTML from authored lesson JSON. That
coupling was accidental: there was no explicit signal for which
steps needed markdown conversion and which were already HTML.

This change renames the key used for the single-string project
instructions case from `content` to `markdown_content` in
WebComponentProject, and teaches InstructionsPanel to branch on the
key present on a step: `content` is displayed as-is, while
`markdown_content` is run through marked.parse. This makes the
distinction explicit rather than relying on marked's HTML
passthrough behaviour.
Previously WebComponentProject held a useEffect that dispatched
setInstructions into the instructions slice whenever
editor.project.instructions changed. Since editing instructions writes
to editor.project.instructions on every keystroke, this fired a redux
action on every keystroke too, just to keep a derived copy of the same
data in sync.

This change replaces that effect with selectInstructionSteps, a
memoized reselect selector (via @reduxjs/toolkit) that computes the
steps to display directly from editor.project.instructions,
instructions.permitOverride, and any steps already loaded into the
slice (e.g. by WebComponentLoader for pre-authored lessons). Nothing
is dispatched to derive it, so InstructionsPanel and ProgressBar can
both read the same selector without WebComponentProject needing to
run first or re-run on every edit.

Alternatively the sync logic could have moved into InstructionsPanel
directly, but ProgressBar (and any future reader) needs the same
derived steps, so a shared selector avoids duplicating the branching
logic per component.
Previously InstructionsPanel mixed two concerns in one component: the
surrounding UI (buttons, tabs, empty state, progress bar) and current
step management, alongside the actual rendering of a step's HTML
(markdown conversion, syntax highlighting, scratchblocks, quiz-ready
signalling) into a ref'd DOM node. That made the file large and meant
quiz questions were rendered through ad-hoc branching rather than a
reusable path.

This change extracts InstructionsStep, a component responsible only
for rendering a single step (or a quiz question, passed in the same
{content} shape) into its own DOM node. InstructionsPanel now computes
which step to show (the real current step, or a synthesized
{content: quiz.questions[...]} step while a quiz is active) and passes
it down, keeping its own responsibility to managing state and the
current step.

This also let two pieces of incidental complexity go: react-tabs
mounts each InstructionsStep instance fresh when its tab is selected,
so the instructionsTab dependency previously needed to force the
content effect to re-run is no longer necessary, and Prism's one-time
config now lives with the only component that renders highlighted code.

Splitting out a dedicated step component also sets up the next step:
editing a single instruction, rather than the whole project
instructions string, will live in InstructionsStep rather than
requiring changes to the surrounding panel.

Test coverage for step rendering (markdown conversion, syntax
highlighting, scratchblocks, quiz-ready signalling) moved to a new
InstructionsStep.test.jsx; InstructionsPanel.test.jsx keeps a mix of
panel-level tests plus one representative example of each moved
behaviour to confirm the wiring still works end-to-end.
Sidebar decided whether to show the instructions menu option by
reading state.instructions.project.steps directly, rather than
through selectInstructionSteps. That state is only populated when
steps are loaded pre-authored (e.g. by WebComponentLoader); for a
single-page project whose instructions are a markdown string in
editor.project.instructions, it stays empty, so Sidebar concluded
there were no instructions and hid the panel entirely.

This change points Sidebar at selectInstructionSteps, the same
derived source InstructionsPanel and ProgressBar already use, so all
three agree on whether instructions exist. Also hardened the selector
itself with optional chaining on state.instructions, since several
existing tests render Sidebar/Project/MobileProject without an
instructions slice in their mock store at all.

Added a regression test reproducing the exact scenario (markdown
string instructions, default permitOverride) and confirmed it fails
without the Sidebar fix and passes with it.
Authors could only edit instructions as one big markdown string, with
no way to create or delete steps. The edit tab now targets the
currently-open step's markdown_content, and Add step/Remove step
buttons (below the pager, always visible while editing so authors can
grow past a single step) insert or remove steps relative to the
current position. Removing the last step falls back to the existing
empty state rather than needing special-case handling.

This moves the saved shape of project.instructions from a single
string to an array of step objects once any editing happens; the read
side already supported this via selectInstructionSteps.
INSTRUCTIONS.md zipped project.instructions verbatim, assuming a
single markdown string. Now that editing produces an array of step
objects, join each step's markdown_content so downloaded projects
still get a readable INSTRUCTIONS.md.
Adds spec-wc-instructions.cy.js: loads a project with existing
multi-step markdown instructions (owned by the logged-in user so
autosave is active), adds a step and edits it, then asserts the
autosaved PUT body contains the full instructions array with the new
step positioned after the one that was open — the actual contract the
project saver now needs to uphold.

Verified locally by running Cypress inside the docker-compose app
container (installed Xvfb + GTK there for headless Electron), since
the host's cached Cypress binary wasn't runnable in this environment.
@zetter-rpf
zetter-rpf temporarily deployed to previews/1582/merge August 7, 2026 15:25 — with GitHub Actions Inactive
Neither field is read anywhere for markdown steps (quiz is only
meaningful as the separate quiz-questions object in
state.instructions.quiz, and title is never read at all). New steps
now only carry markdown_content.
Base automatically changed from instructions-refactor to main August 10, 2026 09:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant