feat(history): add "Revert to this point" button in Time Machine panel
Forward Automerge change that overwrites content fields with snapshot data, preserving meta and full history. Also fixes pre-existing TS errors in folk-flow-river (undefined exitX) and test-full-loop (StepResult type). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
47a3fe32fb
commit
aa4a200f32
|
|
@ -1435,6 +1435,33 @@ export class CommunitySync extends EventTarget {
|
||||||
return Automerge.view(this.#doc, heads);
|
return Automerge.view(this.#doc, heads);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revert document content to the state at a given change hash.
|
||||||
|
* Creates a forward change (preserving full history) and syncs to peers.
|
||||||
|
* Meta (space name/slug/config) is preserved — only content is reverted.
|
||||||
|
*/
|
||||||
|
revertToHash(hash: string): void {
|
||||||
|
const snapshot = Automerge.view(this.#doc, [hash]);
|
||||||
|
const data = JSON.parse(JSON.stringify(snapshot)) as CommunityDoc;
|
||||||
|
|
||||||
|
const newDoc = Automerge.change(this.#doc, makeChangeMessage(`Revert to change ${hash.slice(0, 8)}`), (doc) => {
|
||||||
|
doc.shapes = data.shapes || {};
|
||||||
|
if (data.layers) doc.layers = data.layers;
|
||||||
|
if (data.flows) doc.flows = data.flows;
|
||||||
|
if (data.connections) doc.connections = data.connections;
|
||||||
|
if (data.groups) doc.groups = data.groups;
|
||||||
|
if (data.nestedSpaces) doc.nestedSpaces = data.nestedSpaces;
|
||||||
|
if (data.activeLayerId !== undefined) doc.activeLayerId = data.activeLayerId;
|
||||||
|
if (data.layerViewMode !== undefined) doc.layerViewMode = data.layerViewMode;
|
||||||
|
if (data.commentPins) doc.commentPins = data.commentPins;
|
||||||
|
// Preserve meta — don't revert space name/slug/config
|
||||||
|
});
|
||||||
|
|
||||||
|
this._applyDocChange(newDoc);
|
||||||
|
this.#undoStack.length = 0;
|
||||||
|
this.#redoStack.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get parsed history entries for the activity feed.
|
* Get parsed history entries for the activity feed.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -517,7 +517,7 @@ function renderBranch(b: BranchLayout): string {
|
||||||
<path d="${spinePath}" fill="none" stroke="white" stroke-width="1.5" opacity="0.2" stroke-dasharray="4 12" style="animation:riverCurrent 1s linear infinite;animation-delay:-0.3s"/>` : "";
|
<path d="${spinePath}" fill="none" stroke="white" stroke-width="1.5" opacity="0.2" stroke-dasharray="4 12" style="animation:riverCurrent 1s linear infinite;animation-delay:-0.3s"/>` : "";
|
||||||
|
|
||||||
// Midpoint for label
|
// Midpoint for label
|
||||||
const midX = (b.x1 + exitX + b.x2) / 3;
|
const midX = (b.x1 + b.x2) / 2;
|
||||||
const midY = (b.y1 + b.y2) / 2 - 12;
|
const midY = (b.y1 + b.y2) / 2 - 12;
|
||||||
|
|
||||||
return `${outerStroke}${innerStroke}${waterFlow}
|
return `${outerStroke}${innerStroke}${waterFlow}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ async function step(name: string, fn: () => Promise<StepResult>) {
|
||||||
return result;
|
return result;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.log("ERROR", err.message);
|
console.log("ERROR", err.message);
|
||||||
return { ok: false, error: err.message };
|
return { ok: false, error: err.message } as StepResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -413,6 +413,7 @@ export class RStackHistoryPanel extends HTMLElement {
|
||||||
</div>
|
</div>
|
||||||
` : ""}
|
` : ""}
|
||||||
</div>
|
</div>
|
||||||
|
<button class="revert-btn" id="revert-btn">Revert to this point</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Update the label
|
// Update the label
|
||||||
|
|
@ -446,6 +447,18 @@ export class RStackHistoryPanel extends HTMLElement {
|
||||||
this._onScrub(parseInt(scrubber.value, 10));
|
this._onScrub(parseInt(scrubber.value, 10));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Revert to history point
|
||||||
|
sr.getElementById("revert-btn")?.addEventListener("click", () => {
|
||||||
|
const entry = this._entries[this._timeMachineIndex];
|
||||||
|
if (!entry?.hash) return;
|
||||||
|
if (!confirm(`Revert to change from ${new Date(entry.time).toLocaleString()} by ${entry.username}?\n\nThis creates a new change that restores the document to that state.`)) return;
|
||||||
|
this.dispatchEvent(new CustomEvent("revert-requested", {
|
||||||
|
bubbles: true, composed: true,
|
||||||
|
detail: { hash: entry.hash, index: this._timeMachineIndex },
|
||||||
|
}));
|
||||||
|
this.close();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers ──
|
// ── Helpers ──
|
||||||
|
|
@ -743,6 +756,21 @@ const PANEL_CSS = `
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.revert-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
color: #ef4444;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.revert-btn:hover { background: rgba(239, 68, 68, 0.2); }
|
||||||
|
|
||||||
/* Load more */
|
/* Load more */
|
||||||
.load-more {
|
.load-more {
|
||||||
padding: 12px 20px;
|
padding: 12px 20px;
|
||||||
|
|
|
||||||
|
|
@ -3065,6 +3065,12 @@
|
||||||
const historyPanel = document.querySelector('rstack-history-panel');
|
const historyPanel = document.querySelector('rstack-history-panel');
|
||||||
if (historyPanel) historyPanel.setDoc(sync.doc);
|
if (historyPanel) historyPanel.setDoc(sync.doc);
|
||||||
|
|
||||||
|
// Wire history revert
|
||||||
|
document.querySelector("rstack-history-panel")?.addEventListener("revert-requested", (e) => {
|
||||||
|
const { hash } = (e as CustomEvent).detail;
|
||||||
|
if (hash) sync.revertToHash(hash);
|
||||||
|
});
|
||||||
|
|
||||||
// Non-blocking: open IndexedDB + load cache in background
|
// Non-blocking: open IndexedDB + load cache in background
|
||||||
// UI handlers below register immediately regardless of this outcome
|
// UI handlers below register immediately regardless of this outcome
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue