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 } ) {
);
}
diff --git a/packages/edit-post/src/components/layout/actions-panel.js b/packages/edit-post/src/components/layout/actions-panel.js
new file mode 100644
index 00000000000000..5ac15a9c253c91
--- /dev/null
+++ b/packages/edit-post/src/components/layout/actions-panel.js
@@ -0,0 +1,98 @@
+/**
+ * WordPress dependencies
+ */
+import { EntitiesSavedStates, PostPublishPanel } from '@wordpress/editor';
+import { useSelect, useDispatch } from '@wordpress/data';
+import { Button } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { useCallback } from '@wordpress/element';
+/**
+ * Internal dependencies
+ */
+import PluginPostPublishPanel from '../sidebar/plugin-post-publish-panel';
+import PluginPrePublishPanel from '../sidebar/plugin-pre-publish-panel';
+
+export default function ActionsPanel( {
+ setEntitiesSavedStatesCallback,
+ closeEntitiesSavedStates,
+ isEntitiesSavedStatesOpen,
+} ) {
+ const { closePublishSidebar, togglePublishSidebar } = useDispatch(
+ 'core/edit-post'
+ );
+ const {
+ publishSidebarOpened,
+ hasActiveMetaboxes,
+ isSavingMetaBoxes,
+ hasNonPostEntityChanges,
+ } = useSelect( ( select ) => {
+ return {
+ publishSidebarOpened: select(
+ 'core/edit-post'
+ ).isPublishSidebarOpened(),
+ hasActiveMetaboxes: select( 'core/edit-post' ).hasMetaBoxes(),
+ isSavingMetaBoxes: select( 'core/edit-post' ).isSavingMetaBoxes(),
+ hasNonPostEntityChanges: select(
+ 'core/editor'
+ ).hasNonPostEntityChanges(),
+ };
+ }, [] );
+
+ const openEntitiesSavedStates = useCallback(
+ () => setEntitiesSavedStatesCallback( true ),
+ []
+ );
+
+ // It is ok for these components to be unmounted when not in visual use.
+ // We don't want more than one present at a time, decide which to render.
+ let unmountableContent;
+ if ( publishSidebarOpened ) {
+ unmountableContent = (
+
+ );
+ } else if ( hasNonPostEntityChanges ) {
+ unmountableContent = (
+
+
+
+ );
+ } 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 (
<>
-