Mass guide editor changes #18

Merged
Tyler merged 25 commits from guide_editor_changes into main 2026-06-15 15:37:48 +00:00
Showing only changes of commit a58607f029 - Show all commits
+61
View File
@@ -731,6 +731,7 @@ class GuideEditor {
this.selectStep(step.stepId); this.selectStep(step.stepId);
contextMenu(e.clientX, e.clientY, [ contextMenu(e.clientX, e.clientY, [
{ label: 'Add substep', action: () => this.addSubstep(step.stepId) }, { label: 'Add substep', action: () => this.addSubstep(step.stepId) },
{ label: 'Make substep of…', action: () => this.makeSubstepOf(step.stepId) },
{ label: 'Duplicate step', action: () => this.duplicateSelectedStep() }, { label: 'Duplicate step', action: () => this.duplicateSelectedStep() },
'sep', 'sep',
{ label: 'Move up', action: () => this.moveSelectedStep(-1) }, { label: 'Move up', action: () => this.moveSelectedStep(-1) },
@@ -1289,6 +1290,66 @@ class GuideEditor {
this.onToast(parent ? 'Substep added.' : 'Step added.'); 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() { async duplicateSelectedStep() {
const step = this.currentStep; const step = this.currentStep;
if (!step) return; if (!step) return;