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