diff --git a/package-lock.json b/package-lock.json index 88692998ef96e..089313efc588d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19162,6 +19162,7 @@ "@wordpress/primitives": "file:packages/primitives", "@wordpress/reusable-blocks": "file:packages/reusable-blocks", "@wordpress/url": "file:packages/url", + "@wordpress/viewport": "file:packages/viewport", "classnames": "^2.3.1", "downloadjs": "^1.4.7", "file-saver": "^2.0.2", diff --git a/packages/edit-site/package.json b/packages/edit-site/package.json index 44f8926e76956..16d87150ca312 100644 --- a/packages/edit-site/package.json +++ b/packages/edit-site/package.json @@ -52,6 +52,7 @@ "@wordpress/primitives": "file:../primitives", "@wordpress/reusable-blocks": "file:../reusable-blocks", "@wordpress/url": "file:../url", + "@wordpress/viewport": "file:../viewport", "classnames": "^2.3.1", "downloadjs": "^1.4.7", "file-saver": "^2.0.2", diff --git a/packages/edit-site/src/components/add-new-template/new-template-part.js b/packages/edit-site/src/components/add-new-template/new-template-part.js index f70023c631df4..5f751e9aa50fb 100644 --- a/packages/edit-site/src/components/add-new-template/new-template-part.js +++ b/packages/edit-site/src/components/add-new-template/new-template-part.js @@ -7,9 +7,12 @@ import { kebabCase } from 'lodash'; * WordPress dependencies */ import { useState } from '@wordpress/element'; +import { useDispatch } from '@wordpress/data'; import { Button } from '@wordpress/components'; import { addQueryArgs } from '@wordpress/url'; import apiFetch from '@wordpress/api-fetch'; +import { __ } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; /** * Internal dependencies @@ -18,31 +21,43 @@ import CreateTemplatePartModal from '../create-template-part-modal'; export default function NewTemplatePart( { postType } ) { const [ isModalOpen, setIsModalOpen ] = useState( false ); + const { createErrorNotice } = useDispatch( noticesStore ); async function createTemplatePart( { title, area } ) { if ( ! title ) { + createErrorNotice( __( 'Title is not defined.' ), { + type: 'snackbar', + } ); return; } - const templatePart = await apiFetch( { - path: '/wp/v2/template-parts', - method: 'POST', - data: { - slug: kebabCase( title ), - title, - content: '', - area, - }, - } ); + try { + const templatePart = await apiFetch( { + path: '/wp/v2/template-parts', + method: 'POST', + data: { + slug: kebabCase( title ), + title, + content: '', + area, + }, + } ); - // Navigate to the created template part editor. - window.location.href = addQueryArgs( window.location.href, { - postId: templatePart.id, - postType: 'wp_template_part', - } ); + // Navigate to the created template part editor. + window.location.href = addQueryArgs( window.location.href, { + postId: templatePart.id, + postType: 'wp_template_part', + } ); + } catch ( error ) { + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : __( + 'An error occurred while creating the template part.' + ); - // Wait for async navigation to happen before closing the modal. - await new Promise( () => {} ); + createErrorNotice( errorMessage, { type: 'snackbar' } ); + } } return ( diff --git a/packages/edit-site/src/components/add-new-template/new-template.js b/packages/edit-site/src/components/add-new-template/new-template.js index f311b656ef65f..174d09c0482b2 100644 --- a/packages/edit-site/src/components/add-new-template/new-template.js +++ b/packages/edit-site/src/components/add-new-template/new-template.js @@ -12,11 +12,13 @@ import { MenuItem, NavigableMenu, } from '@wordpress/components'; -import { useSelect } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import { store as editorStore } from '@wordpress/editor'; import { addQueryArgs } from '@wordpress/url'; import apiFetch from '@wordpress/api-fetch'; +import { __ } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; const DEFAULT_TEMPLATE_SLUGS = [ 'front-page', @@ -42,27 +44,44 @@ export default function NewTemplate( { postType } ) { } ), [] ); + const { createErrorNotice } = useDispatch( noticesStore ); async function createTemplate( { slug } ) { - const { title, description } = find( defaultTemplateTypes, { slug } ); + try { + const { title, description } = find( defaultTemplateTypes, { + slug, + } ); - const template = await apiFetch( { - path: '/wp/v2/templates', - method: 'POST', - data: { - excerpt: description, - // Slugs need to be strings, so this is for template `404` - slug: slug.toString(), - status: 'publish', - title, - }, - } ); + const template = await apiFetch( { + path: '/wp/v2/templates', + method: 'POST', + data: { + excerpt: description, + // Slugs need to be strings, so this is for template `404` + slug: slug.toString(), + status: 'publish', + title, + }, + } ); - // Navigate to the created template editor. - window.location.href = addQueryArgs( window.location.href, { - postId: template.id, - postType: 'wp_template', - } ); + // Navigate to the created template editor. + window.location.href = addQueryArgs( window.location.href, { + postId: template.id, + postType: 'wp_template', + } ); + + // Wait for async navigation to happen before closing the modal. + await new Promise( () => {} ); + } catch ( error ) { + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : __( 'An error occurred while creating the template.' ); + + createErrorNotice( errorMessage, { + type: 'snackbar', + } ); + } } const existingTemplateSlugs = map( templates, 'slug' ); diff --git a/packages/edit-site/src/components/list/index.js b/packages/edit-site/src/components/list/index.js index 2e6e8ea8757c3..d02de1c519ad5 100644 --- a/packages/edit-site/src/components/list/index.js +++ b/packages/edit-site/src/components/list/index.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ @@ -6,6 +11,7 @@ import { useSelect } from '@wordpress/data'; import { InterfaceSkeleton } from '@wordpress/interface'; import { __, sprintf } from '@wordpress/i18n'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; +import { EditorSnackbars } from '@wordpress/editor'; /** * Internal dependencies @@ -14,20 +20,27 @@ import useRegisterShortcuts from './use-register-shortcuts'; import Header from './header'; import NavigationSidebar from '../navigation-sidebar'; import Table from './table'; +import { store as editSiteStore } from '../../store'; export default function List( { templateType } ) { useRegisterShortcuts(); - const { previousShortcut, nextShortcut } = useSelect( ( select ) => { - return { - previousShortcut: select( - keyboardShortcutsStore - ).getAllShortcutKeyCombinations( 'core/edit-site/previous-region' ), - nextShortcut: select( - keyboardShortcutsStore - ).getAllShortcutKeyCombinations( 'core/edit-site/next-region' ), - }; - }, [] ); + const { previousShortcut, nextShortcut, isNavigationOpen } = useSelect( + ( select ) => { + return { + previousShortcut: select( + keyboardShortcutsStore + ).getAllShortcutKeyCombinations( + 'core/edit-site/previous-region' + ), + nextShortcut: select( + keyboardShortcutsStore + ).getAllShortcutKeyCombinations( 'core/edit-site/next-region' ), + isNavigationOpen: select( editSiteStore ).isNavigationOpened(), + }; + }, + [] + ); const postType = useSelect( ( select ) => select( coreStore ).getPostType( templateType ), @@ -54,7 +67,9 @@ export default function List( { templateType } ) { return ( } + notices={ } content={
diff --git a/packages/edit-site/src/components/list/style.scss b/packages/edit-site/src/components/list/style.scss index ca2c989434586..f301d1b1ed340 100644 --- a/packages/edit-site/src/components/list/style.scss +++ b/packages/edit-site/src/components/list/style.scss @@ -114,3 +114,9 @@ border-top: none; } } + +.edit-site-list.is-navigation-open .components-snackbar-list { + @include break-medium() { + margin-left: $nav-sidebar-width; + } +} diff --git a/packages/edit-site/src/components/list/table.js b/packages/edit-site/src/components/list/table.js index e532023347caa..907163bddec7f 100644 --- a/packages/edit-site/src/components/list/table.js +++ b/packages/edit-site/src/components/list/table.js @@ -12,6 +12,7 @@ import { } from '@wordpress/components'; import { moreVertical } from '@wordpress/icons'; import { addQueryArgs } from '@wordpress/url'; +import { store as noticesStore } from '@wordpress/notices'; /** * Internal dependencies @@ -23,6 +24,9 @@ import isTemplateRevertable from '../../utils/is-template-revertable'; function Actions( { template } ) { const { removeTemplate, revertTemplate } = useDispatch( editSiteStore ); const { saveEditedEntityRecord } = useDispatch( coreStore ); + const { createSuccessNotice, createErrorNotice } = useDispatch( + noticesStore + ); const isRemovable = isTemplateRemovable( template ); const isRevertable = isTemplateRevertable( template ); @@ -32,8 +36,25 @@ function Actions( { template } ) { } async function revertAndSaveTemplate() { - await revertTemplate( template, { allowUndo: false } ); - await saveEditedEntityRecord( 'postType', template.type, template.id ); + try { + await revertTemplate( template, { allowUndo: false } ); + await saveEditedEntityRecord( + 'postType', + template.type, + template.id + ); + + createSuccessNotice( __( 'Template reverted.' ), { + type: 'snackbar', + } ); + } catch ( error ) { + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : __( 'An error occurred while reverting the template.' ); + + createErrorNotice( errorMessage, { type: 'snackbar' } ); + } } return ( diff --git a/packages/edit-site/src/components/navigation-sidebar/index.js b/packages/edit-site/src/components/navigation-sidebar/index.js index 035160022b920..4d6cbfc7a6170 100644 --- a/packages/edit-site/src/components/navigation-sidebar/index.js +++ b/packages/edit-site/src/components/navigation-sidebar/index.js @@ -1,13 +1,15 @@ /** * WordPress dependencies */ -import { useEffect, useState } from '@wordpress/element'; +import { useEffect } from '@wordpress/element'; import { createSlotFill } from '@wordpress/components'; import { useViewportMatch } from '@wordpress/compose'; +import { useDispatch } from '@wordpress/data'; /** * Internal dependencies */ +import { store as editSiteStore } from '../../store'; import NavigationPanel from './navigation-panel'; import NavigationToggle from './navigation-toggle'; @@ -21,33 +23,24 @@ export default function NavigationSidebar( { activeTemplateType, } ) { const isDesktopViewport = useViewportMatch( 'medium' ); - const [ isNavigationOpen, setIsNavigationOpen ] = useState( - isDefaultOpen && isDesktopViewport - ); + const { setIsNavigationPanelOpened } = useDispatch( editSiteStore ); useEffect( () => { // When transitioning to desktop open the navigation if `isDefaultOpen` is true. if ( isDefaultOpen && isDesktopViewport ) { - setIsNavigationOpen( true ); + setIsNavigationPanelOpened( true ); } // When transitioning to mobile/tablet, close the navigation. if ( ! isDesktopViewport ) { - setIsNavigationOpen( false ); + setIsNavigationPanelOpened( false ); } - }, [ isDefaultOpen, isDesktopViewport ] ); + }, [ isDefaultOpen, isDesktopViewport, setIsNavigationPanelOpened ] ); return ( <> - - + + ); diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/index.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/index.js index e047ec0708012..802c53c2b5654 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/index.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/index.js @@ -14,7 +14,7 @@ import { __experimentalNavigationMenu as NavigationMenu, } from '@wordpress/components'; import { store as coreDataStore } from '@wordpress/core-data'; -import { useSelect } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; import { useEffect, useRef } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { ESCAPE } from '@wordpress/keycodes'; @@ -25,36 +25,37 @@ import { addQueryArgs } from '@wordpress/url'; * Internal dependencies */ import MainDashboardButton from '../../main-dashboard-button'; +import { store as editSiteStore } from '../../../store'; const SITE_EDITOR_KEY = 'site-editor'; -const NavigationPanel = ( { - isOpen, - setIsOpen, - activeItem = SITE_EDITOR_KEY, -} ) => { - const siteTitle = useSelect( ( select ) => { +const NavigationPanel = ( { activeItem = SITE_EDITOR_KEY } ) => { + const { isNavigationOpen, siteTitle } = useSelect( ( select ) => { const { getEntityRecord } = select( coreDataStore ); const siteData = getEntityRecord( 'root', '__unstableBase', undefined ) || {}; - return siteData.name; + return { + siteTitle: siteData.name, + isNavigationOpen: select( editSiteStore ).isNavigationOpened(), + }; }, [] ); + const { setIsNavigationPanelOpened } = useDispatch( editSiteStore ); // Ensures focus is moved to the panel area when it is activated // from a separate component (such as document actions in the header). const panelRef = useRef(); useEffect( () => { - if ( isOpen ) { + if ( isNavigationOpen ) { panelRef.current.focus(); } - }, [ activeItem, isOpen ] ); + }, [ activeItem, isNavigationOpen ] ); const closeOnEscape = ( event ) => { if ( event.keyCode === ESCAPE && ! event.defaultPrevented ) { event.preventDefault(); - setIsOpen( false ); + setIsNavigationPanelOpened( false ); } }; @@ -62,7 +63,7 @@ const NavigationPanel = ( { // eslint-disable-next-line jsx-a11y/no-static-element-interactions
{ - const { getEntityRecord, isResolving } = select( coreDataStore ); - const siteData = - getEntityRecord( 'root', '__unstableBase', undefined ) || {}; +/** + * Internal dependencies + */ +import { store as editSiteStore } from '../../../store'; + +function NavigationToggle( { icon } ) { + const { isNavigationOpen, isRequestingSiteIcon, siteIconUrl } = useSelect( + ( select ) => { + const { getEntityRecord, isResolving } = select( coreDataStore ); + const siteData = + getEntityRecord( 'root', '__unstableBase', undefined ) || {}; - return { - isRequestingSiteIcon: isResolving( 'core', 'getEntityRecord', [ - 'root', - '__unstableBase', - undefined, - ] ), - siteIconUrl: siteData.site_icon_url, - }; - }, [] ); + return { + isNavigationOpen: select( editSiteStore ).isNavigationOpened(), + isRequestingSiteIcon: isResolving( 'core', 'getEntityRecord', [ + 'root', + '__unstableBase', + undefined, + ] ), + siteIconUrl: siteData.site_icon_url, + }; + }, + [] + ); + const { setIsNavigationPanelOpened } = useDispatch( editSiteStore ); const disableMotion = useReducedMotion(); - const toggleNavigationPanel = () => setIsOpen( ( open ) => ! open ); + const toggleNavigationPanel = () => + setIsNavigationPanelOpened( ! isNavigationOpen ); let buttonIcon = ; @@ -60,7 +71,8 @@ function NavigationToggle( { icon, isOpen, setIsOpen } ) { return ( diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/test/index.js b/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/test/index.js index a85afafc57246..0d693733d019a 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/test/index.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/test/index.js @@ -34,6 +34,7 @@ describe( 'NavigationToggle', () => { site_icon_url: 'https://fakeUrl.com', } ), isResolving: () => false, + isNavigationOpened: () => false, } ) ); } ); @@ -53,6 +54,7 @@ describe( 'NavigationToggle', () => { site_icon_url: '', } ), isResolving: () => false, + isNavigationOpened: () => false, } ) ); } ); diff --git a/packages/edit-site/src/index.js b/packages/edit-site/src/index.js index 9b9a00a7a3e54..4ed7f8ef3c5f8 100644 --- a/packages/edit-site/src/index.js +++ b/packages/edit-site/src/index.js @@ -6,20 +6,21 @@ import { registerCoreBlocks, __experimentalRegisterExperimentalCoreBlocks, } from '@wordpress/block-library'; -import { dispatch } from '@wordpress/data'; +import { dispatch, select } from '@wordpress/data'; import { render, unmountComponentAtNode } from '@wordpress/element'; import { __experimentalFetchLinkSuggestions as fetchLinkSuggestions, __experimentalFetchUrlData as fetchUrlData, } from '@wordpress/core-data'; import { store as editorStore } from '@wordpress/editor'; +import { store as viewportStore } from '@wordpress/viewport'; /** * Internal dependencies */ import './plugins'; import './hooks'; -import './store'; +import { store as editSiteStore } from './store'; import Editor from './components/editor'; import List from './components/list'; @@ -84,6 +85,13 @@ export function initializeList( id, templateType, settings ) { defaultTemplatePartAreas: settings.defaultTemplatePartAreas, } ); + // Default the navigation panel to be opened when we're in a bigger screen. + // We update the store synchronously before rendering so that we won't + // trigger an unnecessary re-render with useEffect. + dispatch( editSiteStore ).setIsNavigationPanelOpened( + select( viewportStore ).isViewportMatch( 'medium' ) + ); + render( , target ); } diff --git a/packages/edit-site/src/store/actions.js b/packages/edit-site/src/store/actions.js index 7b3fb3d3a2cf7..1c056b47c2a84 100644 --- a/packages/edit-site/src/store/actions.js +++ b/packages/edit-site/src/store/actions.js @@ -5,7 +5,7 @@ import { parse, __unstableSerializeAndClean } from '@wordpress/blocks'; import { controls, dispatch } from '@wordpress/data'; import { apiFetch } from '@wordpress/data-controls'; import { addQueryArgs, getPathAndQueryString } from '@wordpress/url'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; import { store as coreStore } from '@wordpress/core-data'; import { store as interfaceStore } from '@wordpress/interface'; @@ -111,14 +111,51 @@ export function* addTemplate( template ) { * @param {Object} template The template object. */ export function* removeTemplate( template ) { - yield controls.dispatch( - coreStore, - 'deleteEntityRecord', - 'postType', - template.type, - template.id, - { force: true } - ); + try { + yield controls.dispatch( + coreStore, + 'deleteEntityRecord', + 'postType', + template.type, + template.id, + { force: true } + ); + + const lastError = yield controls.select( + coreStore, + 'getLastEntityDeleteError', + 'postType', + template.type, + template.id + ); + + if ( lastError ) { + throw lastError; + } + + yield controls.dispatch( + noticesStore, + 'createSuccessNotice', + sprintf( + /* translators: The template/part's name. */ + __( '"%s" removed.' ), + template.title.rendered + ), + { type: 'snackbar' } + ); + } catch ( error ) { + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : __( 'An error occurred while deleting the template.' ); + + yield controls.dispatch( + noticesStore, + 'createErrorNotice', + errorMessage, + { type: 'snackbar' } + ); + } } /** diff --git a/packages/edit-site/src/store/test/actions.js b/packages/edit-site/src/store/test/actions.js index 5d45a0f0556ca..06ba5979b5023 100644 --- a/packages/edit-site/src/store/test/actions.js +++ b/packages/edit-site/src/store/test/actions.js @@ -5,7 +5,6 @@ import { toggleFeature, setTemplate, addTemplate, - removeTemplate, setTemplatePart, setPage, showHomepage, @@ -76,29 +75,6 @@ describe( 'actions', () => { } ); } ); - describe( 'removeTemplate', () => { - it( 'should issue a deleteEntityRecord request', () => { - const template = { - id: 'tt1-blocks//general', - type: 'wp_template_part', - }; - - const it = removeTemplate( template ); - expect( it.next().value ).toEqual( { - actionName: 'deleteEntityRecord', - args: [ - 'postType', - 'wp_template_part', - 'tt1-blocks//general', - { force: true }, - ], - storeKey: 'core', - type: '@@data/DISPATCH', - } ); - expect( it.next().done ).toBe( true ); - } ); - } ); - describe( 'setTemplatePart', () => { it( 'should return the SET_TEMPLATE_PART action', () => { const templatePartId = 1;