From a58607f0299b7b69a5e09289bb3224be90681933 Mon Sep 17 00:00:00 2001 From: Twest2 Date: Sat, 13 Jun 2026 20:23:31 -0500 Subject: [PATCH] =?UTF-8?q?Add=20a=20"Make=20substep=20of=E2=80=A6"=20opti?= =?UTF-8?q?on=20to=20the=20step=20context=20menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lets a step be re-parented under another step by entering that step's displayed number, moving it (with its own substeps) to the end of the target's substeps and updating the numbering accordingly. --- app/renderer/editor.js | 61 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/app/renderer/editor.js b/app/renderer/editor.js index 4d6c289..8453e9b 100644 --- a/app/renderer/editor.js +++ b/app/renderer/editor.js @@ -731,6 +731,7 @@ class GuideEditor { this.selectStep(step.stepId); contextMenu(e.clientX, e.clientY, [ { label: 'Add substep', action: () => this.addSubstep(step.stepId) }, + { label: 'Make substep of…', action: () => this.makeSubstepOf(step.stepId) }, { label: 'Duplicate step', action: () => this.duplicateSelectedStep() }, 'sep', { label: 'Move up', action: () => this.moveSelectedStep(-1) }, @@ -1289,6 +1290,66 @@ class GuideEditor { this.onToast(parent ? 'Substep added.' : 'Step added.'); } + /** All step ids whose ancestor chain leads back to `stepId`. */ + getStepDescendantIds(stepId) { + const result = []; + const queue = [stepId]; + while (queue.length) { + const current = queue.shift(); + for (const s of this.steps) { + if (s.parentStepId === current) { + result.push(s.stepId); + queue.push(s.stepId); + } + } + } + return result; + } + + async makeSubstepOf(stepId) { + const step = this.stepMap.get(stepId); + if (!step) return; + const numbers = stepNumberMap(this.steps); + const input = await dialogs.promptText({ + title: 'Make substep', + label: 'Make substep of step #', + placeholder: 'e.g. 2', + }); + if (input == null) return; + const target = input.trim().replace(/^#/, ''); + const targetId = this.steps.find((s) => numbers.get(s.stepId) === target)?.stepId; + if (!targetId) { + this.onToast(`No step numbered "${target}".`, { error: true }); + return; + } + const subtreeIds = new Set([stepId, ...this.getStepDescendantIds(stepId)]); + if (subtreeIds.has(targetId)) { + this.onToast('A step cannot be made a substep of itself or one of its own substeps.', { error: true }); + return; + } + if (step.parentStepId === targetId) { + this.onToast(`“${step.title || 'Untitled step'}” is already a substep of step ${target}.`); + return; + } + + step.parentStepId = targetId; + await api.step.save({ guideId: this.guideId, step }); + + // Move the step (with its own substeps) to sit right after the target + // step's existing substeps, so it becomes the target's last substep. + const order = this.steps.map((s) => s.stepId); + const remaining = order.filter((id) => !subtreeIds.has(id)); + const targetSubtree = new Set([targetId, ...this.getStepDescendantIds(targetId)]); + let insertAt = remaining.indexOf(targetId) + 1; + while (insertAt < remaining.length && targetSubtree.has(remaining[insertAt])) insertAt++; + const movedBlock = order.filter((id) => subtreeIds.has(id)); + remaining.splice(insertAt, 0, ...movedBlock); + await api.step.reorder({ guideId: this.guideId, order: remaining }); + + await this.reload(stepId); + this.onToast(`“${step.title || 'Untitled step'}” is now a substep of step ${target}.`); + } + async duplicateSelectedStep() { const step = this.currentStep; if (!step) return;