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;