Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,9 @@ public function create_item( $request ) {
require_once ABSPATH . 'wp-admin/includes/post.php';
}

// Post-type collaboration support is determined after the autosaves
// controller is selected, so disabled post types must delegate to Core.
if ( wp_is_post_type_collaboration_disabled( $post->post_type ) ) {
// Collaboration support is determined after the autosaves controller
// is selected, so posts with collaboration disabled must delegate to Core.
if ( wp_is_post_collaboration_disabled( $post ) ) {
return parent::create_item( $request );
}

Expand Down
41 changes: 37 additions & 4 deletions lib/experimental/collaboration/collaboration.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,39 @@ function wp_is_post_type_collaboration_disabled( $post_type ) {
}
}

if ( ! function_exists( 'wp_is_post_collaboration_disabled' ) ) {
/**
* Determines whether real-time collaboration is disabled for a post.
*
* Checked when the editor loads, so a status change applies on the next load.
*
* @since 7.2.0
*
* @param int|WP_Post $post Post ID or post object.
* @return bool Whether real-time collaboration is disabled for the post.
*/
function wp_is_post_collaboration_disabled( $post ) {
$post = get_post( $post );
if ( ! $post ) {
return true;
}

if ( wp_is_post_type_collaboration_disabled( $post->post_type ) ) {
return true;
}

/**
* Filters whether real-time collaboration is disabled for a post.
*
* @since 7.2.0
*
* @param bool $disabled Whether real-time collaboration is disabled for the post.
* @param WP_Post $post Post object.
*/
return (bool) apply_filters( 'wp_is_post_collaboration_disabled', false, $post );
}
}

/**
* Disables real-time collaboration for post types that cannot persist the
* CRDT document.
Expand Down Expand Up @@ -340,7 +373,7 @@ function gutenberg_filter_locked_posts_heartbeat_for_rtc( $response, $data = arr
}

$post = get_post( $post_id );
if ( ! $post || wp_is_post_type_collaboration_disabled( $post->post_type ) ) {
if ( ! $post || wp_is_post_collaboration_disabled( $post ) ) {
continue;
}

Expand Down Expand Up @@ -376,7 +409,7 @@ function gutenberg_block_quick_edit_for_active_lock() {
}

$post = get_post( $post_id );
if ( ! $post || wp_is_post_type_collaboration_disabled( $post->post_type ) ) {
if ( ! $post || wp_is_post_collaboration_disabled( $post ) ) {
return;
}

Expand Down Expand Up @@ -504,7 +537,7 @@ function gutenberg_post_list_collaboration_row_actions( $actions, $post ) {
return $actions;
}

if ( wp_is_post_type_collaboration_disabled( $post->post_type ) ) {
if ( wp_is_post_collaboration_disabled( $post ) ) {
return $actions;
}

Expand Down Expand Up @@ -566,7 +599,7 @@ function gutenberg_add_autosave_details_to_editor_settings( $settings, $block_ed

$post = $block_editor_context->post;

if ( wp_is_post_type_collaboration_disabled( $post->post_type ) ) {
if ( wp_is_post_collaboration_disabled( $post ) ) {
return $settings;
}

Expand Down
42 changes: 42 additions & 0 deletions lib/experimental/collaboration/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,45 @@ function gutenberg_override_autosaves_rest_controller( $args ) {
}

add_filter( 'register_post_type_args', 'gutenberg_override_autosaves_rest_controller', 10, 1 );

/**
* Adds a `collaboration_disabled` field to post types that support real-time
* collaboration, so the editor knows whether to sync the post it loads.
*
* @since 7.2.0
*/
function gutenberg_register_collaboration_disabled_rest_field() {
if ( ! wp_is_collaboration_enabled() ) {
return;
}

$post_types = array_values(
array_filter(
get_post_types( array( 'show_in_rest' => true ) ),
static function ( $post_type ) {
return ! wp_is_post_type_collaboration_disabled( $post_type );
}
)
);

register_rest_field(
$post_types,
'collaboration_disabled',
array(
'schema' => array(
'description' => __( 'Whether real-time collaboration is disabled for the post.', 'gutenberg' ),
'type' => 'boolean',
'context' => array( 'edit' ),
'readonly' => true,
),
'get_callback' => static function ( $item ) {
// Templates use a string ID and carry the post ID in `wp_id`.
$post = get_post( isset( $item['wp_id'] ) ? $item['wp_id'] : $item['id'] );

// Leave records that are not stored as posts to the post type check.
return $post ? wp_is_post_collaboration_disabled( $post ) : false;
},
)
);
}
add_action( 'rest_api_init', 'gutenberg_register_collaboration_disabled_rest_field' );
1 change: 1 addition & 0 deletions packages/core-data/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- `Icon`: add the `keywords` field exposed by the icons REST endpoint ([#82367](https://github.com/WordPress/gutenberg/pull/82367)).
- `__experimentalFetchLinkSuggestions`: add a `preferTypes` option to rank matches from one or more search types above the usual order ([#83408](https://github.com/WordPress/gutenberg/pull/83408)).
- Real-time collaboration skips syncing post records whose `collaboration_disabled` field is `true`, which the server sets through the `wp_is_post_collaboration_disabled` filter ([#84026](https://github.com/WordPress/gutenberg/pull/84026)).

### Bug Fixes

Expand Down
4 changes: 2 additions & 2 deletions packages/core-data/src/entities.js
Original file line number Diff line number Diff line change
Expand Up @@ -441,11 +441,11 @@ async function loadPostTypeEntities() {
null
);
},
shouldSync: () =>
shouldSync: ( objectType, objectId, record ) =>
! (
Array.isArray( window._wpCollaborationDisabledPostTypes ) &&
window._wpCollaborationDisabledPostTypes.includes( name )
),
) && ! record?.collaboration_disabled,
};

return entity;
Expand Down
33 changes: 33 additions & 0 deletions packages/core-data/src/test/entities.jsdom.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,39 @@ describe( 'loadPostTypeEntities', () => {
expect( bookEntity.syncConfig.shouldSync() ).toBe( false );
} );

it( 'should not sync post records disabled for collaboration', async () => {
window.__experimentalEnableRealTimeCollaboration = true;
window._wpCollaborationDisabledPostTypes = [];

const mockPostTypes = {
book: {
name: 'Books',
rest_base: 'books',
rest_namespace: 'wp/v2',
},
};

apiFetch.mockResolvedValueOnce( mockPostTypes );

const postTypeLoader = additionalEntityConfigLoaders.find(
( loader ) => loader.kind === 'postType'
);
const entities = await postTypeLoader.loadEntities();
const { shouldSync } = entities.find(
( e ) => e.name === 'book'
).syncConfig;

expect(
shouldSync( 'postType/book', '1', { collaboration_disabled: true } )
).toBe( false );
expect(
shouldSync( 'postType/book', '1', {
collaboration_disabled: false,
} )
).toBe( true );
expect( shouldSync( 'postType/book', '1' ) ).toBe( true );
} );

it( 'should skip taxonomy rest_base when taxonomy is not found in fetched taxonomies', async () => {
window.__experimentalEnableRealTimeCollaboration = true;

Expand Down
7 changes: 6 additions & 1 deletion packages/editor/src/store/private-selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -600,7 +600,12 @@ export const isCollaborationEnabledForCurrentPost = createRegistrySelector(
false !==
syncConfig.shouldSync?.(
`postType/${ currentPostType }`,
currentPostId
currentPostId,
select( coreStore ).getRawEntityRecord(
'postType',
currentPostType,
currentPostId
)
)
);
}
Expand Down
21 changes: 20 additions & 1 deletion packages/editor/src/store/test/private-selectors.jsdom.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,14 @@ describe( 'isCollaborationEnabledForCurrentPost', () => {
function setupRegistry( {
collaborationSupported = true,
syncConfig = {},
record = { id: 123 },
} = {} ) {
isCollaborationEnabledForCurrentPost.registry = {
select: ( store ) => {
if ( store === coreStore ) {
const selectors = {
getEntityConfig: () => ( { syncConfig } ),
getRawEntityRecord: () => record,
};
lock( selectors, {
isCollaborationSupported: () => collaborationSupported,
Expand All @@ -124,7 +126,9 @@ describe( 'isCollaborationEnabledForCurrentPost', () => {
const state = { postType: 'book', postId: 123 };

expect( isCollaborationEnabledForCurrentPost( state ) ).toBe( true );
expect( shouldSync ).toHaveBeenCalledWith( 'postType/book', 123 );
expect( shouldSync ).toHaveBeenCalledWith( 'postType/book', 123, {
id: 123,
} );
} );

it( 'returns false when the current post type sync config does not support persistence', () => {
Expand All @@ -149,6 +153,21 @@ describe( 'isCollaborationEnabledForCurrentPost', () => {

expect( isCollaborationEnabledForCurrentPost( state ) ).toBe( false );
} );

it( 'returns false when the sync config should not sync the current post', () => {
setupRegistry( {
syncConfig: {
shouldSync: ( objectType, objectId, record ) =>
! record.collaboration_disabled,
supportsPersistence: true,
},
record: { id: 123, collaboration_disabled: true },
} );

const state = { postType: 'book', postId: 123 };

expect( isCollaborationEnabledForCurrentPost( state ) ).toBe( false );
} );
} );

describe( 'getDefaultRenderingMode', () => {
Expand Down
4 changes: 4 additions & 0 deletions packages/sync/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### New Features

- `SyncConfig.shouldSync` receives the entity record as an optional third argument, so a config can decide per record whether to sync it ([#84026](https://github.com/WordPress/gutenberg/pull/84026)).

### Bug Fixes

- The sync manager's undo manager no longer stands in for the editor's undo manager. It is the undo history of the loaded entities only: it reports each level Yjs opens through the new `onUndoLevelOpened` record handler (replacing `onUndoStackChange`), and `undo()` and `redo()` take the entity whose level to move and return whether a level was moved. Each entity has its own Yjs undo manager, which replaces the copied `YMultiDocUndoManager`. `stopCapturing()` and `clearRedo()` let the consumer keep its own history in step. The manager exposes `isLoaded()`, and its undo manager exists from creation instead of from the first load. Together with the `core-data` change, undo and redo keep working for entities collaboration does not sync ([#83888](https://github.com/WordPress/gutenberg/pull/83888)).
Expand Down
4 changes: 3 additions & 1 deletion packages/sync/src/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,9 @@ export function createSyncManager( debug = false ): SyncManager {
return; // Already bootstrapped.
}

if ( false === syncConfig.shouldSync?.( objectType, objectId ) ) {
if (
false === syncConfig.shouldSync?.( objectType, objectId, record )
) {
log( 'loadEntity', 'shouldSync false, skipping', entityId );
return; // Sync config indicates that this entity should not be synced.
}
Expand Down
6 changes: 4 additions & 2 deletions packages/sync/src/test/manager.jsdom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1234,7 +1234,8 @@ describe( 'SyncManager', () => {

expect( mockSyncConfig.shouldSync ).toHaveBeenCalledWith(
'post',
'123'
'123',
mockRecord
);
expect(
mockSyncConfig.applyChangesToCRDTDoc
Expand All @@ -1257,7 +1258,8 @@ describe( 'SyncManager', () => {

expect( mockSyncConfig.shouldSync ).toHaveBeenCalledWith(
'post',
'123'
'123',
mockRecord
);
expect(
mockSyncConfig.applyChangesToCRDTDoc
Expand Down
3 changes: 2 additions & 1 deletion packages/sync/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ export interface SyncConfig {
getPersistedCRDTDoc?: ( record: ObjectData ) => string | null;
shouldSync?: (
objectType: ObjectType,
objectId: ObjectID | null
objectId: ObjectID | null,
record?: ObjectData
) => boolean;
supportsPersistence?: boolean;
}
Expand Down
Loading
Loading