Skip to content

Commit 001a256

Browse files
authored
Real-time collaboration: Sync post content and undefined blocks value (#75437)
* Revert change to restoreRevision Reverts this change: #75233 Reverts to: https://github.com/WordPress/gutenberg/blob/8bb681001d7267ea57cceed90d2e3650c6349e2a/packages/editor/src/store/private-actions.js#L619 * Sync post content and undefined blocks value * Add unit tests
1 parent f493156 commit 001a256

3 files changed

Lines changed: 133 additions & 11 deletions

File tree

packages/core-data/src/utils/crdt.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
// Changes that can be applied to a post entity record.
4646
export type PostChanges = Partial< Post > & {
4747
blocks?: Block[];
48+
content?: Post[ 'content' ] | string;
4849
excerpt?: Post[ 'excerpt' ] | string;
4950
selection?: WPSelection;
5051
title?: Post[ 'title' ] | string;
@@ -53,7 +54,9 @@ export type PostChanges = Partial< Post > & {
5354
// A post record as represented in the CRDT document (Y.Map).
5455
export interface YPostRecord extends YMapRecord {
5556
author: number;
56-
blocks: YBlocks;
57+
// Blocks are undefined when they need to be re-parsed from content.
58+
blocks: YBlocks | undefined;
59+
content: string;
5760
categories: number[];
5861
comment_status: string;
5962
date: string | null;
@@ -74,6 +77,7 @@ export interface YPostRecord extends YMapRecord {
7477
const allowedPostProperties = new Set< string >( [
7578
'author',
7679
'blocks',
80+
'content',
7781
'categories',
7882
'comment_status',
7983
'date',
@@ -156,6 +160,14 @@ export function applyPostChangesToCRDTDoc(
156160

157161
switch ( key ) {
158162
case 'blocks': {
163+
// Blocks are undefined when they need to be re-parsed from content.
164+
if ( ! newValue ) {
165+
// Set to undefined instead of deleting the key. This is important
166+
// since we iterate over the Y.Map keys in getPostChangesFromCRDTDoc.
167+
ymap.set( key, undefined );
168+
break;
169+
}
170+
159171
let currentBlocks = ymap.get( key );
160172

161173
// Initialize.
@@ -164,22 +176,20 @@ export function applyPostChangesToCRDTDoc(
164176
ymap.set( key, currentBlocks );
165177
}
166178

167-
// Block[] from local changes.
168-
const newBlocks = ( newValue as PostChanges[ 'blocks' ] ) ?? [];
169-
170179
// Block changes from typing are bundled with a 'selection' update.
171180
// Pass the resulting cursor position to the mergeCrdtBlocks function.
172181
const cursorPosition =
173182
changes.selection?.selectionStart?.offset ?? null;
174183

175184
// Merge blocks does not need `setValue` because it is operating on a
176185
// Yjs type that is already in the Y.Doc.
177-
mergeCrdtBlocks( currentBlocks, newBlocks, cursorPosition );
186+
mergeCrdtBlocks( currentBlocks, newValue, cursorPosition );
178187
break;
179188
}
180189

190+
case 'content':
181191
case 'excerpt': {
182-
const currentValue = ymap.get( 'excerpt' );
192+
const currentValue = ymap.get( key );
183193
const rawNewValue = getRawValue( newValue );
184194

185195
updateMapValue( ymap, key, currentValue, rawNewValue );
@@ -318,11 +328,11 @@ export function getPostChangesFromCRDTDoc(
318328
ydoc.meta?.get( CRDT_DOC_META_PERSISTENCE_KEY ) &&
319329
editedRecord.content
320330
) {
321-
const blocks = ymap.get( 'blocks' ) as YBlocks;
331+
const blocksJson = ymap.get( 'blocks' )?.toJSON() ?? [];
332+
322333
return (
323-
__unstableSerializeAndClean(
324-
blocks.toJSON()
325-
).trim() !== editedRecord.content.raw.trim()
334+
__unstableSerializeAndClean( blocksJson ).trim() !==
335+
getRawValue( editedRecord.content )
326336
);
327337
}
328338

@@ -375,6 +385,7 @@ export function getPostChangesFromCRDTDoc(
375385
return haveValuesChanged( currentValue, newValue );
376386
}
377387

388+
case 'content':
378389
case 'excerpt':
379390
case 'title': {
380391
return haveValuesChanged(

packages/core-data/src/utils/test/crdt.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,46 @@ describe( 'crdt', () => {
163163
expect( blocks ).toBeInstanceOf( Y.Array );
164164
} );
165165

166+
it( 'sets blocks to undefined when blocks value is undefined', () => {
167+
// First, set some blocks.
168+
map.set( 'blocks', new Y.Array< YBlock >() );
169+
170+
const changes = {
171+
blocks: undefined,
172+
};
173+
174+
applyPostChangesToCRDTDoc( doc, changes, mockPostType );
175+
176+
// The key should still exist, but the value should be undefined.
177+
expect( map.has( 'blocks' ) ).toBe( true );
178+
expect( map.get( 'blocks' ) ).toBeUndefined();
179+
} );
180+
181+
it( 'syncs content as a string', () => {
182+
const changes = {
183+
content: 'Hello, world!',
184+
} as PostChanges;
185+
186+
applyPostChangesToCRDTDoc( doc, changes, mockPostType );
187+
188+
expect( map.get( 'content' ) ).toBe( 'Hello, world!' );
189+
} );
190+
191+
it( 'syncs content with RenderedText format', () => {
192+
const changes = {
193+
content: {
194+
raw: '<!-- wp:paragraph --><p>Hello</p><!-- /wp:paragraph -->',
195+
rendered: '<p>Hello</p>',
196+
},
197+
} as PostChanges;
198+
199+
applyPostChangesToCRDTDoc( doc, changes, mockPostType );
200+
201+
expect( map.get( 'content' ) ).toBe(
202+
'<!-- wp:paragraph --><p>Hello</p><!-- /wp:paragraph -->'
203+
);
204+
} );
205+
166206
it( 'syncs meta fields', () => {
167207
const changes = {
168208
meta: {
@@ -352,6 +392,77 @@ describe( 'crdt', () => {
352392
expect( changes ).toHaveProperty( 'blocks' );
353393
} );
354394

395+
it( 'includes undefined blocks in changes', () => {
396+
map.set( 'blocks', undefined );
397+
398+
const editedRecord = {
399+
blocks: [
400+
{
401+
name: 'core/paragraph',
402+
attributes: { content: 'Test' },
403+
innerBlocks: [],
404+
},
405+
],
406+
} as unknown as Post;
407+
408+
const changes = getPostChangesFromCRDTDoc(
409+
doc,
410+
editedRecord,
411+
mockPostType
412+
);
413+
414+
expect( changes ).toHaveProperty( 'blocks' );
415+
expect( changes.blocks ).toBeUndefined();
416+
} );
417+
418+
it( 'detects content changes from string value', () => {
419+
map.set( 'content', 'New content' );
420+
421+
const editedRecord = {
422+
content: 'Old content',
423+
} as unknown as Post;
424+
425+
const changes = getPostChangesFromCRDTDoc(
426+
doc,
427+
editedRecord,
428+
mockPostType
429+
);
430+
431+
expect( changes.content ).toBe( 'New content' );
432+
} );
433+
434+
it( 'detects content changes from RenderedText value', () => {
435+
map.set( 'content', 'New content' );
436+
437+
const editedRecord = {
438+
content: { raw: 'Old content', rendered: 'Old content' },
439+
} as unknown as Post;
440+
441+
const changes = getPostChangesFromCRDTDoc(
442+
doc,
443+
editedRecord,
444+
mockPostType
445+
);
446+
447+
expect( changes.content ).toBe( 'New content' );
448+
} );
449+
450+
it( 'excludes content when unchanged from RenderedText value', () => {
451+
map.set( 'content', 'Same content' );
452+
453+
const editedRecord = {
454+
content: { raw: 'Same content', rendered: 'Same content' },
455+
} as unknown as Post;
456+
457+
const changes = getPostChangesFromCRDTDoc(
458+
doc,
459+
editedRecord,
460+
mockPostType
461+
);
462+
463+
expect( changes ).not.toHaveProperty( 'content' );
464+
} );
465+
355466
it( 'includes meta in changes', () => {
356467
const metaMap = createYMap();
357468
metaMap.set( 'public_meta', 'new value' );

packages/editor/src/store/private-actions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@ export const restoreRevision =
615615

616616
// Build the edits object with all restorable fields from the revision.
617617
const edits = {
618-
blocks: parse( revision.content.raw ),
618+
blocks: undefined,
619619
content: revision.content.raw,
620620
};
621621
if ( revision.title?.raw !== undefined ) {

0 commit comments

Comments
 (0)