diff --git a/docs/reference-guides/data/data-core-edit-site.md b/docs/reference-guides/data/data-core-edit-site.md index 0dad03bbc8ca2..523bb8d2bbff5 100644 --- a/docs/reference-guides/data/data-core-edit-site.md +++ b/docs/reference-guides/data/data-core-edit-site.md @@ -131,6 +131,18 @@ _Returns_ - `Object`: Settings. +### hasPageContentLock + +Whether or not the editor is locked so that only page content can be edited. + +_Parameters_ + +- _state_ `Object`: Global application state. + +_Returns_ + +- `boolean`: Whether or not the editor is locked. + ### isFeatureActive > **Deprecated** @@ -174,6 +186,22 @@ _Returns_ > **Deprecated** +### isPage + +Whether or not the editor has a page loaded into it. + +_Related_ + +- setPage + +_Parameters_ + +- _state_ `Object`: Global application state. + +_Returns_ + +- `boolean`: Whether or not the editor has a page loaded into it. + ### isSaveViewOpened Returns the current opened/closed state of the save panel. @@ -252,6 +280,14 @@ _Returns_ - `number`: The resolved template ID for the page route. +### setHasPageContentLock + +Sets whether or not the editor is locked so that only page content can be edited. + +_Parameters_ + +- _hasPageContentLock_ `boolean`: True to enable lock, false to disable. + ### setHomeTemplateId > **Deprecated** diff --git a/packages/block-library/src/navigation/editor.scss b/packages/block-library/src/navigation/editor.scss index 949a7c773eb6e..4a6a6fe6b4395 100644 --- a/packages/block-library/src/navigation/editor.scss +++ b/packages/block-library/src/navigation/editor.scss @@ -508,20 +508,22 @@ body.editor-styles-wrapper .wp-block-navigation__responsive-container.is-menu-op // so focus is applied naturally on the block container. // It's important the right container has focus, otherwise you can't press // "Delete" to remove the block. -.wp-block-navigation__responsive-container, -.wp-block-navigation__responsive-close { - @include break-small() { - pointer-events: none; - - .wp-block-navigation__responsive-container-close, - .block-editor-block-list__layout * { - pointer-events: all; +.wp-block-navigation:not(.is-editing-disabled) { + .wp-block-navigation__responsive-container, + .wp-block-navigation__responsive-close { + @include break-small() { + pointer-events: none; + + .wp-block-navigation__responsive-container-close, + .block-editor-block-list__layout * { + pointer-events: all; + } } - } - // Page List items should remain inert. - .wp-block-pages-list__item__link { - pointer-events: none; + // Page List items should remain inert. + .wp-block-pages-list__item__link { + pointer-events: none; + } } } diff --git a/packages/block-library/src/post-title/edit.js b/packages/block-library/src/post-title/edit.js index d7e8e2788ebe9..96fd62178b974 100644 --- a/packages/block-library/src/post-title/edit.js +++ b/packages/block-library/src/post-title/edit.js @@ -12,6 +12,7 @@ import { InspectorControls, useBlockProps, PlainText, + privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { ToggleControl, TextControl, PanelBody } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; @@ -23,6 +24,9 @@ import { useEntityProp } from '@wordpress/core-data'; */ import HeadingLevelDropdown from '../heading/heading-level-dropdown'; import { useCanEditEntity } from '../utils/hooks'; +import { unlock } from '../private-apis'; + +const { useBlockEditingMode } = unlock( blockEditorPrivateApis ); export default function PostTitleEdit( { attributes: { level, textAlign, isLink, rel, linkTarget }, @@ -58,6 +62,7 @@ export default function PostTitleEdit( { [ `has-text-align-${ textAlign }` ]: textAlign, } ), } ); + const blockEditingMode = useBlockEditingMode(); let titleElement = { __( 'Title' ) }; @@ -112,20 +117,22 @@ export default function PostTitleEdit( { return ( <> - - - setAttributes( { level: newLevel } ) - } - /> - { - setAttributes( { textAlign: nextAlign } ); - } } - /> - + { blockEditingMode === 'default' && ( + + + setAttributes( { level: newLevel } ) + } + /> + { + setAttributes( { textAlign: nextAlign } ); + } } + /> + + ) } { await enterEditMode(); // Insert a new paragraph right under the first one. - await firstParagraph.focus(); + await firstParagraph.click(); // Once to select the block overlay. + await firstParagraph.click(); // Once again to select the paragraph. await insertBlock( 'Paragraph' ); // Start tracing. diff --git a/packages/e2e-tests/specs/site-editor/settings-sidebar.test.js b/packages/e2e-tests/specs/site-editor/settings-sidebar.test.js index 88bf954e86ce2..ae28019cf0d99 100644 --- a/packages/e2e-tests/specs/site-editor/settings-sidebar.test.js +++ b/packages/e2e-tests/specs/site-editor/settings-sidebar.test.js @@ -27,11 +27,11 @@ async function getActiveTabLabel() { async function getTemplateCard() { return { title: await page.$eval( - '.edit-site-template-card__title', + '.edit-site-sidebar-card__title', ( element ) => element.innerText ), description: await page.$eval( - '.edit-site-template-card__description', + '.edit-site-sidebar-card__description', ( element ) => element.innerText ), }; diff --git a/packages/edit-site/src/components/block-editor/index.js b/packages/edit-site/src/components/block-editor/index.js index cc5e7c8d9254d..a2409b3f1baa2 100644 --- a/packages/edit-site/src/components/block-editor/index.js +++ b/packages/edit-site/src/components/block-editor/index.js @@ -38,6 +38,10 @@ import ResizableEditor from './resizable-editor'; import EditorCanvas from './editor-canvas'; import { unlock } from '../../private-apis'; import EditorCanvasContainer from '../editor-canvas-container'; +import { + PageContentLock, + usePageContentLockNotifications, +} from '../page-content-lock'; const { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis ); @@ -49,20 +53,25 @@ const LAYOUT = { export default function BlockEditor() { const { setIsInserterOpened } = useDispatch( editSiteStore ); - const { storedSettings, templateType, canvasMode } = useSelect( - ( select ) => { - const { getSettings, getEditedPostType, getCanvasMode } = unlock( - select( editSiteStore ) - ); - - return { - storedSettings: getSettings( setIsInserterOpened ), - templateType: getEditedPostType(), - canvasMode: getCanvasMode(), - }; - }, - [ setIsInserterOpened ] - ); + const { storedSettings, templateType, canvasMode, hasPageContentLock } = + useSelect( + ( select ) => { + const { + getSettings, + getEditedPostType, + getCanvasMode, + hasPageContentLock: _hasPageContentLock, + } = unlock( select( editSiteStore ) ); + + return { + storedSettings: getSettings( setIsInserterOpened ), + templateType: getEditedPostType(), + canvasMode: getCanvasMode(), + hasPageContentLock: _hasPageContentLock(), + }; + }, + [ setIsInserterOpened ] + ); const settingsBlockPatterns = storedSettings.__experimentalAdditionalBlockPatterns ?? // WP 6.0 @@ -137,6 +146,7 @@ export default function BlockEditor() { contentRef, useClipboardHandler(), useTypingObserver(), + usePageContentLockNotifications(), ] ); const isMobileViewport = useViewportMatch( 'small', '<' ); const { clearSelectedBlock } = useDispatch( blockEditorStore ); @@ -162,6 +172,7 @@ export default function BlockEditor() { onChange={ onChange } useSubRegistry={ false } > + { hasPageContentLock && } diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index 72a8d41fb22f0..f70c6abd787d4 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -37,7 +37,6 @@ import WelcomeGuide from '../welcome-guide'; import StartTemplateOptions from '../start-template-options'; import { store as editSiteStore } from '../../store'; import { GlobalStylesRenderer } from '../global-styles-renderer'; - import useTitle from '../routes/use-title'; import CanvasSpinner from '../canvas-spinner'; import { unlock } from '../../private-apis'; @@ -74,6 +73,7 @@ export default function Editor( { isLoading } ) { isListViewOpen, showIconLabels, showBlockBreadcrumbs, + hasPageContentLock, } = useSelect( ( select ) => { const { getEditedPostContext, @@ -81,6 +81,7 @@ export default function Editor( { isLoading } ) { getCanvasMode, isInserterOpened, isListViewOpened, + hasPageContentLock: _hasPageContentLock, } = unlock( select( editSiteStore ) ); const { __unstableGetEditorMode } = select( blockEditorStore ); const { getActiveComplementaryArea } = select( interfaceStore ); @@ -105,6 +106,7 @@ export default function Editor( { isLoading } ) { 'core/edit-site', 'showBlockBreadcrumbs' ), + hasPageContentLock: _hasPageContentLock(), }; }, [] ); const { setEditedPostContext } = useDispatch( editSiteStore ); @@ -122,9 +124,10 @@ export default function Editor( { isLoading } ) { const secondarySidebarLabel = isListViewOpen ? __( 'List View' ) : __( 'Block Library' ); - const blockContext = useMemo( - () => ( { - ...context, + const blockContext = useMemo( () => { + const { postType, postId, ...nonPostFields } = context ?? {}; + return { + ...( hasPageContentLock ? context : nonPostFields ), queryContext: [ context?.queryContext || { page: 1 }, ( newQueryContext ) => @@ -136,9 +139,8 @@ export default function Editor( { isLoading } ) { }, } ), ], - } ), - [ context, setEditedPostContext ] - ); + }; + }, [ hasPageContentLock, context, setEditedPostContext ] ); let title; if ( hasLoadedPost ) { @@ -227,7 +229,11 @@ export default function Editor( { isLoading } ) { footer={ shouldShowBlockBreakcrumbs && ( ) } diff --git a/packages/edit-site/src/components/header-edit-mode/document-actions/index.js b/packages/edit-site/src/components/header-edit-mode/document-actions/index.js index 94f8358fda993..a23c9c3595d32 100644 --- a/packages/edit-site/src/components/header-edit-mode/document-actions/index.js +++ b/packages/edit-site/src/components/header-edit-mode/document-actions/index.js @@ -1,8 +1,13 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ import { sprintf, __ } from '@wordpress/i18n'; -import { useDispatch } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; import { Button, VisuallyHidden, @@ -11,6 +16,11 @@ import { } from '@wordpress/components'; import { BlockIcon } from '@wordpress/block-editor'; import { privateApis as commandsPrivateApis } from '@wordpress/commands'; +import { + chevronLeftSmall as chevronLeftSmallIcon, + page as pageIcon, +} from '@wordpress/icons'; +import { useEntityRecord } from '@wordpress/core-data'; import { displayShortcut } from '@wordpress/keycodes'; /** @@ -18,19 +28,62 @@ import { displayShortcut } from '@wordpress/keycodes'; */ import useEditedEntityRecord from '../../use-edited-entity-record'; import { unlock } from '../../../private-apis'; +import { store as editSiteStore } from '../../../store'; const { store: commandsStore } = unlock( commandsPrivateApis ); export default function DocumentActions() { - const { open: openCommandCenter } = useDispatch( commandsStore ); + const isPage = useSelect( ( select ) => select( editSiteStore ).isPage() ); + return isPage ? : ; +} + +function PageDocumentActions() { + const { hasPageContentLock, context } = useSelect( + ( select ) => ( { + hasPageContentLock: select( editSiteStore ).hasPageContentLock(), + context: select( editSiteStore ).getEditedPostContext(), + } ), + [] + ); + + const { hasResolved, editedRecord } = useEntityRecord( + 'postType', + context.postType, + context.postId + ); + + const { setHasPageContentLock } = useDispatch( editSiteStore ); + + if ( ! hasResolved ) { + return null; + } + + if ( ! editedRecord ) { + return ( + + { __( 'Document not found' ) } + + ); + } + + return hasPageContentLock ? ( + + { editedRecord.title } + + ) : ( + setHasPageContentLock( true ) } + /> + ); +} + +function TemplateDocumentActions( { onBack } ) { const { isLoaded, record, getTitle, icon } = useEditedEntityRecord(); - // Return a simple loading indicator until we have information to show. if ( ! isLoaded ) { return null; } - // Return feedback that the template does not seem to exist. if ( ! record ) { return ( @@ -45,31 +98,58 @@ export default function DocumentActions() { : __( 'template' ); return ( - openCommandCenter() } - > - - + + { sprintf( + /* translators: %s: the entity being edited, like "template"*/ + __( 'Editing %s: ' ), + entityLabel + ) } + + { getTitle() } + + ); +} + +function BaseDocumentActions( { icon, children, onBack, isPage = false } ) { + const { open: openCommandCenter } = useDispatch( commandsStore ); + return ( + + { onBack && ( + { + event.stopPropagation(); + onBack(); + } } + > + { __( 'Back' ) } + + ) } + openCommandCenter() } > - - - - { sprintf( - /* translators: %s: the entity being edited, like "template"*/ - __( 'Editing %s: ' ), - entityLabel - ) } - - { getTitle() } - - - - { displayShortcut.primary( 'k' ) } - - + + + + { children } + + + + { displayShortcut.primary( 'k' ) } + + + ); } diff --git a/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss b/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss index 247b901975fd8..8bd3475de6956 100644 --- a/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss +++ b/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss @@ -1,10 +1,7 @@ .edit-site-document-actions { - display: flex; - align-items: center; - gap: $grid-unit; + display: grid; + grid-template-columns: 1fr 2fr 1fr; height: $button-size; - padding: $grid-unit; - justify-content: space-between; // Flex items will, by default, refuse to shrink below a minimum // intrinsic width. In order to shrink this flexbox item, and // subsequently truncate child text, we set an explicit min-width. @@ -20,29 +17,46 @@ } } +.edit-site-document-actions__command { + grid-column: 1 / -1; + display: grid; + grid-template-columns: 1fr 2fr 1fr; + grid-row: 1; +} + + .edit-site-document-actions__title { flex-grow: 1; color: var(--wp-block-synced-color); overflow: hidden; + grid-column: 2 / 3; + &.is-page { + color: $gray-800; + h1 { + color: $gray-800; + } + } h1 { - color: var(--wp-block-synced-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + color: var(--wp-block-synced-color); } } .edit-site-document-actions__shortcut { - flex-shrink: 0; color: $gray-700; - width: #{$grid-unit * 4.5}; + text-align: right; &:hover { color: $gray-700; } } -.edit-site-document-actions__left { +.edit-site-document-actions__back { min-width: $button-size; flex-shrink: 0; + grid-column: 1 / 2; + grid-row: 1; + z-index: 1; } diff --git a/packages/edit-site/src/components/page-content-lock/constants.js b/packages/edit-site/src/components/page-content-lock/constants.js new file mode 100644 index 0000000000000..668fe8af00d69 --- /dev/null +++ b/packages/edit-site/src/components/page-content-lock/constants.js @@ -0,0 +1,5 @@ +export const CONTENT_BLOCK_TYPES = [ + 'core/post-title', + 'core/post-featured-image', + 'core/post-content', +]; diff --git a/packages/edit-site/src/components/page-content-lock/index.js b/packages/edit-site/src/components/page-content-lock/index.js new file mode 100644 index 0000000000000..83d096fb39f5d --- /dev/null +++ b/packages/edit-site/src/components/page-content-lock/index.js @@ -0,0 +1,14 @@ +/** + * Internal dependencies + */ +import { useDisableNonContentBlocks } from './use-disable-non-content-blocks'; + +/** + * Component that when rendered, locks the site editor so that only page content + * can be edited. + */ +export function PageContentLock() { + useDisableNonContentBlocks(); +} + +export { usePageContentLockNotifications } from './use-page-content-lock-notifications'; diff --git a/packages/edit-site/src/components/page-content-lock/use-disable-non-content-blocks.js b/packages/edit-site/src/components/page-content-lock/use-disable-non-content-blocks.js new file mode 100644 index 0000000000000..ce198909877f6 --- /dev/null +++ b/packages/edit-site/src/components/page-content-lock/use-disable-non-content-blocks.js @@ -0,0 +1,44 @@ +/** + * WordPress dependencies + */ +import { createHigherOrderComponent } from '@wordpress/compose'; +import { addFilter, removeFilter } from '@wordpress/hooks'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +import { useEffect } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { unlock } from '../../private-apis'; +import { CONTENT_BLOCK_TYPES } from './constants'; + +const { useBlockEditingMode } = unlock( blockEditorPrivateApis ); + +/** + * Disables non-content blocks using the `useBlockEditingMode` hook. + */ +export function useDisableNonContentBlocks() { + useBlockEditingMode( 'disabled' ); + useEffect( () => { + addFilter( + 'editor.BlockEdit', + 'core/edit-site/disable-non-content-blocks', + withDisableNonContentBlocks + ); + return () => + removeFilter( + 'editor.BlockEdit', + 'core/edit-site/disable-non-content-blocks' + ); + }, [] ); +} + +const withDisableNonContentBlocks = createHigherOrderComponent( + ( BlockEdit ) => ( props ) => { + const isContent = CONTENT_BLOCK_TYPES.includes( props.name ); + const mode = isContent ? 'contentOnly' : undefined; + useBlockEditingMode( mode ); + return ; + }, + 'withBlockEditingMode' +); diff --git a/packages/edit-site/src/components/page-content-lock/use-page-content-lock-notifications.js b/packages/edit-site/src/components/page-content-lock/use-page-content-lock-notifications.js new file mode 100644 index 0000000000000..2a800317a33a9 --- /dev/null +++ b/packages/edit-site/src/components/page-content-lock/use-page-content-lock-notifications.js @@ -0,0 +1,128 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { useEffect, useRef } from '@wordpress/element'; +import { store as noticesStore } from '@wordpress/notices'; +import { __ } from '@wordpress/i18n'; +import { useRefEffect } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import { store as editSiteStore } from '../../store'; + +/** + * Hook that displays notifications that guide the user towards using the + * content vs. template editing modes. + * + * @return {import('react').RefObject} Ref which should be passed + * (using useMergeRefs()) to + * the editor iframe canvas. + */ +export function usePageContentLockNotifications() { + const ref = useEditTemplateNotification(); + useBackToPageNotification(); + return ref; +} + +/** + * Hook that displays a 'Edit your template to edit this block' notification + * when the user is focusing on editing page content and clicks on a locked + * template block. + * + * @return {import('react').RefObject} Ref which should be passed + * (using useMergeRefs()) to + * the editor iframe canvas. + */ +function useEditTemplateNotification() { + const hasPageContentLock = useSelect( + ( select ) => select( editSiteStore ).hasPageContentLock(), + [] + ); + + const alreadySeen = useRef( false ); + + const { createInfoNotice } = useDispatch( noticesStore ); + const { setHasPageContentLock } = useDispatch( editSiteStore ); + + return useRefEffect( + ( node ) => { + const handleClick = ( event ) => { + if ( + ! alreadySeen.current && + hasPageContentLock && + event.target.classList.contains( 'is-root-container' ) + ) { + createInfoNotice( + __( 'Edit your template to edit this block' ), + { + isDismissible: true, + type: 'snackbar', + actions: [ + { + label: __( 'Edit template' ), + onClick: () => + setHasPageContentLock( false ), + }, + ], + } + ); + alreadySeen.current = true; + } + }; + node.addEventListener( 'click', handleClick ); + return () => node.removeEventListener( 'click', handleClick ); + }, + [ + hasPageContentLock, + alreadySeen, + createInfoNotice, + setHasPageContentLock, + ] + ); +} + +/** + * Hook that displays a 'You are editing a template' notification when the user + * switches from focusing on editing page content to editing a template. + */ +function useBackToPageNotification() { + const hasPageContentLock = useSelect( + ( select ) => select( editSiteStore ).hasPageContentLock(), + [] + ); + + const alreadySeen = useRef( false ); + const prevHasPageContentLock = useRef( false ); + + const { createInfoNotice } = useDispatch( noticesStore ); + const { setHasPageContentLock } = useDispatch( editSiteStore ); + + useEffect( () => { + if ( + ! alreadySeen.current && + prevHasPageContentLock.current && + ! hasPageContentLock + ) { + createInfoNotice( __( 'You are editing a template' ), { + isDismissible: true, + type: 'snackbar', + actions: [ + { + label: __( 'Back to page' ), + onClick: () => setHasPageContentLock( true ), + }, + ], + } ); + alreadySeen.current = true; + } + prevHasPageContentLock.current = hasPageContentLock; + }, [ + alreadySeen, + prevHasPageContentLock, + hasPageContentLock, + createInfoNotice, + setHasPageContentLock, + ] ); +} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/index.js b/packages/edit-site/src/components/sidebar-edit-mode/index.js index 5086981f87144..78ada88a4d5fa 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { createSlotFill, PanelBody, PanelRow } from '@wordpress/components'; +import { createSlotFill } from '@wordpress/components'; import { isRTL, __ } from '@wordpress/i18n'; import { drawerLeft, drawerRight } from '@wordpress/icons'; import { useEffect } from '@wordpress/element'; @@ -16,8 +16,8 @@ import DefaultSidebar from './default-sidebar'; import GlobalStylesSidebar from './global-styles-sidebar'; import { STORE_NAME } from '../../store/constants'; import SettingsHeader from './settings-header'; -import LastRevision from './template-revisions'; -import TemplateCard from './template-card'; +import PagePanels from './page-panels'; +import TemplatePanel from './template-panel'; import PluginTemplateSettingPanel from '../plugin-template-setting-panel'; import { SIDEBAR_BLOCK, SIDEBAR_TEMPLATE } from './constants'; import { store as editSiteStore } from '../../store'; @@ -33,6 +33,7 @@ export function SidebarComplementaryAreaFills() { isEditorSidebarOpened, hasBlockSelection, supportsGlobalStyles, + hasPageContentLock, } = useSelect( ( select ) => { const _sidebar = select( interfaceStore ).getActiveComplementaryArea( STORE_NAME ); @@ -47,18 +48,23 @@ export function SidebarComplementaryAreaFills() { hasBlockSelection: !! select( blockEditorStore ).getBlockSelectionStart(), supportsGlobalStyles: ! settings?.supportsTemplatePartsMode, + hasPageContentLock: select( editSiteStore ).hasPageContentLock(), }; }, [] ); const { enableComplementaryArea } = useDispatch( interfaceStore ); useEffect( () => { - if ( ! isEditorSidebarOpened ) return; + // Don't automatically switch tab when the sidebar is closed or when we + // are focused on page content. + if ( ! isEditorSidebarOpened || hasPageContentLock ) { + return; + } if ( hasBlockSelection ) { enableComplementaryArea( STORE_NAME, SIDEBAR_BLOCK ); } else { enableComplementaryArea( STORE_NAME, SIDEBAR_TEMPLATE ); } - }, [ hasBlockSelection, isEditorSidebarOpened ] ); + }, [ hasBlockSelection, isEditorSidebarOpened, hasPageContentLock ] ); let sidebarName = sidebar; if ( ! isEditorSidebarOpened ) { @@ -77,15 +83,11 @@ export function SidebarComplementaryAreaFills() { > { sidebarName === SIDEBAR_TEMPLATE && ( <> - - - - - - + { hasPageContentLock ? ( + + ) : ( + + ) } > ) } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/content-blocks-list.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/content-blocks-list.js new file mode 100644 index 0000000000000..9035c5677f91a --- /dev/null +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/content-blocks-list.js @@ -0,0 +1,77 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { + Button, + __experimentalVStack as VStack, + __experimentalHStack as HStack, + FlexItem, +} from '@wordpress/components'; +import { getBlockType, __experimentalGetBlockLabel } from '@wordpress/blocks'; +import { store as blockEditorStore, BlockIcon } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import { CONTENT_BLOCK_TYPES } from '../../page-content-lock/constants'; + +// TODO: This overlaps a lot with BlockInspectorLockedBlocks in +// @wordpress/block-editor. DRY them into a single component. +export default function ContentBlocksList() { + const contentBlocks = useSelect( ( select ) => { + const { + getClientIdsWithDescendants, + getBlockName, + getBlock, + isBlockSelected, + hasSelectedInnerBlock, + } = select( blockEditorStore ); + return getClientIdsWithDescendants().flatMap( ( clientId ) => { + const blockName = getBlockName( clientId ); + if ( ! CONTENT_BLOCK_TYPES.includes( blockName ) ) { + return []; + } + return [ + { + block: getBlock( clientId ), + isSelected: + isBlockSelected( clientId ) || + hasSelectedInnerBlock( clientId, /* deep: */ true ), + }, + ]; + } ); + }, [] ); + + const { selectBlock } = useDispatch( blockEditorStore ); + + if ( ! contentBlocks.length ) { + return null; + } + + return ( + + { contentBlocks.map( ( { block, isSelected } ) => { + const blockType = getBlockType( block.name ); + return ( + selectBlock( block.clientId ) } + > + + + + { __experimentalGetBlockLabel( + blockType, + block.attributes, + 'list-view' + ) } + + + + ); + } ) } + + ); +} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js new file mode 100644 index 0000000000000..c913a689d54dc --- /dev/null +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js @@ -0,0 +1,89 @@ +/** + * WordPress dependencies + */ +import { + PanelBody, + __experimentalVStack as VStack, + Button, +} from '@wordpress/components'; +import { page as pageIcon } from '@wordpress/icons'; +import { __, sprintf } from '@wordpress/i18n'; +import { humanTimeDiff } from '@wordpress/date'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { useEntityRecord } from '@wordpress/core-data'; +import { BlockContextProvider, BlockPreview } from '@wordpress/block-editor'; +import { useMemo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { store as editSiteStore } from '../../../store'; +import useEditedEntityRecord from '../../use-edited-entity-record'; +import SidebarCard from '../sidebar-card'; +import ContentBlocksList from './content-blocks-list'; + +export default function PagePanels() { + const context = useSelect( + ( select ) => select( editSiteStore ).getEditedPostContext(), + [] + ); + + const { hasResolved: hasPageResolved, editedRecord: page } = + useEntityRecord( 'postType', context.postType, context.postId ); + + const { + isLoaded: isTemplateLoaded, + getTitle: getTemplateTitle, + record: template, + } = useEditedEntityRecord(); + + const { setHasPageContentLock } = useDispatch( editSiteStore ); + + const blockContext = useMemo( + () => ( { ...context, postType: null, postId: null } ), + [ context ] + ); + + if ( ! hasPageResolved || ! isTemplateLoaded ) { + return null; + } + + return ( + <> + + + + + + + + + { getTemplateTitle() } + + + + + + setHasPageContentLock( false ) } + > + { __( 'Edit template' ) } + + + + > + ); +} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss new file mode 100644 index 0000000000000..58178303e48e5 --- /dev/null +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss @@ -0,0 +1,10 @@ +.edit-site-page-panels__edit-template-preview { + border: 1px solid $gray-200; + height: 200px; + max-height: 200px; + overflow: hidden; +} + +.edit-site-page-panels__edit-template-button { + justify-content: center; +} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js b/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js index 8e5e80d9fecc5..b11d9acb2314f 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js @@ -1,9 +1,14 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { useDispatch } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; import { store as interfaceStore } from '@wordpress/interface'; /** @@ -11,27 +16,35 @@ import { store as interfaceStore } from '@wordpress/interface'; */ import { STORE_NAME } from '../../../store/constants'; import { SIDEBAR_BLOCK, SIDEBAR_TEMPLATE } from '../constants'; +import { store as editSiteStore } from '../../../store'; const SettingsHeader = ( { sidebarName } ) => { + const hasPageContentLock = useSelect( ( select ) => + select( editSiteStore ).hasPageContentLock() + ); + const { enableComplementaryArea } = useDispatch( interfaceStore ); const openTemplateSettings = () => enableComplementaryArea( STORE_NAME, SIDEBAR_TEMPLATE ); const openBlockSettings = () => enableComplementaryArea( STORE_NAME, SIDEBAR_BLOCK ); - const [ templateAriaLabel, templateActiveClass ] = - sidebarName === SIDEBAR_TEMPLATE - ? // translators: ARIA label for the Template sidebar tab, selected. - [ __( 'Template (selected)' ), 'is-active' ] - : // translators: ARIA label for the Template Settings Sidebar tab, not selected. - [ __( 'Template' ), '' ]; - - const [ blockAriaLabel, blockActiveClass ] = - sidebarName === SIDEBAR_BLOCK - ? // translators: ARIA label for the Block Settings Sidebar tab, selected. - [ __( 'Block (selected)' ), 'is-active' ] - : // translators: ARIA label for the Block Settings Sidebar tab, not selected. - [ __( 'Block' ), '' ]; + let templateAriaLabel; + if ( hasPageContentLock ) { + templateAriaLabel = + sidebarName === SIDEBAR_TEMPLATE + ? // translators: ARIA label for the Template sidebar tab, selected. + __( 'Page (selected)' ) + : // translators: ARIA label for the Template Settings Sidebar tab, not selected. + __( 'Page' ); + } else { + templateAriaLabel = + sidebarName === SIDEBAR_TEMPLATE + ? // translators: ARIA label for the Template sidebar tab, selected. + __( 'Template (selected)' ) + : // translators: ARIA label for the Template Settings Sidebar tab, not selected. + __( 'Template' ); + } /* Use a list so screen readers will announce how many tabs there are. */ return ( @@ -39,29 +52,39 @@ const SettingsHeader = ( { sidebarName } ) => { - { - // translators: Text label for the Template Settings Sidebar tab. - __( 'Template' ) + data-label={ + hasPageContentLock ? __( 'Page' ) : __( 'Template' ) } + > + { hasPageContentLock ? __( 'Page' ) : __( 'Template' ) } - { - // translators: Text label for the Block Settings Sidebar tab. - __( 'Block' ) - } + { __( 'Block' ) } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/sidebar-card/index.js b/packages/edit-site/src/components/sidebar-edit-mode/sidebar-card/index.js new file mode 100644 index 0000000000000..04e8d5667a2c2 --- /dev/null +++ b/packages/edit-site/src/components/sidebar-edit-mode/sidebar-card/index.js @@ -0,0 +1,34 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { Icon } from '@wordpress/components'; + +export default function SidebarCard( { + className, + title, + icon, + description, + actions, + children, +} ) { + return ( + + + + + { title } + { actions } + + + { description } + + { children } + + + ); +} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/sidebar-card/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/sidebar-card/style.scss new file mode 100644 index 0000000000000..718fe8fb5a0fb --- /dev/null +++ b/packages/edit-site/src/components/sidebar-edit-mode/sidebar-card/style.scss @@ -0,0 +1,34 @@ +.edit-site-sidebar-card { + display: flex; + align-items: flex-start; + + &__content { + flex-grow: 1; + margin-bottom: $grid-unit-05; + } + + &__title { + font-weight: 500; + line-height: $icon-size; + &.edit-site-sidebar-card__title { + margin: 0; + } + } + + &__description { + font-size: $default-font-size; + } + + &__icon { + flex: 0 0 $icon-size; + margin-right: $grid-unit-15; + width: $icon-size; + height: $icon-size; + } + + &__header { + display: flex; + justify-content: space-between; + margin: 0 0 $grid-unit-05; + } +} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-card/index.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js similarity index 60% rename from packages/edit-site/src/components/sidebar-edit-mode/template-card/index.js rename to packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js index d43dca3b803f5..1c369703be5d7 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/template-card/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js @@ -2,10 +2,11 @@ * WordPress dependencies */ import { useSelect } from '@wordpress/data'; -import { Icon } from '@wordpress/components'; +import { PanelRow, PanelBody } from '@wordpress/components'; import { store as editorStore } from '@wordpress/editor'; import { store as coreStore } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies @@ -13,8 +14,10 @@ import { decodeEntities } from '@wordpress/html-entities'; import { store as editSiteStore } from '../../../store'; import TemplateActions from './template-actions'; import TemplateAreas from './template-areas'; +import LastRevision from './last-revision'; +import SidebarCard from '../sidebar-card'; -export default function TemplateCard() { +export default function TemplatePanel() { const { info: { title, description, icon }, template, @@ -38,22 +41,22 @@ export default function TemplateCard() { } return ( - <> - - - - - - { decodeEntities( title ) } - - - - - { decodeEntities( description ) } - - - - - > + + } + > + + + + + + ); } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-revisions/index.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/last-revision.js similarity index 100% rename from packages/edit-site/src/components/sidebar-edit-mode/template-revisions/index.js rename to packages/edit-site/src/components/sidebar-edit-mode/template-panel/last-revision.js diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-card/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss similarity index 50% rename from packages/edit-site/src/components/sidebar-edit-mode/template-card/style.scss rename to packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss index 67054c25d2476..4c8ef94855dcb 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/template-card/style.scss +++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss @@ -1,30 +1,6 @@ .edit-site-template-card { - display: flex; - align-items: flex-start; - - &__content { - flex-grow: 1; - margin-bottom: $grid-unit-05; - } - - &__title { - font-weight: 500; - line-height: $icon-size; - &.edit-site-template-card__title { - margin: 0; - } - } - - &__description { - font-size: $default-font-size; - margin: 0 0 $grid-unit-20; - } - - &__icon { - flex: 0 0 $icon-size; - margin-right: $grid-unit-15; - width: $icon-size; - height: $icon-size; + &__template-areas { + margin-top: $grid-unit-20; } &__template-areas-list { @@ -44,12 +20,6 @@ } } - &__header { - display: flex; - justify-content: space-between; - margin: 0 0 $grid-unit-05; - } - &__actions { line-height: 0; > .components-button.is-small.has-icon { diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-card/template-actions.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-actions.js similarity index 100% rename from packages/edit-site/src/components/sidebar-edit-mode/template-card/template-actions.js rename to packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-actions.js diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-card/template-areas.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-areas.js similarity index 100% rename from packages/edit-site/src/components/sidebar-edit-mode/template-card/template-areas.js rename to packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-areas.js diff --git a/packages/edit-site/src/store/actions.js b/packages/edit-site/src/store/actions.js index a67406349e164..67fbec4811db4 100644 --- a/packages/edit-site/src/store/actions.js +++ b/packages/edit-site/src/store/actions.js @@ -530,3 +530,21 @@ export const switchEditorMode = speak( __( 'Code editor selected' ), 'assertive' ); } }; + +/** + * Sets whether or not the editor is locked so that only page content can be + * edited. + * + * @param {boolean} hasPageContentLock True to enable lock, false to disable. + */ +export const setHasPageContentLock = + ( hasPageContentLock ) => + ( { dispatch, registry } ) => { + if ( hasPageContentLock ) { + registry.dispatch( blockEditorStore ).clearSelectedBlock(); + } + dispatch( { + type: 'SET_HAS_PAGE_CONTENT_LOCK', + hasPageContentLock, + } ); + }; diff --git a/packages/edit-site/src/store/reducer.js b/packages/edit-site/src/store/reducer.js index a46d215f90507..a003ee958894e 100644 --- a/packages/edit-site/src/store/reducer.js +++ b/packages/edit-site/src/store/reducer.js @@ -157,6 +157,25 @@ function editorCanvasContainerView( state = undefined, action ) { return state; } +/** + * Reducer used to track whether the page content is locked. + * + * @param {boolean} state Current state. + * @param {Object} action Dispatched action. + * + * @return {boolean} Updated state. + */ +export function hasPageContentLock( state = false, action ) { + switch ( action.type ) { + case 'SET_EDITED_POST': + return !! action.context?.postId; + case 'SET_HAS_PAGE_CONTENT_LOCK': + return action.hasPageContentLock; + } + + return state; +} + export default combineReducers( { deviceType, settings, @@ -166,4 +185,5 @@ export default combineReducers( { saveViewPanel, canvasMode, editorCanvasContainerView, + hasPageContentLock, } ); diff --git a/packages/edit-site/src/store/selectors.js b/packages/edit-site/src/store/selectors.js index 583f37b55241b..16b6dc588ea26 100644 --- a/packages/edit-site/src/store/selectors.js +++ b/packages/edit-site/src/store/selectors.js @@ -321,3 +321,27 @@ export function isNavigationOpened() { version: '6.4', } ); } + +/** + * Whether or not the editor has a page loaded into it. + * + * @see setPage + * + * @param {Object} state Global application state. + * + * @return {boolean} Whether or not the editor has a page loaded into it. + */ +export function isPage( state ) { + return !! state.editedPost.context?.postId; +} + +/** + * Whether or not the editor is locked so that only page content can be edited. + * + * @param {Object} state Global application state. + * + * @return {boolean} Whether or not the editor is locked. + */ +export function hasPageContentLock( state ) { + return isPage( state ) ? state.hasPageContentLock : false; +} diff --git a/packages/edit-site/src/store/test/actions.js b/packages/edit-site/src/store/test/actions.js index 2df1cc72b6611..cca479e277662 100644 --- a/packages/edit-site/src/store/test/actions.js +++ b/packages/edit-site/src/store/test/actions.js @@ -13,6 +13,7 @@ import { store as preferencesStore } from '@wordpress/preferences'; * Internal dependencies */ import { store as editSiteStore } from '..'; +import { setHasPageContentLock } from '../actions'; const ENTITY_TYPES = { wp_template: { @@ -215,4 +216,34 @@ describe( 'actions', () => { ); } ); } ); + + describe( 'setHasPageContentLock', () => { + it( 'toggles the page content lock on', () => { + const dispatch = jest.fn(); + const clearSelectedBlock = jest.fn(); + const registry = { + dispatch: () => ( { clearSelectedBlock } ), + }; + setHasPageContentLock( true )( { dispatch, registry } ); + expect( clearSelectedBlock ).toHaveBeenCalled(); + expect( dispatch ).toHaveBeenCalledWith( { + type: 'SET_HAS_PAGE_CONTENT_LOCK', + hasPageContentLock: true, + } ); + } ); + + it( 'toggles the page content lock off', () => { + const dispatch = jest.fn(); + const clearSelectedBlock = jest.fn(); + const registry = { + dispatch: () => ( { clearSelectedBlock } ), + }; + setHasPageContentLock( false )( { dispatch, registry } ); + expect( clearSelectedBlock ).not.toHaveBeenCalled(); + expect( dispatch ).toHaveBeenCalledWith( { + type: 'SET_HAS_PAGE_CONTENT_LOCK', + hasPageContentLock: false, + } ); + } ); + } ); } ); diff --git a/packages/edit-site/src/store/test/reducer.js b/packages/edit-site/src/store/test/reducer.js index f6ce205ad6353..1ddc6bfb6fa7b 100644 --- a/packages/edit-site/src/store/test/reducer.js +++ b/packages/edit-site/src/store/test/reducer.js @@ -11,6 +11,7 @@ import { editedPost, blockInserterPanel, listViewPanel, + hasPageContentLock, } from '../reducer'; import { setIsInserterOpened, setIsListViewOpened } from '../actions'; @@ -135,4 +136,47 @@ describe( 'state', () => { ); } ); } ); + + describe( 'hasPageContentLocked()', () => { + it( 'defaults to false', () => { + expect( hasPageContentLock( undefined, {} ) ).toBe( false ); + } ); + + it( 'becomes false when editing a template', () => { + expect( + hasPageContentLock( true, { + type: 'SET_EDITED_POST', + postType: 'wp_template', + } ) + ).toBe( false ); + } ); + + it( 'becomes true when editing a page', () => { + expect( + hasPageContentLock( false, { + type: 'SET_EDITED_POST', + postType: 'wp_template', + context: { + postType: 'page', + postId: 123, + }, + } ) + ).toBe( true ); + } ); + + it( 'can be set', () => { + expect( + hasPageContentLock( false, { + type: 'SET_HAS_PAGE_CONTENT_LOCK', + hasPageContentLock: true, + } ) + ).toBe( true ); + expect( + hasPageContentLock( true, { + type: 'SET_HAS_PAGE_CONTENT_LOCK', + hasPageContentLock: false, + } ) + ).toBe( false ); + } ); + } ); } ); diff --git a/packages/edit-site/src/store/test/selectors.js b/packages/edit-site/src/store/test/selectors.js index 223bcd1f0ba04..d9ed31411ffcc 100644 --- a/packages/edit-site/src/store/test/selectors.js +++ b/packages/edit-site/src/store/test/selectors.js @@ -15,6 +15,8 @@ import { isInserterOpened, isListViewOpened, __unstableGetPreference, + isPage, + hasPageContentLock, } from '../selectors'; describe( 'selectors', () => { @@ -145,4 +147,59 @@ describe( 'selectors', () => { expect( isListViewOpened( state ) ).toBe( false ); } ); } ); + + describe( 'isPage', () => { + it( 'returns true if the edited post type is a page', () => { + const state = { + editedPost: { + postType: 'wp_template', + context: { postType: 'page', postId: 123 }, + }, + }; + expect( isPage( state ) ).toBe( true ); + } ); + + it( 'returns false if the edited post type is a template', () => { + const state = { + editedPost: { + postType: 'wp_template', + }, + }; + expect( isPage( state ) ).toBe( false ); + } ); + } ); + + describe( 'hasPageContentLock', () => { + it( 'returns true if locked and the edited post type is a page', () => { + const state = { + editedPost: { + postType: 'wp_template', + context: { postType: 'page', postId: 123 }, + }, + hasPageContentLock: true, + }; + expect( hasPageContentLock( state ) ).toBe( true ); + } ); + + it( 'returns false if not locked and the edited post type is a page', () => { + const state = { + editedPost: { + postType: 'wp_template', + context: { postType: 'page', postId: 123 }, + }, + hasPageContentLock: false, + }; + expect( hasPageContentLock( state ) ).toBe( false ); + } ); + + it( 'returns false if locked and the edited post type is a template', () => { + const state = { + editedPost: { + postType: 'wp_template', + }, + hasPageContentLock: true, + }; + expect( hasPageContentLock( state ) ).toBe( false ); + } ); + } ); } ); diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index 3e2b34ea65f5f..e31978cf0f45d 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -10,8 +10,10 @@ @import "./components/header-edit-mode/document-actions/style.scss"; @import "./components/list/style.scss"; @import "./components/sidebar-edit-mode/style.scss"; +@import "./components/sidebar-edit-mode/page-panels/style.scss"; @import "./components/sidebar-edit-mode/settings-header/style.scss"; -@import "./components/sidebar-edit-mode/template-card/style.scss"; +@import "./components/sidebar-edit-mode/sidebar-card/style.scss"; +@import "./components/sidebar-edit-mode/template-panel/style.scss"; @import "./components/editor/style.scss"; @import "./components/create-template-part-modal/style.scss"; @import "./components/secondary-sidebar/style.scss";