From 77f9f6b17e15d1126e595437a518ae2747ff02e4 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Sun, 15 Mar 2026 22:25:03 +0100 Subject: [PATCH] Revisions: use useSubRegistry={false} to fix global store selectors (#76152) Co-authored-by: ellatrix Co-authored-by: Mamaduka Co-authored-by: youknowriad Co-authored-by: pbiron --- .../block-inspector/edit-contents.js | 8 +- .../src/components/block-styles/index.js | 8 +- .../block-variation-transforms/index.js | 77 ++++++------ .../src/components/editor-interface/index.js | 14 ++- .../revisions-canvas.js | 116 +++--------------- .../editor/src/components/provider/index.js | 22 +++- .../provider/use-block-editor-settings.js | 11 +- .../provider/use-revision-blocks.js | 105 ++++++++++++++++ .../editor/src/components/sidebar/header.js | 2 +- packages/editor/src/store/private-actions.js | 13 ++ .../editor/src/store/private-selectors.js | 10 ++ packages/editor/src/store/reducer.js | 19 +++ .../specs/editor/various/revisions.spec.js | 75 +++++++++++ 13 files changed, 333 insertions(+), 147 deletions(-) create mode 100644 packages/editor/src/components/provider/use-revision-blocks.js diff --git a/packages/block-editor/src/components/block-inspector/edit-contents.js b/packages/block-editor/src/components/block-inspector/edit-contents.js index fb658159b213d5..8509bd0a89cbb4 100644 --- a/packages/block-editor/src/components/block-inspector/edit-contents.js +++ b/packages/block-editor/src/components/block-inspector/edit-contents.js @@ -92,19 +92,21 @@ export default function EditContents( { clientId } ) { stopEditingContentOnlySection, } = useContentOnlySectionEdit( clientId ); - const { block, onNavigateToEntityRecord } = useSelect( + const { block, onNavigateToEntityRecord, canEdit } = useSelect( ( select ) => { - const { getBlock, getSettings } = select( blockEditorStore ); + const { getBlock, getSettings, canEditBlock } = + select( blockEditorStore ); return { block: getBlock( clientId ), onNavigateToEntityRecord: getSettings().onNavigateToEntityRecord, + canEdit: canEditBlock( clientId ), }; }, [ clientId ] ); - if ( ! isWithinSection && ! isWithinEditedSection ) { + if ( ! canEdit || ( ! isWithinSection && ! isWithinEditedSection ) ) { return null; } diff --git a/packages/block-editor/src/components/block-styles/index.js b/packages/block-editor/src/components/block-styles/index.js index cf88a6053e6c7e..56a8e4697a2c5f 100644 --- a/packages/block-editor/src/components/block-styles/index.js +++ b/packages/block-editor/src/components/block-styles/index.js @@ -7,6 +7,7 @@ import clsx from 'clsx'; * WordPress dependencies */ import { useState } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; import { debounce, useViewportMatch } from '@wordpress/compose'; import { Button, @@ -24,11 +25,16 @@ import BlockStylesPreviewPanel from './preview-panel'; import useStylesForBlocks from './use-styles-for-block'; import { useToolsPanelDropdownMenuProps } from '../global-styles/utils'; import { getDefaultStyle } from './utils'; +import { store as blockEditorStore } from '../../store'; const noop = () => {}; // Block Styles component for the Settings Sidebar. function BlockStyles( { clientId, onSwitch = noop, onHoverClassName = noop } ) { + const canEdit = useSelect( + ( select ) => select( blockEditorStore ).canEditBlock( clientId ), + [ clientId ] + ); const { onSelect, stylesToRender, @@ -43,7 +49,7 @@ function BlockStyles( { clientId, onSwitch = noop, onHoverClassName = noop } ) { const isMobileViewport = useViewportMatch( 'medium', '<' ); const dropdownMenuProps = useToolsPanelDropdownMenuProps(); - if ( ! stylesToRender || stylesToRender.length === 0 ) { + if ( ! canEdit || ! stylesToRender || stylesToRender.length === 0 ) { return null; } diff --git a/packages/block-editor/src/components/block-variation-transforms/index.js b/packages/block-editor/src/components/block-variation-transforms/index.js index f19605e7a81283..acb306c8840f93 100644 --- a/packages/block-editor/src/components/block-variation-transforms/index.js +++ b/packages/block-editor/src/components/block-variation-transforms/index.js @@ -146,41 +146,46 @@ function VariationsToggleGroupControl( { function __experimentalBlockVariationTransforms( { blockClientId } ) { const { updateBlockAttributes } = useDispatch( blockEditorStore ); - const { activeBlockVariation, variations, isContentOnly, isSection } = - useSelect( - ( select ) => { - const { getActiveBlockVariation, getBlockVariations } = - select( blocksStore ); - - const { - getBlockName, - getBlockAttributes, - getBlockEditingMode, - isSectionBlock, - } = unlock( select( blockEditorStore ) ); - - const name = blockClientId && getBlockName( blockClientId ); - - const { hasContentRoleAttribute } = unlock( - select( blocksStore ) - ); - const isContentBlock = hasContentRoleAttribute( name ); - - return { - activeBlockVariation: getActiveBlockVariation( - name, - getBlockAttributes( blockClientId ), - 'transform' - ), - variations: name && getBlockVariations( name, 'transform' ), - isContentOnly: - getBlockEditingMode( blockClientId ) === - 'contentOnly' && ! isContentBlock, - isSection: isSectionBlock( blockClientId ), - }; - }, - [ blockClientId ] - ); + const { + activeBlockVariation, + variations, + canEdit, + isContentOnly, + isSection, + } = useSelect( + ( select ) => { + const { getActiveBlockVariation, getBlockVariations } = + select( blocksStore ); + + const { + getBlockName, + getBlockAttributes, + getBlockEditingMode, + isSectionBlock, + } = unlock( select( blockEditorStore ) ); + const { canEditBlock } = select( blockEditorStore ); + + const name = blockClientId && getBlockName( blockClientId ); + + const { hasContentRoleAttribute } = unlock( select( blocksStore ) ); + const isContentBlock = hasContentRoleAttribute( name ); + + return { + activeBlockVariation: getActiveBlockVariation( + name, + getBlockAttributes( blockClientId ), + 'transform' + ), + variations: name && getBlockVariations( name, 'transform' ), + canEdit: canEditBlock( blockClientId ), + isContentOnly: + getBlockEditingMode( blockClientId ) === 'contentOnly' && + ! isContentBlock, + isSection: isSectionBlock( blockClientId ), + }; + }, + [ blockClientId ] + ); const selectedValue = activeBlockVariation?.name; @@ -205,7 +210,7 @@ function __experimentalBlockVariationTransforms( { blockClientId } ) { } ); }; - if ( ! variations?.length || isContentOnly || isSection ) { + if ( ! variations?.length || ! canEdit || isContentOnly || isSection ) { return null; } diff --git a/packages/editor/src/components/editor-interface/index.js b/packages/editor/src/components/editor-interface/index.js index 080962691a4e1c..3af5e88294071e 100644 --- a/packages/editor/src/components/editor-interface/index.js +++ b/packages/editor/src/components/editor-interface/index.js @@ -7,7 +7,7 @@ import clsx from 'clsx'; * WordPress dependencies */ import { InterfaceSkeleton, ComplementaryArea } from '@wordpress/interface'; -import { useSelect } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { store as preferencesStore } from '@wordpress/preferences'; import { BlockBreadcrumb, BlockToolbar } from '@wordpress/block-editor'; @@ -82,6 +82,7 @@ export default function EditorInterface( { stylesPath, showStylebook, isRevisionsMode, + showDiff, } = useSelect( ( select ) => { const { get } = select( preferencesStore ); const { @@ -94,6 +95,7 @@ export default function EditorInterface( { getStylesPath, getShowStylebook, isRevisionsMode: _isRevisionsMode, + isShowingRevisionDiff, } = unlock( select( editorStore ) ); const editorSettings = getEditorSettings(); @@ -121,8 +123,11 @@ export default function EditorInterface( { getCurrentPostType() === 'attachment' && window?.__experimentalMediaEditor, isRevisionsMode: _isRevisionsMode(), + showDiff: isShowingRevisionDiff(), }; }, [] ); + const { setShowRevisionDiff } = unlock( useDispatch( editorStore ) ); + // Runs unconditionally so join/leave/save notifications are dispatched // regardless of viewport width or whether the header centre area is visible. useCollaboratorNotifications( postId, postType ); @@ -152,9 +157,6 @@ export default function EditorInterface( { [ entitiesSavedStatesCallback ] ); - // Local state for diff toggle in revisions mode. - const [ showDiff, setShowDiff ] = useState( true ); - // When in revisions mode, render the revisions interface. if ( isRevisionsMode ) { return ( @@ -164,10 +166,10 @@ export default function EditorInterface( { header={ setShowDiff( ! showDiff ) } + onToggleDiff={ () => setShowRevisionDiff( ! showDiff ) } /> } - content={ } + content={ } sidebar={ } /> ); diff --git a/packages/editor/src/components/post-revisions-preview/revisions-canvas.js b/packages/editor/src/components/post-revisions-preview/revisions-canvas.js index 37e9364e465fd5..7be5b6a00fe1f9 100644 --- a/packages/editor/src/components/post-revisions-preview/revisions-canvas.js +++ b/packages/editor/src/components/post-revisions-preview/revisions-canvas.js @@ -7,14 +7,9 @@ import clsx from 'clsx'; * WordPress dependencies */ import { Spinner } from '@wordpress/components'; -import { - privateApis as blockEditorPrivateApis, - store as blockEditorStore, -} from '@wordpress/block-editor'; -import { createBlock, parse } from '@wordpress/blocks'; -import { EntityProvider } from '@wordpress/core-data'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; import { useSelect } from '@wordpress/data'; -import { useEffect, useMemo, useRef } from '@wordpress/element'; +import { useEffect } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; /** @@ -28,12 +23,8 @@ import { unregisterDiffFormatTypes, } from './diff-format-types'; import { useDiffMarkers } from './diff-markers'; -import { preserveClientIds } from './preserve-client-ids'; -import { diffRevisionContent } from './block-diff'; -const { ExperimentalBlockEditorProvider, usePrivateStyleOverride } = unlock( - blockEditorPrivateApis -); +const { usePrivateStyleOverride } = unlock( blockEditorPrivateApis ); // SVG filter for removed blocks: grayscale + red tint const REVISION_REMOVED_FILTER_SVG = ` @@ -151,12 +142,11 @@ function CanvasContent( { showDiff } ) { /** * Canvas component that renders a post revision in read-only mode. + * Block preparation and settings are handled by the parent EditorProvider. * - * @param {Object} props Component props. - * @param {boolean} props.showDiff Whether to show diff highlighting. * @return {React.JSX.Element} The revisions canvas component. */ -export default function RevisionsCanvas( { showDiff } ) { +export default function RevisionsCanvas() { useEffect( () => { registerDiffFormatTypes(); return () => { @@ -164,91 +154,23 @@ export default function RevisionsCanvas( { showDiff } ) { }; }, [] ); - const { revision, previousRevision, postType, blockEditorSettings } = - useSelect( ( select ) => { - const { - getCurrentRevision, - getPreviousRevision, - getCurrentPostType, - } = unlock( select( editorStore ) ); - return { - revision: getCurrentRevision(), - previousRevision: getPreviousRevision(), - postType: getCurrentPostType(), - blockEditorSettings: select( blockEditorStore ).getSettings(), - }; - }, [] ); - - // Track previously rendered blocks to preserve clientIds between renders. - const previousBlocksRef = useRef( [] ); - - const blocks = useMemo( () => { - const currentContent = revision?.content?.raw ?? ''; - - let parsedBlocks; - if ( showDiff ) { - const previousContent = previousRevision?.content?.raw || ''; - // diffRevisionContent handles both normal diffing and the case - // where there's no previous revision (oldest revision shows all as added). - parsedBlocks = diffRevisionContent( - currentContent, - previousContent - ); - } else { - // When diff is disabled, just parse the current revision content. - parsedBlocks = parse( currentContent ); - } - - if ( postType === 'wp_navigation' ) { - parsedBlocks = [ - createBlock( - 'core/navigation', - { templateLock: false }, - parsedBlocks - ), - ]; - } - - // Preserve clientIds from previous render to prevent React unmount/remount. - const blocksWithStableIds = preserveClientIds( - parsedBlocks, - previousBlocksRef.current + const { revision, showDiff } = useSelect( ( select ) => { + const { getCurrentRevision, isShowingRevisionDiff } = unlock( + select( editorStore ) ); - - // Update ref for next render. - previousBlocksRef.current = blocksWithStableIds; - - return blocksWithStableIds; - }, [ - revision?.content?.raw, - previousRevision?.content?.raw, - postType, - showDiff, - ] ); - - const settings = useMemo( - () => ( { - ...blockEditorSettings, - isPreviewMode: true, - } ), - [ blockEditorSettings ] - ); + return { + revision: getCurrentRevision(), + showDiff: isShowingRevisionDiff(), + }; + }, [] ); return revision ? ( - // EntityProvider without kind/type/id inherits those from the - // parent context. Only revisionId is added so that useEntityProp - // reads from the revision record instead of the current entity. - - - -
- -
-
-
+ <> + +
+ +
+ ) : (
diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index db74d142899212..7ff5fecccc6a6a 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -34,6 +34,7 @@ import { unlock } from '../../lock-unlock'; import DisableNonPageContentBlocks from './disable-non-page-content-blocks'; import NavigationBlockEditingMode from './navigation-block-editing-mode'; import { useHideBlocksFromInserter } from './use-hide-blocks-from-inserter'; +import { useRevisionBlocks } from './use-revision-blocks'; import useCommands from '../commands'; import useUploadSaveLock from './use-upload-save-lock'; import BlockRemovalWarnings from '../block-removal-warnings'; @@ -78,6 +79,7 @@ const NON_CONTEXTUAL_POST_TYPES = [ * @return {Array} Block editor props. */ function useBlockEditorProps( post, template, mode ) { + const revisionBlocks = useRevisionBlocks(); const rootLevelPost = mode === 'template-locked' ? 'template' : 'post'; const [ postBlocks, onInput, onChange ] = useEntityBlockEditor( 'postType', @@ -116,6 +118,11 @@ function useBlockEditorProps( post, template, mode ) { return postBlocks; }, [ maybeNavigationBlocks, rootLevelPost, templateBlocks, postBlocks ] ); + // In revisions mode, use the revision blocks and disable editing. + if ( revisionBlocks !== null ) { + return [ revisionBlocks, noop, noop ]; + } + // Handle fallback to postBlocks outside of the above useMemo, to ensure // that constructed block templates that call `createBlock` are not generated // too frequently. This ensures that clientIds are stable. @@ -177,6 +184,8 @@ export const ExperimentalEditorProvider = withRegistryProvider( mode, defaultMode, postTypeEntities, + isInRevisionsMode, + currentRevisionId, } = useSelect( ( select ) => { const { @@ -184,6 +193,8 @@ export const ExperimentalEditorProvider = withRegistryProvider( getRenderingMode, __unstableIsEditorReady, getDefaultRenderingMode, + isRevisionsMode: _isRevisionsMode, + getCurrentRevisionId: _getCurrentRevisionId, } = unlock( select( editorStore ) ); const { getEntitiesConfig, getEntityRecordEdits } = select( coreStore ); @@ -224,6 +235,8 @@ export const ExperimentalEditorProvider = withRegistryProvider( post.type === 'wp_template' ? getEntitiesConfig( 'postType' ) : null, + isInRevisionsMode: _isRevisionsMode(), + currentRevisionId: _getCurrentRevisionId(), }; }, [ post.type, post.id, hasTemplate ] @@ -418,14 +431,19 @@ export const ExperimentalEditorProvider = withRegistryProvider( kind="postType" type={ post.type } id={ post.id } + revisionId={ currentRevisionId ?? undefined } > diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js index 1e42727c80d7df..0453955d5842b6 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -139,6 +139,7 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) { sectionRootClientId, deviceType, isNavigationOverlayContext, + isRevisionsMode, } = useSelect( ( select ) => { const { @@ -150,7 +151,9 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) { } = select( coreStore ); const { get } = select( preferencesStore ); const { getBlockTypes } = select( blocksStore ); - const { getDeviceType } = unlock( select( editorStore ) ); + const { getDeviceType, isRevisionsMode: _isRevisionsMode } = unlock( + select( editorStore ) + ); const { getBlocksByName, getBlockAttributes } = select( blockEditorStore ); const siteSettings = canUser( 'read', { @@ -218,6 +221,7 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) { postId )?.area === 'navigation-overlay' : false, + isRevisionsMode: _isRevisionsMode(), }; }, [ postType, postId, isLargeViewport, renderingMode ] @@ -399,8 +403,13 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) { [ isNavigationOverlayContextKey ]: isNavigationOverlayContext, }; + if ( isRevisionsMode ) { + blockEditorSettings.isPreviewMode = true; + } + return blockEditorSettings; }, [ + isRevisionsMode, allowedBlockTypes, allowRightClickOverrides, focusMode, diff --git a/packages/editor/src/components/provider/use-revision-blocks.js b/packages/editor/src/components/provider/use-revision-blocks.js new file mode 100644 index 00000000000000..b6729affa379ef --- /dev/null +++ b/packages/editor/src/components/provider/use-revision-blocks.js @@ -0,0 +1,105 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { useMemo, useRef } from '@wordpress/element'; +import { createBlock, parse } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { store as editorStore } from '../../store'; +import { unlock } from '../../lock-unlock'; +import { diffRevisionContent } from '../post-revisions-preview/block-diff'; +import { preserveClientIds } from '../post-revisions-preview/preserve-client-ids'; + +/** + * Hook that computes revision blocks when in revisions mode. + * + * Returns `null` when not in revisions mode, `[]` when loading, + * or the computed blocks array when ready. + * + * @return {Array|null} The revision blocks, or null if not in revisions mode. + */ +export function useRevisionBlocks() { + const { + isInRevisionsMode, + showDiff, + revision, + previousRevision, + postType, + } = useSelect( ( select ) => { + const { + isRevisionsMode, + isShowingRevisionDiff, + getCurrentRevision, + getPreviousRevision, + } = unlock( select( editorStore ) ); + const { getCurrentPostType } = select( editorStore ); + + const inRevisions = isRevisionsMode(); + return { + isInRevisionsMode: inRevisions, + showDiff: isShowingRevisionDiff(), + revision: inRevisions ? getCurrentRevision() : undefined, + previousRevision: inRevisions ? getPreviousRevision() : undefined, + postType: getCurrentPostType(), + }; + }, [] ); + + // Track previously rendered blocks to preserve clientIds between renders. + const previousBlocksRef = useRef( [] ); + + const blocks = useMemo( () => { + if ( ! isInRevisionsMode ) { + // Clear the ref when exiting revisions mode. + previousBlocksRef.current = []; + return null; + } + + // Revision not loaded yet. + if ( ! revision ) { + return []; + } + + const currentContent = revision?.content?.raw ?? ''; + + let parsedBlocks; + if ( showDiff ) { + const previousContent = previousRevision?.content?.raw || ''; + parsedBlocks = diffRevisionContent( + currentContent, + previousContent + ); + } else { + parsedBlocks = parse( currentContent ); + } + + if ( postType === 'wp_navigation' ) { + parsedBlocks = [ + createBlock( + 'core/navigation', + { templateLock: false }, + parsedBlocks + ), + ]; + } + + const blocksWithStableIds = preserveClientIds( + parsedBlocks, + previousBlocksRef.current + ); + previousBlocksRef.current = blocksWithStableIds; + + return blocksWithStableIds; + }, [ + isInRevisionsMode, + revision, + revision?.content?.raw, + previousRevision?.content?.raw, + postType, + showDiff, + ] ); + + return blocks; +} diff --git a/packages/editor/src/components/sidebar/header.js b/packages/editor/src/components/sidebar/header.js index 1a75c0a9020410..b67fd7acaceb6c 100644 --- a/packages/editor/src/components/sidebar/header.js +++ b/packages/editor/src/components/sidebar/header.js @@ -55,7 +55,7 @@ const SidebarHeader = ( _, ref ) => { > { documentLabel } - { ! isAttachment && ! isRevisionsMode && ( + { ! isAttachment && ( { const clientIdAfter = await headingBlock.getAttribute( 'data-block' ); expect( clientIdAfter ).toBe( clientIdBefore ); } ); + + // Regression test for https://github.com/WordPress/gutenberg/issues/75926 + // Global wp.data.select() calls should see blocks in revisions mode. + test( 'should expose blocks to global selectors in revisions mode', async ( { + editor, + page, + } ) => { + // Register a test block that renders its parent block name + // using the global wp.data.select() (not useSelect). + await page.evaluate( () => { + window.wp.blocks.registerBlockType( 'test/selectors-in-revisions', { + apiVersion: 3, + title: 'Test Selectors in Revisions', + edit: function Edit( { clientId } ) { + const [ parentName, setParentName ] = + window.wp.element.useState( 'none' ); + window.wp.element.useEffect( () => { + const parents = window.wp.data + .select( 'core/block-editor' ) + .getBlockParentsByBlockName( + clientId, + 'core/group' + ); + setParentName( + parents.length > 0 + ? 'has-group-parent' + : 'no-group-parent' + ); + }, [ clientId ] ); + return window.wp.element.createElement( + 'p', + window.wp.blockEditor.useBlockProps(), + parentName + ); + }, + save: () => null, + } ); + } ); + + // Insert a group with the test block inside. + await editor.insertBlock( { + name: 'core/group', + innerBlocks: [ { name: 'test/selectors-in-revisions' } ], + } ); + + // Save draft to create first revision. + await editor.saveDraft(); + + // Add a paragraph after the group to create a second revision. + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Extra paragraph' }, + } ); + await editor.saveDraft(); + + // Enter revisions mode. + await editor.openDocumentSettingsSidebar(); + const settingsSidebar = page.getByRole( 'region', { + name: 'Editor settings', + } ); + await settingsSidebar.getByRole( 'tab', { name: 'Post' } ).click(); + await settingsSidebar + .getByRole( 'button', { name: '2', exact: true } ) + .click(); + + // Wait for revisions mode. + await expect( + page.getByRole( 'button', { name: 'Restore' } ) + ).toBeVisible(); + + // The test block should see its group parent via global select(). + await expect( + editor.canvas.locator( '[data-type="test/selectors-in-revisions"]' ) + ).toHaveText( 'has-group-parent' ); + } ); } );