From 86d3e43618baa2db3d0cb348df89d5c9e20a79b7 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 28 Oct 2021 11:50:37 +0800 Subject: [PATCH 1/4] Add automatic wp_navigation creation Fix issues with multiple menu creation Lift limit on loading of menus Remove attempt to use template part area to generate a name Remove unused client id prop Try creating as auto-draft and publishing on save Only save when selecting menu Show a draft save button on the block toolbar Show loading state when navigation block first loads Remove flicker when selecting block with unsaved inner blocks Polish switch to controlled inner blocks Take drafts into account when naming menus Fix repeated line --- .../src/navigation/edit/index.js | 64 +++++-- .../edit/navigation-menu-name-modal.js | 6 +- .../edit/navigation-menu-publish-button.js | 57 ++++++ .../src/navigation/edit/placeholder/index.js | 6 +- .../navigation/edit/unsaved-inner-blocks.js | 175 ++++++++++++++---- .../edit/use-unsaved-inner-blocks.js | 0 .../block-library/src/navigation/editor.scss | 38 +++- packages/block-library/src/navigation/save.js | 8 +- .../src/navigation/use-navigation-menu.js | 8 +- .../components/entities-saved-states/index.js | 18 ++ 10 files changed, 310 insertions(+), 70 deletions(-) create mode 100644 packages/block-library/src/navigation/edit/navigation-menu-publish-button.js create mode 100644 packages/block-library/src/navigation/edit/use-unsaved-inner-blocks.js diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index 6fa66ea1a1dcb..709c82cb90825 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -38,10 +38,12 @@ import { __ } from '@wordpress/i18n'; import useListViewModal from './use-list-view-modal'; import useNavigationMenu from '../use-navigation-menu'; import Placeholder from './placeholder'; +import PlaceholderPreview from './placeholder/placeholder-preview'; import ResponsiveWrapper from './responsive-wrapper'; import NavigationInnerBlocks from './inner-blocks'; import NavigationMenuSelector from './navigation-menu-selector'; import NavigationMenuNameControl from './navigation-menu-name-control'; +import NavigationMenuPublishButton from './navigation-menu-publish-button'; import UnsavedInnerBlocks from './unsaved-inner-blocks'; import NavigationMenuDeleteControl from './navigation-menu-delete-control'; @@ -75,8 +77,8 @@ function detectColors( colorsDetectionElement, setColor, setBackground ) { function Navigation( { attributes, setAttributes, - isSelected, clientId, + isSelected, className, backgroundColor, setBackgroundColor, @@ -108,13 +110,26 @@ function Navigation( { `navigationMenu/${ navigationMenuId }` ); - const innerBlocks = useSelect( - ( select ) => select( blockEditorStore ).getBlocks( clientId ), + const { innerBlocks, isInnerBlockSelected } = useSelect( + ( select ) => { + const { getBlocks, hasSelectedInnerBlock } = select( + blockEditorStore + ); + return { + innerBlocks: getBlocks( clientId ), + isInnerBlockSelected: hasSelectedInnerBlock( clientId, true ), + }; + }, [ clientId ] ); const hasExistingNavItems = !! innerBlocks.length; const { replaceInnerBlocks, selectBlock } = useDispatch( blockEditorStore ); + const [ + hasSavedUnsavedInnerBlocks, + setHasSavedUnsavedInnerBlocks, + ] = useState( false ); + const [ isPlaceholderShown, setIsPlaceholderShown ] = useState( ! hasExistingNavItems ); @@ -127,10 +142,13 @@ function Navigation( { isNavigationMenuResolved, isNavigationMenuMissing, canSwitchNavigationMenu, - hasResolvedNavigationMenu, + hasResolvedNavigationMenus, + navigationMenus, + navigationMenu, } = useNavigationMenu( navigationMenuId ); const navRef = useRef(); + const isDraftNavigationMenu = navigationMenu?.status === 'draft'; const { listViewToolbarButton, listViewModal } = useListViewModal( clientId @@ -203,19 +221,23 @@ function Navigation( { // If the block has inner blocks, but no menu id, this was an older // navigation block added before the block used a wp_navigation entity. + // Either this block was saved in the content or inserted by a pattern. // Consider this 'unsaved'. Offer an uncontrolled version of inner blocks, - // with a prompt to 'save'. - const hasUnsavedBlocks = - hasExistingNavItems && navigationMenuId === undefined; + // that automatically saves the menu. + const hasUnsavedBlocks = hasExistingNavItems && ! isEntityAvailable; if ( hasUnsavedBlocks ) { return ( - setAttributes( { navigationMenuId: post.id } ) - } + navigationMenus={ navigationMenus } + hasSelection={ isSelected || isInnerBlockSelected } + hasSavedUnsavedInnerBlocks={ hasSavedUnsavedInnerBlocks } + onSave={ ( post ) => { + setHasSavedUnsavedInnerBlocks( true ); + // Switch to using the wp_navigation entity. + setAttributes( { navigationMenuId: post.id } ); + } } /> ); } @@ -261,8 +283,8 @@ function Navigation( { > - - { isEntityAvailable && ( + { ! isDraftNavigationMenu && isEntityAvailable && ( + ) } - ) } - + + ) } { hasItemJustificationControls && ( ) } { listViewToolbarButton } + + { isDraftNavigationMenu && ( + + ) } + { listViewModal } @@ -420,11 +447,14 @@ function Navigation( { selectBlock( clientId ); } } canSwitchNavigationMenu={ canSwitchNavigationMenu } - hasResolvedNavigationMenu={ - hasResolvedNavigationMenu + hasResolvedNavigationMenus={ + hasResolvedNavigationMenus } /> ) } + { ! isEntityAvailable && ! isPlaceholderShown && ( + + ) } { ! isPlaceholderShown && ( - { __( 'Create' ) } + { finishButtonText } diff --git a/packages/block-library/src/navigation/edit/navigation-menu-publish-button.js b/packages/block-library/src/navigation/edit/navigation-menu-publish-button.js new file mode 100644 index 0000000000000..fd4a766d1f093 --- /dev/null +++ b/packages/block-library/src/navigation/edit/navigation-menu-publish-button.js @@ -0,0 +1,57 @@ +/** + * WordPress dependencies + */ +import { ToolbarButton } from '@wordpress/components'; +import { + useEntityId, + useEntityProp, + store as coreStore, +} from '@wordpress/core-data'; +import { useDispatch } from '@wordpress/data'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import NavigationMenuNameModal from './navigation-menu-name-modal'; + +export default function NavigationMenuPublishButton() { + const [ isNameModalVisible, setIsNameModalVisible ] = useState( false ); + const id = useEntityId( 'postType', 'wp_navigation' ); + const [ navigationMenuTitle ] = useEntityProp( + 'postType', + 'wp_navigation', + 'title' + ); + const { editEntityRecord, saveEditedEntityRecord } = useDispatch( + coreStore + ); + + return ( + <> + setIsNameModalVisible( true ) }> + { __( 'Save as' ) } + + { isNameModalVisible && ( + setIsNameModalVisible( false ) } + finishButtonText={ __( 'Save' ) } + onFinish={ ( updatedTitle ) => { + editEntityRecord( 'postType', 'wp_navigation', id, { + title: updatedTitle, + status: 'publish', + } ); + saveEditedEntityRecord( + 'postType', + 'wp_navigation', + id + ); + } } + /> + ) } + + ); +} diff --git a/packages/block-library/src/navigation/edit/placeholder/index.js b/packages/block-library/src/navigation/edit/placeholder/index.js index 9c19c88e309a1..aca458843150d 100644 --- a/packages/block-library/src/navigation/edit/placeholder/index.js +++ b/packages/block-library/src/navigation/edit/placeholder/index.js @@ -88,7 +88,7 @@ const ExistingMenusDropdown = ( { export default function NavigationPlaceholder( { onFinish, canSwitchNavigationMenu, - hasResolvedNavigationMenu, + hasResolvedNavigationMenus, } ) { const [ selectedMenu, setSelectedMenu ] = useState(); @@ -189,10 +189,10 @@ export default function NavigationPlaceholder( { return ( <> - { ( ! hasResolvedNavigationMenu || isStillLoading ) && ( + { ( ! hasResolvedNavigationMenus || isStillLoading ) && ( ) } - { hasResolvedNavigationMenu && ! isStillLoading && ( + { hasResolvedNavigationMenus && ! isStillLoading && (
diff --git a/packages/block-library/src/navigation/edit/unsaved-inner-blocks.js b/packages/block-library/src/navigation/edit/unsaved-inner-blocks.js index 23dd393dea8f6..1f3641ea93e67 100644 --- a/packages/block-library/src/navigation/edit/unsaved-inner-blocks.js +++ b/packages/block-library/src/navigation/edit/unsaved-inner-blocks.js @@ -1,38 +1,89 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ -import { useInnerBlocksProps, Warning } from '@wordpress/block-editor'; +import { useInnerBlocksProps } from '@wordpress/block-editor'; import { serialize } from '@wordpress/blocks'; -import { Button, Disabled } from '@wordpress/components'; +import { Disabled, Spinner } from '@wordpress/components'; import { store as coreStore } from '@wordpress/core-data'; -import { useDispatch } from '@wordpress/data'; -import { useCallback, useState } from '@wordpress/element'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { + useCallback, + useContext, + useEffect, + useRef, + useState, +} from '@wordpress/element'; import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import NavigationMenuNameModal from './navigation-menu-name-modal'; +import useNavigationMenu from '../use-navigation-menu'; + +const NOOP = () => {}; +const DRAFT_MENU_PARAMS = [ + 'postType', + 'wp_navigation', + { status: 'draft', per_page: -1 }, +]; export default function UnsavedInnerBlocks( { blockProps, blocks, + hasSavedUnsavedInnerBlocks, onSave, - isSelected, + hasSelection, } ) { + const [ controlledBlocks ] = useState( blocks ); + const isDisabled = useContext( Disabled.Context ); + const savingLock = useRef( false ); + const innerBlocksProps = useInnerBlocksProps( blockProps, { - renderAppender: false, - } ); - const [ isModalVisible, setIsModalVisible ] = useState( false ); + renderAppender: hasSelection ? undefined : false, + // Make the inner blocks 'controlled'. This allows the block to always + // work with controlled inner blocks, smoothing out the switch to using + // an entity. + value: controlledBlocks, + onChange: NOOP, + onInput: NOOP, + } ); const { saveEntityRecord } = useDispatch( coreStore ); + const { + isSaving, + draftNavigationMenus, + hasResolvedDraftNavigationMenus, + } = useSelect( ( select ) => { + const { + getEntityRecords, + hasFinishedResolution, + isSavingEntityRecord, + } = select( coreStore ); + + return { + isSaving: isSavingEntityRecord( 'postType', 'wp_navigation' ), + draftNavigationMenus: getEntityRecords( ...DRAFT_MENU_PARAMS ), + hasResolvedDraftNavigationMenus: hasFinishedResolution( + 'getEntityRecords', + DRAFT_MENU_PARAMS + ), + }; + }, [] ); + + const { hasResolvedNavigationMenus, navigationMenus } = useNavigationMenu(); + const createNavigationMenu = useCallback( - async ( title = __( 'Untitled Navigation Menu' ) ) => { + async ( title ) => { const record = { title, content: serialize( blocks ), - status: 'publish', + status: 'draft', }; const navigationMenu = await saveEntityRecord( @@ -46,41 +97,83 @@ export default function UnsavedInnerBlocks( { [ blocks, serialize, saveEntityRecord ] ); + // Automatically save the uncontrolled blocks. + useEffect( async () => { + // The block will be disabled when used in a BlockPreview. + // In this case avoid automatic creation of a wp_navigation post. + // Otherwise the user will be spammed with lots of menus! + // + // Also ensure other navigation menus have loaded so an + // accurate name can be created. + // + // Don't try saving when another save is already + // in progress. + // + // And finally only create the menu when the block is selected, + // which is an indication they want to start editing. + if ( + hasSavedUnsavedInnerBlocks || + isDisabled || + isSaving || + savingLock.current || + ! hasResolvedDraftNavigationMenus || + ! hasResolvedNavigationMenus || + ! hasSelection + ) { + return; + } + + savingLock.current = true; + const title = __( 'Untitled menu' ); + + // Determine how many menus start with the untitled title. + const matchingMenuTitleCount = [ + ...draftNavigationMenus, + ...navigationMenus, + ].reduce( + ( count, menu ) => + menu?.title?.raw?.startsWith( title ) ? count + 1 : count, + 0 + ); + + // Append a number to the end of the title if a menu with + // the same name exists. + const titleWithCount = + matchingMenuTitleCount > 0 + ? `${ title } ${ matchingMenuTitleCount + 1 }` + : title; + + const menu = await createNavigationMenu( titleWithCount ); + onSave( menu ); + savingLock.current = false; + }, [ + isDisabled, + isSaving, + hasResolvedDraftNavigationMenus, + hasResolvedNavigationMenus, + draftNavigationMenus, + navigationMenus, + hasSelection, + createNavigationMenu, + ] ); + return ( <> - { isModalVisible && ( - { - setIsModalVisible( false ); - } } - onFinish={ async ( title ) => { - const menu = await createNavigationMenu( title ); - onSave( menu ); - } } - /> - ) } ); } diff --git a/packages/block-library/src/navigation/edit/use-unsaved-inner-blocks.js b/packages/block-library/src/navigation/edit/use-unsaved-inner-blocks.js new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/block-library/src/navigation/editor.scss b/packages/block-library/src/navigation/editor.scss index 0e18fd0a06574..836b0af1d23c8 100644 --- a/packages/block-library/src/navigation/editor.scss +++ b/packages/block-library/src/navigation/editor.scss @@ -522,12 +522,42 @@ body.editor-styles-wrapper } } -.wp-block-navigation__unsaved-changes-warning { - width: 100%; +@keyframes fadein { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} - .block-editor-warning__actions { - margin-top: 0; +.wp-block-navigation__unsaved-changes { + position: relative; + + .components-spinner { + position: absolute; + top: calc(50% - #{$spinner-size} / 2); + left: calc(50% - #{$spinner-size} / 2); + + // Delay showing the saving spinner until after 2 seconds. + // This should ensure it only shows for slow connections. + opacity: 0; + animation: 0.5s linear 2s 1 normal forwards fadein; + } +} + +@keyframes fadeinhalf { + 0% { + opacity: 1; } + 100% { + opacity: 0.5; + } +} + +.wp-block-navigation__unsaved-changes-overlay.is-saving { + opacity: 1; + animation: 0.5s linear 2s 1 normal forwards fadeinhalf; } .wp-block-navigation-delete-menu-button { diff --git a/packages/block-library/src/navigation/save.js b/packages/block-library/src/navigation/save.js index 17571d8f30d2d..c563ca923ddfd 100644 --- a/packages/block-library/src/navigation/save.js +++ b/packages/block-library/src/navigation/save.js @@ -3,6 +3,12 @@ */ import { InnerBlocks } from '@wordpress/block-editor'; -export default function save() { +export default function save( { attributes } ) { + if ( attributes.navigationMenuId ) { + // Avoid rendering inner blocks when a navigationMenuId is defined. + // When this id is defined the inner blocks are loaded from the + // `wp_navigation` entity rather than the hard-coded block html. + return; + } return ; } diff --git a/packages/block-library/src/navigation/use-navigation-menu.js b/packages/block-library/src/navigation/use-navigation-menu.js index 49f751b39321f..1bf42dd968f04 100644 --- a/packages/block-library/src/navigation/use-navigation-menu.js +++ b/packages/block-library/src/navigation/use-navigation-menu.js @@ -28,7 +28,11 @@ export default function useNavigationMenu( navigationMenuId ) { ) : false; - const navigationMenuMultipleArgs = [ 'postType', 'wp_navigation' ]; + const navigationMenuMultipleArgs = [ + 'postType', + 'wp_navigation', + { per_page: -1 }, + ]; const navigationMenus = getEntityRecords( ...navigationMenuMultipleArgs ); @@ -42,7 +46,7 @@ export default function useNavigationMenu( navigationMenuId ) { isNavigationMenuMissing: hasResolvedNavigationMenu && ! navigationMenu, canSwitchNavigationMenu, - hasResolvedNavigationMenu: hasFinishedResolution( + hasResolvedNavigationMenus: hasFinishedResolution( 'getEntityRecords', navigationMenuMultipleArgs ), diff --git a/packages/editor/src/components/entities-saved-states/index.js b/packages/editor/src/components/entities-saved-states/index.js index 35a754b6df00a..7e91e50b2a493 100644 --- a/packages/editor/src/components/entities-saved-states/index.js +++ b/packages/editor/src/components/entities-saved-states/index.js @@ -27,6 +27,13 @@ const TRANSLATED_SITE_PROPERTIES = { page_on_front: __( 'Page on front' ), }; +const PUBLISH_ON_SAVE_ENTITIES = [ + { + kind: 'postType', + name: 'wp_navigation', + }, +]; + export default function EntitiesSavedStates( { close } ) { const saveButtonRef = useRef(); const { dirtyEntityRecords } = useSelect( ( select ) => { @@ -63,6 +70,7 @@ export default function EntitiesSavedStates( { close } ) { }; }, [] ); const { + editEntityRecord, saveEditedEntityRecord, __experimentalSaveSpecifiedEntityEdits: saveSpecifiedEntityEdits, } = useDispatch( coreStore ); @@ -130,6 +138,16 @@ export default function EntitiesSavedStates( { close } ) { if ( 'root' === kind && 'site' === name ) { siteItemsToSave.push( property ); } else { + if ( + PUBLISH_ON_SAVE_ENTITIES.some( + ( typeToPublish ) => + typeToPublish.kind === kind && + typeToPublish.name === name + ) + ) { + editEntityRecord( kind, name, key, { status: 'publish' } ); + } + saveEditedEntityRecord( kind, name, key ); } } ); From cdaef532d7da993d907237945bae54e22672ca8a Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 2 Nov 2021 16:45:46 +0800 Subject: [PATCH 2/4] Remove unused file --- .../block-library/src/navigation/edit/use-unsaved-inner-blocks.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/block-library/src/navigation/edit/use-unsaved-inner-blocks.js diff --git a/packages/block-library/src/navigation/edit/use-unsaved-inner-blocks.js b/packages/block-library/src/navigation/edit/use-unsaved-inner-blocks.js deleted file mode 100644 index e69de29bb2d1d..0000000000000 From c8af4c5cd6552074b0c1155d01c8f4d90617dcdd Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 2 Nov 2021 16:48:19 +0800 Subject: [PATCH 3/4] Remove unusued useState --- .../src/navigation/edit/unsaved-inner-blocks.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/block-library/src/navigation/edit/unsaved-inner-blocks.js b/packages/block-library/src/navigation/edit/unsaved-inner-blocks.js index 1f3641ea93e67..a2f8985ce002f 100644 --- a/packages/block-library/src/navigation/edit/unsaved-inner-blocks.js +++ b/packages/block-library/src/navigation/edit/unsaved-inner-blocks.js @@ -11,13 +11,7 @@ import { serialize } from '@wordpress/blocks'; import { Disabled, Spinner } from '@wordpress/components'; import { store as coreStore } from '@wordpress/core-data'; import { useDispatch, useSelect } from '@wordpress/data'; -import { - useCallback, - useContext, - useEffect, - useRef, - useState, -} from '@wordpress/element'; +import { useCallback, useContext, useEffect, useRef } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; /** @@ -39,7 +33,6 @@ export default function UnsavedInnerBlocks( { onSave, hasSelection, } ) { - const [ controlledBlocks ] = useState( blocks ); const isDisabled = useContext( Disabled.Context ); const savingLock = useRef( false ); @@ -49,7 +42,7 @@ export default function UnsavedInnerBlocks( { // Make the inner blocks 'controlled'. This allows the block to always // work with controlled inner blocks, smoothing out the switch to using // an entity. - value: controlledBlocks, + value: blocks, onChange: NOOP, onInput: NOOP, } ); From 5e7f9cd67337e97c4ef297f44f282f6573215e5d Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 2 Nov 2021 16:54:27 +0800 Subject: [PATCH 4/4] Update animations --- packages/block-library/src/navigation/editor.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/block-library/src/navigation/editor.scss b/packages/block-library/src/navigation/editor.scss index 836b0af1d23c8..2be4d048bec21 100644 --- a/packages/block-library/src/navigation/editor.scss +++ b/packages/block-library/src/navigation/editor.scss @@ -542,11 +542,11 @@ body.editor-styles-wrapper // Delay showing the saving spinner until after 2 seconds. // This should ensure it only shows for slow connections. opacity: 0; - animation: 0.5s linear 2s 1 normal forwards fadein; + animation: 0.5s linear 2s normal forwards fadein; } } -@keyframes fadeinhalf { +@keyframes fadeouthalf { 0% { opacity: 1; } @@ -557,7 +557,7 @@ body.editor-styles-wrapper .wp-block-navigation__unsaved-changes-overlay.is-saving { opacity: 1; - animation: 0.5s linear 2s 1 normal forwards fadeinhalf; + animation: 0.5s linear 2s normal forwards fadeouthalf; } .wp-block-navigation-delete-menu-button {