From 92f75c1480238323df4cac499c0560a9b32cbda7 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Wed, 3 Nov 2021 15:32:01 +0800 Subject: [PATCH] Try creating nav menu on pattern insertion or when the block has uncontrolled inner blocks (#36024) * 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 * Remove unused file * Remove unusued useState * Update animations --- .../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 | 168 +++++++++++++----- .../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 ++ 9 files changed, 303 insertions(+), 70 deletions(-) create mode 100644 packages/block-library/src/navigation/edit/navigation-menu-publish-button.js diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index 6fa66ea1a1dcbe..709c82cb90825e 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 00000000000000..fd4a766d1f0935 --- /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 9c19c88e309a16..aca458843150de 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 23dd393dea8f6a..a2f8985ce002f7 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,82 @@ +/** + * 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 } 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 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: blocks, + 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 +90,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/editor.scss b/packages/block-library/src/navigation/editor.scss index b06efa6512b815..c768f1e10261fb 100644 --- a/packages/block-library/src/navigation/editor.scss +++ b/packages/block-library/src/navigation/editor.scss @@ -531,12 +531,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 normal forwards fadein; + } +} + +@keyframes fadeouthalf { + 0% { + opacity: 1; } + 100% { + opacity: 0.5; + } +} + +.wp-block-navigation__unsaved-changes-overlay.is-saving { + opacity: 1; + animation: 0.5s linear 2s normal forwards fadeouthalf; } .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 17571d8f30d2de..c563ca923ddfdc 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 49f751b39321f0..1bf42dd968f048 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 35a754b6df00ad..7e91e50b2a493b 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 ); } } );