Skip to content

Commit 78277d7

Browse files
committed
feat: make snapshot creation optional so YHub history drives the versioning sidebar
Make `create` an optional VersioningEndpoints method and expose a `canCreateSnapshot` flag. When unsupported, the default CurrentSnapshot UI renders a plain 'Current Version' row instead of a name input + Save button. YHub now omits `create` (its activity timeline is the source of truth) and inlines its pre-restore PATCH /ydoc backup into `restore`.
1 parent 1f24337 commit 78277d7

9 files changed

Lines changed: 101 additions & 94 deletions

File tree

examples/07-collaboration/12-multi-doc-versioning/src/DocumentEditor.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,17 @@ export function DocumentEditor({
173173
}),
174174
);
175175

176-
// Load existing snapshots on mount so pre-seeded versions show up
176+
// The version history is derived entirely from YHub's activity timeline.
177+
// Fetch it once on mount so the sidebar reflects the server's history rather
178+
// than only changes made during this session.
177179
const versioning = useExtension(VersioningExtension, { editor });
178180
useEffect(() => {
179-
versioning.listSnapshots();
181+
const interval = setInterval(() => {
182+
versioning.listSnapshots();
183+
}, 10000);
184+
return () => {
185+
clearInterval(interval);
186+
};
180187
}, [versioning]);
181188

182189
const { previewedSnapshotId } = useExtensionState(VersioningExtension, {

examples/07-collaboration/12-multi-doc-versioning/src/HistorySidebar.tsx

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,15 @@
11
import { VersioningSidebar } from "@blocknote/react";
2-
import { useState } from "react";
32

43
export function HistorySidebar() {
5-
const [filter, setFilter] = useState<"named" | "all">("all");
6-
74
return (
85
<aside className="history-sidebar">
96
<div className="history-header">
107
<span className="history-title">History</span>
11-
<div className="history-filter">
12-
<button
13-
className={
14-
"history-filter-btn" + (filter === "all" ? " active" : "")
15-
}
16-
onClick={() => setFilter("all")}
17-
>
18-
All
19-
</button>
20-
<button
21-
className={
22-
"history-filter-btn" + (filter === "named" ? " active" : "")
23-
}
24-
onClick={() => setFilter("named")}
25-
>
26-
Named
27-
</button>
28-
</div>
298
</div>
309
<div className="history-content">
31-
<VersioningSidebar filter={filter} />
10+
{/* YHub derives the version list from its activity timeline and has no
11+
concept of named snapshots, so there's no "Named" filter to offer. */}
12+
<VersioningSidebar />
3213
</div>
3314
</aside>
3415
);

packages/core/src/extensions/Versioning/Versioning.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ function setup(opts?: {
8282
const savedBlocks = editor.document;
8383
setEditorText(editor, text);
8484
const blocks = editor.document;
85-
const snapshot = await endpoints.create(blocks, { name });
85+
const snapshot = await endpoints.create!(blocks, { name });
8686
// Restore original text.
8787
editor.replaceBlocks(editor.document, savedBlocks);
8888
return snapshot;
@@ -123,7 +123,7 @@ describe("VersioningExtension", () => {
123123
vi.useFakeTimers();
124124

125125
// Seed snapshots with distinct timestamps directly via endpoints.
126-
await ctx.endpoints.create([
126+
await ctx.endpoints.create!([
127127
{
128128
id: "1",
129129
type: "paragraph" as const,
@@ -133,7 +133,7 @@ describe("VersioningExtension", () => {
133133
},
134134
]);
135135
vi.advanceTimersByTime(1000);
136-
await ctx.endpoints.create([
136+
await ctx.endpoints.create!([
137137
{
138138
id: "2",
139139
type: "paragraph" as const,
@@ -143,7 +143,7 @@ describe("VersioningExtension", () => {
143143
},
144144
]);
145145
vi.advanceTimersByTime(1000);
146-
await ctx.endpoints.create([
146+
await ctx.endpoints.create!([
147147
{
148148
id: "3",
149149
type: "paragraph" as const,
@@ -167,7 +167,7 @@ describe("VersioningExtension", () => {
167167
it("reflects backend changes on subsequent calls", async () => {
168168
expect(await ctx.ext.listSnapshots()).toEqual([]);
169169

170-
await ctx.endpoints.create([
170+
await ctx.endpoints.create!([
171171
{
172172
id: "1",
173173
type: "paragraph" as const,
@@ -190,7 +190,7 @@ describe("VersioningExtension", () => {
190190
it("captures the current state and adds the snapshot to the store", async () => {
191191
setEditorText(ctx.editor, "my document content");
192192

193-
const snapshot = await ctx.ext.createSnapshot({ name: "Draft 1" });
193+
const snapshot = await ctx.ext.createSnapshot!({ name: "Draft 1" });
194194

195195
expect(snapshot.name).toBe("Draft 1");
196196
expect(snapshot.id).toBeDefined();
@@ -211,7 +211,7 @@ describe("VersioningExtension", () => {
211211
// List so the store knows about the seeded snapshot.
212212
await ctx.ext.listSnapshots();
213213

214-
const newer = await ctx.ext.createSnapshot({ name: "Newer" });
214+
const newer = await ctx.ext.createSnapshot!({ name: "Newer" });
215215

216216
expect(ctx.ext.store.state.snapshots[0]!.id).toBe(newer.id);
217217
expect(ctx.ext.store.state.snapshots[1]!.id).toBe(old.id);
@@ -353,13 +353,13 @@ describe("VersioningExtension", () => {
353353

354354
// 1. Create version 1.
355355
setEditorText(ctx.editor, "doc v1");
356-
const v1 = await ctx.ext.createSnapshot({ name: "Version 1" });
356+
const v1 = await ctx.ext.createSnapshot!({ name: "Version 1" });
357357

358358
vi.advanceTimersByTime(1000);
359359

360360
// 2. Modify and create version 2.
361361
setEditorText(ctx.editor, "doc v2");
362-
const v2 = await ctx.ext.createSnapshot({ name: "Version 2" });
362+
const v2 = await ctx.ext.createSnapshot!({ name: "Version 2" });
363363
expect(ctx.ext.store.state.snapshots[0]!.id).toBe(v2.id);
364364

365365
// 3. Preview v1 with diff comparison against v2.

packages/core/src/extensions/Versioning/Versioning.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,12 @@ export interface VersioningEndpoints<I = any, O = any, A = any> {
8383
list: () => Promise<VersionSnapshot[]>;
8484
/**
8585
* Create a new snapshot for this document with the current content.
86+
*
87+
* @note if not provided, the UI will not offer a way to save a new snapshot.
88+
* This is appropriate for backends that record continuous history rather than
89+
* discrete, user-created snapshots (e.g. YHub's activity timeline).
8690
*/
87-
create: (
91+
create?: (
8892
fragment: I,
8993
options?: CreateSnapshotOptions,
9094
) => Promise<VersionSnapshot>;
@@ -270,16 +274,23 @@ export const VersioningExtension = createExtension(
270274
await updateSnapshots();
271275
return store.state.snapshots;
272276
},
273-
createSnapshot: async (
274-
options?: CreateSnapshotOptions,
275-
): Promise<VersionSnapshot> => {
276-
const snapshot = await endpoints.create(getCurrentState(), options);
277-
store.setState((state) => ({
278-
...state,
279-
snapshots: sortSnapshotsNewestFirst([...state.snapshots, snapshot]),
280-
}));
281-
return snapshot;
282-
},
277+
canCreateSnapshot: endpoints.create !== undefined,
278+
createSnapshot: endpoints.create
279+
? async (options?: CreateSnapshotOptions): Promise<VersionSnapshot> => {
280+
const snapshot = await endpoints.create!(
281+
getCurrentState(),
282+
options,
283+
);
284+
store.setState((state) => ({
285+
...state,
286+
snapshots: sortSnapshotsNewestFirst([
287+
...state.snapshots,
288+
snapshot,
289+
]),
290+
}));
291+
return snapshot;
292+
}
293+
: undefined,
283294
canRestoreSnapshot: endpoints.restore !== undefined,
284295
restoreSnapshot: endpoints.restore
285296
? async (id: string) => {

packages/core/src/extensions/Versioning/inMemoryVersioning.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ describe("createInMemoryVersioningEndpoints", () => {
5454
},
5555
];
5656

57-
const snap = await endpoints.create(blocks, { name: "v1" });
57+
const snap = await endpoints.create!(blocks, { name: "v1" });
5858
expect(snap.name).toBe("v1");
5959
expect(snap.id).toBeDefined();
6060

@@ -69,7 +69,7 @@ describe("createInMemoryVersioningEndpoints", () => {
6969
try {
7070
const endpoints = createInMemoryVersioningEndpoints();
7171

72-
const s1 = await endpoints.create([
72+
const s1 = await endpoints.create!([
7373
{
7474
id: "1",
7575
type: "paragraph" as const,
@@ -79,7 +79,7 @@ describe("createInMemoryVersioningEndpoints", () => {
7979
},
8080
]);
8181
vi.advanceTimersByTime(1000);
82-
const s2 = await endpoints.create([
82+
const s2 = await endpoints.create!([
8383
{
8484
id: "2",
8585
type: "paragraph" as const,
@@ -109,7 +109,7 @@ describe("createInMemoryVersioningEndpoints", () => {
109109
children: [],
110110
},
111111
];
112-
const snap = await endpoints.create(original);
112+
const snap = await endpoints.create!(original);
113113

114114
const currentDoc = [
115115
{
@@ -137,7 +137,7 @@ describe("createInMemoryVersioningEndpoints", () => {
137137

138138
it("updates snapshot name", async () => {
139139
const endpoints = createInMemoryVersioningEndpoints();
140-
const snap = await endpoints.create(
140+
const snap = await endpoints.create!(
141141
[
142142
{
143143
id: "1",
@@ -266,14 +266,14 @@ describe("VersioningExtension + in-memory adapter", () => {
266266
const ext = VersioningExtension(adapter)({ editor });
267267

268268
// 1. Create a snapshot of "initial doc"
269-
const snap1 = await ext.createSnapshot({ name: "v1" });
269+
const snap1 = await ext.createSnapshot!({ name: "v1" });
270270
expect(snap1.name).toBe("v1");
271271

272272
// 2. Modify the document
273273
setEditorText(editor, "modified doc");
274274

275275
// 3. Create another snapshot
276-
await ext.createSnapshot({ name: "v2" });
276+
await ext.createSnapshot!({ name: "v2" });
277277

278278
// 4. List — both present
279279
const list = await ext.listSnapshots();
@@ -309,9 +309,9 @@ describe("VersioningExtension + in-memory adapter", () => {
309309
const adapter = createInMemoryVersioningAdapter(editor);
310310
const ext = VersioningExtension(adapter)({ editor });
311311

312-
const snap1 = await ext.createSnapshot({ name: "baseline" });
312+
const snap1 = await ext.createSnapshot!({ name: "baseline" });
313313
setEditorText(editor, "changed doc");
314-
const snap2 = await ext.createSnapshot({ name: "current" });
314+
const snap2 = await ext.createSnapshot!({ name: "current" });
315315

316316
// Preview snap2 compared to snap1. The in-memory preview controller
317317
// ignores the compareTo content (no diff rendering), but the call should
@@ -327,7 +327,7 @@ describe("VersioningExtension + in-memory adapter", () => {
327327
const adapter = createInMemoryVersioningAdapter(editor);
328328
const ext = VersioningExtension(adapter)({ editor });
329329

330-
const snap = await ext.createSnapshot({ name: "draft" });
330+
const snap = await ext.createSnapshot!({ name: "draft" });
331331
await ext.updateSnapshotName!(snap.id, "final");
332332

333333
// Store was updated optimistically

packages/core/src/y/extensions/Versioning.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ describe("Yjs versioning integration (VersioningExtension + in-memory endpoints)
270270
ctx.editor.replaceBlocks(ctx.editor.document, [
271271
{ type: "paragraph", content: "Snapshot content" },
272272
]);
273-
const snapshot = await versioning.createSnapshot({ name: "v1" });
273+
const snapshot = await versioning.createSnapshot!({ name: "v1" });
274274

275275
ctx.editor.replaceBlocks(ctx.editor.document, [
276276
{ type: "paragraph", content: "Current content" },
@@ -290,7 +290,7 @@ describe("Yjs versioning integration (VersioningExtension + in-memory endpoints)
290290
ctx.editor.replaceBlocks(ctx.editor.document, [
291291
{ type: "paragraph", content: "Saved state" },
292292
]);
293-
const snapshot = await versioning.createSnapshot({ name: "v1" });
293+
const snapshot = await versioning.createSnapshot!({ name: "v1" });
294294

295295
ctx.editor.replaceBlocks(ctx.editor.document, [
296296
{ type: "paragraph", content: "Live state" },
@@ -310,12 +310,12 @@ describe("Yjs versioning integration (VersioningExtension + in-memory endpoints)
310310
ctx.editor.replaceBlocks(ctx.editor.document, [
311311
{ type: "paragraph", content: "Version 1" },
312312
]);
313-
const v1 = await versioning.createSnapshot({ name: "v1" });
313+
const v1 = await versioning.createSnapshot!({ name: "v1" });
314314

315315
ctx.editor.replaceBlocks(ctx.editor.document, [
316316
{ type: "paragraph", content: "Version 2" },
317317
]);
318-
const v2 = await versioning.createSnapshot({ name: "v2" });
318+
const v2 = await versioning.createSnapshot!({ name: "v2" });
319319

320320
ctx.editor.replaceBlocks(ctx.editor.document, [
321321
{ type: "paragraph", content: "Current state" },
@@ -345,7 +345,7 @@ describe("Yjs versioning integration (VersioningExtension + in-memory endpoints)
345345
ctx.editor.replaceBlocks(ctx.editor.document, [
346346
{ type: "paragraph", content: "Content" },
347347
]);
348-
const snap = await versioning.createSnapshot({ name: "v1" });
348+
const snap = await versioning.createSnapshot!({ name: "v1" });
349349

350350
// applyRestore is a no-op for the Yjs adapter (the backend applies the
351351
// restore and the change propagates over live sync), so restoreSnapshot
@@ -362,17 +362,17 @@ describe("Yjs versioning integration (VersioningExtension + in-memory endpoints)
362362
ctx.editor.replaceBlocks(ctx.editor.document, [
363363
{ type: "paragraph", content: "Version 1" },
364364
]);
365-
const v1 = await versioning.createSnapshot({ name: "v1" });
365+
const v1 = await versioning.createSnapshot!({ name: "v1" });
366366

367367
ctx.editor.replaceBlocks(ctx.editor.document, [
368368
{ type: "paragraph", content: "Version 2" },
369369
]);
370-
const v2 = await versioning.createSnapshot({ name: "v2" });
370+
const v2 = await versioning.createSnapshot!({ name: "v2" });
371371

372372
ctx.editor.replaceBlocks(ctx.editor.document, [
373373
{ type: "paragraph", content: "Version 3" },
374374
]);
375-
await versioning.createSnapshot({ name: "v3" });
375+
await versioning.createSnapshot!({ name: "v3" });
376376

377377
ctx.editor.replaceBlocks(ctx.editor.document, [
378378
{ type: "paragraph", content: "Current live" },

0 commit comments

Comments
 (0)