From 4830c2c2b95dc612b73efffbd3ed03cd93cd83fa Mon Sep 17 00:00:00 2001 From: Addison Stavlo Date: Thu, 30 Apr 2020 20:11:17 -0400 Subject: [PATCH] Move Entities Saved States from Modal to Sidebar (#21522) * moved to sidebar, functional in post editor / hidden in site * copied over some styles, working in site editor now * added close button * added icons and styles * added selection feature * refactored store goo * refactored z-index * refactored entities saved states * refactored scss * added test for actions * added tests for reducer * added selector test * fixing lint * fixing more lint * cleanup bad auto-pretty * last of cleanup? * renamed select entity button * added actions/reducers/tests to edit-post * added store things to edit-site * restructured to run on isOpen/closePanel props * removed from editor package store * created Actions component for post editor layout * added save panel hidden button to post editor * added hidden button to site editor * added styles and class names * fixed z-index name issue * restored z-indexes to previous values, no need with non-overlapping panels * refactor ActionsPanel, always mount entitites" * removed unnecessary vars and imports * make save component always mounted in edit site" * added some comments * fixed closePanel for selecting entity in small width * removed entities actions/reducers from edit-site editor * passed localstate down through props in edit-site * removed entities goo from edit-site store * added parentBlockId as dep to callback in entitiy selection * converted entities to local state in edit-post layout * set up edit-post save panel on local state props * removed entities goo from edit-post store * fixed z-index naming convention * refactored actions panel * fixed comment misspellings * refactored entitiesSavedStates icon enum * removed unnecessary comments * added comment to redundant looking useCallback * updated description titles in test to no longer say 'modal' * added tests for panels/a11y buttons rendering * minor name and descriptor changes * added waitForSelector to failing post-visibility test * changed waitFor to wait for button to be clickable * changed 'Select entity' button to 'Select' * Update packages/base-styles/_z-index.scss Co-Authored-By: Enrique Piqueras * applying some suggestions * Update packages/edit-post/src/components/layout/actions-panel.js Co-Authored-By: Enrique Piqueras * added comment to setting callback * updated callback goo * fixed useCallback for dismissing panel * updates prop name from closePanel to close * updated how we clear the callback * simplified logic on callback setting and open/close * minor comment nit Co-authored-by: Enrique Piqueras --- packages/base-styles/_z-index.scss | 8 +- .../editor/various/post-visibility.test.js | 5 + .../experiments/multi-entity-saving.test.js | 107 ++++++++-- .../edit-post/src/components/header/index.js | 9 +- .../header/post-publish-button-or-toggle.js | 2 + .../src/components/layout/actions-panel.js | 98 ++++++++++ .../edit-post/src/components/layout/index.js | 71 ++++--- .../src/components/layout/style.scss | 5 +- .../edit-site/src/components/editor/index.js | 56 +++++- .../src/components/editor/style.scss | 23 +++ .../edit-site/src/components/header/index.js | 6 +- .../src/components/save-button/index.js | 11 +- packages/edit-site/src/style.scss | 1 + .../components/entities-saved-states/index.js | 184 +++++++++++++----- .../entities-saved-states/style.scss | 77 +++++++- .../components/post-publish-button/index.js | 13 +- 16 files changed, 541 insertions(+), 135 deletions(-) create mode 100644 packages/edit-post/src/components/layout/actions-panel.js create mode 100644 packages/edit-site/src/components/editor/style.scss diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index cb3fc857703208..e7cb6414121292 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -69,7 +69,13 @@ $z-layers: ( ".edit-post-layout .edit-post-post-publish-panel": 100001, // For larger views, the wp-admin navbar dropdown should be at top of // the Publish Post sidebar. - ".edit-post-layout .edit-post-post-publish-panel-break-medium": 99998, + ".edit-post-layout .edit-post-post-publish-panel {greater than small}": 99998, + + ".entities-saved-states__panel": 100001, + // For larger views, the wp-admin navbar dropdown should be on top of + // the multi-entity saving sidebar. + ".entities-saved-states__panel {greater than small}": 99998, + ".edit-site-editor__toggle-save-panel": 100000, // Show sidebar in greater than small viewports above editor related elements // but bellow #adminmenuback { z-index: 100 } diff --git a/packages/e2e-tests/specs/editor/various/post-visibility.test.js b/packages/e2e-tests/specs/editor/various/post-visibility.test.js index dc04a8e2a8ee21..6844b9bd97345b 100644 --- a/packages/e2e-tests/specs/editor/various/post-visibility.test.js +++ b/packages/e2e-tests/specs/editor/various/post-visibility.test.js @@ -63,6 +63,11 @@ describe( 'Post visibility', () => { // Enter a title for this post. await page.type( '.editor-post-title__input', ' Changed' ); + // Wait for the button to be clickable before attempting to click. + // This could cause errors when we try to click before changes are registered. + await page.waitForSelector( + '.editor-post-publish-button[aria-disabled="false"]' + ); await page.click( '.editor-post-publish-button' ); const currentStatus = await page.evaluate( () => { diff --git a/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js b/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js index 20f7611b05341e..65293151779c09 100644 --- a/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js +++ b/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js @@ -4,8 +4,7 @@ import { createNewPost, insertBlock, - disablePrePublishChecks, - publishPostWithPrePublishChecksDisabled, + publishPost, visitAdminPage, } from '@wordpress/e2e-test-utils'; import { addQueryArgs } from '@wordpress/url'; @@ -20,24 +19,14 @@ import { import { trashExistingPosts } from '../../config/setup-test-framework'; describe( 'Multi-entity save flow', () => { - // Selectors. + // Selectors - usable between Post/Site editors. const checkedBoxSelector = '.components-checkbox-control__checked'; const checkboxInputSelector = '.components-checkbox-control__input'; - const demoTemplateSelector = '//button[contains(., "front-page")]'; - const draftSavedSelector = '.editor-post-saved-state.is-saved'; const entitiesSaveSelector = '.editor-entities-saved-states__save-button'; - const multiSaveSelector = - '.editor-post-publish-button__button.has-changes-dot'; - const savePostSelector = '.editor-post-publish-button__button'; - const disabledSavePostSelector = `${ savePostSelector }[aria-disabled=true]`; - const enabledSavePostSelector = `${ savePostSelector }[aria-disabled=false]`; - const saveSiteSelector = '.edit-site-save-button__button'; - const activeSaveSiteSelector = `${ saveSiteSelector }[aria-disabled=false]`; - const disabledSaveSiteSelector = `${ saveSiteSelector }[aria-disabled=true]`; - const templateDropdownSelector = - '.components-dropdown-menu__toggle[aria-label="Switch Template"]'; const templatePartSelector = '*[data-type="core/template-part"]'; const activatedTemplatePartSelector = `${ templatePartSelector } .block-editor-inner-blocks`; + const savePanelSelector = '.entities-saved-states__panel'; + const closePanelButtonSelector = 'button[aria-label="Close panel"]'; // Reusable assertions across Post/Site editors. const assertAllBoxesChecked = async () => { @@ -45,6 +34,14 @@ describe( 'Multi-entity save flow', () => { const checkboxInputs = await page.$$( checkboxInputSelector ); expect( checkedBoxes.length - checkboxInputs.length ).toBe( 0 ); }; + const assertExistance = async ( selector, shouldBePresent ) => { + const element = await page.$( selector ); + if ( shouldBePresent ) { + expect( element ).not.toBeNull(); + } else { + expect( element ).toBeNull(); + } + }; // Setup & Teardown. const requiredExperiments = [ '#gutenberg-full-site-editing', @@ -60,6 +57,19 @@ describe( 'Multi-entity save flow', () => { } ); describe( 'Post Editor', () => { + // Selectors - Post editor specific. + const draftSavedSelector = '.editor-post-saved-state.is-saved'; + const multiSaveSelector = + '.editor-post-publish-button__button.has-changes-dot'; + const savePostSelector = '.editor-post-publish-button__button'; + const disabledSavePostSelector = `${ savePostSelector }[aria-disabled=true]`; + const enabledSavePostSelector = `${ savePostSelector }[aria-disabled=false]`; + const publishA11ySelector = + '.edit-post-layout__toggle-publish-panel-button'; + const saveA11ySelector = + '.edit-post-layout__toggle-entities-saved-states-panel-button'; + const publishPanelSelector = '.editor-post-publish-panel'; + // Reusable assertions inside Post editor. const assertMultiSaveEnabled = async () => { const multiSaveButton = await page.waitForSelector( @@ -75,7 +85,6 @@ describe( 'Multi-entity save flow', () => { describe( 'Pre-Publish state', () => { it( 'Should not trigger multi-entity save button with only post edited', async () => { await createNewPost(); - await disablePrePublishChecks(); // Edit the page some. await page.click( '.editor-post-title' ); await page.keyboard.type( 'Test Post...' ); @@ -84,6 +93,13 @@ describe( 'Multi-entity save flow', () => { await assertMultiSaveDisabled(); } ); + it( 'Should only have publish panel a11y button active with only post edited', async () => { + await assertExistance( publishA11ySelector, true ); + await assertExistance( saveA11ySelector, false ); + await assertExistance( publishPanelSelector, false ); + await assertExistance( savePanelSelector, false ); + } ); + it( 'Should trigger multi-entity save button once template part edited', async () => { // Create new template part. await insertBlock( 'Template Part' ); @@ -101,14 +117,39 @@ describe( 'Multi-entity save flow', () => { await assertMultiSaveEnabled(); } ); - it( 'Clicking should open modal with boxes checked by default', async () => { + it( 'Should only have save panel a11y button active after child entities edited', async () => { + await assertExistance( publishA11ySelector, false ); + await assertExistance( saveA11ySelector, true ); + await assertExistance( publishPanelSelector, false ); + await assertExistance( savePanelSelector, false ); + } ); + + it( 'Clicking should open panel with boxes checked by default', async () => { await page.click( savePostSelector ); + await page.waitForSelector( savePanelSelector ); await assertAllBoxesChecked(); } ); - it( 'Saving should result in items being saved', async () => { + it( 'Should not show other panels (or their a11y buttons) while save panel opened', async () => { + await assertExistance( publishA11ySelector, false ); + await assertExistance( saveA11ySelector, false ); + await assertExistance( publishPanelSelector, false ); + } ); + + it( 'Publish panel should open after saving, no other panels (or their a11y buttons) should be present', async () => { + // Save entities and wait for publish panel. await page.click( entitiesSaveSelector ); + await page.waitForSelector( publishPanelSelector ); + + await assertExistance( publishA11ySelector, false ); + await assertExistance( saveA11ySelector, false ); + await assertExistance( savePanelSelector, false ); + + // Close publish panel. + await page.click( closePanelButtonSelector ); + } ); + it( 'Saving should result in items being saved', async () => { // Verify post is saved. const draftSaved = await page.waitForSelector( draftSavedSelector @@ -122,13 +163,17 @@ describe( 'Multi-entity save flow', () => { describe( 'Published state', () => { it( 'Update button disabled after publish', async () => { - await publishPostWithPrePublishChecksDisabled(); + await publishPost(); const disabledSaveButton = await page.$( disabledSavePostSelector ); expect( disabledSaveButton ).not.toBeNull(); } ); + it( 'should not have save a11y button when no changes', async () => { + await assertExistance( saveA11ySelector, false ); + } ); + it( 'Update button enabled after editing post', async () => { await page.click( '.editor-post-title' ); await page.keyboard.type( '...more title!' ); @@ -149,10 +194,23 @@ describe( 'Multi-entity save flow', () => { await page.keyboard.press( 'Enter' ); await assertMultiSaveEnabled(); } ); + + it( 'save a11y button enables after editing template part', async () => { + await assertExistance( saveA11ySelector, true ); + } ); } ); } ); describe( 'Site Editor', () => { + // Selectors - Site editor specific. + const demoTemplateSelector = '//button[contains(., "front-page")]'; + const saveSiteSelector = '.edit-site-save-button__button'; + const activeSaveSiteSelector = `${ saveSiteSelector }[aria-disabled=false]`; + const disabledSaveSiteSelector = `${ saveSiteSelector }[aria-disabled=true]`; + const templateDropdownSelector = + '.components-dropdown-menu__toggle[aria-label="Switch Template"]'; + const saveA11ySelector = '.edit-site-editor__toggle-save-panel-button'; + it( 'Should be enabled after edits', async () => { // Navigate to site editor. const query = addQueryArgs( '', { @@ -177,11 +235,20 @@ describe( 'Multi-entity save flow', () => { expect( enabledButton ).not.toBeNull(); } ); - it( 'Clicking button should open modal with boxes checked', async () => { + it( 'save a11y button should be present', async () => { + await assertExistance( saveA11ySelector, true ); + } ); + + it( 'Clicking button should open panel with boxes checked', async () => { await page.click( activeSaveSiteSelector ); + await page.waitForSelector( savePanelSelector ); await assertAllBoxesChecked(); } ); + it( 'save a11y button should not be present with save panel open', async () => { + await assertExistance( saveA11ySelector, false ); + } ); + it( 'Saving should result in items being saved', async () => { await page.click( entitiesSaveSelector ); const disabledButton = await page.waitForSelector( diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index 153ddaab9ab951..127fad3a25dc70 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -17,7 +17,11 @@ import MoreMenu from './more-menu'; import PostPublishButtonOrToggle from './post-publish-button-or-toggle'; import { default as DevicePreview } from '../device-preview'; -function Header( { onToggleInserter, isInserterOpen } ) { +function Header( { + onToggleInserter, + isInserterOpen, + setEntitiesSavedStatesCallback, +} ) { const { shortcut, hasActiveMetaboxes, @@ -89,6 +93,9 @@ function Header( { onToggleInserter, isInserterOpen } ) { + + ); + } else { + unmountableContent = ( +
+ +
+ ); + } + + // Since EntitiesSavedStates controls its own panel, we can keep it + // always mounted to retain its own component state (such as checkboxes). + return ( + <> + + { ! isEntitiesSavedStatesOpen && unmountableContent } + + ); +} diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index c25ace812ff87c..e48945c56734cf 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -11,7 +11,6 @@ import { LocalAutosaveMonitor, UnsavedChangesWarning, EditorNotices, - PostPublishPanel, EditorKeyboardShortcutsRegister, } from '@wordpress/editor'; import { useSelect, useDispatch } from '@wordpress/data'; @@ -33,7 +32,7 @@ import { FullscreenMode, InterfaceSkeleton, } from '@wordpress/interface'; -import { useState, useEffect } from '@wordpress/element'; +import { useState, useEffect, useCallback } from '@wordpress/element'; import { close } from '@wordpress/icons'; /** @@ -49,9 +48,8 @@ import BrowserURL from '../browser-url'; import Header from '../header'; import SettingsSidebar from '../sidebar/settings-sidebar'; import MetaBoxes from '../meta-boxes'; -import PluginPostPublishPanel from '../sidebar/plugin-post-publish-panel'; -import PluginPrePublishPanel from '../sidebar/plugin-pre-publish-panel'; import WelcomeGuide from '../welcome-guide'; +import ActionsPanel from './actions-panel'; const interfaceLabels = { leftSidebar: __( 'Block Library' ), @@ -61,12 +59,9 @@ function Layout() { const [ isInserterOpen, setIsInserterOpen ] = useState( false ); const isMobileViewport = useViewportMatch( 'medium', '<' ); const isHugeViewport = useViewportMatch( 'huge', '>=' ); - const { - closePublishSidebar, - openGeneralSidebar, - closeGeneralSidebar, - togglePublishSidebar, - } = useDispatch( 'core/edit-post' ); + const { openGeneralSidebar, closeGeneralSidebar } = useDispatch( + 'core/edit-post' + ); const { mode, isFullscreenActive, @@ -75,7 +70,6 @@ function Layout() { pluginSidebarOpened, publishSidebarOpened, hasActiveMetaboxes, - isSaving, hasFixedToolbar, previousShortcut, nextShortcut, @@ -101,7 +95,6 @@ function Layout() { isRichEditingEnabled: select( 'core/editor' ).getEditorSettings() .richEditingEnabled, hasActiveMetaboxes: select( 'core/edit-post' ).hasMetaBoxes(), - isSaving: select( 'core/edit-post' ).isSavingMetaBoxes(), previousShortcut: select( 'core/keyboard-shortcuts' ).getAllShortcutRawKeyCombinations( @@ -136,6 +129,22 @@ function Layout() { } }, [ isInserterOpen, isHugeViewport ] ); + // Local state for save panel. + // Note 'thruthy' callback implies an open panel. + const [ + entitiesSavedStatesCallback, + setEntitiesSavedStatesCallback, + ] = useState( false ); + const closeEntitiesSavedStates = useCallback( + ( arg ) => { + if ( typeof entitiesSavedStatesCallback === 'function' ) { + entitiesSavedStatesCallback( arg ); + } + setEntitiesSavedStatesCallback( false ); + }, + [ entitiesSavedStatesCallback ] + ); + return ( <> @@ -155,6 +164,9 @@ function Layout() { onToggleInserter={ () => setIsInserterOpen( ! isInserterOpen ) } + setEntitiesSavedStatesCallback={ + setEntitiesSavedStatesCallback + } /> } leftSidebar={ @@ -234,30 +246,17 @@ function Layout() { ) } actions={ - publishSidebarOpened ? ( - - ) : ( -
- -
- ) + } shortcuts={ { previous: previousShortcut, diff --git a/packages/edit-post/src/components/layout/style.scss b/packages/edit-post/src/components/layout/style.scss index 340284a8613b15..65c1b49625ea19 100644 --- a/packages/edit-post/src/components/layout/style.scss +++ b/packages/edit-post/src/components/layout/style.scss @@ -31,7 +31,7 @@ overflow: auto; @include break-medium() { - z-index: z-index(".edit-post-layout .edit-post-post-publish-panel-break-medium"); + z-index: z-index(".edit-post-layout .edit-post-post-publish-panel {greater than small}"); top: $admin-bar-height; left: auto; width: $sidebar-width; @@ -67,7 +67,8 @@ .edit-post-layout__toggle-publish-panel, -.edit-post-layout__toogle-sidebar-panel { +.edit-post-layout__toogle-sidebar-panel, +.edit-post-layout__toggle-entities-saved-states-panel { z-index: z-index(".edit-post-layout__toogle-sidebar-panel"); position: fixed !important; // Need to override the default relative positionning top: -9999em; diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index cb268b80b84604..80c1671a94b19b 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -6,6 +6,7 @@ import { useContext, useState, useMemo, + useCallback, } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { @@ -13,6 +14,7 @@ import { DropZoneProvider, Popover, FocusReturnProvider, + Button, } from '@wordpress/components'; import { EntityProvider } from '@wordpress/core-data'; import { @@ -23,6 +25,8 @@ import { } from '@wordpress/block-editor'; import { useViewportMatch } from '@wordpress/compose'; import { FullscreenMode, InterfaceSkeleton } from '@wordpress/interface'; +import { EntitiesSavedStates } from '@wordpress/editor'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies @@ -49,6 +53,7 @@ function Editor( { settings: _settings } ) { ), [ settings.templateType, settings.templateId ] ); + const context = useMemo( () => ( { settings, setSettings } ), [ settings, setSettings, @@ -68,6 +73,19 @@ function Editor( { settings: _settings } ) { const inlineStyles = useResizeCanvas( deviceType ); + const [ + isEntitiesSavedStatesOpen, + setIsEntitiesSavedStatesOpen, + ] = useState( false ); + const openEntitiesSavedStates = useCallback( + () => setIsEntitiesSavedStatesOpen( true ), + [] + ); + const closeEntitiesSavedStates = useCallback( + () => setIsEntitiesSavedStatesOpen( false ), + [] + ); + return template ? ( <> @@ -84,7 +102,13 @@ function Editor( { settings: _settings } ) { } - header={
} + header={ +
+ } content={ } + actions={ + <> + + { ! isEntitiesSavedStatesOpen && ( +
+ +
+ ) } + + } footer={ } /> diff --git a/packages/edit-site/src/components/editor/style.scss b/packages/edit-site/src/components/editor/style.scss new file mode 100644 index 00000000000000..06e7ef13145b9e --- /dev/null +++ b/packages/edit-site/src/components/editor/style.scss @@ -0,0 +1,23 @@ +.edit-site-editor__toggle-save-panel { + z-index: z-index(".edit-site-editor__toggle-save-panel"); + position: fixed !important; // Need to override the default relative positioning + top: -9999em; + bottom: auto; + left: auto; + right: 0; + width: $sidebar-width; + background-color: $white; + border: 1px dotted $light-gray-500; + height: auto !important; // Need to override the default sidebar positioning + padding: $grid-unit-30; + display: flex; + justify-content: center; + + .interface-interface-skeleton__actions:focus &, + .interface-interface-skeleton__actions:focus-within &, + .interface-interface-skeleton__actions:focus &, + .interface-interface-skeleton__actions:focus-within & { + top: auto; + bottom: 0; + } +} diff --git a/packages/edit-site/src/components/header/index.js b/packages/edit-site/src/components/header/index.js index abe10fc23af061..c8f5b0d5b6225d 100644 --- a/packages/edit-site/src/components/header/index.js +++ b/packages/edit-site/src/components/header/index.js @@ -22,7 +22,7 @@ import SaveButton from '../save-button'; const inserterToggleProps = { isPrimary: true }; -export default function Header() { +export default function Header( { openEntitiesSavedStates } ) { const { settings, setSettings } = useEditorContext(); const setActiveTemplateId = useCallback( ( newTemplateId ) => @@ -88,7 +88,9 @@ export default function Header() { deviceType={ deviceType } setDeviceType={ setPreviewDeviceType } /> - + diff --git a/packages/edit-site/src/components/save-button/index.js b/packages/edit-site/src/components/save-button/index.js index 28af9be05d723b..75c9c0b56da416 100644 --- a/packages/edit-site/src/components/save-button/index.js +++ b/packages/edit-site/src/components/save-button/index.js @@ -7,18 +7,17 @@ import { some } from 'lodash'; * WordPress dependencies */ import { useEntityProp } from '@wordpress/core-data'; -import { useEffect, useState, useCallback } from '@wordpress/element'; +import { useEffect } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { EntitiesSavedStates } from '@wordpress/editor'; /** * Internal dependencies */ import { useEditorContext } from '../editor'; -export default function SaveButton() { +export default function SaveButton( { openEntitiesSavedStates } ) { const { settings } = useEditorContext(); const [ , setStatus ] = useEntityProp( 'postType', @@ -52,9 +51,6 @@ export default function SaveButton() { } ); const disabled = ! isDirty || isSaving; - const [ isOpen, setIsOpen ] = useState( false ); - const open = useCallback( () => setIsOpen( true ), [] ); - const close = useCallback( () => setIsOpen( false ), [] ); return ( <> - ); } diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index 6d6f2e6680c57c..2bc2db62ed594e 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -7,6 +7,7 @@ @import "./components/notices/style.scss"; @import "./components/sidebar/style.scss"; @import "./components/template-switcher/style.scss"; +@import "./components/editor/style.scss"; // In order to use mix-blend-mode, this element needs to have an explicitly set background-color. // We scope it to .wp-toolbar to be wp-admin only, to prevent bleed into other implementations. diff --git a/packages/editor/src/components/entities-saved-states/index.js b/packages/editor/src/components/entities-saved-states/index.js index 460823f3bcf856..c25efcb06bf6d9 100644 --- a/packages/editor/src/components/entities-saved-states/index.js +++ b/packages/editor/src/components/entities-saved-states/index.js @@ -6,22 +6,90 @@ import { some, groupBy } from 'lodash'; /** * WordPress dependencies */ -import { CheckboxControl, Modal, Button } from '@wordpress/components'; +import { + CheckboxControl, + Button, + PanelBody, + PanelRow, +} from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useSelect, useDispatch } from '@wordpress/data'; -import { useState } from '@wordpress/element'; +import { useState, useCallback } from '@wordpress/element'; +import { + close as closeIcon, + page, + layout, + grid, + blockDefault, +} from '@wordpress/icons'; + +const ENTITY_NAME_ICONS = { + site: layout, + page, + post: grid, + wp_template: grid, +}; + +function EntityRecordState( { record, checked, onChange, closePanel } ) { + const { name, kind, title, key } = record; + const parentBlockId = useSelect( ( select ) => { + // Get entity's blocks. + const { blocks = [] } = select( 'core' ).getEditedEntityRecord( + kind, + name, + key + ); + // Get parents of the entity's first block. + const parents = select( 'core/block-editor' ).getBlockParents( + blocks[ 0 ]?.clientId + ); + // Return closest parent block's clientId. + return parents[ parents.length - 1 ]; + }, [] ); + + const { selectBlock } = useDispatch( 'core/block-editor' ); + const selectParentBlock = useCallback( () => selectBlock( parentBlockId ), [ + parentBlockId, + ] ); + + const selectAndDismiss = useCallback( () => { + selectBlock( parentBlockId ); + closePanel(); + }, [ parentBlockId ] ); -function EntityRecordState( { record, checked, onChange } ) { return ( - { record.title || __( 'Untitled' ) } } - checked={ checked } - onChange={ onChange } - /> + + { title || __( 'Untitled' ) } } + checked={ checked } + onChange={ onChange } + /> + { parentBlockId ? ( + <> + + + + ) : null } + ); } -function EntityTypeList( { list, unselectedEntities, setUnselectedEntities } ) { +function EntityTypeList( { + list, + unselectedEntities, + setUnselectedEntities, + closePanel, +} ) { const firstRecord = list[ 0 ]; const entity = useSelect( ( select ) => @@ -29,9 +97,12 @@ function EntityTypeList( { list, unselectedEntities, setUnselectedEntities } ) { [ firstRecord.kind, firstRecord.name ] ); + // Set icon based on type of entity. + const { name } = firstRecord; + const icon = ENTITY_NAME_ICONS[ name ] || blockDefault; + return ( -
-

{ entity.label }

+ { list.map( ( record ) => { return ( setUnselectedEntities( record, value ) } + closePanel={ closePanel } /> ); } ) } -
+ ); } -export default function EntitiesSavedStates( { isOpen, onRequestClose } ) { - const dirtyEntityRecords = useSelect( - ( select ) => select( 'core' ).__experimentalGetDirtyEntityRecords(), - [] - ); +export default function EntitiesSavedStates( { isOpen, close } ) { + const { dirtyEntityRecords } = useSelect( ( select ) => { + return { + dirtyEntityRecords: select( + 'core' + ).__experimentalGetDirtyEntityRecords(), + }; + }, [] ); const { saveEditedEntityRecord } = useDispatch( 'core' ); // To group entities by type. @@ -106,40 +181,51 @@ export default function EntitiesSavedStates( { isOpen, onRequestClose } ) { saveEditedEntityRecord( kind, name, key ); } ); - onRequestClose( entitiesToSave ); + close( entitiesToSave ); }; - return ( - isOpen && ( - onRequestClose() } - contentLabel={ __( 'Select items to save.' ) } - > - { partitionedSavables.map( ( list ) => { - return ( - - ); - } ) } + // Explicitly define this with no argument passed. Using `close` on + // its own will use the event object in place of the expected saved entities. + const dismissPanel = useCallback( () => close(), [ close ] ); + return isOpen ? ( +
+
- - ) - ); + onClick={ dismissPanel } + icon={ closeIcon } + label={ __( 'Close panel' ) } + /> +
+ +
+

+ { __( 'Please review the following changes to save:' ) } +

+
+ + { partitionedSavables.map( ( list ) => { + return ( + + ); + } ) } + + +
+ ) : null; } diff --git a/packages/editor/src/components/entities-saved-states/style.scss b/packages/editor/src/components/entities-saved-states/style.scss index 6a04c3db879c89..10975d4148842f 100644 --- a/packages/editor/src/components/entities-saved-states/style.scss +++ b/packages/editor/src/components/entities-saved-states/style.scss @@ -1,11 +1,70 @@ -.editor-entities-saved-states__save-button { - display: block; - margin-left: auto; - margin-right: 0; -} -.editor-entities-saved-states__entity-type-list { - h2 { - font-size: 18px; - margin: 20px 0 10px; +.entities-saved-states__panel { + @include reset; + background: $white; + position: fixed; + z-index: z-index(".entities-saved-states__panel"); + top: $admin-bar-height-big; + bottom: 0; + right: 0; + left: 0; + overflow: auto; + box-sizing: border-box; + + .entities-saved-states__find-entity { + display: none; + } + .entities-saved-states__find-entity-small { + display: block; + } + + @include break-medium() { + z-index: z-index(".entities-saved-states__panel {greater than small}"); + top: $admin-bar-height; + left: auto; + width: $sidebar-width; + border-left: $border-width solid $light-gray-500; + + body.is-fullscreen-mode & { + top: 0; + } + + .entities-saved-states__find-entity { + display: block; + } + .entities-saved-states__find-entity-small { + display: none; + } + } + + .entities-saved-states__panel-header { + background: $white; + padding-left: $grid-unit-10; + padding-right: $grid-unit-10; + height: $header-height + $border-width; + border-bottom: $border-width solid $light-gray-500; + display: flex; + align-items: center; + align-content: space-between; + + .components-button.has-icon { + position: absolute; + right: $grid-unit-10; + } + } + + .entities-saved-states__text-prompt { + border-bottom: $border-width solid $light-gray-500; + + h2 { + padding: $grid-unit-20; + padding-bottom: $grid-unit-15; + margin: 0; + font-size: $big-font-size; + } + } + + .editor-entities-saved-states__save-button { + display: block; + margin: $grid-unit-15 auto; } } diff --git a/packages/editor/src/components/post-publish-button/index.js b/packages/editor/src/components/post-publish-button/index.js index c2383f52c19afb..46ebe017edbfd7 100644 --- a/packages/editor/src/components/post-publish-button/index.js +++ b/packages/editor/src/components/post-publish-button/index.js @@ -16,7 +16,6 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import EntitiesSavedStates from '../entities-saved-states'; import PublishButtonLabel from './label'; export class PostPublishButton extends Component { @@ -49,6 +48,13 @@ export class PostPublishButton extends Component { this.setState( { entitiesSavedStatesCallback: () => callback( ...args ), } ); + // Open the save panel by setting its callback. + // To set a function on the useState hook, we must set it + // with another function (() => myFunction). Passing the + // function on its own will cause an error when called. + this.props.setEntitiesSavedStatesCallback( + () => this.closeEntitiesSavedStates + ); return noop; } @@ -96,7 +102,6 @@ export class PostPublishButton extends Component { visibility, hasNonPostEntityChanges, } = this.props; - const { entitiesSavedStatesCallback } = this.state; const isButtonDisabled = isSaving || @@ -170,10 +175,6 @@ export class PostPublishButton extends Component { const componentChildren = isToggle ? toggleChildren : buttonChildren; return ( <> -