From 6ce4322a24f165db9b0fc5070771c21910115dc8 Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 8 Oct 2026 16:13:28 +0200 Subject: [PATCH 01/10] fix(versioning): don't attribute lost content to whoever removed it (experimental) Behind `experimental.lostContentAttribution` in the collaboration options; it only changes what a version diff shows. With it, versions are decoded keeping deleted content, so content lost with a concurrently deleted block, or with a moved block's lost copy, shows as deleted without an author instead of being credited to the deleter or the mover. Content inserted and deleted between the two versions stays hidden. Also, on by default: a change without a known author shows "Deleted" or "Inserted" when hovered, and a change to only a block's id isn't shown. --- .../14-suggestion-gallery/src/App.tsx | 2 + .../14-suggestion-gallery/src/scenarios.ts | 4 +- packages/core/src/y/extensions/Versioning.ts | 2 + packages/core/src/y/extensions/index.ts | 9 + .../core/src/y/extensions/snapshotCodec.ts | 9 +- .../core/src/y/extensions/snapshotPreview.ts | 143 +++++- .../extensions/versionDiffAttribution.test.ts | 13 +- .../__snapshots__/versioning.test.tsx.snap | 431 +++++++++++++++--- .../y-prosemirror/versioning.test.tsx | 2 + 9 files changed, 519 insertions(+), 96 deletions(-) diff --git a/examples/07-collaboration/14-suggestion-gallery/src/App.tsx b/examples/07-collaboration/14-suggestion-gallery/src/App.tsx index 7373487f19..3fb0039b62 100644 --- a/examples/07-collaboration/14-suggestion-gallery/src/App.tsx +++ b/examples/07-collaboration/14-suggestion-gallery/src/App.tsx @@ -415,6 +415,8 @@ function VersionMerge({ fragment: setup.afterDoc.get("doc"), provider: { awareness: setup.diffAwareness }, user: USER_A, + // The gallery shows the experimental diff refinements. + experimental: { lostContentAttribution: true }, }, }), ); diff --git a/examples/07-collaboration/14-suggestion-gallery/src/scenarios.ts b/examples/07-collaboration/14-suggestion-gallery/src/scenarios.ts index 123b72f860..734303b435 100644 --- a/examples/07-collaboration/14-suggestion-gallery/src/scenarios.ts +++ b/examples/07-collaboration/14-suggestion-gallery/src/scenarios.ts @@ -1171,8 +1171,8 @@ export const scenarios: SuggestionScenario[] = [ note: "X is lost: B's move inserts a copy into Parent, which A deletes.", }, { - severity: "high", - note: "Versioning shows X as deleted by B, though B only moved it. To be fixed by #3166.", + severity: "info", + note: "Versioning shows X deleted without an author: B only moved it, and A never saw it there.", }, ], title: "Move a block into a block that is deleted", diff --git a/packages/core/src/y/extensions/Versioning.ts b/packages/core/src/y/extensions/Versioning.ts index 3f14359cec..5f8e1a8f1c 100644 --- a/packages/core/src/y/extensions/Versioning.ts +++ b/packages/core/src/y/extensions/Versioning.ts @@ -8,6 +8,7 @@ import type { StyleSchema, } from "../../schema/index.js"; import { ForkYDocExtension } from "./ForkYDoc.js"; +import { CollaborationExtension } from "./index.js"; import { serializeFragment } from "./snapshotCodec.js"; import { showSnapshotPreview } from "./snapshotPreview.js"; @@ -59,6 +60,7 @@ export function createYVersionView< content, comparison?.content, comparison?.attributions, + editor.getExtension(CollaborationExtension)?.experimental, ); }, close() { diff --git a/packages/core/src/y/extensions/index.ts b/packages/core/src/y/extensions/index.ts index 64d376c478..6e2b2394d0 100644 --- a/packages/core/src/y/extensions/index.ts +++ b/packages/core/src/y/extensions/index.ts @@ -9,6 +9,7 @@ import { normalizeToUserStore, UserStoreOrResolver } from "../../user/index.js"; import { AttributionExtension } from "./AttributionExtension.js"; import { ForkYDocExtension } from "./ForkYDoc.js"; import { RelativePositionMappingExtension } from "./RelativePositionMapping.js"; +import type { ExperimentalVersionDiffs } from "./snapshotPreview.js"; import { SuggestionsExtension } from "./Suggestions.js"; import { CollaborationUser, YCursorExtension } from "./YCursorPlugin.js"; import type { GetAttributionMarkClassName } from "./YAttributionMarks.js"; @@ -66,6 +67,12 @@ export type CollaborationOptions = { * dropped for that mark. See {@link GetAttributionMarkClassName}. */ getAttributionMarkClassName?: GetAttributionMarkClassName; + + /** + * Experimental refinements of how version history diffs are shown. They only + * change what a diff shows, never what is stored. + */ + experimental?: ExperimentalVersionDiffs; }; export const CollaborationExtension = createExtension( @@ -81,6 +88,7 @@ export const CollaborationExtension = createExtension( return { key: "collaboration", fragment: options.fragment, + experimental: options.experimental ?? {}, userStore, blockNoteExtensions: [ options.suggestionDoc @@ -129,6 +137,7 @@ export function withCollaboration< }; } +export type { ExperimentalVersionDiffs } from "./snapshotPreview.js"; export * from "./AttributionExtension.js"; export * from "./DiffVersioningExtension.js"; export * from "./ForkYDoc.js"; diff --git a/packages/core/src/y/extensions/snapshotCodec.ts b/packages/core/src/y/extensions/snapshotCodec.ts index 985c68502f..c47ac5941e 100644 --- a/packages/core/src/y/extensions/snapshotCodec.ts +++ b/packages/core/src/y/extensions/snapshotCodec.ts @@ -16,11 +16,12 @@ export type DecodedFragment = { export function decodeFragmentUpdate( fragment: Y.Node, content: Uint8Array, - opts?: { suggestionDoc?: boolean }, + opts?: { suggestionDoc?: boolean; keepDeleted?: boolean }, ): DecodedFragment { - const doc = new Y.Doc( - opts?.suggestionDoc ? { isSuggestionDoc: true } : undefined, - ); + const doc = new Y.Doc({ + ...(opts?.suggestionDoc ? { isSuggestionDoc: true } : {}), + ...(opts?.keepDeleted ? { gc: false } : {}), + }); try { Y.applyUpdateV2(doc, content); return { doc, fragment: findTypeInOtherYdoc(fragment, doc) }; diff --git a/packages/core/src/y/extensions/snapshotPreview.ts b/packages/core/src/y/extensions/snapshotPreview.ts index 7f801cf2e0..a9c01732fa 100644 --- a/packages/core/src/y/extensions/snapshotPreview.ts +++ b/packages/core/src/y/extensions/snapshotPreview.ts @@ -9,6 +9,122 @@ import { destroyDecodedFragment, } from "./snapshotCodec.js"; +/** + * Experimental refinements of how a diff between two versions is shown. Each + * one only changes what the diff shows, never what is stored, so they can be + * turned on or off at any time. + */ +export type ExperimentalVersionDiffs = { + /** + * Don't credit content that was lost with a concurrently deleted block, or + * with a moved block's lost copy, to the user whose change removed it: show + * it as deleted without an author. + */ + lostContentAttribution?: boolean; +}; + +/** + * Snapshots are decoded with `gc: false`, so content inside a deleted block keeps + * its own attribution: content swept away with a concurrently deleted block shows + * no author instead of inheriting the deleter. The stock renderer hides content + * that was inserted and deleted between the two versions only once it has been + * garbage collected; hide it here too. + */ +class SnapshotDiffRenderer extends Y.DiffRenderer { + override readContent( + ...[contents, client, clock, deleted, content, shouldRender]: Parameters< + Y.DiffRenderer["readContent"] + > + ) { + const start = contents.length; + super.readContent(contents, client, clock, deleted, content, shouldRender); + if (deleted) { + for (let i = contents.length - 1; i >= start; i--) { + if (this.inserts.has(client, contents[i].clock)) { + contents.splice(i, 1); + } + } + } + } +} + +/** + * Moved blocks that are gone, with their content. + * + * A move (indenting, dragging, a type change, ...) deletes a block and inserts + * a copy with the same id. While a copy is shown, the diff shows the move as + * is. Once every copy is gone, only the original's deletion is shown, and + * that names the mover, who may not have deleted it: the copy can be lost + * with a concurrently deleted parent, or deleted later by someone else. Such + * deletions are shown without an author. + */ +function lostMoves(doc: Y.Doc, baseline: Y.Doc): Y.IdSet { + const { inserted, deleted } = changesSince(doc, baseline); + const copies = new Map(); + for (const item of itemsIn(doc, inserted)) { + if (isBlock(item)) { + const id = blockId(item.content.type); + copies.set(id, [...(copies.get(id) ?? []), item]); + } + } + // Deleted content that was in the earlier version. + const removed = itemsIn(doc, Y.diffIdSet(deleted, inserted)); + const lost = removed.filter(isBlock).flatMap((item) => { + const id = blockId(item.content.type); + const moved = id == null ? undefined : copies.get(id); + return moved?.every((copy) => copy.deleted) ? [item.content.type] : []; + }); + const ids = Y.createIdSet(); + for (const item of removed) { + if ( + lost.some((block) => item === block._item || Y.isParentOf(block, item)) + ) { + ids.add(item.id.client, item.id.clock, item.length); + } + } + return ids; +} + +/** The ids inserted, and the ids deleted, in `doc` since `baseline`. */ +function changesSince(doc: Y.Doc, baseline: Y.Doc) { + return { + inserted: Y.diffIdSet( + Y.createInsertSetFromStructStore(doc.store, false), + Y.createInsertSetFromStructStore(baseline.store, false), + ), + deleted: Y.diffIdSet( + Y.createDeleteSetFromStructStore(doc.store), + Y.createDeleteSetFromStructStore(baseline.store), + ), + }; +} + +/** The items of `doc` that `ids` covers. */ +function itemsIn(doc: Y.Doc, ids: Y.IdSet): Y.Item[] { + const items: Y.Item[] = []; + // A transaction only because the iteration may split items; content is unchanged. + doc.transact((tr) => + Y.iterateStructsByIdSet(tr, ids, (struct) => { + if (struct instanceof Y.Item) { + items.push(struct); + } + }), + ); + return items; +} + +function isBlock(item: Y.Item): item is Y.Item & { content: Y.ContentType } { + return ( + item.content instanceof Y.ContentType && + item.content.type.name === "blockContainer" + ); +} + +/** A block's id, also when the block is deleted (its attributes read as unset). */ +function blockId(block: Y.Node): unknown { + return block._map.get("id")?.content.getContent().at(-1); +} + /** Whether the item with this id was already in `baseline`. */ function inBaseline(baseline: Y.Doc, id: Y.ID): boolean { const last = baseline.store.clients.get(id.client)?.at(-1); @@ -227,14 +343,21 @@ export function showSnapshotPreview( snapshotContent: Uint8Array, compareToContent?: Uint8Array, attributions?: Y.ContentMap, + experimental: ExperimentalVersionDiffs = {}, ): void { + // Deleted content is needed to tell what a user deleted from what was lost + // with something else. + const keepDeleted = experimental.lostContentAttribution === true; const baseline = compareToContent ? decodeFragmentUpdate(fragment, compareToContent, { suggestionDoc: true, + keepDeleted, }) : undefined; try { - const snapshot = decodeFragmentUpdate(fragment, snapshotContent); + const snapshot = decodeFragmentUpdate(fragment, snapshotContent, { + keepDeleted, + }); try { let renderAttributions = attributions; if (baseline) { @@ -246,23 +369,25 @@ export function showSnapshotPreview( added, ); if (attributions) { + const deletes = Y.mergeIdMaps([attributions.deletes, added.deletes]); renderAttributions = Y.createContentMap( Y.mergeIdMaps([attributions.inserts, added.inserts]), - Y.mergeIdMaps([attributions.deletes, added.deletes]), + experimental.lostContentAttribution + ? Y.diffIdMap(deletes, lostMoves(snapshot.doc, baseline.doc)) + : deletes, ); } } + const options = renderAttributions + ? { attributions: renderAttributions } + : undefined; editor.exec( configureYProsemirror({ ytype: snapshot.fragment, renderer: baseline - ? Y.createDiffRenderer( - baseline.doc, - snapshot.doc, - renderAttributions - ? { attributions: renderAttributions } - : undefined, - ) + ? keepDeleted + ? new SnapshotDiffRenderer(baseline.doc, snapshot.doc, options) + : Y.createDiffRenderer(baseline.doc, snapshot.doc, options) : undefined, }), ); diff --git a/packages/core/src/y/extensions/versionDiffAttribution.test.ts b/packages/core/src/y/extensions/versionDiffAttribution.test.ts index 8fb59c4dba..2310f0caef 100644 --- a/packages/core/src/y/extensions/versionDiffAttribution.test.ts +++ b/packages/core/src/y/extensions/versionDiffAttribution.test.ts @@ -23,6 +23,7 @@ function collaborativeEditor(doc: Y.Doc) { collaboration: { fragment: doc.get("doc"), user: { name: "Test", color: "#ff0000" }, + experimental: { lostContentAttribution: true }, }, }), ); @@ -149,8 +150,7 @@ function showDiff( } describe("version diff of a deleted block", () => { - // To be fixed by #3166. - it.fails("does not attribute content added concurrently inside it to the deleter", () => { + it("does not attribute content added concurrently inside it to the deleter", () => { const base = baseDocument([ { id: "parent", @@ -193,8 +193,7 @@ describe("version diff of a deleted block", () => { ]); }); - // To be fixed by #3166. - it.fails("names no author when hovering content removed with it", () => { + it("names no author when hovering content removed with it", () => { const base = baseDocument([ { id: "parent", type: "paragraph", content: "Parent" }, { id: "next", type: "paragraph", content: "Next" }, @@ -311,8 +310,7 @@ describe("version diff of a moved block", () => { ).toEqual(["[block moved]: bob"]); }); - // To be fixed by #3166. - it.fails("names no author for a block moved into a concurrently deleted one", () => { + it("names no author for a block moved into a concurrently deleted one", () => { const base = blocks(); const bob = editOf(base, 2, nest); const alice = editOf(base, 1, removeParent); @@ -340,8 +338,7 @@ describe("version diff of a moved block", () => { ).toEqual(["[block parent]: alice"]); }); - // To be fixed by #3166. - it.fails("names no author for a moved block that someone else deletes", () => { + it("names no author for a moved block that someone else deletes", () => { const base = blocks(); const server = history(base); server.apply(editOf(base, 1, nest), "alice"); diff --git a/tests/src/end-to-end/y-prosemirror/__snapshots__/versioning.test.tsx.snap b/tests/src/end-to-end/y-prosemirror/__snapshots__/versioning.test.tsx.snap index 243de25f9c..f9a92dc1aa 100644 --- a/tests/src/end-to-end/y-prosemirror/__snapshots__/versioning.test.tsx.snap +++ b/tests/src/end-to-end/y-prosemirror/__snapshots__/versioning.test.tsx.snap @@ -2,28 +2,60 @@ exports[`versioning diff: A adds column then row, B adds column 1`] = ` [ - "insert A", - "insert A", - "insert "C1" A", - "insert B", - "insert B", - "insert "D1" B", - "insert A", - "insert A", - "insert "C2" A", - "insert B", - "insert B", - "insert "D2" B", - "insert A", - "insert A", - "insert A", - "insert "A3" A", - "insert A", - "insert A", - "insert "B3" A", - "insert A", - "insert A", - "insert "C3" A", + "delete block "A1B1A2B2" B,A", + "delete B,A", + "delete B,A", + "delete B,A", + "delete B,A", + "delete "A1" B,A", + "delete B,A", + "delete B,A", + "delete "B1" B,A", + "delete B,A", + "delete B,A", + "delete B,A", + "delete "A2" B,A", + "delete B,A", + "delete B,A", + "delete "B2" B,A", + "insert block "A1B1C1D1A2B2C2D2A3B3C3" B,A", + "insert
B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A1" B,A", + "insert B,A", + "insert B,A", + "insert "B1" B,A", + "insert B,A", + "insert B,A", + "insert "C1" B,A", + "insert B,A", + "insert B,A", + "insert "D1" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A2" B,A", + "insert B,A", + "insert B,A", + "insert "B2" B,A", + "insert B,A", + "insert B,A", + "insert "C2" B,A", + "insert B,A", + "insert B,A", + "insert "D2" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A3" B,A", + "insert B,A", + "insert B,A", + "insert "B3" B,A", + "insert B,A", + "insert B,A", + "insert "C3" B,A", "insert ", "insert ", ] @@ -31,29 +63,61 @@ exports[`versioning diff: A adds column then row, B adds column 1`] = ` exports[`versioning diff: A adds row then column, B adds row 1`] = ` [ - "insert A", - "insert A", - "insert "C1" A", - "insert A", - "insert A", - "insert "C2" A", - "insert A", - "insert A", - "insert A", - "insert "A3" A", - "insert A", - "insert A", - "insert "B3" A", - "insert A", - "insert A", - "insert "C3" A", - "insert B", - "insert B", - "insert B", - "insert "D1" B", - "insert B", - "insert B", - "insert "D2" B", + "delete block "A1B1A2B2" B,A", + "delete
B,A", + "delete B,A", + "delete B,A", + "delete B,A", + "delete "A1" B,A", + "delete B,A", + "delete B,A", + "delete "B1" B,A", + "delete B,A", + "delete B,A", + "delete B,A", + "delete "A2" B,A", + "delete B,A", + "delete B,A", + "delete "B2" B,A", + "insert block "A1B1C1A2B2C2A3B3C3D1D2" B,A", + "insert
B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A1" B,A", + "insert B,A", + "insert B,A", + "insert "B1" B,A", + "insert B,A", + "insert B,A", + "insert "C1" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A2" B,A", + "insert B,A", + "insert B,A", + "insert "B2" B,A", + "insert B,A", + "insert B,A", + "insert "C2" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A3" B,A", + "insert B,A", + "insert B,A", + "insert "B3" B,A", + "insert B,A", + "insert B,A", + "insert "C3" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "D1" B,A", + "insert B,A", + "insert B,A", + "insert "D2" B,A", "insert ", "insert ", ] @@ -415,19 +479,51 @@ exports[`versioning diff: Add column 1`] = ` exports[`versioning diff: Add column vs add row 1`] = ` [ - "insert A", - "insert A", - "insert "C1" A", - "insert A", - "insert A", - "insert "C2" A", - "insert B", - "insert B", - "insert B", - "insert "A3" B", - "insert B", - "insert B", - "insert "B3" B", + "delete block "A1B1A2B2" B,A", + "delete
B,A", + "delete B,A", + "delete B,A", + "delete B,A", + "delete "A1" B,A", + "delete B,A", + "delete B,A", + "delete "B1" B,A", + "delete B,A", + "delete B,A", + "delete B,A", + "delete "A2" B,A", + "delete B,A", + "delete B,A", + "delete "B2" B,A", + "insert block "A1B1C1A2B2C2A3B3" B,A", + "insert
B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A1" B,A", + "insert B,A", + "insert B,A", + "insert "B1" B,A", + "insert B,A", + "insert B,A", + "insert "C1" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A2" B,A", + "insert B,A", + "insert B,A", + "insert "B2" B,A", + "insert B,A", + "insert B,A", + "insert "C2" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A3" B,A", + "insert B,A", + "insert B,A", + "insert "B3" B,A", "insert ", "insert ", ] @@ -496,19 +592,51 @@ exports[`versioning diff: Add row 1`] = ` exports[`versioning diff: Add row vs add column 1`] = ` [ - "insert B", - "insert B", - "insert "C1" B", - "insert B", - "insert B", - "insert "C2" B", - "insert A", - "insert A", - "insert A", - "insert "A3" A", - "insert A", - "insert A", - "insert "B3" A", + "delete block "A1B1A2B2" B,A", + "delete
B,A", + "delete B,A", + "delete B,A", + "delete B,A", + "delete "A1" B,A", + "delete B,A", + "delete B,A", + "delete "B1" B,A", + "delete B,A", + "delete B,A", + "delete B,A", + "delete "A2" B,A", + "delete B,A", + "delete B,A", + "delete "B2" B,A", + "insert block "A1B1C1A2B2C2A3B3" B,A", + "insert
B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A1" B,A", + "insert B,A", + "insert B,A", + "insert "B1" B,A", + "insert B,A", + "insert B,A", + "insert "C1" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A2" B,A", + "insert B,A", + "insert B,A", + "insert "B2" B,A", + "insert B,A", + "insert B,A", + "insert "C2" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A3" B,A", + "insert B,A", + "insert B,A", + "insert "B3" B,A", "insert ", "insert ", ] @@ -523,6 +651,8 @@ exports[`versioning diff: Bold vs italic 1`] = ` exports[`versioning diff: Both nest a new block under N0 1`] = ` [ "delete block "N0" A,B", + "delete A,B", + "delete "N0" A,B", "insert block "N0" A", "insert A", "insert "N0" A", @@ -543,7 +673,11 @@ exports[`versioning diff: Both nest a new block under N0 1`] = ` exports[`versioning diff: Cascading indents 1`] = ` [ "delete block "N0" A", + "delete A", + "delete "N0" A", "delete block "N1" A,B", + "delete A,B", + "delete "N1" A,B", "insert block "N0" A", "insert A", "insert "N0" A", @@ -552,6 +686,8 @@ exports[`versioning diff: Cascading indents 1`] = ` "insert A", "insert "N1" A", "delete block "N2" B", + "delete B", + "delete "N2" B", "insert block "N1" B", "insert B", "insert "N1" B", @@ -571,6 +707,12 @@ exports[`versioning diff: Center-align 1`] = ` exports[`versioning diff: Change a parent's type vs edit its child 1`] = ` [ "delete block "Parent" A", + "delete A", + "delete "Parent" A", + "delete A", + "delete block "Child" A", + "delete A", + "delete "Child" A", "insert block "Parent" A", "insert A", "insert "Parent" A", @@ -590,6 +732,12 @@ exports[`versioning diff: Change image source 1`] = ` exports[`versioning diff: Change type of a parent block 1`] = ` [ "delete block "N0" A", + "delete A", + "delete "N0" A", + "delete A", + "delete block "N1" A", + "delete A", + "delete "N1" A", "insert block "N0" A", "insert A", "insert "N0" A", @@ -617,6 +765,8 @@ exports[`versioning diff: Create two columns 1`] = ` exports[`versioning diff: Delete a code block 1`] = ` [ "delete block "const x = 1;" A", + "delete A", + "delete "const x = 1;" A", "insert block "" A", "insert A", ] @@ -625,6 +775,7 @@ exports[`versioning diff: Delete a code block 1`] = ` exports[`versioning diff: Delete a divider 1`] = ` [ "delete block "" A", + "delete A", "insert block "" A", "insert A", ] @@ -633,6 +784,12 @@ exports[`versioning diff: Delete a divider 1`] = ` exports[`versioning diff: Delete a nested block 1`] = ` [ "delete block "Parent" A", + "delete A", + "delete "Parent" A", + "delete A", + "delete block "Child" A", + "delete A", + "delete "Child" A", "insert block "Parent" A", "insert A", "insert "Parent" A", @@ -642,6 +799,12 @@ exports[`versioning diff: Delete a nested block 1`] = ` exports[`versioning diff: Delete a parent block 1`] = ` [ "delete block "Parent" A", + "delete A", + "delete "Parent" A", + "delete A", + "delete block "Child" A", + "delete A", + "delete "Child" A", "insert block "" A", "insert A", ] @@ -650,6 +813,12 @@ exports[`versioning diff: Delete a parent block 1`] = ` exports[`versioning diff: Delete a parent vs change its child's type 1`] = ` [ "delete block "Parent" A", + "delete A", + "delete "Parent" A", + "delete A", + "delete block "Child"", + "delete ", + "delete "Child"", ] `; @@ -662,7 +831,11 @@ exports[`versioning diff: Delete a parent vs type in its child 1`] = ` exports[`versioning diff: Delete column vs add row 1`] = ` [ "delete A", + "delete A", + "delete "B1" A", "delete A", + "delete A", + "delete "B2" A", "insert B", "insert B", "insert B", @@ -676,6 +849,7 @@ exports[`versioning diff: Delete column vs add row 1`] = ` exports[`versioning diff: Delete image 1`] = ` [ "delete block "" A", + "delete A", "insert block "" A", "insert A", ] @@ -684,12 +858,21 @@ exports[`versioning diff: Delete image 1`] = ` exports[`versioning diff: Delete one of two empty blocks 1`] = ` [ "delete block "" A", + "delete A", ] `; exports[`versioning diff: Delete parent with mixed children 1`] = ` [ "delete block "Parent" A", + "delete A", + "delete "Parent" A", + "delete A", + "delete block "Nested paragraph" A", + "delete A", + "delete "Nested paragraph" A", + "delete block "" A", + "delete A", "insert block "" A", "insert A", ] @@ -697,10 +880,34 @@ exports[`versioning diff: Delete parent with mixed children 1`] = ` exports[`versioning diff: Delete row vs add column 1`] = ` [ + "delete block "A1B1A2B2" A", + "delete
A", + "delete A", + "delete A", + "delete A", + "delete "A1" A", + "delete A", + "delete A", + "delete "B1" A", + "delete A", + "delete A", + "delete A", + "delete "A2" A", + "delete A", + "delete A", + "delete "B2" A", + "insert block "A1B1C1" B", + "insert
B", + "insert B", + "insert B", + "insert B", + "insert "A1" B", + "insert B", + "insert B", + "insert "B1" B", "insert B", "insert B", "insert "C1" B", - "delete A", ] `; @@ -738,6 +945,11 @@ exports[`versioning diff: Edit a link 1`] = ` exports[`versioning diff: Edit text vs change to heading 1`] = ` [ "delete block "hello world" B", + "delete B", + "delete "hello " B", + "delete "wo" B,A", + "delete "r" B", + "delete "ld" B,A", "insert block "hello world" B", "insert B", "insert "hello world" B", @@ -766,6 +978,8 @@ exports[`versioning diff: Fix typo vs delete word 1`] = ` exports[`versioning diff: Heading vs list item 1`] = ` [ "delete block "hello world" A,B", + "delete A,B", + "delete "hello world" A,B", "insert block "hello world" A", "insert A", "insert "hello world" A", @@ -785,7 +999,11 @@ exports[`versioning diff: Highlight a column 1`] = ` exports[`versioning diff: Indent a block 1`] = ` [ "delete block "N0" A", + "delete A", + "delete "N0" A", "delete block "N1" A", + "delete A", + "delete "N1" A", "insert block "N0" A", "insert A", "insert "N0" A", @@ -828,6 +1046,8 @@ exports[`versioning diff: Insert an image 1`] = ` exports[`versioning diff: List item → paragraph 1`] = ` [ "delete block "hello world" A", + "delete A", + "delete "hello world" A", "insert block "hello world" A", "insert A", "insert "hello world" A", @@ -838,6 +1058,8 @@ exports[`versioning diff: Merge blocks vs edit block B 1`] = ` [ "insert "Second" A", "delete block "Second" A", + "delete A", + "delete "Second" A", ] `; @@ -846,6 +1068,8 @@ exports[`versioning diff: Merge cells 1`] = ` "attrs colspan:A colwidth:A", "insert "+B1" A", "delete A", + "delete A", + "delete "B1" A", "insert ", "insert ", ] @@ -854,7 +1078,15 @@ exports[`versioning diff: Merge cells 1`] = ` exports[`versioning diff: Move a block into a block that is deleted 1`] = ` [ "delete block "Parent" A", - "delete block "X" B", + "delete A", + "delete "Parent" A", + "delete A", + "delete block "Child" A", + "delete A", + "delete "Child" A", + "delete block "X"", + "delete ", + "delete "X"", ] `; @@ -864,6 +1096,8 @@ exports[`versioning diff: Move paragraph up 1`] = ` "insert A", "insert "Middle" A", "delete block "Middle" A", + "delete A", + "delete "Middle" A", ] `; @@ -877,13 +1111,23 @@ exports[`versioning diff: Move paragraph with children 1`] = ` "insert A", "insert "Child" A", "delete block "Parent" A", + "delete A", + "delete "Parent" A", + "delete A", + "delete block "Child" A", + "delete A", + "delete "Child" A", ] `; exports[`versioning diff: Nest a bullet under another 1`] = ` [ "delete block "Parent" A", + "delete A", + "delete "Parent" A", "delete block "Child" A", + "delete A", + "delete "Child" A", "insert block "Parent" A", "insert A", "insert "Parent" A", @@ -897,7 +1141,11 @@ exports[`versioning diff: Nest a bullet under another 1`] = ` exports[`versioning diff: Nest blocks into a block that is moved 1`] = ` [ "delete block "R" B", + "delete B", + "delete "R" B", "delete block "Q" B,A", + "delete B,A", + "delete "Q" B,A", "insert block "R" B", "insert B", "insert "R" B", @@ -906,8 +1154,14 @@ exports[`versioning diff: Nest blocks into a block that is moved 1`] = ` "insert B", "insert "Q" B", "delete block "B1" A", + "delete A", + "delete "B1" A", "delete block "B2" A", + "delete A", + "delete "B2" A", "delete block "B3" A", + "delete A", + "delete "B3" A", "insert block "Q" A", "insert A", "insert "Q" A", @@ -927,6 +1181,8 @@ exports[`versioning diff: Nest blocks into a block that is moved 1`] = ` exports[`versioning diff: Paragraph → heading 1`] = ` [ "delete block "hello world" A", + "delete A", + "delete "hello world" A", "insert block "hello world" A", "insert A", "insert "hello world" A", @@ -936,6 +1192,14 @@ exports[`versioning diff: Paragraph → heading 1`] = ` exports[`versioning diff: Remove a column 1`] = ` [ "delete A", + "delete A", + "delete block "Left column" A", + "delete A", + "delete "Left column" A", + "delete A", + "delete block "Right column" A", + "delete A", + "delete "Right column" A", "insert block "Left column" A", "insert A", "insert "Left column" A", @@ -951,12 +1215,17 @@ exports[`versioning diff: Remove a link 1`] = ` exports[`versioning diff: Remove a middle column 1`] = ` [ "delete A", + "delete block "Right column" A", + "delete A", + "delete "Right column" A", ] `; exports[`versioning diff: Remove a paragraph 1`] = ` [ "delete block "Body text" A", + "delete A", + "delete "Body text" A", ] `; @@ -969,13 +1238,23 @@ exports[`versioning diff: Remove bold 1`] = ` exports[`versioning diff: Remove column 1`] = ` [ "delete A", + "delete A", + "delete "B1" A", "delete A", + "delete A", + "delete "B2" A", ] `; exports[`versioning diff: Remove row 1`] = ` [ "delete A", + "delete A", + "delete A", + "delete "A2" A", + "delete A", + "delete A", + "delete "B2" A", ] `; @@ -1030,6 +1309,12 @@ exports[`versioning diff: Text color vs background color 1`] = ` exports[`versioning diff: Unindent a block 1`] = ` [ "delete block "N0" A", + "delete A", + "delete "N0" A", + "delete A", + "delete block "N1" A", + "delete A", + "delete "N1" A", "insert block "N0" A", "insert A", "insert "N0" A", diff --git a/tests/src/end-to-end/y-prosemirror/versioning.test.tsx b/tests/src/end-to-end/y-prosemirror/versioning.test.tsx index 4723a97674..76bb315a90 100644 --- a/tests/src/end-to-end/y-prosemirror/versioning.test.tsx +++ b/tests/src/end-to-end/y-prosemirror/versioning.test.tsx @@ -65,6 +65,8 @@ function mountEditor(doc: Y.Doc): { fragment: doc.get("doc"), provider: undefined, user: { name: "User", color: "#8a6d1a" }, + // As the gallery: with the experimental diff refinements. + experimental: { lostContentAttribution: true }, }, }), ); From bfa68e8111677ecf27a926a5bdfebad96d2fefc9 Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 8 Oct 2026 19:19:45 +0200 Subject: [PATCH 02/10] feat(gallery): toggle the experimental version diff flags The Diff in Versioning mode is off by default, as in the editor; a checkbox per flag (kept in the URL) turns it on. A note can depend on the flags, so the sidebar and the notes show the severity for the current flags. The versioning snapshot records each scenario with the flags off and all on. --- .../14-suggestion-gallery/src/App.tsx | 132 +- .../14-suggestion-gallery/src/scenarios.ts | 9 + .../14-suggestion-gallery/src/style.css | 17 + .../__snapshots__/versioning.test.tsx.snap | 1278 +++++++++++++++-- .../y-prosemirror/versioning.test.tsx | 29 +- 5 files changed, 1353 insertions(+), 112 deletions(-) diff --git a/examples/07-collaboration/14-suggestion-gallery/src/App.tsx b/examples/07-collaboration/14-suggestion-gallery/src/App.tsx index 3fb0039b62..aa428ead67 100644 --- a/examples/07-collaboration/14-suggestion-gallery/src/App.tsx +++ b/examples/07-collaboration/14-suggestion-gallery/src/App.tsx @@ -5,6 +5,7 @@ import "./style.css"; import type { GalleryEditor } from "./gallerySchema"; import { createYVersionView, + type ExperimentalVersionDiffs, SuggestionsExtension, withCollaboration, } from "@blocknote/core/y"; @@ -22,10 +23,59 @@ import { createVersionMerge, docFromBlocks, } from "./scenarioDocs"; -import { scenarios, SuggestionScenario } from "./scenarios"; +import { Feedback, scenarios, SuggestionScenario } from "./scenarios"; type Mode = "suggestions" | "versioning"; +type Experiment = keyof ExperimentalVersionDiffs; + +// The experimental version diff flags the Diff can toggle, off by default as +// in the editor. Kept in the URL, so a link opens the same view. +const EXPERIMENTS: { key: Experiment; label: string }[] = [ + { key: "lostContentAttribution", label: "Lost content attribution" }, +]; + +function readExperiments(): ExperimentalVersionDiffs { + const params = new URLSearchParams(window.location.search); + return Object.fromEntries( + EXPERIMENTS.map(({ key }) => [key, params.get(key) === "1"]), + ); +} + +function writeExperiments(experimental: ExperimentalVersionDiffs) { + const url = new URL(window.location.href); + for (const { key } of EXPERIMENTS) { + if (experimental[key]) { + url.searchParams.set(key, "1"); + } else { + url.searchParams.delete(key); + } + } + window.history.replaceState(null, "", url); +} + +const ALL_EXPERIMENTS: ExperimentalVersionDiffs = Object.fromEntries( + EXPERIMENTS.map(({ key }) => [key, true]), +); + +// A note with `when` describes the Diff with those flags, so it only shows in +// Versioning mode with matching flags. +function applies( + f: Feedback, + mode: Mode, + experimental: ExperimentalVersionDiffs, +): boolean { + if (!f.when) { + return true; + } + return ( + mode === "versioning" && + Object.entries(f.when).every( + ([key, on]) => (experimental[key as Experiment] ?? false) === on, + ) + ); +} + function makeAwareness(doc: Y.Doc, name: string, color: string): Awareness { const awareness = new Awareness(doc); awareness.setLocalStateField("user", { name, color }); @@ -333,7 +383,13 @@ function versioningUsers(scenario: SuggestionScenario): VersioningUser[] { * editing any user re-merges (and re-diffs); editing Version 1 resets every user * back to a fresh clone (via the `nonce` remount). */ -function VersioningView({ scenario }: { scenario: SuggestionScenario }) { +function VersioningView({ + scenario, + experimental, +}: { + scenario: SuggestionScenario; + experimental: ExperimentalVersionDiffs; +}) { const [setup] = useState(() => { const beforeDoc = docFromBlocks(scenario.initial); return { @@ -375,6 +431,7 @@ function VersioningView({ scenario }: { scenario: SuggestionScenario }) { beforeDoc={setup.beforeDoc} users={setup.users} applyInitial={nonce === 0} + experimental={experimental} /> ); @@ -390,10 +447,12 @@ function VersionMerge({ beforeDoc, users, applyInitial, + experimental, }: { beforeDoc: Y.Doc; users: VersioningUser[]; applyInitial: boolean; + experimental: ExperimentalVersionDiffs; }) { const [setup] = useState(() => { // Records which user authored each merged change, so the Diff can color @@ -415,8 +474,7 @@ function VersionMerge({ fragment: setup.afterDoc.get("doc"), provider: { awareness: setup.diffAwareness }, user: USER_A, - // The gallery shows the experimental diff refinements. - experimental: { lostContentAttribution: true }, + experimental, }, }), ); @@ -533,8 +591,10 @@ const SEVERITY = { // The most-severe note across a scenario's feedback — a known crash counts as // high — or null if it has none. Drives the sidebar indicator. -function topSeverity(s: SuggestionScenario): "high" | "low" | "info" | null { - const fb = s.feedback ?? []; +function topSeverity( + s: SuggestionScenario, + fb: Feedback[], +): "high" | "low" | "info" | null { if (s.knownCrash || fb.some((f) => f.severity === "high")) { return "high"; } @@ -544,15 +604,39 @@ function topSeverity(s: SuggestionScenario): "high" | "low" | "info" | null { return fb.some((f) => f.severity === "info") ? "info" : null; } -function severityBadge(s: SuggestionScenario): string { - const sev = topSeverity(s); - return sev ? SEVERITY[sev].icon + " " : ""; +function notesFor( + s: SuggestionScenario, + mode: Mode, + experimental: ExperimentalVersionDiffs, +): Feedback[] { + return (s.feedback ?? []).filter((f) => applies(f, mode, experimental)); +} + +// The severity with the flags off (the default) and, for a scenario the flags +// affect, in parentheses the severity with all of them on (green: no issue +// left). The checkboxes don't change it. +function severityBadge(s: SuggestionScenario, mode: Mode): string { + const sev = topSeverity(s, notesFor(s, mode, {})); + let badge = sev ? SEVERITY[sev].icon + " " : ""; + if (mode === "versioning" && s.feedback?.some((f) => f.when)) { + const best = topSeverity(s, notesFor(s, mode, ALL_EXPERIMENTS)); + badge += `(${best === "high" || best === "low" ? SEVERITY[best].icon : "🟢"}) `; + } + return badge; } export default function App() { const [selectedId, setSelectedId] = useState(scenarios[0].id); const [mode, setMode] = useState("versioning"); + const [experimental, setExperimental] = useState(readExperiments); const selected = scenarios.find((s) => s.id === selectedId)!; + const feedback = notesFor(selected, mode, experimental); + + function toggle(key: Experiment) { + const next = { ...experimental, [key]: !experimental[key] }; + writeExperiments(next); + setExperimental(next); + } const categories = [...new Set(scenarios.map((s) => s.category))]; @@ -575,7 +659,7 @@ export default function App() { } onClick={() => setSelectedId(s.id)} > - {severityBadge(s)} + {severityBadge(s, mode)} {s.kind === "concurrent" ? "👥 " : ""} {s.title} @@ -607,14 +691,30 @@ export default function App() { - {selected.feedback && selected.feedback.length > 0 && ( + {mode === "versioning" && ( +
+ Experimental: + {EXPERIMENTS.map(({ key, label }) => ( + + ))} +
+ )} + + {feedback.length > 0 && (
- {selected.feedback.some((f) => f.severity !== "info") + {feedback.some((f) => f.severity !== "info") ? "Known issues" : "Notes"}
- {[...selected.feedback] + {[...feedback] .sort( (a, b) => SEVERITY[a.severity].rank - SEVERITY[b.severity].rank, ) @@ -632,9 +732,11 @@ export default function App() {
)} - + {mode === "versioning" ? ( - + ) : ( )} diff --git a/examples/07-collaboration/14-suggestion-gallery/src/scenarios.ts b/examples/07-collaboration/14-suggestion-gallery/src/scenarios.ts index 734303b435..f9a1af39c2 100644 --- a/examples/07-collaboration/14-suggestion-gallery/src/scenarios.ts +++ b/examples/07-collaboration/14-suggestion-gallery/src/scenarios.ts @@ -1,3 +1,4 @@ +import type { ExperimentalVersionDiffs } from "@blocknote/core/y"; import { testDocumentBlocks } from "@shared/testDocumentBlocks.js"; import type { GalleryEditor, GalleryPartialBlock } from "./gallerySchema"; @@ -26,6 +27,8 @@ import type { GalleryEditor, GalleryPartialBlock } from "./gallerySchema"; export type Feedback = { severity: "info" | "low" | "high"; note: string; + /** Show the note only in Versioning mode, with these experimental flags. */ + when?: ExperimentalVersionDiffs; }; export type SingleScenario = { @@ -1170,8 +1173,14 @@ export const scenarios: SuggestionScenario[] = [ severity: "low", note: "X is lost: B's move inserts a copy into Parent, which A deletes.", }, + { + severity: "high", + when: { lostContentAttribution: false }, + note: "Versioning shows X as deleted by B, though B only moved it.", + }, { severity: "info", + when: { lostContentAttribution: true }, note: "Versioning shows X deleted without an author: B only moved it, and A never saw it there.", }, ], diff --git a/examples/07-collaboration/14-suggestion-gallery/src/style.css b/examples/07-collaboration/14-suggestion-gallery/src/style.css index 034b54a5fd..4898bdec6c 100644 --- a/examples/07-collaboration/14-suggestion-gallery/src/style.css +++ b/examples/07-collaboration/14-suggestion-gallery/src/style.css @@ -105,6 +105,23 @@ font-weight: 600; } +.bn-gallery-experiments { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 4px 16px; + margin-bottom: 16px; + font-size: 13px; + color: var(--gallery-muted); +} + +.bn-gallery-experiments label { + display: inline-flex; + align-items: center; + gap: 6px; + cursor: pointer; +} + .bn-gallery-title { font-size: 20px; margin: 0 0 4px; diff --git a/tests/src/end-to-end/y-prosemirror/__snapshots__/versioning.test.tsx.snap b/tests/src/end-to-end/y-prosemirror/__snapshots__/versioning.test.tsx.snap index f9a92dc1aa..75c5ccc703 100644 --- a/tests/src/end-to-end/y-prosemirror/__snapshots__/versioning.test.tsx.snap +++ b/tests/src/end-to-end/y-prosemirror/__snapshots__/versioning.test.tsx.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`versioning diff: A adds column then row, B adds column 1`] = ` +exports[`versioning diff (experimental): A adds column then row, B adds column 1`] = ` [ "delete block "A1B1A2B2" B,A", "delete
B,A", @@ -61,7 +61,7 @@ exports[`versioning diff: A adds column then row, B adds column 1`] = ` ] `; -exports[`versioning diff: A adds row then column, B adds row 1`] = ` +exports[`versioning diff (experimental): A adds row then column, B adds row 1`] = ` [ "delete block "A1B1A2B2" B,A", "delete
B,A", @@ -123,7 +123,7 @@ exports[`versioning diff: A adds row then column, B adds row 1`] = ` ] `; -exports[`versioning diff: Add a block to a column 1`] = ` +exports[`versioning diff (experimental): Add a block to a column 1`] = ` [ "insert block "Added to the left column" A", "insert A", @@ -131,13 +131,13 @@ exports[`versioning diff: Add a block to a column 1`] = ` ] `; -exports[`versioning diff: Add a link 1`] = ` +exports[`versioning diff (experimental): Add a link 1`] = ` [ "format "the site" A", ] `; -exports[`versioning diff: Add a whole document 1`] = ` +exports[`versioning diff (experimental): Add a whole document 1`] = ` [ "insert block "Welcome to this demo 🙌!" A", "insert A", @@ -431,20 +431,20 @@ exports[`versioning diff: Add a whole document 1`] = ` ] `; -exports[`versioning diff: Add an empty block 1`] = ` +exports[`versioning diff (experimental): Add an empty block 1`] = ` [ "insert block "" A", "insert A", ] `; -exports[`versioning diff: Add bold 1`] = ` +exports[`versioning diff (experimental): Add bold 1`] = ` [ "format "world" A", ] `; -exports[`versioning diff: Add bullet item 1`] = ` +exports[`versioning diff (experimental): Add bullet item 1`] = ` [ "insert A", "insert block "New bullet" A", @@ -453,7 +453,7 @@ exports[`versioning diff: Add bullet item 1`] = ` ] `; -exports[`versioning diff: Add colored block with child 1`] = ` +exports[`versioning diff (experimental): Add colored block with child 1`] = ` [ "insert A", "insert block "Colored parent" A", @@ -466,7 +466,7 @@ exports[`versioning diff: Add colored block with child 1`] = ` ] `; -exports[`versioning diff: Add column 1`] = ` +exports[`versioning diff (experimental): Add column 1`] = ` [ "insert A", "insert A", @@ -477,7 +477,7 @@ exports[`versioning diff: Add column 1`] = ` ] `; -exports[`versioning diff: Add column vs add row 1`] = ` +exports[`versioning diff (experimental): Add column vs add row 1`] = ` [ "delete block "A1B1A2B2" B,A", "delete
B,A", @@ -529,7 +529,7 @@ exports[`versioning diff: Add column vs add row 1`] = ` ] `; -exports[`versioning diff: Add heading 1`] = ` +exports[`versioning diff (experimental): Add heading 1`] = ` [ "insert A", "insert block "New heading" A", @@ -538,13 +538,13 @@ exports[`versioning diff: Add heading 1`] = ` ] `; -exports[`versioning diff: Add italic over bold 1`] = ` +exports[`versioning diff (experimental): Add italic over bold 1`] = ` [ "format "world" A", ] `; -exports[`versioning diff: Add nested bullets 1`] = ` +exports[`versioning diff (experimental): Add nested bullets 1`] = ` [ "insert A", "insert block "Level 0" A", @@ -561,7 +561,7 @@ exports[`versioning diff: Add nested bullets 1`] = ` ] `; -exports[`versioning diff: Add numbered item 1`] = ` +exports[`versioning diff (experimental): Add numbered item 1`] = ` [ "insert A", "insert block "New numbered" A", @@ -570,7 +570,7 @@ exports[`versioning diff: Add numbered item 1`] = ` ] `; -exports[`versioning diff: Add paragraph after a block 1`] = ` +exports[`versioning diff (experimental): Add paragraph after a block 1`] = ` [ "insert block "Body text" A", "insert A", @@ -578,7 +578,7 @@ exports[`versioning diff: Add paragraph after a block 1`] = ` ] `; -exports[`versioning diff: Add row 1`] = ` +exports[`versioning diff (experimental): Add row 1`] = ` [ "insert A", "insert A", @@ -590,7 +590,7 @@ exports[`versioning diff: Add row 1`] = ` ] `; -exports[`versioning diff: Add row vs add column 1`] = ` +exports[`versioning diff (experimental): Add row vs add column 1`] = ` [ "delete block "A1B1A2B2" B,A", "delete
B,A", @@ -642,13 +642,13 @@ exports[`versioning diff: Add row vs add column 1`] = ` ] `; -exports[`versioning diff: Bold vs italic 1`] = ` +exports[`versioning diff (experimental): Bold vs italic 1`] = ` [ "format "world" A,B", ] `; -exports[`versioning diff: Both nest a new block under N0 1`] = ` +exports[`versioning diff (experimental): Both nest a new block under N0 1`] = ` [ "delete block "N0" A,B", "delete A,B", @@ -670,7 +670,7 @@ exports[`versioning diff: Both nest a new block under N0 1`] = ` ] `; -exports[`versioning diff: Cascading indents 1`] = ` +exports[`versioning diff (experimental): Cascading indents 1`] = ` [ "delete block "N0" A", "delete A", @@ -698,13 +698,13 @@ exports[`versioning diff: Cascading indents 1`] = ` ] `; -exports[`versioning diff: Center-align 1`] = ` +exports[`versioning diff (experimental): Center-align 1`] = ` [ "attrs textAlignment:A", ] `; -exports[`versioning diff: Change a parent's type vs edit its child 1`] = ` +exports[`versioning diff (experimental): Change a parent's type vs edit its child 1`] = ` [ "delete block "Parent" A", "delete A", @@ -723,13 +723,13 @@ exports[`versioning diff: Change a parent's type vs edit its child 1`] = ` ] `; -exports[`versioning diff: Change image source 1`] = ` +exports[`versioning diff (experimental): Change image source 1`] = ` [ "attrs url:A", ] `; -exports[`versioning diff: Change type of a parent block 1`] = ` +exports[`versioning diff (experimental): Change type of a parent block 1`] = ` [ "delete block "N0" A", "delete A", @@ -748,7 +748,7 @@ exports[`versioning diff: Change type of a parent block 1`] = ` ] `; -exports[`versioning diff: Create two columns 1`] = ` +exports[`versioning diff (experimental): Create two columns 1`] = ` [ "insert A", "insert A", @@ -762,7 +762,7 @@ exports[`versioning diff: Create two columns 1`] = ` ] `; -exports[`versioning diff: Delete a code block 1`] = ` +exports[`versioning diff (experimental): Delete a code block 1`] = ` [ "delete block "const x = 1;" A", "delete A", @@ -772,7 +772,7 @@ exports[`versioning diff: Delete a code block 1`] = ` ] `; -exports[`versioning diff: Delete a divider 1`] = ` +exports[`versioning diff (experimental): Delete a divider 1`] = ` [ "delete block "" A", "delete A", @@ -781,7 +781,7 @@ exports[`versioning diff: Delete a divider 1`] = ` ] `; -exports[`versioning diff: Delete a nested block 1`] = ` +exports[`versioning diff (experimental): Delete a nested block 1`] = ` [ "delete block "Parent" A", "delete A", @@ -796,7 +796,7 @@ exports[`versioning diff: Delete a nested block 1`] = ` ] `; -exports[`versioning diff: Delete a parent block 1`] = ` +exports[`versioning diff (experimental): Delete a parent block 1`] = ` [ "delete block "Parent" A", "delete A", @@ -810,7 +810,7 @@ exports[`versioning diff: Delete a parent block 1`] = ` ] `; -exports[`versioning diff: Delete a parent vs change its child's type 1`] = ` +exports[`versioning diff (experimental): Delete a parent vs change its child's type 1`] = ` [ "delete block "Parent" A", "delete A", @@ -822,13 +822,7 @@ exports[`versioning diff: Delete a parent vs change its child's type 1`] = ` ] `; -exports[`versioning diff: Delete a parent vs type in its child 1`] = ` -[ - "delete block "Parent" A", -] -`; - -exports[`versioning diff: Delete column vs add row 1`] = ` +exports[`versioning diff (experimental): Delete column vs add row 1`] = ` [ "delete A", "delete A", @@ -846,7 +840,7 @@ exports[`versioning diff: Delete column vs add row 1`] = ` ] `; -exports[`versioning diff: Delete image 1`] = ` +exports[`versioning diff (experimental): Delete image 1`] = ` [ "delete block "" A", "delete A", @@ -855,14 +849,14 @@ exports[`versioning diff: Delete image 1`] = ` ] `; -exports[`versioning diff: Delete one of two empty blocks 1`] = ` +exports[`versioning diff (experimental): Delete one of two empty blocks 1`] = ` [ "delete block "" A", "delete A", ] `; -exports[`versioning diff: Delete parent with mixed children 1`] = ` +exports[`versioning diff (experimental): Delete parent with mixed children 1`] = ` [ "delete block "Parent" A", "delete A", @@ -878,7 +872,7 @@ exports[`versioning diff: Delete parent with mixed children 1`] = ` ] `; -exports[`versioning diff: Delete row vs add column 1`] = ` +exports[`versioning diff (experimental): Delete row vs add column 1`] = ` [ "delete block "A1B1A2B2" A", "delete
A", @@ -911,19 +905,19 @@ exports[`versioning diff: Delete row vs add column 1`] = ` ] `; -exports[`versioning diff: Demote heading 1`] = ` +exports[`versioning diff (experimental): Demote heading 1`] = ` [ "attrs level:A", ] `; -exports[`versioning diff: Edit a cell 1`] = ` +exports[`versioning diff (experimental): Edit a cell 1`] = ` [ "insert " edited" A", ] `; -exports[`versioning diff: Edit a diagram 1`] = ` +exports[`versioning diff (experimental): Edit a diagram 1`] = ` [ "delete "Rev" A", "insert "Publ" A", @@ -932,7 +926,7 @@ exports[`versioning diff: Edit a diagram 1`] = ` ] `; -exports[`versioning diff: Edit a link 1`] = ` +exports[`versioning diff (experimental): Edit a link 1`] = ` [ "format "the " A", "delete "old" A", @@ -942,7 +936,7 @@ exports[`versioning diff: Edit a link 1`] = ` ] `; -exports[`versioning diff: Edit text vs change to heading 1`] = ` +exports[`versioning diff (experimental): Edit text vs change to heading 1`] = ` [ "delete block "hello world" B", "delete B", @@ -956,7 +950,7 @@ exports[`versioning diff: Edit text vs change to heading 1`] = ` ] `; -exports[`versioning diff: Enter at the start of a heading 1`] = ` +exports[`versioning diff (experimental): Enter at the start of a heading 1`] = ` [ "delete "Title" A", "insert block "Title" A", @@ -965,7 +959,7 @@ exports[`versioning diff: Enter at the start of a heading 1`] = ` ] `; -exports[`versioning diff: Fix typo vs delete word 1`] = ` +exports[`versioning diff (experimental): Fix typo vs delete word 1`] = ` [ "delete "w" B", "insert "o" A", @@ -975,7 +969,7 @@ exports[`versioning diff: Fix typo vs delete word 1`] = ` ] `; -exports[`versioning diff: Heading vs list item 1`] = ` +exports[`versioning diff (experimental): Heading vs list item 1`] = ` [ "delete block "hello world" A,B", "delete A,B", @@ -989,14 +983,14 @@ exports[`versioning diff: Heading vs list item 1`] = ` ] `; -exports[`versioning diff: Highlight a column 1`] = ` +exports[`versioning diff (experimental): Highlight a column 1`] = ` [ "attrs backgroundColor:A", "attrs backgroundColor:A", ] `; -exports[`versioning diff: Indent a block 1`] = ` +exports[`versioning diff (experimental): Indent a block 1`] = ` [ "delete block "N0" A", "delete A", @@ -1014,28 +1008,14 @@ exports[`versioning diff: Indent a block 1`] = ` ] `; -exports[`versioning diff: Indent a block vs edit its text 1`] = ` -[ - "delete block "N0" A", - "delete block "N1" A", - "insert block "N0" A", - "insert A", - "insert "N0" A", - "insert A", - "insert block "N1" A", - "insert A", - "insert "N1" A", -] -`; - -exports[`versioning diff: Insert a divider 1`] = ` +exports[`versioning diff (experimental): Insert a divider 1`] = ` [ "insert block "" A", "insert A", ] `; -exports[`versioning diff: Insert an image 1`] = ` +exports[`versioning diff (experimental): Insert an image 1`] = ` [ "insert A", "insert block "" A", @@ -1043,7 +1023,7 @@ exports[`versioning diff: Insert an image 1`] = ` ] `; -exports[`versioning diff: List item → paragraph 1`] = ` +exports[`versioning diff (experimental): List item → paragraph 1`] = ` [ "delete block "hello world" A", "delete A", @@ -1054,7 +1034,7 @@ exports[`versioning diff: List item → paragraph 1`] = ` ] `; -exports[`versioning diff: Merge blocks vs edit block B 1`] = ` +exports[`versioning diff (experimental): Merge blocks vs edit block B 1`] = ` [ "insert "Second" A", "delete block "Second" A", @@ -1063,7 +1043,7 @@ exports[`versioning diff: Merge blocks vs edit block B 1`] = ` ] `; -exports[`versioning diff: Merge cells 1`] = ` +exports[`versioning diff (experimental): Merge cells 1`] = ` [ "attrs colspan:A colwidth:A", "insert "+B1" A", @@ -1075,7 +1055,7 @@ exports[`versioning diff: Merge cells 1`] = ` ] `; -exports[`versioning diff: Move a block into a block that is deleted 1`] = ` +exports[`versioning diff (experimental): Move a block into a block that is deleted 1`] = ` [ "delete block "Parent" A", "delete A", @@ -1090,7 +1070,7 @@ exports[`versioning diff: Move a block into a block that is deleted 1`] = ` ] `; -exports[`versioning diff: Move paragraph up 1`] = ` +exports[`versioning diff (experimental): Move paragraph up 1`] = ` [ "insert block "Middle" A", "insert A", @@ -1101,7 +1081,7 @@ exports[`versioning diff: Move paragraph up 1`] = ` ] `; -exports[`versioning diff: Move paragraph with children 1`] = ` +exports[`versioning diff (experimental): Move paragraph with children 1`] = ` [ "insert block "Parent" A", "insert A", @@ -1120,7 +1100,7 @@ exports[`versioning diff: Move paragraph with children 1`] = ` ] `; -exports[`versioning diff: Nest a bullet under another 1`] = ` +exports[`versioning diff (experimental): Nest a bullet under another 1`] = ` [ "delete block "Parent" A", "delete A", @@ -1138,7 +1118,7 @@ exports[`versioning diff: Nest a bullet under another 1`] = ` ] `; -exports[`versioning diff: Nest blocks into a block that is moved 1`] = ` +exports[`versioning diff (experimental): Nest blocks into a block that is moved 1`] = ` [ "delete block "R" B", "delete B", @@ -1178,7 +1158,7 @@ exports[`versioning diff: Nest blocks into a block that is moved 1`] = ` ] `; -exports[`versioning diff: Paragraph → heading 1`] = ` +exports[`versioning diff (experimental): Paragraph → heading 1`] = ` [ "delete block "hello world" A", "delete A", @@ -1189,7 +1169,7 @@ exports[`versioning diff: Paragraph → heading 1`] = ` ] `; -exports[`versioning diff: Remove a column 1`] = ` +exports[`versioning diff (experimental): Remove a column 1`] = ` [ "delete A", "delete A", @@ -1206,13 +1186,13 @@ exports[`versioning diff: Remove a column 1`] = ` ] `; -exports[`versioning diff: Remove a link 1`] = ` +exports[`versioning diff (experimental): Remove a link 1`] = ` [ "format "the site" A", ] `; -exports[`versioning diff: Remove a middle column 1`] = ` +exports[`versioning diff (experimental): Remove a middle column 1`] = ` [ "delete A", "delete block "Right column" A", @@ -1221,7 +1201,7 @@ exports[`versioning diff: Remove a middle column 1`] = ` ] `; -exports[`versioning diff: Remove a paragraph 1`] = ` +exports[`versioning diff (experimental): Remove a paragraph 1`] = ` [ "delete block "Body text" A", "delete A", @@ -1229,13 +1209,13 @@ exports[`versioning diff: Remove a paragraph 1`] = ` ] `; -exports[`versioning diff: Remove bold 1`] = ` +exports[`versioning diff (experimental): Remove bold 1`] = ` [ "format "world" A", ] `; -exports[`versioning diff: Remove column 1`] = ` +exports[`versioning diff (experimental): Remove column 1`] = ` [ "delete A", "delete A", @@ -1246,7 +1226,7 @@ exports[`versioning diff: Remove column 1`] = ` ] `; -exports[`versioning diff: Remove row 1`] = ` +exports[`versioning diff (experimental): Remove row 1`] = ` [ "delete A", "delete A", @@ -1258,14 +1238,14 @@ exports[`versioning diff: Remove row 1`] = ` ] `; -exports[`versioning diff: Remove the only block 1`] = ` +exports[`versioning diff (experimental): Remove the only block 1`] = ` [ "attrs block "Only block" id:A", "delete "Only block" A", ] `; -exports[`versioning diff: Rename a word 1`] = ` +exports[`versioning diff (experimental): Rename a word 1`] = ` [ "delete "wo" A", "insert "unive" A", @@ -1274,13 +1254,13 @@ exports[`versioning diff: Rename a word 1`] = ` ] `; -exports[`versioning diff: Resize image 1`] = ` +exports[`versioning diff (experimental): Resize image 1`] = ` [ "attrs previewWidth:A", ] `; -exports[`versioning diff: Split a block vs type at end 1`] = ` +exports[`versioning diff (experimental): Split a block vs type at end 1`] = ` [ "delete "world" B", "insert "!" A", @@ -1290,7 +1270,7 @@ exports[`versioning diff: Split a block vs type at end 1`] = ` ] `; -exports[`versioning diff: Split a merged cell 1`] = ` +exports[`versioning diff (experimental): Split a merged cell 1`] = ` [ "attrs colspan:A colwidth:A", "delete "+B1" A", @@ -1300,13 +1280,13 @@ exports[`versioning diff: Split a merged cell 1`] = ` ] `; -exports[`versioning diff: Text color vs background color 1`] = ` +exports[`versioning diff (experimental): Text color vs background color 1`] = ` [ "attrs backgroundColor:B textColor:A", ] `; -exports[`versioning diff: Unindent a block 1`] = ` +exports[`versioning diff (experimental): Unindent a block 1`] = ` [ "delete block "N0" A", "delete A", @@ -1323,3 +1303,1119 @@ exports[`versioning diff: Unindent a block 1`] = ` "insert "N1" A", ] `; + +exports[`versioning diff: A adds column then row, B adds column 1`] = ` +[ + "delete block "A1B1A2B2" B,A", + "insert block "A1B1C1D1A2B2C2D2A3B3C3" B,A", + "insert
B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A1" B,A", + "insert B,A", + "insert B,A", + "insert "B1" B,A", + "insert B,A", + "insert B,A", + "insert "C1" B,A", + "insert B,A", + "insert B,A", + "insert "D1" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A2" B,A", + "insert B,A", + "insert B,A", + "insert "B2" B,A", + "insert B,A", + "insert B,A", + "insert "C2" B,A", + "insert B,A", + "insert B,A", + "insert "D2" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A3" B,A", + "insert B,A", + "insert B,A", + "insert "B3" B,A", + "insert B,A", + "insert B,A", + "insert "C3" B,A", + "insert ", + "insert ", +] +`; + +exports[`versioning diff: A adds row then column, B adds row 1`] = ` +[ + "delete block "A1B1A2B2" B,A", + "insert block "A1B1C1A2B2C2A3B3C3D1D2" B,A", + "insert
B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A1" B,A", + "insert B,A", + "insert B,A", + "insert "B1" B,A", + "insert B,A", + "insert B,A", + "insert "C1" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A2" B,A", + "insert B,A", + "insert B,A", + "insert "B2" B,A", + "insert B,A", + "insert B,A", + "insert "C2" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A3" B,A", + "insert B,A", + "insert B,A", + "insert "B3" B,A", + "insert B,A", + "insert B,A", + "insert "C3" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "D1" B,A", + "insert B,A", + "insert B,A", + "insert "D2" B,A", + "insert ", + "insert ", +] +`; + +exports[`versioning diff: Add a block to a column 1`] = ` +[ + "insert block "Added to the left column" A", + "insert A", + "insert "Added to the left column" A", +] +`; + +exports[`versioning diff: Add a link 1`] = ` +[ + "format "the site" A", +] +`; + +exports[`versioning diff: Add a whole document 1`] = ` +[ + "insert block "Welcome to this demo 🙌!" A", + "insert A", + "insert "Welcome to this " A", + "format "Welcome to this " A", + "insert "demo 🙌!" A", + "format "demo 🙌!" A", + "insert A", + "insert block "Hello World nested" A", + "insert A", + "insert "Hello World nested" A", + "insert A", + "insert block "Hello World double nested" A", + "insert A", + "insert "Hello World double nested" A", + "insert block "This paragraph has a background color" A", + "insert A", + "insert "This paragraph has a background color" A", + "format "This paragraph has a background color" A", + "insert block "Paragraph" A", + "insert A", + "insert "Paragraph" A", + "insert block "" A", + "insert A", + "insert block "Heading" A", + "insert A", + "insert "Heading" A", + "insert block "Heading right" A", + "insert A", + "insert "Heading right" A", + "insert block "Heading 2" A", + "insert A", + "insert "Heading 2" A", + "insert block "Heading 3" A", + "insert A", + "insert "Heading 3" A", + "insert block "Heading 4" A", + "insert A", + "insert "Heading 4" A", + "insert block "Heading 5" A", + "insert A", + "insert "Heading 5" A", + "insert block "Heading 6" A", + "insert A", + "insert "Heading 6" A", + "insert block "Emojis: 😀 🎉 🚀 👍 👍🏽 🌍 🚶‍♀️" A", + "insert A", + "insert "Emojis: 😀 🎉 🚀 👍 👍🏽 🌍 🚶‍♀️" A", + "insert block "Centered paragraph" A", + "insert A", + "insert "Centered paragraph" A", + "insert block "justified paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." A", + "insert A", + "insert "justified paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." A", + "insert block "" A", + "insert A", + "insert block "Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." A", + "insert A", + "insert "Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." A", + "insert A", + "insert block "Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." A", + "insert A", + "insert "Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." A", + "insert block "Bullet List Item right. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." A", + "insert A", + "insert "Bullet List Item right. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." A", + "insert block "Numbered List Item 1" A", + "insert A", + "insert "Numbered List Item 1" A", + "insert block "Numbered List Item 2" A", + "insert A", + "insert "Numbered List Item 2" A", + "insert A", + "insert block "Numbered List Item Nested 1" A", + "insert A", + "insert "Numbered List Item Nested 1" A", + "insert block "Numbered List Item Nested 2" A", + "insert A", + "insert "Numbered List Item Nested 2" A", + "insert block "Numbered List Item Nested funky right" A", + "insert A", + "insert "Numbered List Item Nested funky right" A", + "insert block "Numbered List Item Nested funky center" A", + "insert A", + "insert "Numbered List Item Nested funky center" A", + "insert block "Numbered List Item" A", + "insert A", + "insert "Numbered List Item" A", + "insert block "Check List Item" A", + "insert A", + "insert "Check List Item" A", + "insert block "Checked List Item" A", + "insert A", + "insert "Checked List Item" A", + "insert block "Numbered List Item starting at 5" A", + "insert A", + "insert "Numbered List Item starting at 5" A", + "insert block "Numbered List Item 6" A", + "insert A", + "insert "Numbered List Item 6" A", + "insert block "Toggle List Item" A", + "insert A", + "insert "Toggle List Item" A", + "insert A", + "insert block "Content nested inside the toggle list item." A", + "insert A", + "insert "Content nested inside the toggle list item." A", + "insert block "A nested bullet inside the toggle" A", + "insert A", + "insert "A nested bullet inside the toggle" A", + "insert block "Toggle Heading" A", + "insert A", + "insert "Toggle Heading" A", + "insert A", + "insert block "Content nested inside the toggle heading." A", + "insert A", + "insert "Content nested inside the toggle heading." A", + "insert block "Wide CellTable CellTable CellWide CellTable CellTable CellWide CellTable CellTable Cell" A", + "insert
A", + "insert A", + "insert A", + "insert A", + "insert "Wide Cell" A", + "insert A", + "insert A", + "insert "Table Cell" A", + "insert A", + "insert A", + "insert "Table Cell" A", + "insert A", + "insert A", + "insert A", + "insert "Wide Cell" A", + "insert A", + "insert A", + "insert "Table Cell" A", + "insert A", + "insert A", + "insert "Table Cell" A", + "insert A", + "insert A", + "insert A", + "insert "Wide Cell" A", + "insert A", + "insert A", + "insert "Table Cell" A", + "insert A", + "insert A", + "insert "Table Cell" A", + "insert block "" A", + "insert A", + "insert block "" A", + "insert A", + "insert block "" A", + "insert A", + "insert block "" A", + "insert
A", + "insert A", + "insert A", + "insert A", + "insert "Table Header 1" A", + "insert A", + "insert A", + "insert "Table Header 2" A", + "insert A", + "insert A", + "insert "Table Header 3" A", + "insert A", + "insert A", + "insert A", + "insert "Table Cell 4" A", + "insert A", + "insert A", + "insert "Table Cell Bold Colored 5" A", + "format "Table Cell Bold Colored 5" A", + "insert A", + "insert A", + "insert "Table Cell 6" A", + "insert A", + "insert A", + "insert A", + "insert "Table Cell 7" A", + "insert A", + "insert A", + "insert "Table Cell 8" A", + "insert A", + "insert A", + "insert "Table Cell 9" A", + "insert block "Merged HeaderHeader CHeader AHeader BHeader C2Merged RowsCell B1Cell C1Cell B2Cell C2" A", + "insert
A", + "insert A", + "insert A", + "insert A", + "insert "Merged Header" A", + "insert A", + "insert A", + "insert "Header C" A", + "insert A", + "insert A", + "insert A", + "insert "Header A" A", + "insert A", + "insert A", + "insert "Header B" A", + "insert A", + "insert A", + "insert "Header C2" A", + "insert A", + "insert A", + "insert A", + "insert "Merged Rows" A", + "insert A", + "insert A", + "insert "Cell B1" A", + "insert A", + "insert A", + "insert "Cell C1" A", + "insert A", + "insert A", + "insert A", + "insert "Cell B2" A", + "insert A", + "insert A", + "insert "Cell C2" A", + "insert block "A hard line breakwithin a single paragraph" A", + "insert A", + "insert "A hard line break" A", + "insert A", + "insert "within a single paragraph" A", + "insert block "const helloWorld = (message) => {\\n console.log(\\"Hello World\\", message);\\n};" A", + "insert A", + "insert "const helloWorld = (message) => {\\n console.log(\\"Hello World\\", message);\\n};" A", + "insert block "Some inline code: var foo = 'bar';" A", + "insert A", + "insert "Some inline code: " A", + "format "Some inline code: " A", + "insert "var foo = 'bar';" A", + "format "var foo = 'bar';" A", + "insert A", + "insert A", + "insert block "This paragraph is in a column!" A", + "insert A", + "insert "This paragraph is in a column!" A", + "insert A", + "insert block "So is this heading!" A", + "insert A", + "insert "So is this heading!" A", + "insert A", + "insert block "You can have multiple blocks in a column too" A", + "insert A", + "insert "You can have multiple blocks in a column too" A", + "insert block "Block 1" A", + "insert A", + "insert "Block 1" A", + "insert block "Block 2" A", + "insert A", + "insert "Block 2" A", + "insert block "Block 3" A", + "insert A", + "insert "Block 3" A", + "insert block "" A", + "insert A", + "insert block "All those moments will be lost in time, like tears in rain." A", + "insert A", + "insert "All those moments will be lost in time, like tears in rain." A", +] +`; + +exports[`versioning diff: Add an empty block 1`] = ` +[ + "insert block "" A", + "insert A", +] +`; + +exports[`versioning diff: Add bold 1`] = ` +[ + "format "world" A", +] +`; + +exports[`versioning diff: Add bullet item 1`] = ` +[ + "insert A", + "insert block "New bullet" A", + "insert A", + "insert "New bullet" A", +] +`; + +exports[`versioning diff: Add colored block with child 1`] = ` +[ + "insert A", + "insert block "Colored parent" A", + "insert A", + "insert "Colored parent" A", + "insert A", + "insert block "Child block" A", + "insert A", + "insert "Child block" A", +] +`; + +exports[`versioning diff: Add column 1`] = ` +[ + "insert A", + "insert A", + "insert "C1" A", + "insert A", + "insert A", + "insert "C2" A", +] +`; + +exports[`versioning diff: Add column vs add row 1`] = ` +[ + "delete block "A1B1A2B2" B,A", + "insert block "A1B1C1A2B2C2A3B3" B,A", + "insert
B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A1" B,A", + "insert B,A", + "insert B,A", + "insert "B1" B,A", + "insert B,A", + "insert B,A", + "insert "C1" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A2" B,A", + "insert B,A", + "insert B,A", + "insert "B2" B,A", + "insert B,A", + "insert B,A", + "insert "C2" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A3" B,A", + "insert B,A", + "insert B,A", + "insert "B3" B,A", + "insert ", + "insert ", +] +`; + +exports[`versioning diff: Add heading 1`] = ` +[ + "insert A", + "insert block "New heading" A", + "insert A", + "insert "New heading" A", +] +`; + +exports[`versioning diff: Add italic over bold 1`] = ` +[ + "format "world" A", +] +`; + +exports[`versioning diff: Add nested bullets 1`] = ` +[ + "insert A", + "insert block "Level 0" A", + "insert A", + "insert "Level 0" A", + "insert A", + "insert block "Level 1" A", + "insert A", + "insert "Level 1" A", + "insert A", + "insert block "Level 2" A", + "insert A", + "insert "Level 2" A", +] +`; + +exports[`versioning diff: Add numbered item 1`] = ` +[ + "insert A", + "insert block "New numbered" A", + "insert A", + "insert "New numbered" A", +] +`; + +exports[`versioning diff: Add paragraph after a block 1`] = ` +[ + "insert block "Body text" A", + "insert A", + "insert "Body text" A", +] +`; + +exports[`versioning diff: Add row 1`] = ` +[ + "insert A", + "insert A", + "insert A", + "insert "A3" A", + "insert A", + "insert A", + "insert "B3" A", +] +`; + +exports[`versioning diff: Add row vs add column 1`] = ` +[ + "delete block "A1B1A2B2" B,A", + "insert block "A1B1C1A2B2C2A3B3" B,A", + "insert
B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A1" B,A", + "insert B,A", + "insert B,A", + "insert "B1" B,A", + "insert B,A", + "insert B,A", + "insert "C1" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A2" B,A", + "insert B,A", + "insert B,A", + "insert "B2" B,A", + "insert B,A", + "insert B,A", + "insert "C2" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A3" B,A", + "insert B,A", + "insert B,A", + "insert "B3" B,A", + "insert ", + "insert ", +] +`; + +exports[`versioning diff: Bold vs italic 1`] = ` +[ + "format "world" A,B", +] +`; + +exports[`versioning diff: Both nest a new block under N0 1`] = ` +[ + "delete block "N0" A,B", + "insert block "N0" A", + "insert A", + "insert "N0" A", + "insert A", + "insert block "N1" A", + "insert A", + "insert "N1" A", + "insert block "N0" B", + "insert B", + "insert "N0" B", + "insert B", + "insert block "N2" B", + "insert B", + "insert "N2" B", +] +`; + +exports[`versioning diff: Cascading indents 1`] = ` +[ + "delete block "N0" A", + "delete block "N1" A,B", + "insert block "N0" A", + "insert A", + "insert "N0" A", + "insert A", + "insert block "N1" A", + "insert A", + "insert "N1" A", + "delete block "N2" B", + "insert block "N1" B", + "insert B", + "insert "N1" B", + "insert B", + "insert block "N2" B", + "insert B", + "insert "N2" B", +] +`; + +exports[`versioning diff: Center-align 1`] = ` +[ + "attrs textAlignment:A", +] +`; + +exports[`versioning diff: Change a parent's type vs edit its child 1`] = ` +[ + "delete block "Parent" A", + "insert block "Parent" A", + "insert A", + "insert "Parent" A", + "insert A", + "insert block "Child" A", + "insert A", + "insert "Child" A", +] +`; + +exports[`versioning diff: Change image source 1`] = ` +[ + "attrs url:A", +] +`; + +exports[`versioning diff: Change type of a parent block 1`] = ` +[ + "delete block "N0" A", + "insert block "N0" A", + "insert A", + "insert "N0" A", + "insert A", + "insert block "N1" A", + "insert A", + "insert "N1" A", +] +`; + +exports[`versioning diff: Create two columns 1`] = ` +[ + "insert A", + "insert A", + "insert block "Left column" A", + "insert A", + "insert "Left column" A", + "insert A", + "insert block "Right column" A", + "insert A", + "insert "Right column" A", +] +`; + +exports[`versioning diff: Delete a code block 1`] = ` +[ + "delete block "const x = 1;" A", + "insert block "" A", + "insert A", +] +`; + +exports[`versioning diff: Delete a divider 1`] = ` +[ + "delete block "" A", + "insert block "" A", + "insert A", +] +`; + +exports[`versioning diff: Delete a nested block 1`] = ` +[ + "delete block "Parent" A", + "insert block "Parent" A", + "insert A", + "insert "Parent" A", +] +`; + +exports[`versioning diff: Delete a parent block 1`] = ` +[ + "delete block "Parent" A", + "insert block "" A", + "insert A", +] +`; + +exports[`versioning diff: Delete a parent vs change its child's type 1`] = ` +[ + "delete block "Parent" A", +] +`; + +exports[`versioning diff: Delete a parent vs type in its child 1`] = ` +[ + "delete block "Parent" A", +] +`; + +exports[`versioning diff: Delete column vs add row 1`] = ` +[ + "delete A", + "delete A", + "insert B", + "insert B", + "insert B", + "insert "A3" B", + "insert B", + "insert B", + "insert "B3" B", +] +`; + +exports[`versioning diff: Delete image 1`] = ` +[ + "delete block "" A", + "insert block "" A", + "insert A", +] +`; + +exports[`versioning diff: Delete one of two empty blocks 1`] = ` +[ + "delete block "" A", +] +`; + +exports[`versioning diff: Delete parent with mixed children 1`] = ` +[ + "delete block "Parent" A", + "insert block "" A", + "insert A", +] +`; + +exports[`versioning diff: Delete row vs add column 1`] = ` +[ + "delete block "A1B1A2B2" A", + "insert block "A1B1C1" B", + "insert
B", + "insert B", + "insert B", + "insert B", + "insert "A1" B", + "insert B", + "insert B", + "insert "B1" B", + "insert B", + "insert B", + "insert "C1" B", +] +`; + +exports[`versioning diff: Demote heading 1`] = ` +[ + "attrs level:A", +] +`; + +exports[`versioning diff: Edit a cell 1`] = ` +[ + "insert " edited" A", +] +`; + +exports[`versioning diff: Edit a diagram 1`] = ` +[ + "delete "Rev" A", + "insert "Publ" A", + "delete "ew" A", + "insert "sh" A", +] +`; + +exports[`versioning diff: Edit a link 1`] = ` +[ + "format "the " A", + "delete "old" A", + "insert "new" A", + "format "new" A", + "format " site" A", +] +`; + +exports[`versioning diff: Edit text vs change to heading 1`] = ` +[ + "delete block "hello world" B", + "insert block "hello world" B", + "insert B", + "insert "hello world" B", +] +`; + +exports[`versioning diff: Enter at the start of a heading 1`] = ` +[ + "delete "Title" A", + "insert block "Title" A", + "insert A", + "insert "Title" A", +] +`; + +exports[`versioning diff: Fix typo vs delete word 1`] = ` +[ + "delete "w" B", + "insert "o" A", + "delete "r" B", + "delete "o" B,A", + "delete "ld" B", +] +`; + +exports[`versioning diff: Heading vs list item 1`] = ` +[ + "delete block "hello world" A,B", + "insert block "hello world" A", + "insert A", + "insert "hello world" A", + "insert block "hello world" B", + "insert B", + "insert "hello world" B", +] +`; + +exports[`versioning diff: Highlight a column 1`] = ` +[ + "attrs backgroundColor:A", + "attrs backgroundColor:A", +] +`; + +exports[`versioning diff: Indent a block 1`] = ` +[ + "delete block "N0" A", + "delete block "N1" A", + "insert block "N0" A", + "insert A", + "insert "N0" A", + "insert A", + "insert block "N1" A", + "insert A", + "insert "N1" A", +] +`; + +exports[`versioning diff: Indent a block vs edit its text 1`] = ` +[ + "delete block "N0" A", + "delete block "N1" A", + "insert block "N0" A", + "insert A", + "insert "N0" A", + "insert A", + "insert block "N1" A", + "insert A", + "insert "N1" A", +] +`; + +exports[`versioning diff: Insert a divider 1`] = ` +[ + "insert block "" A", + "insert A", +] +`; + +exports[`versioning diff: Insert an image 1`] = ` +[ + "insert A", + "insert block "" A", + "insert A", +] +`; + +exports[`versioning diff: List item → paragraph 1`] = ` +[ + "delete block "hello world" A", + "insert block "hello world" A", + "insert A", + "insert "hello world" A", +] +`; + +exports[`versioning diff: Merge blocks vs edit block B 1`] = ` +[ + "insert "Second" A", + "delete block "Second" A", +] +`; + +exports[`versioning diff: Merge cells 1`] = ` +[ + "attrs colspan:A colwidth:A", + "insert "+B1" A", + "delete A", + "insert ", + "insert ", +] +`; + +exports[`versioning diff: Move a block into a block that is deleted 1`] = ` +[ + "delete block "Parent" A", + "delete block "X" B", +] +`; + +exports[`versioning diff: Move paragraph up 1`] = ` +[ + "insert block "Middle" A", + "insert A", + "insert "Middle" A", + "delete block "Middle" A", +] +`; + +exports[`versioning diff: Move paragraph with children 1`] = ` +[ + "insert block "Parent" A", + "insert A", + "insert "Parent" A", + "insert A", + "insert block "Child" A", + "insert A", + "insert "Child" A", + "delete block "Parent" A", +] +`; + +exports[`versioning diff: Nest a bullet under another 1`] = ` +[ + "delete block "Parent" A", + "delete block "Child" A", + "insert block "Parent" A", + "insert A", + "insert "Parent" A", + "insert A", + "insert block "Child" A", + "insert A", + "insert "Child" A", +] +`; + +exports[`versioning diff: Nest blocks into a block that is moved 1`] = ` +[ + "delete block "R" B", + "delete block "Q" B,A", + "insert block "R" B", + "insert B", + "insert "R" B", + "insert B", + "insert block "Q" B", + "insert B", + "insert "Q" B", + "delete block "B1" A", + "delete block "B2" A", + "delete block "B3" A", + "insert block "Q" A", + "insert A", + "insert "Q" A", + "insert A", + "insert block "B1" A", + "insert A", + "insert "B1" A", + "insert block "B2" A", + "insert A", + "insert "B2" A", + "insert block "B3" A", + "insert A", + "insert "B3" A", +] +`; + +exports[`versioning diff: Paragraph → heading 1`] = ` +[ + "delete block "hello world" A", + "insert block "hello world" A", + "insert A", + "insert "hello world" A", +] +`; + +exports[`versioning diff: Remove a column 1`] = ` +[ + "delete A", + "insert block "Left column" A", + "insert A", + "insert "Left column" A", +] +`; + +exports[`versioning diff: Remove a link 1`] = ` +[ + "format "the site" A", +] +`; + +exports[`versioning diff: Remove a middle column 1`] = ` +[ + "delete A", +] +`; + +exports[`versioning diff: Remove a paragraph 1`] = ` +[ + "delete block "Body text" A", +] +`; + +exports[`versioning diff: Remove bold 1`] = ` +[ + "format "world" A", +] +`; + +exports[`versioning diff: Remove column 1`] = ` +[ + "delete A", + "delete A", +] +`; + +exports[`versioning diff: Remove row 1`] = ` +[ + "delete A", +] +`; + +exports[`versioning diff: Remove the only block 1`] = ` +[ + "attrs block "Only block" id:A", + "delete "Only block" A", +] +`; + +exports[`versioning diff: Rename a word 1`] = ` +[ + "delete "wo" A", + "insert "unive" A", + "delete "ld" A", + "insert "se" A", +] +`; + +exports[`versioning diff: Resize image 1`] = ` +[ + "attrs previewWidth:A", +] +`; + +exports[`versioning diff: Split a block vs type at end 1`] = ` +[ + "delete "world" B", + "insert "!" A", + "insert block "world" B", + "insert B", + "insert "world" B", +] +`; + +exports[`versioning diff: Split a merged cell 1`] = ` +[ + "attrs colspan:A colwidth:A", + "delete "+B1" A", + "insert A", + "insert A", + "insert "B1" A", +] +`; + +exports[`versioning diff: Text color vs background color 1`] = ` +[ + "attrs backgroundColor:B textColor:A", +] +`; + +exports[`versioning diff: Unindent a block 1`] = ` +[ + "delete block "N0" A", + "insert block "N0" A", + "insert A", + "insert "N0" A", + "insert block "N1" A", + "insert A", + "insert "N1" A", +] +`; diff --git a/tests/src/end-to-end/y-prosemirror/versioning.test.tsx b/tests/src/end-to-end/y-prosemirror/versioning.test.tsx index 76bb315a90..054918e845 100644 --- a/tests/src/end-to-end/y-prosemirror/versioning.test.tsx +++ b/tests/src/end-to-end/y-prosemirror/versioning.test.tsx @@ -17,6 +17,7 @@ import { blocksToYDoc, getAttributeChanges, createYVersionView, + type ExperimentalVersionDiffs, withCollaboration, } from "@blocknote/core/y"; import * as Y from "@y/y"; @@ -52,7 +53,10 @@ function cloneWithId(source: Y.Doc, clientID: number): Y.Doc { } /** Mount a collaborative editor on `doc`, returning it + a teardown. */ -function mountEditor(doc: Y.Doc): { +function mountEditor( + doc: Y.Doc, + experimental: ExperimentalVersionDiffs = {}, +): { editor: GalleryEditor; teardown: () => void; } { @@ -65,8 +69,7 @@ function mountEditor(doc: Y.Doc): { fragment: doc.get("doc"), provider: undefined, user: { name: "User", color: "#8a6d1a" }, - // As the gallery: with the experimental diff refinements. - experimental: { lostContentAttribution: true }, + experimental, }, }), ); @@ -95,14 +98,25 @@ const propertyChanges = new Map([ ["prop-image-source", "url"], ]); -for (const scenario of scenarios) { +// Each scenario's diff with the experimental flags off (as in the editor) and +// all on. +const cases = scenarios.flatMap((scenario) => [ + { scenario, name: "versioning diff", experimental: {} }, + { + scenario, + name: "versioning diff (experimental)", + experimental: { lostContentAttribution: true }, + }, +]); + +for (const { scenario, name, experimental } of cases) { const applies = scenario.kind === "single" ? [scenario.apply] : [scenario.applyA, scenario.applyB]; const runner = VERSIONING_CRASHES.has(scenario.id) ? test.skip : test; - runner(`versioning diff: ${scenario.title}`, async () => { + runner(`${name}: ${scenario.title}`, async () => { const teardown: Array<() => void> = []; try { // "Before": the scenario's initial blocks, seeded synchronously. @@ -138,7 +152,10 @@ for (const scenario of scenarios) { // The versioning diff render — this is the path that throws for the // nested-move / table-merge crashers. - const { editor: diffEditor, teardown: unmount } = mountEditor(afterDoc); + const { editor: diffEditor, teardown: unmount } = mountEditor( + afterDoc, + experimental, + ); teardown.push(unmount); const view = createYVersionView(diffEditor, afterDoc.get("doc")).open(); teardown.push(() => view.close()); From 1bbbacbd4577c4d9fbcf957ab3f99e4087cf85a3 Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 8 Oct 2026 20:04:10 +0200 Subject: [PATCH 03/10] fix(versioning): show content deleted with its block under the block's mark With gc: false, each piece of content deleted with its block kept its own delete mark, so hovering it showed a second, identical tooltip. Content whose deletion matches its parent's now renders inside the parent's deletion, as before. Content deleted by someone else, or without an author, keeps its own mark. --- .../14-suggestion-gallery/src/scenarios.ts | 5 + .../core/src/y/extensions/snapshotPreview.ts | 34 +++ .../__snapshots__/versioning.test.tsx.snap | 219 ++---------------- 3 files changed, 53 insertions(+), 205 deletions(-) diff --git a/examples/07-collaboration/14-suggestion-gallery/src/scenarios.ts b/examples/07-collaboration/14-suggestion-gallery/src/scenarios.ts index f9a1af39c2..e306cde739 100644 --- a/examples/07-collaboration/14-suggestion-gallery/src/scenarios.ts +++ b/examples/07-collaboration/14-suggestion-gallery/src/scenarios.ts @@ -1358,6 +1358,11 @@ export const scenarios: SuggestionScenario[] = [ severity: "high", note: "The diff shows the block as deleted and inserted again, all credited to B: changing the type re-creates it.", }, + { + severity: "info", + when: { lostContentAttribution: true }, + note: "Versioning also shows the letters A deleted inside the replaced paragraph, as deleted by A and B.", + }, { severity: "low", note: "User A's content edit is lost — it's overwritten by B's simultaneous block-type change. This is a consequence of the schema fix.", diff --git a/packages/core/src/y/extensions/snapshotPreview.ts b/packages/core/src/y/extensions/snapshotPreview.ts index a9c01732fa..cf512ffbd6 100644 --- a/packages/core/src/y/extensions/snapshotPreview.ts +++ b/packages/core/src/y/extensions/snapshotPreview.ts @@ -42,10 +42,44 @@ class SnapshotDiffRenderer extends Y.DiffRenderer { for (let i = contents.length - 1; i >= start; i--) { if (this.inserts.has(client, contents[i].clock)) { contents.splice(i, 1); + } else if (this.deletedWithParent(client, contents[i])) { + // Shown inside the parent's deletion, as without `gc: false`. + contents[i].deleted = false; + contents[i].attrs = null; } } } } + + /** Whether the content's deletion is the same as its deleted parent's. */ + private deletedWithParent( + client: number, + { + clock, + attrs, + }: { clock: number; attrs: Y.ContentAttribute[] | null }, + ): boolean { + const structs = this._nextDoc.store.clients.get(client); + const item = structs?.[Y.findIndexSS(structs, clock)]; + const parent = item instanceof Y.Item ? item.parent?._item : null; + if (!parent?.deleted) { + return false; + } + const [own] = this.deletes.slice(parent.id.client, parent.id.clock, 1); + return sameAttributes(own?.attrs ?? null, attrs); + } +} + +function sameAttributes( + a: Y.ContentAttribute[] | null, + b: Y.ContentAttribute[] | null, +): boolean { + return ( + a?.length === b?.length && + (a ?? []).every((x) => + (b ?? []).some((y) => x.name === y.name && x.val === y.val), + ) + ); } /** diff --git a/tests/src/end-to-end/y-prosemirror/__snapshots__/versioning.test.tsx.snap b/tests/src/end-to-end/y-prosemirror/__snapshots__/versioning.test.tsx.snap index 75c5ccc703..7d9401dec5 100644 --- a/tests/src/end-to-end/y-prosemirror/__snapshots__/versioning.test.tsx.snap +++ b/tests/src/end-to-end/y-prosemirror/__snapshots__/versioning.test.tsx.snap @@ -3,21 +3,6 @@ exports[`versioning diff (experimental): A adds column then row, B adds column 1`] = ` [ "delete block "A1B1A2B2" B,A", - "delete
B,A", - "delete B,A", - "delete B,A", - "delete B,A", - "delete "A1" B,A", - "delete B,A", - "delete B,A", - "delete "B1" B,A", - "delete B,A", - "delete B,A", - "delete B,A", - "delete "A2" B,A", - "delete B,A", - "delete B,A", - "delete "B2" B,A", "insert block "A1B1C1D1A2B2C2D2A3B3C3" B,A", "insert
B,A", "insert B,A", @@ -64,21 +49,6 @@ exports[`versioning diff (experimental): A adds column then row, B adds column 1 exports[`versioning diff (experimental): A adds row then column, B adds row 1`] = ` [ "delete block "A1B1A2B2" B,A", - "delete
B,A", - "delete B,A", - "delete B,A", - "delete B,A", - "delete "A1" B,A", - "delete B,A", - "delete B,A", - "delete "B1" B,A", - "delete B,A", - "delete B,A", - "delete B,A", - "delete "A2" B,A", - "delete B,A", - "delete B,A", - "delete "B2" B,A", "insert block "A1B1C1A2B2C2A3B3C3D1D2" B,A", "insert
B,A", "insert B,A", @@ -480,21 +450,6 @@ exports[`versioning diff (experimental): Add column 1`] = ` exports[`versioning diff (experimental): Add column vs add row 1`] = ` [ "delete block "A1B1A2B2" B,A", - "delete
B,A", - "delete B,A", - "delete B,A", - "delete B,A", - "delete "A1" B,A", - "delete B,A", - "delete B,A", - "delete "B1" B,A", - "delete B,A", - "delete B,A", - "delete B,A", - "delete "A2" B,A", - "delete B,A", - "delete B,A", - "delete "B2" B,A", "insert block "A1B1C1A2B2C2A3B3" B,A", "insert
B,A", "insert B,A", @@ -593,21 +548,6 @@ exports[`versioning diff (experimental): Add row 1`] = ` exports[`versioning diff (experimental): Add row vs add column 1`] = ` [ "delete block "A1B1A2B2" B,A", - "delete
B,A", - "delete B,A", - "delete B,A", - "delete B,A", - "delete "A1" B,A", - "delete B,A", - "delete B,A", - "delete "B1" B,A", - "delete B,A", - "delete B,A", - "delete B,A", - "delete "A2" B,A", - "delete B,A", - "delete B,A", - "delete "B2" B,A", "insert block "A1B1C1A2B2C2A3B3" B,A", "insert
B,A", "insert B,A", @@ -651,8 +591,6 @@ exports[`versioning diff (experimental): Bold vs italic 1`] = ` exports[`versioning diff (experimental): Both nest a new block under N0 1`] = ` [ "delete block "N0" A,B", - "delete A,B", - "delete "N0" A,B", "insert block "N0" A", "insert A", "insert "N0" A", @@ -673,11 +611,7 @@ exports[`versioning diff (experimental): Both nest a new block under N0 1`] = ` exports[`versioning diff (experimental): Cascading indents 1`] = ` [ "delete block "N0" A", - "delete A", - "delete "N0" A", "delete block "N1" A,B", - "delete A,B", - "delete "N1" A,B", "insert block "N0" A", "insert A", "insert "N0" A", @@ -686,8 +620,6 @@ exports[`versioning diff (experimental): Cascading indents 1`] = ` "insert A", "insert "N1" A", "delete block "N2" B", - "delete B", - "delete "N2" B", "insert block "N1" B", "insert B", "insert "N1" B", @@ -707,12 +639,6 @@ exports[`versioning diff (experimental): Center-align 1`] = ` exports[`versioning diff (experimental): Change a parent's type vs edit its child 1`] = ` [ "delete block "Parent" A", - "delete A", - "delete "Parent" A", - "delete A", - "delete block "Child" A", - "delete A", - "delete "Child" A", "insert block "Parent" A", "insert A", "insert "Parent" A", @@ -732,12 +658,6 @@ exports[`versioning diff (experimental): Change image source 1`] = ` exports[`versioning diff (experimental): Change type of a parent block 1`] = ` [ "delete block "N0" A", - "delete A", - "delete "N0" A", - "delete A", - "delete block "N1" A", - "delete A", - "delete "N1" A", "insert block "N0" A", "insert A", "insert "N0" A", @@ -765,8 +685,6 @@ exports[`versioning diff (experimental): Create two columns 1`] = ` exports[`versioning diff (experimental): Delete a code block 1`] = ` [ "delete block "const x = 1;" A", - "delete A", - "delete "const x = 1;" A", "insert block "" A", "insert A", ] @@ -775,7 +693,6 @@ exports[`versioning diff (experimental): Delete a code block 1`] = ` exports[`versioning diff (experimental): Delete a divider 1`] = ` [ "delete block "" A", - "delete A", "insert block "" A", "insert A", ] @@ -784,12 +701,6 @@ exports[`versioning diff (experimental): Delete a divider 1`] = ` exports[`versioning diff (experimental): Delete a nested block 1`] = ` [ "delete block "Parent" A", - "delete A", - "delete "Parent" A", - "delete A", - "delete block "Child" A", - "delete A", - "delete "Child" A", "insert block "Parent" A", "insert A", "insert "Parent" A", @@ -799,12 +710,6 @@ exports[`versioning diff (experimental): Delete a nested block 1`] = ` exports[`versioning diff (experimental): Delete a parent block 1`] = ` [ "delete block "Parent" A", - "delete A", - "delete "Parent" A", - "delete A", - "delete block "Child" A", - "delete A", - "delete "Child" A", "insert block "" A", "insert A", ] @@ -813,23 +718,14 @@ exports[`versioning diff (experimental): Delete a parent block 1`] = ` exports[`versioning diff (experimental): Delete a parent vs change its child's type 1`] = ` [ "delete block "Parent" A", - "delete A", - "delete "Parent" A", - "delete A", "delete block "Child"", - "delete ", - "delete "Child"", ] `; exports[`versioning diff (experimental): Delete column vs add row 1`] = ` [ "delete A", - "delete A", - "delete "B1" A", "delete A", - "delete A", - "delete "B2" A", "insert B", "insert B", "insert B", @@ -843,7 +739,6 @@ exports[`versioning diff (experimental): Delete column vs add row 1`] = ` exports[`versioning diff (experimental): Delete image 1`] = ` [ "delete block "" A", - "delete A", "insert block "" A", "insert A", ] @@ -852,21 +747,12 @@ exports[`versioning diff (experimental): Delete image 1`] = ` exports[`versioning diff (experimental): Delete one of two empty blocks 1`] = ` [ "delete block "" A", - "delete A", ] `; exports[`versioning diff (experimental): Delete parent with mixed children 1`] = ` [ "delete block "Parent" A", - "delete A", - "delete "Parent" A", - "delete A", - "delete block "Nested paragraph" A", - "delete A", - "delete "Nested paragraph" A", - "delete block "" A", - "delete A", "insert block "" A", "insert A", ] @@ -875,21 +761,6 @@ exports[`versioning diff (experimental): Delete parent with mixed children 1`] = exports[`versioning diff (experimental): Delete row vs add column 1`] = ` [ "delete block "A1B1A2B2" A", - "delete
A", - "delete A", - "delete A", - "delete A", - "delete "A1" A", - "delete A", - "delete A", - "delete "B1" A", - "delete A", - "delete A", - "delete A", - "delete "A2" A", - "delete A", - "delete A", - "delete "B2" A", "insert block "A1B1C1" B", "insert
B", "insert B", @@ -939,10 +810,7 @@ exports[`versioning diff (experimental): Edit a link 1`] = ` exports[`versioning diff (experimental): Edit text vs change to heading 1`] = ` [ "delete block "hello world" B", - "delete B", - "delete "hello " B", "delete "wo" B,A", - "delete "r" B", "delete "ld" B,A", "insert block "hello world" B", "insert B", @@ -972,8 +840,6 @@ exports[`versioning diff (experimental): Fix typo vs delete word 1`] = ` exports[`versioning diff (experimental): Heading vs list item 1`] = ` [ "delete block "hello world" A,B", - "delete A,B", - "delete "hello world" A,B", "insert block "hello world" A", "insert A", "insert "hello world" A", @@ -993,11 +859,21 @@ exports[`versioning diff (experimental): Highlight a column 1`] = ` exports[`versioning diff (experimental): Indent a block 1`] = ` [ "delete block "N0" A", - "delete A", - "delete "N0" A", "delete block "N1" A", - "delete A", - "delete "N1" A", + "insert block "N0" A", + "insert A", + "insert "N0" A", + "insert A", + "insert block "N1" A", + "insert A", + "insert "N1" A", +] +`; + +exports[`versioning diff (experimental): Indent a block vs edit its text 1`] = ` +[ + "delete block "N0" A", + "delete block "N1" A", "insert block "N0" A", "insert A", "insert "N0" A", @@ -1026,8 +902,6 @@ exports[`versioning diff (experimental): Insert an image 1`] = ` exports[`versioning diff (experimental): List item → paragraph 1`] = ` [ "delete block "hello world" A", - "delete A", - "delete "hello world" A", "insert block "hello world" A", "insert A", "insert "hello world" A", @@ -1038,8 +912,6 @@ exports[`versioning diff (experimental): Merge blocks vs edit block B 1`] = ` [ "insert "Second" A", "delete block "Second" A", - "delete A", - "delete "Second" A", ] `; @@ -1048,8 +920,6 @@ exports[`versioning diff (experimental): Merge cells 1`] = ` "attrs colspan:A colwidth:A", "insert "+B1" A", "delete A", - "delete A", - "delete "B1" A", "insert ", "insert ", ] @@ -1058,15 +928,7 @@ exports[`versioning diff (experimental): Merge cells 1`] = ` exports[`versioning diff (experimental): Move a block into a block that is deleted 1`] = ` [ "delete block "Parent" A", - "delete A", - "delete "Parent" A", - "delete A", - "delete block "Child" A", - "delete A", - "delete "Child" A", "delete block "X"", - "delete ", - "delete "X"", ] `; @@ -1076,8 +938,6 @@ exports[`versioning diff (experimental): Move paragraph up 1`] = ` "insert A", "insert "Middle" A", "delete block "Middle" A", - "delete A", - "delete "Middle" A", ] `; @@ -1091,23 +951,13 @@ exports[`versioning diff (experimental): Move paragraph with children 1`] = ` "insert A", "insert "Child" A", "delete block "Parent" A", - "delete A", - "delete "Parent" A", - "delete A", - "delete block "Child" A", - "delete A", - "delete "Child" A", ] `; exports[`versioning diff (experimental): Nest a bullet under another 1`] = ` [ "delete block "Parent" A", - "delete A", - "delete "Parent" A", "delete block "Child" A", - "delete A", - "delete "Child" A", "insert block "Parent" A", "insert A", "insert "Parent" A", @@ -1121,11 +971,7 @@ exports[`versioning diff (experimental): Nest a bullet under another 1`] = ` exports[`versioning diff (experimental): Nest blocks into a block that is moved 1`] = ` [ "delete block "R" B", - "delete B", - "delete "R" B", "delete block "Q" B,A", - "delete B,A", - "delete "Q" B,A", "insert block "R" B", "insert B", "insert "R" B", @@ -1134,14 +980,8 @@ exports[`versioning diff (experimental): Nest blocks into a block that is moved "insert B", "insert "Q" B", "delete block "B1" A", - "delete A", - "delete "B1" A", "delete block "B2" A", - "delete A", - "delete "B2" A", "delete block "B3" A", - "delete A", - "delete "B3" A", "insert block "Q" A", "insert A", "insert "Q" A", @@ -1161,8 +1001,6 @@ exports[`versioning diff (experimental): Nest blocks into a block that is moved exports[`versioning diff (experimental): Paragraph → heading 1`] = ` [ "delete block "hello world" A", - "delete A", - "delete "hello world" A", "insert block "hello world" A", "insert A", "insert "hello world" A", @@ -1172,14 +1010,6 @@ exports[`versioning diff (experimental): Paragraph → heading 1`] = ` exports[`versioning diff (experimental): Remove a column 1`] = ` [ "delete A", - "delete A", - "delete block "Left column" A", - "delete A", - "delete "Left column" A", - "delete A", - "delete block "Right column" A", - "delete A", - "delete "Right column" A", "insert block "Left column" A", "insert A", "insert "Left column" A", @@ -1195,17 +1025,12 @@ exports[`versioning diff (experimental): Remove a link 1`] = ` exports[`versioning diff (experimental): Remove a middle column 1`] = ` [ "delete A", - "delete block "Right column" A", - "delete A", - "delete "Right column" A", ] `; exports[`versioning diff (experimental): Remove a paragraph 1`] = ` [ "delete block "Body text" A", - "delete A", - "delete "Body text" A", ] `; @@ -1218,23 +1043,13 @@ exports[`versioning diff (experimental): Remove bold 1`] = ` exports[`versioning diff (experimental): Remove column 1`] = ` [ "delete A", - "delete A", - "delete "B1" A", "delete A", - "delete A", - "delete "B2" A", ] `; exports[`versioning diff (experimental): Remove row 1`] = ` [ "delete A", - "delete A", - "delete A", - "delete "A2" A", - "delete A", - "delete A", - "delete "B2" A", ] `; @@ -1289,12 +1104,6 @@ exports[`versioning diff (experimental): Text color vs background color 1`] = ` exports[`versioning diff (experimental): Unindent a block 1`] = ` [ "delete block "N0" A", - "delete A", - "delete "N0" A", - "delete A", - "delete block "N1" A", - "delete A", - "delete "N1" A", "insert block "N0" A", "insert A", "insert "N0" A", From c16997f11912f3d8d200abc6101248f1a8a9274d Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 8 Oct 2026 22:03:56 +0200 Subject: [PATCH 04/10] fix(versioning): keep the other deleters of a lost move A lost move's original was shown without an author, to not credit the mover. But someone else may have deleted it too, knowingly: deleting its parent, for example. Only the movers are left out now. --- .../core/src/y/extensions/snapshotPreview.ts | 79 +++- .../__snapshots__/versioning.test.tsx.snap | 443 ++++++------------ 2 files changed, 207 insertions(+), 315 deletions(-) diff --git a/packages/core/src/y/extensions/snapshotPreview.ts b/packages/core/src/y/extensions/snapshotPreview.ts index cf512ffbd6..2d0c0b6e2a 100644 --- a/packages/core/src/y/extensions/snapshotPreview.ts +++ b/packages/core/src/y/extensions/snapshotPreview.ts @@ -70,29 +70,40 @@ class SnapshotDiffRenderer extends Y.DiffRenderer { } } +/** Whether two attributions show the same, ignoring timestamps (`deleteAt`, ...). */ function sameAttributes( a: Y.ContentAttribute[] | null, b: Y.ContentAttribute[] | null, ): boolean { + function shown(attrs: Y.ContentAttribute[] | null) { + return (attrs ?? []).filter((attr) => !attr.name.endsWith("At")); + } + const [x, y] = [shown(a), shown(b)]; return ( - a?.length === b?.length && - (a ?? []).every((x) => - (b ?? []).some((y) => x.name === y.name && x.val === y.val), - ) + (a === null) === (b === null) && + x.length === y.length && + x.every((p) => y.some((q) => p.name === q.name && p.val === q.val)) ); } /** - * Moved blocks that are gone, with their content. + * The delete attributions of moved blocks that are gone, with their content, + * without the movers. * * A move (indenting, dragging, a type change, ...) deletes a block and inserts * a copy with the same id. While a copy is shown, the diff shows the move as * is. Once every copy is gone, only the original's deletion is shown, and * that names the mover, who may not have deleted it: the copy can be lost - * with a concurrently deleted parent, or deleted later by someone else. Such - * deletions are shown without an author. + * with a concurrently deleted parent, or deleted later by someone else. So the + * movers are left out; whoever else deleted the original (e.g. with its + * parent) still is. With nobody left, it's shown without an author. */ -function lostMoves(doc: Y.Doc, baseline: Y.Doc): Y.IdSet { +function withoutLostMovers( + doc: Y.Doc, + baseline: Y.Doc, + attributions: Y.ContentMap, + deletes: Y.IdMap, +): Y.IdMap { const { inserted, deleted } = changesSince(doc, baseline); const copies = new Map(); for (const item of itemsIn(doc, inserted)) { @@ -106,17 +117,48 @@ function lostMoves(doc: Y.Doc, baseline: Y.Doc): Y.IdSet { const lost = removed.filter(isBlock).flatMap((item) => { const id = blockId(item.content.type); const moved = id == null ? undefined : copies.get(id); - return moved?.every((copy) => copy.deleted) ? [item.content.type] : []; + if (!moved?.every((copy) => copy.deleted)) { + return []; + } + const movers = new Set( + moved.flatMap((copy) => + ( + attributions.inserts.slice(copy.id.client, copy.id.clock, 1)[0] + ?.attrs ?? [] + ) + .filter((attr) => attr.name === "insert") + .map((attr) => attr.val), + ), + ); + return [{ block: item.content.type, movers }]; }); - const ids = Y.createIdSet(); + const replaced = Y.createIdSet(); + const replacement: Y.IdMap = Y.createIdMap(); for (const item of removed) { - if ( - lost.some((block) => item === block._item || Y.isParentOf(block, item)) - ) { - ids.add(item.id.client, item.id.clock, item.length); + const { movers } = + lost.find( + ({ block }) => item === block._item || Y.isParentOf(block, item), + ) ?? {}; + if (!movers) { + continue; + } + const { client } = item.id; + for (const range of deletes.slice(client, item.id.clock, item.length)) { + if (!range.attrs) { + continue; + } + const ids = Y.createIdSet(); + ids.add(client, range.clock, range.len); + replaced.add(client, range.clock, range.len); + const kept = range.attrs.filter( + (attr) => !(attr.name === "delete" && movers.has(attr.val)), + ); + if (kept.some((attr) => attr.name === "delete")) { + Y.insertIntoIdMap(replacement, Y.createIdMapFromIdSet(ids, kept)); + } } } - return ids; + return Y.mergeIdMaps([Y.diffIdMap(deletes, replaced), replacement]); } /** The ids inserted, and the ids deleted, in `doc` since `baseline`. */ @@ -407,7 +449,12 @@ export function showSnapshotPreview( renderAttributions = Y.createContentMap( Y.mergeIdMaps([attributions.inserts, added.inserts]), experimental.lostContentAttribution - ? Y.diffIdMap(deletes, lostMoves(snapshot.doc, baseline.doc)) + ? withoutLostMovers( + snapshot.doc, + baseline.doc, + attributions, + deletes, + ) : deletes, ); } diff --git a/tests/src/end-to-end/y-prosemirror/__snapshots__/versioning.test.tsx.snap b/tests/src/end-to-end/y-prosemirror/__snapshots__/versioning.test.tsx.snap index 7d9401dec5..50dbf53a74 100644 --- a/tests/src/end-to-end/y-prosemirror/__snapshots__/versioning.test.tsx.snap +++ b/tests/src/end-to-end/y-prosemirror/__snapshots__/versioning.test.tsx.snap @@ -2,45 +2,28 @@ exports[`versioning diff (experimental): A adds column then row, B adds column 1`] = ` [ - "delete block "A1B1A2B2" B,A", - "insert block "A1B1C1D1A2B2C2D2A3B3C3" B,A", - "insert
B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A1" B,A", - "insert B,A", - "insert B,A", - "insert "B1" B,A", - "insert B,A", - "insert B,A", - "insert "C1" B,A", - "insert B,A", - "insert B,A", - "insert "D1" B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A2" B,A", - "insert B,A", - "insert B,A", - "insert "B2" B,A", - "insert B,A", - "insert B,A", - "insert "C2" B,A", - "insert B,A", - "insert B,A", - "insert "D2" B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A3" B,A", - "insert B,A", - "insert B,A", - "insert "B3" B,A", - "insert B,A", - "insert B,A", - "insert "C3" B,A", + "insert A", + "insert A", + "insert "C1" A", + "insert B", + "insert B", + "insert "D1" B", + "insert A", + "insert A", + "insert "C2" A", + "insert B", + "insert B", + "insert "D2" B", + "insert A", + "insert A", + "insert A", + "insert "A3" A", + "insert A", + "insert A", + "insert "B3" A", + "insert A", + "insert A", + "insert "C3" A", "insert ", "insert ", ] @@ -48,46 +31,29 @@ exports[`versioning diff (experimental): A adds column then row, B adds column 1 exports[`versioning diff (experimental): A adds row then column, B adds row 1`] = ` [ - "delete block "A1B1A2B2" B,A", - "insert block "A1B1C1A2B2C2A3B3C3D1D2" B,A", - "insert
B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A1" B,A", - "insert B,A", - "insert B,A", - "insert "B1" B,A", - "insert B,A", - "insert B,A", - "insert "C1" B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A2" B,A", - "insert B,A", - "insert B,A", - "insert "B2" B,A", - "insert B,A", - "insert B,A", - "insert "C2" B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A3" B,A", - "insert B,A", - "insert B,A", - "insert "B3" B,A", - "insert B,A", - "insert B,A", - "insert "C3" B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "D1" B,A", - "insert B,A", - "insert B,A", - "insert "D2" B,A", + "insert A", + "insert A", + "insert "C1" A", + "insert A", + "insert A", + "insert "C2" A", + "insert A", + "insert A", + "insert A", + "insert "A3" A", + "insert A", + "insert A", + "insert "B3" A", + "insert A", + "insert A", + "insert "C3" A", + "insert B", + "insert B", + "insert B", + "insert "D1" B", + "insert B", + "insert B", + "insert "D2" B", "insert ", "insert ", ] @@ -449,36 +415,19 @@ exports[`versioning diff (experimental): Add column 1`] = ` exports[`versioning diff (experimental): Add column vs add row 1`] = ` [ - "delete block "A1B1A2B2" B,A", - "insert block "A1B1C1A2B2C2A3B3" B,A", - "insert
B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A1" B,A", - "insert B,A", - "insert B,A", - "insert "B1" B,A", - "insert B,A", - "insert B,A", - "insert "C1" B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A2" B,A", - "insert B,A", - "insert B,A", - "insert "B2" B,A", - "insert B,A", - "insert B,A", - "insert "C2" B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A3" B,A", - "insert B,A", - "insert B,A", - "insert "B3" B,A", + "insert A", + "insert A", + "insert "C1" A", + "insert A", + "insert A", + "insert "C2" A", + "insert B", + "insert B", + "insert B", + "insert "A3" B", + "insert B", + "insert B", + "insert "B3" B", "insert ", "insert ", ] @@ -547,36 +496,19 @@ exports[`versioning diff (experimental): Add row 1`] = ` exports[`versioning diff (experimental): Add row vs add column 1`] = ` [ - "delete block "A1B1A2B2" B,A", - "insert block "A1B1C1A2B2C2A3B3" B,A", - "insert
B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A1" B,A", - "insert B,A", - "insert B,A", - "insert "B1" B,A", - "insert B,A", - "insert B,A", - "insert "C1" B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A2" B,A", - "insert B,A", - "insert B,A", - "insert "B2" B,A", - "insert B,A", - "insert B,A", - "insert "C2" B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A3" B,A", - "insert B,A", - "insert B,A", - "insert "B3" B,A", + "insert B", + "insert B", + "insert "C1" B", + "insert B", + "insert B", + "insert "C2" B", + "insert A", + "insert A", + "insert A", + "insert "A3" A", + "insert A", + "insert A", + "insert "B3" A", "insert ", "insert ", ] @@ -718,7 +650,6 @@ exports[`versioning diff (experimental): Delete a parent block 1`] = ` exports[`versioning diff (experimental): Delete a parent vs change its child's type 1`] = ` [ "delete block "Parent" A", - "delete block "Child"", ] `; @@ -760,19 +691,10 @@ exports[`versioning diff (experimental): Delete parent with mixed children 1`] = exports[`versioning diff (experimental): Delete row vs add column 1`] = ` [ - "delete block "A1B1A2B2" A", - "insert block "A1B1C1" B", - "insert
B", - "insert B", - "insert B", - "insert B", - "insert "A1" B", - "insert B", - "insert B", - "insert "B1" B", "insert B", "insert B", "insert "C1" B", + "delete A", ] `; @@ -1115,45 +1037,28 @@ exports[`versioning diff (experimental): Unindent a block 1`] = ` exports[`versioning diff: A adds column then row, B adds column 1`] = ` [ - "delete block "A1B1A2B2" B,A", - "insert block "A1B1C1D1A2B2C2D2A3B3C3" B,A", - "insert
B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A1" B,A", - "insert B,A", - "insert B,A", - "insert "B1" B,A", - "insert B,A", - "insert B,A", - "insert "C1" B,A", - "insert B,A", - "insert B,A", - "insert "D1" B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A2" B,A", - "insert B,A", - "insert B,A", - "insert "B2" B,A", - "insert B,A", - "insert B,A", - "insert "C2" B,A", - "insert B,A", - "insert B,A", - "insert "D2" B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A3" B,A", - "insert B,A", - "insert B,A", - "insert "B3" B,A", - "insert B,A", - "insert B,A", - "insert "C3" B,A", + "insert A", + "insert A", + "insert "C1" A", + "insert B", + "insert B", + "insert "D1" B", + "insert A", + "insert A", + "insert "C2" A", + "insert B", + "insert B", + "insert "D2" B", + "insert A", + "insert A", + "insert A", + "insert "A3" A", + "insert A", + "insert A", + "insert "B3" A", + "insert A", + "insert A", + "insert "C3" A", "insert ", "insert ", ] @@ -1161,46 +1066,29 @@ exports[`versioning diff: A adds column then row, B adds column 1`] = ` exports[`versioning diff: A adds row then column, B adds row 1`] = ` [ - "delete block "A1B1A2B2" B,A", - "insert block "A1B1C1A2B2C2A3B3C3D1D2" B,A", - "insert
B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A1" B,A", - "insert B,A", - "insert B,A", - "insert "B1" B,A", - "insert B,A", - "insert B,A", - "insert "C1" B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A2" B,A", - "insert B,A", - "insert B,A", - "insert "B2" B,A", - "insert B,A", - "insert B,A", - "insert "C2" B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A3" B,A", - "insert B,A", - "insert B,A", - "insert "B3" B,A", - "insert B,A", - "insert B,A", - "insert "C3" B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "D1" B,A", - "insert B,A", - "insert B,A", - "insert "D2" B,A", + "insert A", + "insert A", + "insert "C1" A", + "insert A", + "insert A", + "insert "C2" A", + "insert A", + "insert A", + "insert A", + "insert "A3" A", + "insert A", + "insert A", + "insert "B3" A", + "insert A", + "insert A", + "insert "C3" A", + "insert B", + "insert B", + "insert B", + "insert "D1" B", + "insert B", + "insert B", + "insert "D2" B", "insert ", "insert ", ] @@ -1562,36 +1450,19 @@ exports[`versioning diff: Add column 1`] = ` exports[`versioning diff: Add column vs add row 1`] = ` [ - "delete block "A1B1A2B2" B,A", - "insert block "A1B1C1A2B2C2A3B3" B,A", - "insert
B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A1" B,A", - "insert B,A", - "insert B,A", - "insert "B1" B,A", - "insert B,A", - "insert B,A", - "insert "C1" B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A2" B,A", - "insert B,A", - "insert B,A", - "insert "B2" B,A", - "insert B,A", - "insert B,A", - "insert "C2" B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A3" B,A", - "insert B,A", - "insert B,A", - "insert "B3" B,A", + "insert A", + "insert A", + "insert "C1" A", + "insert A", + "insert A", + "insert "C2" A", + "insert B", + "insert B", + "insert B", + "insert "A3" B", + "insert B", + "insert B", + "insert "B3" B", "insert ", "insert ", ] @@ -1660,36 +1531,19 @@ exports[`versioning diff: Add row 1`] = ` exports[`versioning diff: Add row vs add column 1`] = ` [ - "delete block "A1B1A2B2" B,A", - "insert block "A1B1C1A2B2C2A3B3" B,A", - "insert
B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A1" B,A", - "insert B,A", - "insert B,A", - "insert "B1" B,A", - "insert B,A", - "insert B,A", - "insert "C1" B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A2" B,A", - "insert B,A", - "insert B,A", - "insert "B2" B,A", - "insert B,A", - "insert B,A", - "insert "C2" B,A", - "insert B,A", - "insert B,A", - "insert B,A", - "insert "A3" B,A", - "insert B,A", - "insert B,A", - "insert "B3" B,A", + "insert B", + "insert B", + "insert "C1" B", + "insert B", + "insert B", + "insert "C2" B", + "insert A", + "insert A", + "insert A", + "insert "A3" A", + "insert A", + "insert A", + "insert "B3" A", "insert ", "insert ", ] @@ -1878,19 +1732,10 @@ exports[`versioning diff: Delete parent with mixed children 1`] = ` exports[`versioning diff: Delete row vs add column 1`] = ` [ - "delete block "A1B1A2B2" A", - "insert block "A1B1C1" B", - "insert
B", - "insert B", - "insert B", - "insert B", - "insert "A1" B", - "insert B", - "insert B", - "insert "B1" B", "insert B", "insert B", "insert "C1" B", + "delete A", ] `; From 46c9fb524568bc32ed487147c710906ab3b8b98a Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 8 Oct 2026 22:35:20 +0200 Subject: [PATCH 05/10] refactor(versioning): one option for the experimental version diff fixes `experimental.versionDiffFixes` takes one value, each including the fixes before it, so only combinations that make sense can be set. The first is "implicitDeleteAttribution". The gallery offers it as one choice. --- .../14-suggestion-gallery/src/App.tsx | 77 ++++++++++--------- .../14-suggestion-gallery/src/scenarios.ts | 12 +-- packages/core/src/y/extensions/index.ts | 7 +- .../core/src/y/extensions/snapshotPreview.ts | 46 ++++++++--- .../extensions/versionDiffAttribution.test.ts | 2 +- .../__snapshots__/versioning.test.tsx.snap | 6 ++ .../y-prosemirror/versioning.test.tsx | 9 +-- 7 files changed, 97 insertions(+), 62 deletions(-) diff --git a/examples/07-collaboration/14-suggestion-gallery/src/App.tsx b/examples/07-collaboration/14-suggestion-gallery/src/App.tsx index aa428ead67..aed1c13dfc 100644 --- a/examples/07-collaboration/14-suggestion-gallery/src/App.tsx +++ b/examples/07-collaboration/14-suggestion-gallery/src/App.tsx @@ -6,6 +6,8 @@ import type { GalleryEditor } from "./gallerySchema"; import { createYVersionView, type ExperimentalVersionDiffs, + type VersionDiffFixes, + versionDiffFixesIncluded, SuggestionsExtension, withCollaboration, } from "@blocknote/core/y"; @@ -27,39 +29,37 @@ import { Feedback, scenarios, SuggestionScenario } from "./scenarios"; type Mode = "suggestions" | "versioning"; -type Experiment = keyof ExperimentalVersionDiffs; - -// The experimental version diff flags the Diff can toggle, off by default as +// The experimental version diff fixes the Diff can show, none by default as // in the editor. Kept in the URL, so a link opens the same view. -const EXPERIMENTS: { key: Experiment; label: string }[] = [ - { key: "lostContentAttribution", label: "Lost content attribution" }, +const FIXES: { value: VersionDiffFixes | undefined; label: string }[] = [ + { value: undefined, label: "Default" }, + { value: "implicitDeleteAttribution", label: "Implicit delete attribution" }, ]; +const ALL_FIXES: ExperimentalVersionDiffs = { + versionDiffFixes: FIXES[FIXES.length - 1].value, +}; -function readExperiments(): ExperimentalVersionDiffs { - const params = new URLSearchParams(window.location.search); - return Object.fromEntries( - EXPERIMENTS.map(({ key }) => [key, params.get(key) === "1"]), +function readFixes(): ExperimentalVersionDiffs { + const value = new URLSearchParams(window.location.search).get( + "versionDiffFixes", ); + return { + versionDiffFixes: FIXES.find((fixes) => fixes.value === value)?.value, + }; } -function writeExperiments(experimental: ExperimentalVersionDiffs) { +function writeFixes(experimental: ExperimentalVersionDiffs) { const url = new URL(window.location.href); - for (const { key } of EXPERIMENTS) { - if (experimental[key]) { - url.searchParams.set(key, "1"); - } else { - url.searchParams.delete(key); - } + if (experimental.versionDiffFixes) { + url.searchParams.set("versionDiffFixes", experimental.versionDiffFixes); + } else { + url.searchParams.delete("versionDiffFixes"); } window.history.replaceState(null, "", url); } -const ALL_EXPERIMENTS: ExperimentalVersionDiffs = Object.fromEntries( - EXPERIMENTS.map(({ key }) => [key, true]), -); - -// A note with `when` describes the Diff with those flags, so it only shows in -// Versioning mode with matching flags. +// A note with `when` describes the Diff with or without those fixes, so it +// only shows in Versioning mode when they match. function applies( f: Feedback, mode: Mode, @@ -68,10 +68,13 @@ function applies( if (!f.when) { return true; } + const included = experimental.versionDiffFixes + ? versionDiffFixesIncluded[experimental.versionDiffFixes] + : []; return ( mode === "versioning" && Object.entries(f.when).every( - ([key, on]) => (experimental[key as Experiment] ?? false) === on, + ([fix, on]) => included.some((each) => each === fix) === on, ) ); } @@ -612,14 +615,14 @@ function notesFor( return (s.feedback ?? []).filter((f) => applies(f, mode, experimental)); } -// The severity with the flags off (the default) and, for a scenario the flags -// affect, in parentheses the severity with all of them on (green: no issue -// left). The checkboxes don't change it. +// The severity without fixes (the default) and, for a scenario the fixes +// affect, in parentheses the severity with all of them (green: no issue left). +// The chosen fixes don't change it. function severityBadge(s: SuggestionScenario, mode: Mode): string { const sev = topSeverity(s, notesFor(s, mode, {})); let badge = sev ? SEVERITY[sev].icon + " " : ""; if (mode === "versioning" && s.feedback?.some((f) => f.when)) { - const best = topSeverity(s, notesFor(s, mode, ALL_EXPERIMENTS)); + const best = topSeverity(s, notesFor(s, mode, ALL_FIXES)); badge += `(${best === "high" || best === "low" ? SEVERITY[best].icon : "🟢"}) `; } return badge; @@ -628,13 +631,12 @@ function severityBadge(s: SuggestionScenario, mode: Mode): string { export default function App() { const [selectedId, setSelectedId] = useState(scenarios[0].id); const [mode, setMode] = useState("versioning"); - const [experimental, setExperimental] = useState(readExperiments); + const [experimental, setExperimental] = useState(readFixes); const selected = scenarios.find((s) => s.id === selectedId)!; const feedback = notesFor(selected, mode, experimental); - function toggle(key: Experiment) { - const next = { ...experimental, [key]: !experimental[key] }; - writeExperiments(next); + function choose(next: ExperimentalVersionDiffs) { + writeFixes(next); setExperimental(next); } @@ -693,13 +695,14 @@ export default function App() { {mode === "versioning" && (
- Experimental: - {EXPERIMENTS.map(({ key, label }) => ( -