Mass guide editor changes #18
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user