2 Commits
Author SHA1 Message Date
Tyler 20d69619c2 fix: preserve block editor state on save
Template tests / tests (push) Successful in 1m41s
2026-06-13 15:51:23 -05:00
Tyler 2274544ed8 Keep block reorder in place 2026-06-13 15:29:44 -05:00
+77 -16
View File
@@ -529,13 +529,42 @@ class GuideEditor {
this.pendingSave = true;
this.saveStepDebounced();
};
const findBlockCard = (kind, id) => {
for (const card of this.dom.blocksList.querySelectorAll('.block-card[data-block-id]')) {
if (card.dataset.blockKind === kind && card.dataset.blockId === id) return card;
}
return null;
};
const refreshBlockCard = (entry, index, total) => {
const card = findBlockCard(entry.kind, entry.block.id);
if (!card) return false;
const orderLabel = card.querySelector('[data-block-order]');
const moveUpBtn = card.querySelector('[data-block-move="up"]');
const moveDownBtn = card.querySelector('[data-block-move="down"]');
if (orderLabel) {
orderLabel.textContent = `#${Number.isFinite(entry.block.order) ? entry.block.order : index + 1}`;
}
if (moveUpBtn) moveUpBtn.disabled = index === 0;
if (moveDownBtn) moveDownBtn.disabled = index === total - 1;
this.dom.blocksList.append(card);
return true;
};
const reflowBlockCards = () => {
const blocksNow = orderedStepBlocks(step);
for (const [index, entry] of blocksNow.entries()) {
if (!refreshBlockCard(entry, index, blocksNow.length)) {
this.renderBlocksPanel();
return;
}
}
};
const moveBlock = (source, target) => {
if (!source || !target || source.kind === target.kind && source.block.id === target.block.id) return;
const swap = source.block.order;
source.block.order = target.block.order;
target.block.order = swap;
save();
this.renderBlocksPanel();
reflowBlockCards();
};
const removeBtn = (onRemove) => el('button.icon.danger', {
type: 'button', title: 'Remove block',
@@ -547,15 +576,25 @@ class GuideEditor {
const { kind, block } = entry;
const canMoveUp = index > 0;
const canMoveDown = index < blocks.length - 1;
const moveUp = () => moveBlock(entry, blocks[index - 1]);
const moveDown = () => moveBlock(entry, blocks[index + 1]);
const moveUp = () => {
const currentBlocks = orderedStepBlocks(step);
const currentIndex = currentBlocks.findIndex((item) => item.kind === kind && item.block.id === block.id);
if (currentIndex > 0) moveBlock(currentBlocks[currentIndex], currentBlocks[currentIndex - 1]);
};
const moveDown = () => {
const currentBlocks = orderedStepBlocks(step);
const currentIndex = currentBlocks.findIndex((item) => item.kind === kind && item.block.id === block.id);
if (currentIndex >= 0 && currentIndex < currentBlocks.length - 1) {
moveBlock(currentBlocks[currentIndex], currentBlocks[currentIndex + 1]);
}
};
const header = el('div.row', {},
el('strong', {}, blockLabel(kind)),
el('span.muted', {}, `#${Number.isFinite(block.order) ? block.order : index + 1}`),
el('span.muted', { dataset: { blockOrder: 'true' } }, `#${Number.isFinite(block.order) ? block.order : index + 1}`),
el('span.spacer'),
el('button.icon', { type: 'button', title: 'Move block up', disabled: !canMoveUp, onClick: moveUp }, '↑'),
el('button.icon', { type: 'button', title: 'Move block down', disabled: !canMoveDown, onClick: moveDown }, '↓'),
el('button.icon', { type: 'button', title: 'Move block up', disabled: !canMoveUp, onClick: moveUp, dataset: { blockMove: 'up' } }, '↑'),
el('button.icon', { type: 'button', title: 'Move block down', disabled: !canMoveDown, onClick: moveDown, dataset: { blockMove: 'down' } }, '↓'),
removeBtn(() => {
if (kind === 'text') step.textBlocks = (step.textBlocks || []).filter((b) => b !== block);
else if (kind === 'code') step.codeBlocks = (step.codeBlocks || []).filter((b) => b !== block);
@@ -1062,9 +1101,7 @@ class GuideEditor {
size: record.image.size,
step,
});
this.stepMap.set(saved.stepId, saved);
const idx = this.steps.findIndex((s) => s.stepId === saved.stepId);
if (idx >= 0) this.steps[idx] = saved;
this.commitSavedStep(saved);
} else {
await this.flushStep(step);
}
@@ -1100,14 +1137,11 @@ class GuideEditor {
if (!step) return;
this.pendingSave = false;
const saved = await api.step.save({ guideId: this.guideId, step });
this.stepMap.set(saved.stepId, saved);
// Keep the steps array in sync — it holds the objects the list renders.
const idx = this.steps.findIndex((s) => s.stepId === saved.stepId);
if (idx >= 0) this.steps[idx] = saved;
if (this.selectedStepId === saved.stepId) {
const committed = this.commitSavedStep(saved);
if (this.selectedStepId === committed.stepId) {
this.renderStepList();
this.syncStepFields();
this.canvas.setAnnotations(saved.annotations || []);
this.canvas.setAnnotations(committed.annotations || []);
// Rebuilding the annotation editor while the user is typing in one of
// its inputs would steal focus, so skip it in that case.
if (!this.dom.annotationEditor.contains(document.activeElement)) {
@@ -1115,7 +1149,34 @@ class GuideEditor {
}
this.emitMeta();
}
return saved;
return committed;
}
commitSavedStep(saved) {
if (!saved || !saved.stepId) return saved;
const existing = this.stepMap.get(saved.stepId);
if (!existing || existing === saved) {
this.stepMap.set(saved.stepId, saved);
const idx = this.steps.findIndex((s) => s.stepId === saved.stepId);
if (idx >= 0) this.steps[idx] = saved;
return saved;
}
// Keep the live block arrays so block-card closures keep pointing at the
// same objects after the backend returns a fresh saved step.
const preservedBlocks = {
textBlocks: existing.textBlocks,
codeBlocks: existing.codeBlocks,
tableBlocks: existing.tableBlocks,
};
for (const key of Object.keys(existing)) {
if (!(key in saved)) delete existing[key];
}
Object.assign(existing, saved, preservedBlocks);
this.stepMap.set(existing.stepId, existing);
const idx = this.steps.findIndex((s) => s.stepId === existing.stepId);
if (idx >= 0) this.steps[idx] = existing;
return existing;
}
async flushGuide() {