From 06363996d9dc7e5975c0554d1a74a0d8b3012d68 Mon Sep 17 00:00:00 2001 From: Max Schmeling Date: Fri, 5 Jun 2026 12:55:58 -0500 Subject: [PATCH 1/6] RTC: Allow disabling collaboration by post type --- lib/compat/wordpress-7.1/collaboration.php | 38 +++++++++++++- packages/core-data/src/entities.js | 5 ++ packages/core-data/src/test/entities.js | 51 ++++++++++++++++++ .../editor/src/store/private-selectors.js | 11 +++- .../src/store/test/private-selectors.js | 52 +++++++++++++++++++ 5 files changed, 155 insertions(+), 2 deletions(-) diff --git a/lib/compat/wordpress-7.1/collaboration.php b/lib/compat/wordpress-7.1/collaboration.php index f6d46d3ac0ce39..6be8fd7181e3d7 100644 --- a/lib/compat/wordpress-7.1/collaboration.php +++ b/lib/compat/wordpress-7.1/collaboration.php @@ -203,6 +203,32 @@ function wp_is_collaboration_allowed() { } } +if ( ! function_exists( 'wp_is_post_type_collaboration_enabled' ) ) { + /** + * Determines whether real-time collaboration is enabled for a post type. + * + * @since 7.1.0 + * + * @param string $post_type Post type name. + * @return bool Whether real-time collaboration is enabled for the post type. + */ + function wp_is_post_type_collaboration_enabled( $post_type ) { + if ( ! post_type_exists( $post_type ) ) { + return false; + } + + /** + * Filters whether real-time collaboration is enabled for a post type. + * + * @since 7.1.0 + * + * @param bool $enabled Whether real-time collaboration is enabled for the post type. + * @param string $post_type Post type name. + */ + return (bool) apply_filters( 'wp_is_post_type_collaboration_enabled', true, $post_type ); + } +} + /** * Injects the real-time collaboration setting into a global variable. * @@ -224,9 +250,19 @@ function gutenberg_inject_real_time_collaboration_setting() { $enabled = false; } + $disabled_post_types = array_values( + array_filter( + get_post_types( array( 'show_in_rest' => true ) ), + static function ( $post_type ) { + return ! wp_is_post_type_collaboration_enabled( $post_type ); + } + ) + ); + wp_add_inline_script( 'wp-core-data', - 'window._wpCollaborationEnabled = ' . wp_json_encode( $enabled ) . ';', + 'window._wpCollaborationEnabled = ' . wp_json_encode( $enabled ) . ';' . + 'window._wpCollaborationDisabledPostTypes = ' . wp_json_encode( $disabled_post_types ) . ';', 'after' ); } diff --git a/packages/core-data/src/entities.js b/packages/core-data/src/entities.js index 477351d5a0167b..9b5202ddd3caba 100644 --- a/packages/core-data/src/entities.js +++ b/packages/core-data/src/entities.js @@ -458,6 +458,11 @@ async function loadPostTypeEntities() { null ); }, + shouldSync: () => + ! ( + Array.isArray( window._wpCollaborationDisabledPostTypes ) && + window._wpCollaborationDisabledPostTypes.includes( name ) + ), }; return entity; diff --git a/packages/core-data/src/test/entities.js b/packages/core-data/src/test/entities.js index 5f45462655f5eb..0f59bcabda198c 100644 --- a/packages/core-data/src/test/entities.js +++ b/packages/core-data/src/test/entities.js @@ -136,15 +136,20 @@ describe( 'prePersistPostType', () => { describe( 'loadPostTypeEntities', () => { let originalCollaborationEnabled; + let originalCollaborationDisabledPostTypes; beforeEach( () => { apiFetch.mockReset(); applyPostChangesToCRDTDoc.mockReset(); originalCollaborationEnabled = window._wpCollaborationEnabled; + originalCollaborationDisabledPostTypes = + window._wpCollaborationDisabledPostTypes; } ); afterEach( () => { window._wpCollaborationEnabled = originalCollaborationEnabled; + window._wpCollaborationDisabledPostTypes = + originalCollaborationDisabledPostTypes; } ); it( 'should include custom taxonomy rest_bases in synced properties when collaboration is enabled', async () => { @@ -224,6 +229,52 @@ describe( 'loadPostTypeEntities', () => { expect( syncedProperties ).not.toContain( 'tags' ); } ); + it( 'should sync post type entities by default', async () => { + window._wpCollaborationEnabled = false; + window._wpCollaborationDisabledPostTypes = undefined; + + const mockPostTypes = { + post: { + name: 'Posts', + rest_base: 'posts', + rest_namespace: 'wp/v2', + }, + }; + + apiFetch.mockResolvedValueOnce( mockPostTypes ); + + const postTypeLoader = additionalEntityConfigLoaders.find( + ( loader ) => loader.kind === 'postType' + ); + const entities = await postTypeLoader.loadEntities(); + const postEntity = entities.find( ( e ) => e.name === 'post' ); + + expect( postEntity.syncConfig.shouldSync() ).toBe( true ); + } ); + + it( 'should not sync post type entities disabled for collaboration', async () => { + window._wpCollaborationEnabled = false; + window._wpCollaborationDisabledPostTypes = [ 'book' ]; + + 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 bookEntity = entities.find( ( e ) => e.name === 'book' ); + + expect( bookEntity.syncConfig.shouldSync() ).toBe( false ); + } ); + it( 'should skip taxonomy rest_base when taxonomy is not found in fetched taxonomies', async () => { window._wpCollaborationEnabled = true; diff --git a/packages/editor/src/store/private-selectors.js b/packages/editor/src/store/private-selectors.js index d5dfa8d2434bdc..f12949298354e5 100644 --- a/packages/editor/src/store/private-selectors.js +++ b/packages/editor/src/store/private-selectors.js @@ -25,6 +25,7 @@ import { getRenderingMode, getCurrentPost, getCurrentPostType, + getCurrentPostId, getEditorSettings, getCurrentPostRevisionsCount, } from './selectors'; @@ -598,13 +599,21 @@ export const isCollaborationEnabledForCurrentPost = createRegistrySelector( } const currentPostType = getCurrentPostType( state ); + const currentPostId = getCurrentPostId( state ); const entityConfig = select( coreStore ).getEntityConfig( 'postType', currentPostType ); + const syncConfig = entityConfig?.syncConfig; return Boolean( - entityConfig?.syncConfig && window._wpCollaborationEnabled + syncConfig && + window._wpCollaborationEnabled && + false !== + syncConfig.shouldSync?.( + `postType/${ currentPostType }`, + currentPostId + ) ); } ); diff --git a/packages/editor/src/store/test/private-selectors.js b/packages/editor/src/store/test/private-selectors.js index ce216099376d68..07fc5e4e6eac0f 100644 --- a/packages/editor/src/store/test/private-selectors.js +++ b/packages/editor/src/store/test/private-selectors.js @@ -10,7 +10,9 @@ import { store as preferencesStore } from '@wordpress/preferences'; import { getDefaultRenderingMode, getPostBlocksByName, + isCollaborationEnabledForCurrentPost, } from '../private-selectors'; +import { lock } from '../../lock-unlock'; describe( 'getPostBlocksByName', () => { const state = { @@ -86,6 +88,56 @@ describe( 'getPostBlocksByName', () => { } ); } ); +describe( 'isCollaborationEnabledForCurrentPost', () => { + let originalCollaborationEnabled; + + beforeEach( () => { + originalCollaborationEnabled = window._wpCollaborationEnabled; + window._wpCollaborationEnabled = true; + } ); + + afterEach( () => { + window._wpCollaborationEnabled = originalCollaborationEnabled; + } ); + + function setupRegistry( { + collaborationSupported = true, + syncConfig = {}, + } = {} ) { + isCollaborationEnabledForCurrentPost.registry = { + select: ( store ) => { + if ( store === coreStore ) { + const selectors = { + getEntityConfig: () => ( { syncConfig } ), + }; + lock( selectors, { + isCollaborationSupported: () => collaborationSupported, + } ); + return selectors; + } + }, + }; + } + + it( 'returns true when the current post type sync config should sync', () => { + const shouldSync = jest.fn( () => true ); + setupRegistry( { syncConfig: { shouldSync } } ); + + const state = { postType: 'book', postId: 123 }; + + expect( isCollaborationEnabledForCurrentPost( state ) ).toBe( true ); + expect( shouldSync ).toHaveBeenCalledWith( 'postType/book', 123 ); + } ); + + it( 'returns false when the current post type sync config should not sync', () => { + setupRegistry( { syncConfig: { shouldSync: () => false } } ); + + const state = { postType: 'book', postId: 123 }; + + expect( isCollaborationEnabledForCurrentPost( state ) ).toBe( false ); + } ); +} ); + describe( 'getDefaultRenderingMode', () => { function setupRegistry( { supportsEditor = true, From e559e1d95ec8de5f7675a73ce8ff86aff48db47c Mon Sep 17 00:00:00 2001 From: Max Schmeling Date: Wed, 10 Jun 2026 14:24:47 -0500 Subject: [PATCH 2/6] Rename post type collaboration opt-out filter --- lib/compat/wordpress-7.1/collaboration.php | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/lib/compat/wordpress-7.1/collaboration.php b/lib/compat/wordpress-7.1/collaboration.php index 6be8fd7181e3d7..f5aa9b08ade712 100644 --- a/lib/compat/wordpress-7.1/collaboration.php +++ b/lib/compat/wordpress-7.1/collaboration.php @@ -203,29 +203,29 @@ function wp_is_collaboration_allowed() { } } -if ( ! function_exists( 'wp_is_post_type_collaboration_enabled' ) ) { +if ( ! function_exists( 'wp_is_post_type_collaboration_disabled' ) ) { /** - * Determines whether real-time collaboration is enabled for a post type. + * Determines whether real-time collaboration is disabled for a post type. * * @since 7.1.0 * * @param string $post_type Post type name. - * @return bool Whether real-time collaboration is enabled for the post type. + * @return bool Whether real-time collaboration is disabled for the post type. */ - function wp_is_post_type_collaboration_enabled( $post_type ) { + function wp_is_post_type_collaboration_disabled( $post_type ) { if ( ! post_type_exists( $post_type ) ) { - return false; + return true; } /** - * Filters whether real-time collaboration is enabled for a post type. + * Filters whether real-time collaboration is disabled for a post type. * * @since 7.1.0 * - * @param bool $enabled Whether real-time collaboration is enabled for the post type. + * @param bool $disabled Whether real-time collaboration is disabled for the post type. * @param string $post_type Post type name. */ - return (bool) apply_filters( 'wp_is_post_type_collaboration_enabled', true, $post_type ); + return (bool) apply_filters( 'wp_is_post_type_collaboration_disabled', false, $post_type ); } } @@ -253,9 +253,7 @@ function gutenberg_inject_real_time_collaboration_setting() { $disabled_post_types = array_values( array_filter( get_post_types( array( 'show_in_rest' => true ) ), - static function ( $post_type ) { - return ! wp_is_post_type_collaboration_enabled( $post_type ); - } + 'wp_is_post_type_collaboration_disabled' ) ); From 7d5e45d50b0700f0f8a39ab168bdee9027be3d46 Mon Sep 17 00:00:00 2001 From: Max Schmeling Date: Wed, 10 Jun 2026 14:33:42 -0500 Subject: [PATCH 3/6] Add Core backport changelog entry --- backport-changelog/7.1/11734.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 backport-changelog/7.1/11734.md diff --git a/backport-changelog/7.1/11734.md b/backport-changelog/7.1/11734.md new file mode 100644 index 00000000000000..4d33fb328af19b --- /dev/null +++ b/backport-changelog/7.1/11734.md @@ -0,0 +1,4 @@ +https://github.com/WordPress/wordpress-develop/pull/11734 + +* https://github.com/WordPress/gutenberg/pull/77502 +* https://github.com/WordPress/gutenberg/pull/78984 From 2b059bb2d0c8d42075ea6ab35dc5f07444c84e00 Mon Sep 17 00:00:00 2001 From: Max Schmeling Date: Thu, 11 Jun 2026 11:31:41 -0500 Subject: [PATCH 4/6] Remove Core backport changelog entry --- backport-changelog/7.1/11734.md | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 backport-changelog/7.1/11734.md diff --git a/backport-changelog/7.1/11734.md b/backport-changelog/7.1/11734.md deleted file mode 100644 index 4d33fb328af19b..00000000000000 --- a/backport-changelog/7.1/11734.md +++ /dev/null @@ -1,4 +0,0 @@ -https://github.com/WordPress/wordpress-develop/pull/11734 - -* https://github.com/WordPress/gutenberg/pull/77502 -* https://github.com/WordPress/gutenberg/pull/78984 From 15cf7aa6921cc89984a766be803ae4f7bb6ec6ba Mon Sep 17 00:00:00 2001 From: Max Schmeling Date: Thu, 11 Jun 2026 11:34:37 -0500 Subject: [PATCH 5/6] Require persistence for current post collaboration --- .../editor/src/store/private-selectors.js | 1 + .../src/store/test/private-selectors.js | 21 +++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/store/private-selectors.js b/packages/editor/src/store/private-selectors.js index f12949298354e5..0913ea428ade63 100644 --- a/packages/editor/src/store/private-selectors.js +++ b/packages/editor/src/store/private-selectors.js @@ -608,6 +608,7 @@ export const isCollaborationEnabledForCurrentPost = createRegistrySelector( return Boolean( syncConfig && + syncConfig.supportsPersistence && window._wpCollaborationEnabled && false !== syncConfig.shouldSync?.( diff --git a/packages/editor/src/store/test/private-selectors.js b/packages/editor/src/store/test/private-selectors.js index 07fc5e4e6eac0f..59c9560f1c5c1f 100644 --- a/packages/editor/src/store/test/private-selectors.js +++ b/packages/editor/src/store/test/private-selectors.js @@ -121,7 +121,9 @@ describe( 'isCollaborationEnabledForCurrentPost', () => { it( 'returns true when the current post type sync config should sync', () => { const shouldSync = jest.fn( () => true ); - setupRegistry( { syncConfig: { shouldSync } } ); + setupRegistry( { + syncConfig: { shouldSync, supportsPersistence: true }, + } ); const state = { postType: 'book', postId: 123 }; @@ -129,8 +131,23 @@ describe( 'isCollaborationEnabledForCurrentPost', () => { expect( shouldSync ).toHaveBeenCalledWith( 'postType/book', 123 ); } ); + it( 'returns false when the current post type sync config does not support persistence', () => { + const shouldSync = jest.fn( () => true ); + setupRegistry( { syncConfig: { shouldSync } } ); + + const state = { postType: 'book', postId: 123 }; + + expect( isCollaborationEnabledForCurrentPost( state ) ).toBe( false ); + expect( shouldSync ).not.toHaveBeenCalled(); + } ); + it( 'returns false when the current post type sync config should not sync', () => { - setupRegistry( { syncConfig: { shouldSync: () => false } } ); + setupRegistry( { + syncConfig: { + shouldSync: () => false, + supportsPersistence: true, + }, + } ); const state = { postType: 'book', postId: 123 }; From af6728527ea6485a5eb56933b24836f4b1b177d3 Mon Sep 17 00:00:00 2001 From: Max Schmeling Date: Thu, 11 Jun 2026 11:56:30 -0500 Subject: [PATCH 6/6] Skip collaboration row actions for disabled post types --- lib/compat/wordpress-7.1/collaboration.php | 4 ++ .../postListCollaborationRowActions.php | 72 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 phpunit/tests/collaboration/postListCollaborationRowActions.php diff --git a/lib/compat/wordpress-7.1/collaboration.php b/lib/compat/wordpress-7.1/collaboration.php index 66e4de93b802a0..6f7337b561e3aa 100644 --- a/lib/compat/wordpress-7.1/collaboration.php +++ b/lib/compat/wordpress-7.1/collaboration.php @@ -436,6 +436,10 @@ function gutenberg_post_list_collaboration_row_actions( $actions, $post ) { return $actions; } + if ( wp_is_post_type_collaboration_disabled( $post->post_type ) ) { + return $actions; + } + $title = _draft_or_post_title( $post->ID ); /* diff --git a/phpunit/tests/collaboration/postListCollaborationRowActions.php b/phpunit/tests/collaboration/postListCollaborationRowActions.php new file mode 100644 index 00000000000000..b5e366a5aa58fd --- /dev/null +++ b/phpunit/tests/collaboration/postListCollaborationRowActions.php @@ -0,0 +1,72 @@ +user->create( array( 'role' => 'administrator' ) ); + self::$post_id = $factory->post->create( + array( + 'post_title' => 'Collaboration row action post', + 'post_type' => 'post', + ) + ); + } + + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + wp_delete_post( self::$post_id, true ); + } + + public function set_up() { + parent::set_up(); + + wp_set_current_user( self::$admin_id ); + } + + public function tear_down() { + remove_filter( 'wp_is_post_type_collaboration_disabled', array( $this, 'disable_post_collaboration' ) ); + + parent::tear_down(); + } + + public function test_post_list_collaboration_row_actions_include_join_action() { + $post = get_post( self::$post_id ); + $actions = array( + 'edit' => 'Edit', + ); + + $filtered_actions = gutenberg_post_list_collaboration_row_actions( $actions, $post ); + + $this->assertStringContainsString( 'edit-action-text', $filtered_actions['edit'] ); + $this->assertStringContainsString( 'join-action-text', $filtered_actions['edit'] ); + $this->assertStringContainsString( '>Join', $filtered_actions['edit'] ); + } + + public function test_post_list_collaboration_row_actions_are_unchanged_for_disabled_post_type() { + add_filter( 'wp_is_post_type_collaboration_disabled', array( $this, 'disable_post_collaboration' ), 10, 2 ); + + $post = get_post( self::$post_id ); + $actions = array( + 'edit' => 'Edit', + ); + + $this->assertSame( + $actions, + gutenberg_post_list_collaboration_row_actions( $actions, $post ) + ); + } + + public function disable_post_collaboration( $disabled, $post_type ) { + return 'post' === $post_type ? true : $disabled; + } +}