diff --git a/lib/community-sync.ts b/lib/community-sync.ts index 9889202..5d72303 100644 --- a/lib/community-sync.ts +++ b/lib/community-sync.ts @@ -1435,6 +1435,33 @@ export class CommunitySync extends EventTarget { 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. */ diff --git a/modules/rflows/components/folk-flow-river.ts b/modules/rflows/components/folk-flow-river.ts index e049b69..e8831d0 100644 --- a/modules/rflows/components/folk-flow-river.ts +++ b/modules/rflows/components/folk-flow-river.ts @@ -517,7 +517,7 @@ function renderBranch(b: BranchLayout): string { ` : ""; // 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; return `${outerStroke}${innerStroke}${waterFlow} diff --git a/scripts/test-full-loop.ts b/scripts/test-full-loop.ts index 46e9cc3..1825df1 100644 --- a/scripts/test-full-loop.ts +++ b/scripts/test-full-loop.ts @@ -30,7 +30,7 @@ async function step(name: string, fn: () => Promise) { return result; } catch (err: any) { console.log("ERROR", err.message); - return { ok: false, error: err.message }; + return { ok: false, error: err.message } as StepResult; } } diff --git a/shared/components/rstack-history-panel.ts b/shared/components/rstack-history-panel.ts index 54b0424..bf07cbe 100644 --- a/shared/components/rstack-history-panel.ts +++ b/shared/components/rstack-history-panel.ts @@ -413,6 +413,7 @@ export class RStackHistoryPanel extends HTMLElement { ` : ""} + `; // Update the label @@ -446,6 +447,18 @@ export class RStackHistoryPanel extends HTMLElement { 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 ── @@ -743,6 +756,21 @@ const PANEL_CSS = ` 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 { padding: 12px 20px; diff --git a/website/canvas.html b/website/canvas.html index d40feb0..da0c701 100644 --- a/website/canvas.html +++ b/website/canvas.html @@ -3065,6 +3065,12 @@ const historyPanel = document.querySelector('rstack-history-panel'); 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 // UI handlers below register immediately regardless of this outcome (async () => {