diff --git a/docs/reference-guides/data/data-core-editor.md b/docs/reference-guides/data/data-core-editor.md index 2c4185d3a998f..7531549f294d5 100644 --- a/docs/reference-guides/data/data-core-editor.md +++ b/docs/reference-guides/data/data-core-editor.md @@ -1415,7 +1415,6 @@ _Returns_ Returns an action used to set the rendering mode of the post editor. We support multiple rendering modes: -- `all`: This is the default mode. It renders the post editor with all the features available. If a template is provided, it's preferred over the post. - `post-only`: This mode extracts the post blocks from the template and renders only those. The idea is to allow the user to edit the post/page in isolation without the wrapping template. - `template-locked`: This mode renders both the template and the post blocks but the template blocks are locked and can't be edited. The post blocks are editable. diff --git a/packages/block-editor/src/components/block-switcher/index.js b/packages/block-editor/src/components/block-switcher/index.js index 8a8d1908f3854..2d75b80269efe 100644 --- a/packages/block-editor/src/components/block-switcher/index.js +++ b/packages/block-editor/src/components/block-switcher/index.js @@ -160,7 +160,7 @@ function BlockSwitcherDropdownMenuContents( { ); } -export const BlockSwitcher = ( { clientIds } ) => { +export const BlockSwitcher = ( { clientIds, disabled } ) => { const { canRemove, hasBlockStyles, @@ -229,8 +229,8 @@ export const BlockSwitcher = ( { clientIds } ) => { const blockSwitcherLabel = isSingleBlock ? blockTitle : __( 'Multiple blocks selected' ); - const hideDropdown = ! hasBlockStyles && ! canRemove; + const hideDropdown = disabled || ( ! hasBlockStyles && ! canRemove ); if ( hideDropdown ) { return ( diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index 591d705b2c325..8d28bd001670f 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -170,26 +170,29 @@ export function PrivateBlockToolbar( { { isUsingBindings && canBindBlock( blockName ) && ( ) } - { ( shouldShowVisualToolbar || isMultiToolbar ) && - isDefaultEditingMode && ( -
- - - { ! isMultiToolbar && ( - + + + { isDefaultEditingMode && ( + <> + { ! isMultiToolbar && ( + + ) } + - ) } - - -
- ) } + + ) } +
+ + ) } { shouldShowVisualToolbar && isMultiToolbar && ( diff --git a/packages/block-library/src/template-part/edit/index.js b/packages/block-library/src/template-part/edit/index.js index 34a60e2659bf0..26beaa8b6d928 100644 --- a/packages/block-library/src/template-part/edit/index.js +++ b/packages/block-library/src/template-part/edit/index.js @@ -11,8 +11,15 @@ import { useHasRecursion, InspectorControls, __experimentalBlockPatternsList as BlockPatternsList, + BlockControls, } from '@wordpress/block-editor'; -import { PanelBody, Spinner, Modal, MenuItem } from '@wordpress/components'; +import { + PanelBody, + Spinner, + Modal, + MenuItem, + ToolbarButton, +} from '@wordpress/components'; import { useAsyncList } from '@wordpress/compose'; import { __, sprintf } from '@wordpress/i18n'; import { store as coreStore } from '@wordpress/core-data'; @@ -104,11 +111,17 @@ export default function TemplatePartEdit( { const [ isTemplatePartSelectionOpen, setIsTemplatePartSelectionOpen ] = useState( false ); - const { isResolved, hasInnerBlocks, isMissing, area } = useSelect( + const { + isResolved, + hasInnerBlocks, + isMissing, + area, + onNavigateToEntityRecord, + } = useSelect( ( select ) => { const { getEditedEntityRecord, hasFinishedResolution } = select( coreStore ); - const { getBlockCount } = select( blockEditorStore ); + const { getBlockCount, getSettings } = select( blockEditorStore ); const getEntityArgs = [ 'postType', @@ -134,6 +147,8 @@ export default function TemplatePartEdit( { ( ! entityRecord || Object.keys( entityRecord ).length === 0 ), area: _area, + onNavigateToEntityRecord: + getSettings().onNavigateToEntityRecord, }; }, [ templatePartId, attributes.area, clientId ] @@ -217,6 +232,20 @@ export default function TemplatePartEdit( { return ( <> + { isEntityAvailable && onNavigateToEntityRecord && ( + + + onNavigateToEntityRecord( { + postId: templatePartId, + postType: 'wp_template_part', + } ) + } + > + { __( 'Edit' ) } + + + ) } { + const { getSettings } = select( blockEditorStore ); + return getSettings()?.supportsLayout; + }, [] ); + const [ defaultLayout ] = useSettings( 'layout' ); + if ( themeSupportsLayout ) { + return layout?.inherit ? defaultLayout || {} : layout; + } +} + export default function TemplatePartInnerBlocks( { postId: id, hasInnerBlocks, @@ -17,13 +43,6 @@ export default function TemplatePartInnerBlocks( { tagName: TagName, blockProps, } ) { - const themeSupportsLayout = useSelect( ( select ) => { - const { getSettings } = select( blockEditorStore ); - return getSettings()?.supportsLayout; - }, [] ); - const [ defaultLayout ] = useSettings( 'layout' ); - const usedLayout = layout?.inherit ? defaultLayout || {} : layout; - const [ blocks, onInput, onChange ] = useEntityBlockEditor( 'postType', 'wp_template_part', @@ -34,10 +53,8 @@ export default function TemplatePartInnerBlocks( { value: blocks, onInput, onChange, - renderAppender: hasInnerBlocks - ? undefined - : InnerBlocks.ButtonBlockAppender, - layout: themeSupportsLayout ? usedLayout : undefined, + renderAppender: useRenderAppender( hasInnerBlocks ), + layout: useLayout( layout ), } ); return ; diff --git a/packages/block-library/src/template-part/editor.scss b/packages/block-library/src/template-part/editor.scss index c60f93c1874b5..17f2e79212a79 100644 --- a/packages/block-library/src/template-part/editor.scss +++ b/packages/block-library/src/template-part/editor.scss @@ -25,9 +25,13 @@ .is-outline-mode .block-editor-block-list__block:not(.remove-outline).wp-block-template-part, .is-outline-mode .block-editor-block-list__block:not(.remove-outline).is-reusable { - &.is-highlighted, - &.is-selected { - box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-block-synced-color); + &.is-highlighted::after, + &.is-selected::after { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-block-synced-color); + } + + &.is-hovered::after { + box-shadow: 0 0 0 $border-width var(--wp-block-synced-color); } &.block-editor-block-list__block:not([contenteditable]):focus { @@ -40,3 +44,7 @@ } } } + +.is-outline-mode .block-editor-block-list__block:not(.remove-outline).wp-block-template-part.has-editable-outline::after { + border: none; +} diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js index fd44d3dae4ca4..9d6dfefa7ec6a 100644 --- a/packages/edit-post/src/editor.js +++ b/packages/edit-post/src/editor.js @@ -32,11 +32,14 @@ function Editor( { ...props } ) { const { - initialPost, currentPost, onNavigateToEntityRecord, onNavigateToPreviousEntityRecord, - } = useNavigateToEntityRecord( initialPostId, initialPostType ); + } = useNavigateToEntityRecord( + initialPostId, + initialPostType, + 'post-only' + ); const { post, template } = useSelect( ( select ) => { @@ -80,6 +83,13 @@ function Editor( { [ settings, onNavigateToEntityRecord, onNavigateToPreviousEntityRecord ] ); + const initialPost = useMemo( () => { + return { + type: initialPostType, + id: initialPostId, + }; + }, [ initialPostType, initialPostId ] ); + if ( ! post ) { return null; } diff --git a/packages/edit-post/src/hooks/use-navigate-to-entity-record.js b/packages/edit-post/src/hooks/use-navigate-to-entity-record.js index e891e7a3c5e6b..b8c39865ecee8 100644 --- a/packages/edit-post/src/hooks/use-navigate-to-entity-record.js +++ b/packages/edit-post/src/hooks/use-navigate-to-entity-record.js @@ -1,7 +1,9 @@ /** * WordPress dependencies */ -import { useCallback, useReducer, useMemo } from '@wordpress/element'; +import { useCallback, useReducer } from '@wordpress/element'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as editorStore } from '@wordpress/editor'; /** * A hook that records the 'entity' history in the post editor as a user @@ -11,20 +13,22 @@ import { useCallback, useReducer, useMemo } from '@wordpress/element'; * * Used to control displaying UI elements like the back button. * - * @param {number} initialPostId The post id of the post when the editor loaded. - * @param {string} initialPostType The post type of the post when the editor loaded. + * @param {number} initialPostId The post id of the post when the editor loaded. + * @param {string} initialPostType The post type of the post when the editor loaded. + * @param {string} defaultRenderingMode The rendering mode to switch to when navigating. * * @return {Object} An object containing the `currentPost` variable and * `onNavigateToEntityRecord` and `onNavigateToPreviousEntityRecord` functions. */ export default function useNavigateToEntityRecord( initialPostId, - initialPostType + initialPostType, + defaultRenderingMode ) { const [ postHistory, dispatch ] = useReducer( - ( historyState, { type, post } ) => { + ( historyState, { type, post, previousRenderingMode } ) => { if ( type === 'push' ) { - return [ ...historyState, post ]; + return [ ...historyState, { post, previousRenderingMode } ]; } if ( type === 'pop' ) { // Try to leave one item in the history. @@ -34,32 +38,41 @@ export default function useNavigateToEntityRecord( } return historyState; }, - [ { postId: initialPostId, postType: initialPostType } ] + [ + { + post: { postId: initialPostId, postType: initialPostType }, + }, + ] ); - const initialPost = useMemo( () => { - return { - type: initialPostType, - id: initialPostId, - }; - }, [ initialPostType, initialPostId ] ); + const { post, previousRenderingMode } = + postHistory[ postHistory.length - 1 ]; - const onNavigateToEntityRecord = useCallback( ( params ) => { - dispatch( { - type: 'push', - post: { postId: params.postId, postType: params.postType }, - } ); - }, [] ); + const { getRenderingMode } = useSelect( editorStore ); + const { setRenderingMode } = useDispatch( editorStore ); + + const onNavigateToEntityRecord = useCallback( + ( params ) => { + dispatch( { + type: 'push', + post: { postId: params.postId, postType: params.postType }, + // Save the current rendering mode so we can restore it when navigating back. + previousRenderingMode: getRenderingMode(), + } ); + setRenderingMode( defaultRenderingMode ); + }, + [ getRenderingMode, setRenderingMode, defaultRenderingMode ] + ); const onNavigateToPreviousEntityRecord = useCallback( () => { dispatch( { type: 'pop' } ); - }, [] ); - - const currentPost = postHistory[ postHistory.length - 1 ]; + if ( previousRenderingMode ) { + setRenderingMode( previousRenderingMode ); + } + }, [ setRenderingMode, previousRenderingMode ] ); return { - currentPost, - initialPost, + currentPost: post, onNavigateToEntityRecord, onNavigateToPreviousEntityRecord: postHistory.length > 1 diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-content.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-content.js index dd40bcaef9f70..18a742add41b2 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-content.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-content.js @@ -6,7 +6,6 @@ import { store as blockEditorStore, privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; -import { useMemo } from '@wordpress/element'; /** * Internal dependencies @@ -15,15 +14,16 @@ import { unlock } from '../../../lock-unlock'; const { BlockQuickNavigation } = unlock( blockEditorPrivateApis ); +const PAGE_CONTENT_BLOCKS = [ + 'core/post-content', + 'core/post-featured-image', + 'core/post-title', +]; + export default function PageContent() { - const clientIdsTree = useSelect( - ( select ) => - unlock( select( blockEditorStore ) ).getEnabledClientIdsTree(), - [] - ); - const clientIds = useMemo( - () => clientIdsTree.map( ( { clientId } ) => clientId ), - [ clientIdsTree ] - ); + const clientIds = useSelect( ( select ) => { + const { getBlocksByName } = select( blockEditorStore ); + return getBlocksByName( PAGE_CONTENT_BLOCKS ); + }, [] ); return ; } diff --git a/packages/edit-site/src/hooks/index.js b/packages/edit-site/src/hooks/index.js index 513634c55b8f0..185d069f78479 100644 --- a/packages/edit-site/src/hooks/index.js +++ b/packages/edit-site/src/hooks/index.js @@ -3,4 +3,3 @@ */ import './components'; import './push-changes-to-global-styles'; -import './template-part-edit'; diff --git a/packages/edit-site/src/hooks/template-part-edit.js b/packages/edit-site/src/hooks/template-part-edit.js deleted file mode 100644 index 1c5c47a68fbfb..0000000000000 --- a/packages/edit-site/src/hooks/template-part-edit.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { useSelect } from '@wordpress/data'; -import { BlockControls } from '@wordpress/block-editor'; -import { store as coreStore } from '@wordpress/core-data'; -import { ToolbarButton } from '@wordpress/components'; -import { addFilter } from '@wordpress/hooks'; -import { createHigherOrderComponent } from '@wordpress/compose'; -import { privateApis as routerPrivateApis } from '@wordpress/router'; - -/** - * Internal dependencies - */ -import { useLink } from '../components/routes/link'; -import { unlock } from '../lock-unlock'; -import { TEMPLATE_PART_POST_TYPE } from '../utils/constants'; - -const { useLocation } = unlock( routerPrivateApis ); - -function EditTemplatePartMenuItem( { attributes } ) { - const { theme, slug } = attributes; - const { params } = useLocation(); - const templatePart = useSelect( - ( select ) => { - const { getCurrentTheme, getEntityRecord } = select( coreStore ); - - return getEntityRecord( - 'postType', - TEMPLATE_PART_POST_TYPE, - // Ideally this should be an official public API. - `${ theme || getCurrentTheme()?.stylesheet }//${ slug }` - ); - }, - [ theme, slug ] - ); - - const linkProps = useLink( - { - postId: templatePart?.id, - postType: templatePart?.type, - canvas: 'edit', - }, - { - fromTemplateId: params.postId || templatePart?.id, - } - ); - - if ( ! templatePart ) { - return null; - } - - return ( - { - linkProps.onClick( event ); - } } - > - { __( 'Edit' ) } - - ); -} - -export const withEditBlockControls = createHigherOrderComponent( - ( BlockEdit ) => ( props ) => { - const { attributes, name } = props; - const isDisplayed = name === 'core/template-part' && attributes.slug; - - return ( - <> - - { isDisplayed && ( - - - - ) } - - ); - }, - 'withEditBlockControls' -); - -addFilter( - 'editor.BlockEdit', - 'core/edit-site/template-part-edit-button', - withEditBlockControls -); diff --git a/packages/editor/src/components/provider/disable-non-page-content-blocks.js b/packages/editor/src/components/provider/disable-non-page-content-blocks.js index fd4722ebe40f4..bfcee3365d412 100644 --- a/packages/editor/src/components/provider/disable-non-page-content-blocks.js +++ b/packages/editor/src/components/provider/disable-non-page-content-blocks.js @@ -5,49 +5,68 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { useEffect } from '@wordpress/element'; -const PAGE_CONTENT_BLOCKS = [ - 'core/post-title', - 'core/post-featured-image', +const CONTENT_ONLY_BLOCKS = [ 'core/post-content', + 'core/post-featured-image', + 'core/post-title', + 'core/template-part', ]; -function useDisableNonPageContentBlocks() { - const contentIds = useSelect( ( select ) => { +/** + * Component that when rendered, makes it so that the site editor allows only + * page content to be edited. + */ +export default function DisableNonPageContentBlocks() { + const contentOnlyIds = useSelect( ( select ) => { const { getBlocksByName, getBlockParents, getBlockName } = select( blockEditorStore ); - return getBlocksByName( PAGE_CONTENT_BLOCKS ).filter( ( clientId ) => + return getBlocksByName( CONTENT_ONLY_BLOCKS ).filter( ( clientId ) => getBlockParents( clientId ).every( ( parentClientId ) => { const parentBlockName = getBlockName( parentClientId ); return ( + // Ignore descendents of the query block. parentBlockName !== 'core/query' && - ! PAGE_CONTENT_BLOCKS.includes( parentBlockName ) + // Enable only the top-most block. + ! CONTENT_ONLY_BLOCKS.includes( parentBlockName ) ); } ) ); }, [] ); + const disabledIds = useSelect( ( select ) => { + const { getBlocksByName, getBlockOrder } = select( blockEditorStore ); + return getBlocksByName( [ 'core/template-part' ] ).flatMap( + ( clientId ) => getBlockOrder( clientId ) + ); + }, [] ); + const { setBlockEditingMode, unsetBlockEditingMode } = useDispatch( blockEditorStore ); useEffect( () => { - setBlockEditingMode( '', 'disabled' ); // Disable editing at the root level. - - for ( const contentId of contentIds ) { - setBlockEditingMode( contentId, 'contentOnly' ); // Re-enable each content block. + setBlockEditingMode( '', 'disabled' ); + for ( const clientId of contentOnlyIds ) { + setBlockEditingMode( clientId, 'contentOnly' ); + } + for ( const clientId of disabledIds ) { + setBlockEditingMode( clientId, 'disabled' ); } + return () => { unsetBlockEditingMode( '' ); - for ( const contentId of contentIds ) { - unsetBlockEditingMode( contentId ); + for ( const clientId of contentOnlyIds ) { + unsetBlockEditingMode( clientId ); + } + for ( const clientId of disabledIds ) { + unsetBlockEditingMode( clientId ); } }; - }, [ contentIds, setBlockEditingMode, unsetBlockEditingMode ] ); -} + }, [ + contentOnlyIds, + disabledIds, + setBlockEditingMode, + unsetBlockEditingMode, + ] ); -/** - * Component that when rendered, makes it so that the site editor allows only - * page content to be edited. - */ -export default function DisableNonPageContentBlocks() { - useDisableNonPageContentBlocks(); + return null; } diff --git a/packages/editor/src/components/provider/test/disable-non-page-content-blocks.js b/packages/editor/src/components/provider/test/disable-non-page-content-blocks.js index d76530828a799..d7c051a0959a3 100644 --- a/packages/editor/src/components/provider/test/disable-non-page-content-blocks.js +++ b/packages/editor/src/components/provider/test/disable-non-page-content-blocks.js @@ -55,6 +55,13 @@ describe( 'DisableNonPageContentBlocks', () => { getBlockName( state, clientId ) { return testBlocks[ clientId ]; }, + getBlockOrder( state, rootClientId ) { + return Object.keys( testBlocks ).filter( + ( clientId ) => + clientId.startsWith( rootClientId ) && + clientId !== rootClientId + ); + }, }, actions: { setBlockEditingMode, @@ -69,22 +76,36 @@ describe( 'DisableNonPageContentBlocks', () => { ); - expect( setBlockEditingMode.mock.calls ).toEqual( [ - [ '', 'disabled' ], // root - [ '10', 'contentOnly' ], // post-title - [ '11', 'contentOnly' ], // post-featured-image - [ '12', 'contentOnly' ], // post-content - // NOT the post-featured-image nested within post-content - // NOT any of the content blocks within query - ] ); + expect( setBlockEditingMode.mock.calls ).toEqual( + expect.arrayContaining( [ + [ '', 'disabled' ], // root + [ '0', 'contentOnly' ], // core/template-part + [ '00', 'disabled' ], // core/site-title + [ '01', 'disabled' ], // core/navigation + [ '10', 'contentOnly' ], // post-title + [ '11', 'contentOnly' ], // post-featured-image + [ '12', 'contentOnly' ], // post-content + [ '3', 'contentOnly' ], // core/template-part + [ '30', 'disabled' ], // core/paragraph + // NOT the post-featured-image nested within post-content + // NOT any of the content blocks within query + ] ) + ); unmount(); - expect( unsetBlockEditingMode.mock.calls ).toEqual( [ - [ '' ], // root - [ '10' ], // post-title - [ '11' ], // post-featured-image - [ '12' ], // post-content - ] ); + expect( unsetBlockEditingMode.mock.calls ).toEqual( + expect.arrayContaining( [ + [ '' ], // root + [ '0' ], // core/template-part + [ '00' ], // core/site-title + [ '01' ], // core/navigation + [ '10' ], // post-title + [ '11' ], // post-featured-image + [ '12' ], // post-content + [ '3' ], // core/template-part + [ '30' ], // core/paragraph + ] ) + ); } ); } ); diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 33119aa2e75e6..91963c491885d 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -583,7 +583,6 @@ export function updateEditorSettings( settings ) { /** * Returns an action used to set the rendering mode of the post editor. We support multiple rendering modes: * - * - `all`: This is the default mode. It renders the post editor with all the features available. If a template is provided, it's preferred over the post. * - `post-only`: This mode extracts the post blocks from the template and renders only those. The idea is to allow the user to edit the post/page in isolation without the wrapping template. * - `template-locked`: This mode renders both the template and the post blocks but the template blocks are locked and can't be edited. The post blocks are editable. * diff --git a/test/e2e/specs/site-editor/pages.spec.js b/test/e2e/specs/site-editor/pages.spec.js index 717fcfe0a39aa..98a46d90c6797 100644 --- a/test/e2e/specs/site-editor/pages.spec.js +++ b/test/e2e/specs/site-editor/pages.spec.js @@ -101,18 +101,6 @@ test.describe( 'Pages', () => { /* * Test create page.Test creating a new page and editing the template. */ - // Selecting a block in the template should display a notice. - await editor.canvas - .getByRole( 'document', { - name: 'Block: Site Title', - } ) - .click( { force: true } ); - await expect( - page.locator( - 'role=button[name="Dismiss this notice"i] >> text="Edit your template to edit this block."' - ) - ).toBeVisible(); - // Switch to template editing focus. await editor.openDocumentSettingsSidebar(); await page