Skip to content

Commit bb192fe

Browse files
committed
feat(versioning): show copied blocks once in version diffs (experimental)
Behind `experimental.blockCopyDiffs` in the collaboration options; it only changes what a version diff shows. A type change or a move stores a copy of the block, so the diff showed it as a deleted and an inserted block, with the copied content credited to whoever made the change. Pair each copy with its original (by block id) and show it once: - a type change between text blocks as a formatting change; - a move, including an indent, as "Moved"; - a copy in the same place (its children changed) as unchanged. Copied content keeps its authors. Content the earlier version had that no copy kept still shows as deleted.
1 parent 38624d6 commit bb192fe

38 files changed

Lines changed: 967 additions & 147 deletions

File tree

‎examples/07-collaboration/14-suggestion-gallery/src/App.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ type Mode = "suggestions" | "versioning";
3434
const FIXES: { value: VersionDiffFixes | undefined; label: string }[] = [
3535
{ value: undefined, label: "Default" },
3636
{ value: "implicitDeleteAttribution", label: "Implicit delete attribution" },
37+
{
38+
value: "implicitDeleteAttributionAndRecreatedBlocks",
39+
label: "+ re-created blocks",
40+
},
3741
];
3842
const ALL_FIXES: ExperimentalVersionDiffs = {
3943
versionDiffFixes: FIXES[FIXES.length - 1].value,

‎examples/07-collaboration/14-suggestion-gallery/src/scenarios.ts‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,26 @@ function posBeforeText(editor: GalleryEditor, text: string): number {
117117
return pos;
118118
}
119119

120+
// Versioning notes for a block copy: a type change or a move stores a copy of
121+
// the block, which the `recreatedBlocks` fix shows once.
122+
const typeChangeNote: Feedback = {
123+
severity: "info",
124+
when: { recreatedBlocks: true },
125+
note: "Versioning shows the type change as a formatting change.",
126+
};
127+
const moveNote: Feedback = {
128+
severity: "info",
129+
when: { recreatedBlocks: true },
130+
note: "Versioning shows the block as moved: at its new place, and struck through at its old place unless it was only indented or outdented.",
131+
};
132+
function lostEditNote(user: string): Feedback {
133+
return {
134+
severity: "info",
135+
when: { recreatedBlocks: true },
136+
note: `Versioning shows only the type change: the result doesn't have ${user}'s edit, so the diff doesn't show it.`,
137+
};
138+
}
139+
120140
export const scenarios: SuggestionScenario[] = [
121141
{
122142
kind: "single",
@@ -241,12 +261,14 @@ export const scenarios: SuggestionScenario[] = [
241261
kind: "single",
242262
id: "nest-bullet-existing",
243263
feedback: [
264+
moveNote,
244265
{
245266
severity: "low",
246267
note: "Nested bullets all render as • instead of •/◦/▪ — the suggestion-mark wrappers (display: contents) break the depth-detecting CSS chains. Fix: compute each bullet's nesting level in JS and expose it as data-bullet-level, then pick the glyph with a wrapper-independent attribute selector (as numbered lists do with data-index).",
247268
},
248269
{
249270
severity: "high",
271+
when: { recreatedBlocks: false },
250272
note: "Indenting re-creates Parent and Child as new blocks (the schema fix stores a block that gains or loses its children as a new block). The diff therefore shows both as deleted and inserted again, all credited to whoever indented.",
251273
},
252274
],
@@ -413,8 +435,10 @@ export const scenarios: SuggestionScenario[] = [
413435
kind: "single",
414436
id: "type-list-to-paragraph",
415437
feedback: [
438+
typeChangeNote,
416439
{
417440
severity: "high",
441+
when: { recreatedBlocks: false },
418442
note: "Changing the type re-creates the block as a new one (with the schema fix, a block's type can't change in place). The diff therefore shows it as deleted and inserted again, all credited to whoever changed the type.",
419443
},
420444
],
@@ -435,8 +459,10 @@ export const scenarios: SuggestionScenario[] = [
435459
kind: "single",
436460
id: "type-paragraph-to-heading",
437461
feedback: [
462+
typeChangeNote,
438463
{
439464
severity: "high",
465+
when: { recreatedBlocks: false },
440466
note: "Changing the type re-creates the block as a new one (with the schema fix, a block's type can't change in place). The diff therefore shows it as deleted and inserted again, all credited to whoever changed the type.",
441467
},
442468
],
@@ -599,8 +625,10 @@ export const scenarios: SuggestionScenario[] = [
599625
kind: "single",
600626
id: "move-paragraph-up",
601627
feedback: [
628+
moveNote,
602629
{
603630
severity: "high",
631+
when: { recreatedBlocks: false },
604632
note: "Moving re-creates the block as a new one at its new place. The diff therefore shows it as deleted at its old place and inserted at its new one, all credited to the mover.",
605633
},
606634
],
@@ -620,8 +648,10 @@ export const scenarios: SuggestionScenario[] = [
620648
kind: "single",
621649
id: "move-paragraph-with-children",
622650
feedback: [
651+
moveNote,
623652
{
624653
severity: "high",
654+
when: { recreatedBlocks: false },
625655
note: "Moving re-creates the block, with its child, as a new one at its new place. The diff therefore shows it as deleted at its old place and inserted at its new one, all credited to the mover.",
626656
},
627657
],
@@ -646,8 +676,10 @@ export const scenarios: SuggestionScenario[] = [
646676
kind: "single",
647677
id: "nesting-indent",
648678
feedback: [
679+
moveNote,
649680
{
650681
severity: "high",
682+
when: { recreatedBlocks: false },
651683
note: "Indenting re-creates N0 and N1 as new blocks (the schema fix stores a block that gains or loses its children as a new block). The diff therefore shows both as deleted and inserted again, all credited to whoever indented.",
652684
},
653685
],
@@ -669,8 +701,10 @@ export const scenarios: SuggestionScenario[] = [
669701
kind: "single",
670702
id: "nesting-unindent",
671703
feedback: [
704+
moveNote,
672705
{
673706
severity: "high",
707+
when: { recreatedBlocks: false },
674708
note: "Outdenting re-creates N0 and N1 as new blocks (the schema fix stores a block that gains or loses its children as a new block). The diff therefore shows N0 and N1 as deleted and inserted again, all credited to whoever outdented.",
675709
},
676710
],
@@ -694,8 +728,10 @@ export const scenarios: SuggestionScenario[] = [
694728
kind: "single",
695729
id: "nesting-change-parent-type",
696730
feedback: [
731+
typeChangeNote,
697732
{
698733
severity: "high",
734+
when: { recreatedBlocks: false },
699735
note: "Changing the type re-creates N0, children included, as a new block (with the schema fix, a block's type can't change in place). The diff therefore shows it as deleted and inserted again, all credited to whoever changed the type.",
700736
},
701737
],
@@ -1047,6 +1083,12 @@ export const scenarios: SuggestionScenario[] = [
10471083
feedback: [
10481084
{
10491085
severity: "high",
1086+
when: { recreatedBlocks: true },
1087+
note: "Versioning shows N0 unchanged and N2 as moved, but N1's two copies as inserted by A and by B.",
1088+
},
1089+
{
1090+
severity: "high",
1091+
when: { recreatedBlocks: false },
10501092
note: "The diff also shows N0 and N2 as deleted and inserted again: indenting re-creates blocks (see Indent a block).",
10511093
},
10521094
{
@@ -1075,12 +1117,18 @@ export const scenarios: SuggestionScenario[] = [
10751117
kind: "concurrent",
10761118
id: "concurrent-indent-vs-edit",
10771119
feedback: [
1120+
{
1121+
severity: "info",
1122+
when: { recreatedBlocks: true },
1123+
note: "Versioning shows N0 unchanged and N1 as moved. The result doesn't have B's edit, so the diff doesn't show it.",
1124+
},
10781125
{
10791126
severity: "low",
10801127
note: "B's edit is lost: A's indent re-creates N1 as a new block, which doesn't have B's concurrent edit.",
10811128
},
10821129
{
10831130
severity: "high",
1131+
when: { recreatedBlocks: false },
10841132
note: "The diff also shows N0 and N1 as deleted and inserted again, all credited to A: indenting re-creates blocks (see Indent a block).",
10851133
},
10861134
],
@@ -1106,6 +1154,12 @@ export const scenarios: SuggestionScenario[] = [
11061154
feedback: [
11071155
{
11081156
severity: "high",
1157+
when: { recreatedBlocks: true },
1158+
note: "Versioning shows R unchanged and B1–B3 as moved, but Q's two copies as inserted by B and by A.",
1159+
},
1160+
{
1161+
severity: "high",
1162+
when: { recreatedBlocks: false },
11091163
note: "The diff also shows R and B1–B3 as deleted and inserted again: indenting and moving re-create blocks.",
11101164
},
11111165
{
@@ -1136,8 +1190,10 @@ export const scenarios: SuggestionScenario[] = [
11361190
kind: "concurrent",
11371191
id: "concurrent-parent-type-vs-child-edit",
11381192
feedback: [
1193+
lostEditNote("B"),
11391194
{
11401195
severity: "high",
1196+
when: { recreatedBlocks: false },
11411197
note: "The diff also shows Parent and Child as deleted and inserted again, all credited to A: changing the type re-creates blocks (see Change type of a parent block).",
11421198
},
11431199
{
@@ -1354,8 +1410,10 @@ export const scenarios: SuggestionScenario[] = [
13541410
kind: "concurrent",
13551411
id: "concurrent-text-vs-heading",
13561412
feedback: [
1413+
lostEditNote("A"),
13571414
{
13581415
severity: "high",
1416+
when: { recreatedBlocks: false },
13591417
note: "The diff shows the block as deleted and inserted again, all credited to B: changing the type re-creates it.",
13601418
},
13611419
{
@@ -1735,8 +1793,10 @@ export const scenarios: SuggestionScenario[] = [
17351793
kind: "single",
17361794
id: "remove-1-column",
17371795
feedback: [
1796+
moveNote,
17381797
{
17391798
severity: "high",
1799+
when: { recreatedBlocks: false },
17401800
note: "Removing the column re-creates Left column as a new block outside the columns. The diff therefore shows it as inserted, as if it were new, credited to whoever removed the column.",
17411801
},
17421802
],

‎packages/core/src/editor/Block.css‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,6 +1122,32 @@ content span instead, using the `--user-color-*` properties that cascade down
11221122
from the wrapper. The `.bn-root ins, .bn-root del` rules above still style
11231123
serialized/static output, where the wrapper is a real, painted box.
11241124
*/
1125+
/* A moved block's text keeps its authors, so it has no mark of its own: its
1126+
text element gets the inserted-text highlight instead. It shrinks to the text
1127+
(a flex item), or breaks per line in code blocks (an inline <code>). */
1128+
ins[data-moved] > .bn-suggestion-node .bn-inline-content {
1129+
--bn-suggestion-token-color: currentColor;
1130+
box-decoration-break: clone;
1131+
-webkit-box-decoration-break: clone;
1132+
background-color: color-mix(in srgb, var(--user-color-light) 50%, white);
1133+
color: var(--user-color-dark);
1134+
border-radius: 4px;
1135+
}
1136+
1137+
.dark.bn-root ins[data-moved] > .bn-suggestion-node .bn-inline-content,
1138+
ins[data-moved]
1139+
> .bn-suggestion-node
1140+
.bn-block-content[data-content-type="codeBlock"]
1141+
> pre
1142+
.bn-inline-content {
1143+
background-color: color-mix(
1144+
in srgb,
1145+
var(--user-color-light) 25%,
1146+
var(--bn-suggestion-surface, var(--bn-colors-editor-background))
1147+
);
1148+
color: white;
1149+
}
1150+
11251151
.bn-suggestion-mark {
11261152
--bn-suggestion-token-color: currentColor;
11271153
background-color: color-mix(in srgb, var(--user-color-light) 50%, white);

‎packages/core/src/i18n/locales/ar.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,8 @@ export const ar: Dictionary = {
399399
deleted_by: (users: string) => `حُذف بواسطة: ${users}`,
400400
changed: "مُعدَّل",
401401
changed_by: (users: string) => `عُدّل بواسطة: ${users}`,
402+
moved: "تم النقل",
403+
moved_by: (users: string) => `نقله: ${users}`,
402404
formatting_change_by: (formats: string, users: string) =>
403405
`تغيير التنسيق (${formats}) بواسطة: ${users}`,
404406
},

‎packages/core/src/i18n/locales/de.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,8 @@ export const de: Dictionary = {
433433
deleted_by: (users: string) => `Gelöscht von: ${users}`,
434434
changed: "Geändert",
435435
changed_by: (users: string) => `Geändert von: ${users}`,
436+
moved: "Verschoben",
437+
moved_by: (users: string) => `Verschoben von: ${users}`,
436438
formatting_change_by: (formats: string, users: string) =>
437439
`Formatierungsänderung (${formats}) von: ${users}`,
438440
},

‎packages/core/src/i18n/locales/en.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,8 @@ export const en = {
414414
deleted_by: (users: string) => `Deleted by: ${users}`,
415415
changed: "Changed",
416416
changed_by: (users: string) => `Changed by: ${users}`,
417+
moved: "Moved",
418+
moved_by: (users: string) => `Moved by: ${users}`,
417419
formatting_change_by: (formats: string, users: string) =>
418420
`Formatting change (${formats}) by: ${users}`,
419421
},

‎packages/core/src/i18n/locales/es.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,8 @@ export const es: Dictionary = {
412412
deleted_by: (users: string) => `Eliminado por: ${users}`,
413413
changed: "Modificado",
414414
changed_by: (users: string) => `Modificado por: ${users}`,
415+
moved: "Movido",
416+
moved_by: (users: string) => `Movido por: ${users}`,
415417
formatting_change_by: (formats: string, users: string) =>
416418
`Cambio de formato (${formats}) por: ${users}`,
417419
},

‎packages/core/src/i18n/locales/fa.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,8 @@ export const fa = {
383383
deleted_by: (users: string) => `حذف‌شده توسط: ${users}`,
384384
changed: "تغییر\u200cیافته",
385385
changed_by: (users: string) => `تغییر\u200cیافته توسط: ${users}`,
386+
moved: "منتقل شد",
387+
moved_by: (users: string) => `منتقل شده توسط: ${users}`,
386388
formatting_change_by: (formats: string, users: string) =>
387389
`تغییر قالب‌بندی (${formats}) توسط: ${users}`,
388390
},

‎packages/core/src/i18n/locales/fr.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,8 @@ export const fr: Dictionary = {
460460
deleted_by: (users: string) => `Supprimé par : ${users}`,
461461
changed: "Modifié",
462462
changed_by: (users: string) => `Modifié par : ${users}`,
463+
moved: "Déplacé",
464+
moved_by: (users: string) => `Déplacé par : ${users}`,
463465
formatting_change_by: (formats: string, users: string) =>
464466
`Modification de mise en forme (${formats}) par : ${users}`,
465467
},

‎packages/core/src/i18n/locales/he.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,8 @@ export const he: Dictionary = {
414414
deleted_by: (users: string) => `נמחק על ידי: ${users}`,
415415
changed: "שונה",
416416
changed_by: (users: string) => `שונה על ידי: ${users}`,
417+
moved: "הועבר",
418+
moved_by: (users: string) => `הועבר על ידי: ${users}`,
417419
formatting_change_by: (formats: string, users: string) =>
418420
`שינוי עיצוב (${formats}) על ידי: ${users}`,
419421
},

0 commit comments

Comments
 (0)