From f3959f8bc6ce0490e9157d0261b9c02e1be55ec2 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Fri, 1 Jul 2022 18:18:49 +1000 Subject: [PATCH] Rewrite URL (Permalink) panel as a popover (#42033) * Rewrite URL (Permalink) panel as a popover * Left truncate the URL * Wrap long URLs * Use 'View post' instead of 'URL' * Don't show 'View post' if slug is not editable * Break long URLs * Update E2E tests * Fix unit tests Co-authored-by: George Mamadashvili --- ...anel.test.js => sidebar-permalink.test.js} | 29 +-- .../src/components/preferences-modal/index.js | 22 +-- .../preferences-modal/test/index.js | 6 +- .../src/components/sidebar/post-link/index.js | 179 ------------------ .../components/sidebar/post-link/style.scss | 20 -- .../sidebar/post-schedule/style.scss | 2 - .../components/sidebar/post-status/index.js | 2 + .../src/components/sidebar/post-url/index.js | 42 ++++ .../components/sidebar/post-url/style.scss | 23 +++ .../sidebar/settings-sidebar/index.js | 2 - packages/edit-post/src/style.scss | 2 +- packages/editor/src/components/index.js | 3 + .../editor/src/components/post-url/check.js | 38 ++++ .../editor/src/components/post-url/index.js | 122 ++++++++++++ .../editor/src/components/post-url/label.js | 18 ++ .../editor/src/components/post-url/style.scss | 16 ++ packages/editor/src/style.scss | 1 + 17 files changed, 284 insertions(+), 243 deletions(-) rename packages/e2e-tests/specs/editor/various/{sidebar-permalink-panel.test.js => sidebar-permalink.test.js} (54%) delete mode 100644 packages/edit-post/src/components/sidebar/post-link/index.js delete mode 100644 packages/edit-post/src/components/sidebar/post-link/style.scss create mode 100644 packages/edit-post/src/components/sidebar/post-url/index.js create mode 100644 packages/edit-post/src/components/sidebar/post-url/style.scss create mode 100644 packages/editor/src/components/post-url/check.js create mode 100644 packages/editor/src/components/post-url/index.js create mode 100644 packages/editor/src/components/post-url/label.js create mode 100644 packages/editor/src/components/post-url/style.scss diff --git a/packages/e2e-tests/specs/editor/various/sidebar-permalink-panel.test.js b/packages/e2e-tests/specs/editor/various/sidebar-permalink.test.js similarity index 54% rename from packages/e2e-tests/specs/editor/various/sidebar-permalink-panel.test.js rename to packages/e2e-tests/specs/editor/various/sidebar-permalink.test.js index 35b51001788cd..5ada2ecc83712 100644 --- a/packages/e2e-tests/specs/editor/various/sidebar-permalink-panel.test.js +++ b/packages/e2e-tests/specs/editor/various/sidebar-permalink.test.js @@ -5,17 +5,17 @@ import { activatePlugin, createNewPost, deactivatePlugin, - findSidebarPanelWithTitle, publishPost, } from '@wordpress/e2e-test-utils'; -const permalinkPanelXPath = `//div[contains(@class, "edit-post-sidebar")]//button[contains(@class, "components-panel__body-toggle") and contains(text(),"Permalink")]`; +// TODO: Use a more accessible selector. +const urlRowSelector = '.edit-post-post-url'; // This tests are not together with the remaining sidebar tests, -// because we need to publish/save a post, to correctly test the permalink panel. +// because we need to publish/save a post, to correctly test the permalink row. // The sidebar test suit enforces that focus is never lost, but during save operations // the focus is lost and a new element is focused once the save is completed. -describe( 'Sidebar Permalink Panel', () => { +describe( 'Sidebar Permalink', () => { beforeAll( async () => { await activatePlugin( 'gutenberg-test-custom-post-types' ); } ); @@ -24,39 +24,30 @@ describe( 'Sidebar Permalink Panel', () => { await deactivatePlugin( 'gutenberg-test-custom-post-types' ); } ); - it( 'should allow permalink sidebar panel to be removed', async () => { - await createNewPost(); - await page.evaluate( () => { - const { removeEditorPanel } = wp.data.dispatch( 'core/edit-post' ); - removeEditorPanel( 'post-link' ); - } ); - expect( await page.$x( permalinkPanelXPath ) ).toEqual( [] ); - } ); - - it( 'should not render link panel when post is publicly queryable but not public', async () => { + it( 'should not render URL when post is publicly queryable but not public', async () => { await createNewPost( { postType: 'public_q_not_public' } ); await page.keyboard.type( 'aaaaa' ); await publishPost(); // Start editing again. await page.type( '.editor-post-title__input', ' (Updated)' ); - expect( await page.$x( permalinkPanelXPath ) ).toEqual( [] ); + expect( await page.$( urlRowSelector ) ).toBeNull(); } ); - it( 'should not render link panel when post is public but not publicly queryable', async () => { + it( 'should not render URL when post is public but not publicly queryable', async () => { await createNewPost( { postType: 'not_public_q_public' } ); await page.keyboard.type( 'aaaaa' ); await publishPost(); // Start editing again. await page.type( '.editor-post-title__input', ' (Updated)' ); - expect( await page.$x( permalinkPanelXPath ) ).toEqual( [] ); + expect( await page.$( urlRowSelector ) ).toBeNull(); } ); - it( 'should render link panel when post is public and publicly queryable', async () => { + it( 'should render URL when post is public and publicly queryable', async () => { await createNewPost( { postType: 'public_q_public' } ); await page.keyboard.type( 'aaaaa' ); await publishPost(); // Start editing again. await page.type( '.editor-post-title__input', ' (Updated)' ); - expect( await findSidebarPanelWithTitle( 'Permalink' ) ).toBeDefined(); + expect( await page.$( urlRowSelector ) ).not.toBeNull(); } ); } ); diff --git a/packages/edit-post/src/components/preferences-modal/index.js b/packages/edit-post/src/components/preferences-modal/index.js index f263d026c830a..9e3e1a53f4a87 100644 --- a/packages/edit-post/src/components/preferences-modal/index.js +++ b/packages/edit-post/src/components/preferences-modal/index.js @@ -19,7 +19,6 @@ import { PostTypeSupportCheck, store as editorStore, } from '@wordpress/editor'; -import { store as coreStore } from '@wordpress/core-data'; import { PreferencesModal, PreferencesModalTabs, @@ -45,15 +44,10 @@ const MODAL_NAME = 'edit-post/preferences'; export default function EditPostPreferencesModal() { const isLargeViewport = useViewportMatch( 'medium' ); const { closeModal } = useDispatch( editPostStore ); - const { isModalActive, isViewable } = useSelect( ( select ) => { - const { getEditedPostAttribute } = select( editorStore ); - const { getPostType } = select( coreStore ); - const postType = getPostType( getEditedPostAttribute( 'type' ) ); - return { - isModalActive: select( editPostStore ).isModalActive( MODAL_NAME ), - isViewable: get( postType, [ 'viewable' ], false ), - }; - }, [] ); + const isModalActive = useSelect( + ( select ) => select( editPostStore ).isModalActive( MODAL_NAME ), + [] + ); const showBlockBreadcrumbsOption = useSelect( ( select ) => { const { getEditorSettings } = select( editorStore ); @@ -200,12 +194,6 @@ export default function EditPostPreferencesModal() { ) } > - { isViewable && ( - - ) } ( jest.fn() ); describe( 'EditPostPreferencesModal', () => { describe( 'should match snapshot when the modal is active', () => { it( 'large viewports', () => { - useSelect.mockImplementation( () => ( { isModalActive: true } ) ); + useSelect.mockImplementation( () => true ); useViewportMatch.mockImplementation( () => true ); const wrapper = shallow( ); expect( wrapper ).toMatchSnapshot(); } ); it( 'small viewports', () => { - useSelect.mockImplementation( () => ( { isModalActive: true } ) ); + useSelect.mockImplementation( () => true ); useViewportMatch.mockImplementation( () => false ); const wrapper = shallow( ); expect( wrapper ).toMatchSnapshot(); @@ -35,7 +35,7 @@ describe( 'EditPostPreferencesModal', () => { } ); it( 'should not render when the modal is not active', () => { - useSelect.mockImplementation( () => ( { isModalActive: false } ) ); + useSelect.mockImplementation( () => false ); const wrapper = shallow( ); expect( wrapper.isEmptyRender() ).toBe( true ); } ); diff --git a/packages/edit-post/src/components/sidebar/post-link/index.js b/packages/edit-post/src/components/sidebar/post-link/index.js deleted file mode 100644 index 01669afdbe4f8..0000000000000 --- a/packages/edit-post/src/components/sidebar/post-link/index.js +++ /dev/null @@ -1,179 +0,0 @@ -/** - * External dependencies - */ -import { get } from 'lodash'; - -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { PanelBody, TextControl, ExternalLink } from '@wordpress/components'; -import { withSelect, withDispatch } from '@wordpress/data'; -import { compose, ifCondition } from '@wordpress/compose'; -import { store as editorStore } from '@wordpress/editor'; -import { safeDecodeURIComponent, cleanForSlug } from '@wordpress/url'; -import { store as coreStore } from '@wordpress/core-data'; -import { useState } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { store as editPostStore } from '../../../store'; - -/** - * Module Constants - */ -const PANEL_NAME = 'post-link'; - -function PostLink( { - isOpened, - onTogglePanel, - isEditable, - postLink, - permalinkPrefix, - permalinkSuffix, - editPermalink, - postSlug, - postTypeLabel, -} ) { - const [ forceEmptyField, setForceEmptyField ] = useState( false ); - - let prefixElement, postNameElement, suffixElement; - if ( isEditable ) { - prefixElement = permalinkPrefix && ( - - { permalinkPrefix } - - ); - postNameElement = postSlug && ( - - { postSlug } - - ); - suffixElement = permalinkSuffix && ( - - { permalinkSuffix } - - ); - } - - return ( - - { isEditable && ( -
- { - editPermalink( newValue ); - // When we delete the field the permalink gets - // reverted to the original value. - // The forceEmptyField logic allows the user to have - // the field temporarily empty while typing. - if ( ! newValue ) { - if ( ! forceEmptyField ) { - setForceEmptyField( true ); - } - return; - } - if ( forceEmptyField ) { - setForceEmptyField( false ); - } - } } - onBlur={ ( event ) => { - editPermalink( cleanForSlug( event.target.value ) ); - if ( forceEmptyField ) { - setForceEmptyField( false ); - } - } } - /> -

- { __( 'The last part of the URL.' ) }{ ' ' } - - { __( 'Read about permalinks' ) } - -

-
- ) } -

- { postTypeLabel || __( 'View post' ) } -

-
- - { isEditable ? ( - <> - { prefixElement } - { postNameElement } - { suffixElement } - - ) : ( - postLink - ) } - -
-
- ); -} - -export default compose( [ - withSelect( ( select ) => { - const { - isPermalinkEditable, - getCurrentPost, - isCurrentPostPublished, - getPermalinkParts, - getEditedPostAttribute, - getEditedPostSlug, - } = select( editorStore ); - const { isEditorPanelEnabled, isEditorPanelOpened } = - select( editPostStore ); - const { getPostType } = select( coreStore ); - - const { link } = getCurrentPost(); - - const postTypeName = getEditedPostAttribute( 'type' ); - const postType = getPostType( postTypeName ); - const permalinkParts = getPermalinkParts(); - - return { - postLink: link, - isEditable: isPermalinkEditable(), - isPublished: isCurrentPostPublished(), - isOpened: isEditorPanelOpened( PANEL_NAME ), - isEnabled: isEditorPanelEnabled( PANEL_NAME ), - isViewable: get( postType, [ 'viewable' ], false ), - postSlug: safeDecodeURIComponent( getEditedPostSlug() ), - postTypeLabel: get( postType, [ 'labels', 'view_item' ] ), - hasPermalinkParts: !! permalinkParts, - permalinkPrefix: permalinkParts?.prefix, - permalinkSuffix: permalinkParts?.suffix, - }; - } ), - ifCondition( ( { isEnabled, postLink, isViewable, hasPermalinkParts } ) => { - return isEnabled && postLink && isViewable && hasPermalinkParts; - } ), - withDispatch( ( dispatch ) => { - const { toggleEditorPanelOpened } = dispatch( editPostStore ); - const { editPost } = dispatch( editorStore ); - return { - onTogglePanel: () => toggleEditorPanelOpened( PANEL_NAME ), - editPermalink: ( newSlug ) => { - editPost( { slug: newSlug } ); - }, - }; - } ), -] )( PostLink ); diff --git a/packages/edit-post/src/components/sidebar/post-link/style.scss b/packages/edit-post/src/components/sidebar/post-link/style.scss deleted file mode 100644 index ab442190b61e6..0000000000000 --- a/packages/edit-post/src/components/sidebar/post-link/style.scss +++ /dev/null @@ -1,20 +0,0 @@ -.edit-post-post-link__link-post-name { - font-weight: 600; -} - -.edit-post-post-link__preview-label { - font-weight: 400; - margin: 0; -} - -.edit-post-post-link__link { - text-align: left; - word-wrap: break-word; - display: block; -} - -/* rtl:begin:ignore */ -.edit-post-post-link__preview-link-container { - direction: ltr; -} -/* rtl:end:ignore */ diff --git a/packages/edit-post/src/components/sidebar/post-schedule/style.scss b/packages/edit-post/src/components/sidebar/post-schedule/style.scss index 39e049bfbeed2..8e251f47f5857 100644 --- a/packages/edit-post/src/components/sidebar/post-schedule/style.scss +++ b/packages/edit-post/src/components/sidebar/post-schedule/style.scss @@ -2,13 +2,11 @@ width: 100%; position: relative; justify-content: flex-start; - align-items: flex-start; span { display: block; width: 45%; flex-shrink: 0; - padding: 6px 0; } } diff --git a/packages/edit-post/src/components/sidebar/post-status/index.js b/packages/edit-post/src/components/sidebar/post-status/index.js index b0ffe8749b1fe..7eb4e074ff48d 100644 --- a/packages/edit-post/src/components/sidebar/post-status/index.js +++ b/packages/edit-post/src/components/sidebar/post-status/index.js @@ -20,6 +20,7 @@ import PostPendingStatus from '../post-pending-status'; import PluginPostStatusInfo from '../plugin-post-status-info'; import { store as editPostStore } from '../../../store'; import PostTemplate from '../post-template'; +import PostURL from '../post-url'; /** * Module Constants @@ -39,6 +40,7 @@ function PostStatus( { isOpened, onTogglePanel } ) { <> + diff --git a/packages/edit-post/src/components/sidebar/post-url/index.js b/packages/edit-post/src/components/sidebar/post-url/index.js new file mode 100644 index 0000000000000..2b06190c2f1b2 --- /dev/null +++ b/packages/edit-post/src/components/sidebar/post-url/index.js @@ -0,0 +1,42 @@ +/** + * WordPress dependencies + */ +import { useRef } from '@wordpress/element'; +import { PanelRow, Dropdown, Button } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { + PostURLCheck, + PostURLLabel, + PostURL as PostURLForm, +} from '@wordpress/editor'; + +export default function PostURL() { + const anchorRef = useRef(); + + return ( + + + { __( 'URL' ) } + ( + + ) } + renderContent={ ( { onClose } ) => ( + + ) } + /> + + + ); +} diff --git a/packages/edit-post/src/components/sidebar/post-url/style.scss b/packages/edit-post/src/components/sidebar/post-url/style.scss new file mode 100644 index 0000000000000..dbe6d983efc64 --- /dev/null +++ b/packages/edit-post/src/components/sidebar/post-url/style.scss @@ -0,0 +1,23 @@ +.edit-post-post-url { + width: 100%; + justify-content: flex-start; + + span { + display: block; + width: 45%; + flex-shrink: 0; + } +} + +.components-button.edit-post-post-url__toggle { + text-align: left; + white-space: normal; + height: auto; + word-break: break-word; +} + +.edit-post-post-url__dialog .editor-post-url { + // sidebar width - popover padding - form margin + min-width: $sidebar-width - $grid-unit-20 - $grid-unit-20; + margin: $grid-unit-10; +} diff --git a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js index 23943b9c00599..42c9805212cb1 100644 --- a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js +++ b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js @@ -19,7 +19,6 @@ import LastRevision from '../last-revision'; import PostTaxonomies from '../post-taxonomies'; import FeaturedImage from '../featured-image'; import PostExcerpt from '../post-excerpt'; -import PostLink from '../post-link'; import DiscussionPanel from '../discussion-panel'; import PageAttributes from '../page-attributes'; import MetaBoxes from '../../meta-boxes'; @@ -87,7 +86,6 @@ const SettingsSidebar = () => { - diff --git a/packages/edit-post/src/style.scss b/packages/edit-post/src/style.scss index 5130efe2b1b93..0e42397b6f85b 100644 --- a/packages/edit-post/src/style.scss +++ b/packages/edit-post/src/style.scss @@ -11,11 +11,11 @@ @import "./components/sidebar/style.scss"; @import "./components/sidebar/last-revision/style.scss"; @import "./components/sidebar/post-author/style.scss"; -@import "./components/sidebar/post-link/style.scss"; @import "./components/sidebar/post-schedule/style.scss"; @import "./components/sidebar/post-slug/style.scss"; @import "./components/sidebar/post-status/style.scss"; @import "./components/sidebar/post-template/style.scss"; +@import "./components/sidebar/post-url/style.scss"; @import "./components/sidebar/post-visibility/style.scss"; @import "./components/sidebar/settings-header/style.scss"; @import "./components/sidebar/template-summary/style.scss"; diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index c0edc87d48f6a..2427b52c6032f 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -59,6 +59,9 @@ export { default as PostTitle } from './post-title'; export { default as PostTrash } from './post-trash'; export { default as PostTrashCheck } from './post-trash/check'; export { default as PostTypeSupportCheck } from './post-type-support-check'; +export { default as PostURL } from './post-url'; +export { default as PostURLCheck } from './post-url/check'; +export { default as PostURLLabel } from './post-url/label'; export { default as PostVisibility } from './post-visibility'; export { default as PostVisibilityLabel } from './post-visibility/label'; export { default as PostVisibilityCheck } from './post-visibility/check'; diff --git a/packages/editor/src/components/post-url/check.js b/packages/editor/src/components/post-url/check.js new file mode 100644 index 0000000000000..65c54846a702b --- /dev/null +++ b/packages/editor/src/components/post-url/check.js @@ -0,0 +1,38 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import { store as editorStore } from '../../store'; + +export default function PostURLCheck( { children } ) { + const isVisible = useSelect( ( select ) => { + const postTypeSlug = select( editorStore ).getCurrentPostType(); + const postType = select( coreStore ).getPostType( postTypeSlug ); + if ( ! postType?.viewable ) { + return false; + } + + const post = select( editorStore ).getCurrentPost(); + if ( ! post.link ) { + return false; + } + + const permalinkParts = select( editorStore ).getPermalinkParts(); + if ( ! permalinkParts ) { + return false; + } + + return true; + }, [] ); + + if ( ! isVisible ) { + return null; + } + + return children; +} diff --git a/packages/editor/src/components/post-url/index.js b/packages/editor/src/components/post-url/index.js new file mode 100644 index 0000000000000..32dfbbc63706f --- /dev/null +++ b/packages/editor/src/components/post-url/index.js @@ -0,0 +1,122 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { safeDecodeURIComponent, cleanForSlug } from '@wordpress/url'; +import { useState } from '@wordpress/element'; +import { __experimentalInspectorPopoverHeader as InspectorPopoverHeader } from '@wordpress/block-editor'; +import { __ } from '@wordpress/i18n'; +import { TextControl, ExternalLink } from '@wordpress/components'; +import { store as coreStore } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import { store as editorStore } from '../../store'; + +export default function PostURL( { onClose } ) { + const { + isEditable, + postSlug, + viewPostLabel, + postLink, + permalinkPrefix, + permalinkSuffix, + } = useSelect( ( select ) => { + const postTypeSlug = select( editorStore ).getCurrentPostType(); + const postType = select( coreStore ).getPostType( postTypeSlug ); + const permalinkParts = select( editorStore ).getPermalinkParts(); + return { + isEditable: select( editorStore ).isPermalinkEditable(), + postSlug: safeDecodeURIComponent( + select( editorStore ).getEditedPostSlug() + ), + viewPostLabel: postType?.labels.view_item, + postLink: select( editorStore ).getCurrentPost().link, + permalinkPrefix: permalinkParts?.prefix, + permalinkSuffix: permalinkParts?.suffix, + }; + }, [] ); + + const { editPost } = useDispatch( editorStore ); + + const [ forceEmptyField, setForceEmptyField ] = useState( false ); + + return ( +
+ + { isEditable && ( + + { __( 'The last part of the URL.' ) }{ ' ' } + + { __( 'Learn more.' ) } + + + } + onChange={ ( newValue ) => { + editPost( { slug: newValue } ); + // When we delete the field the permalink gets + // reverted to the original value. + // The forceEmptyField logic allows the user to have + // the field temporarily empty while typing. + if ( ! newValue ) { + if ( ! forceEmptyField ) { + setForceEmptyField( true ); + } + return; + } + if ( forceEmptyField ) { + setForceEmptyField( false ); + } + } } + onBlur={ ( event ) => { + editPost( { + slug: cleanForSlug( event.target.value ), + } ); + if ( forceEmptyField ) { + setForceEmptyField( false ); + } + } } + /> + ) } + { isEditable && ( +

+ { viewPostLabel ?? __( 'View post' ) } +

+ ) } +

+ + { isEditable ? ( + <> + + { permalinkPrefix } + + + { postSlug } + + + { permalinkSuffix } + + + ) : ( + postLink + ) } + +

+
+ ); +} diff --git a/packages/editor/src/components/post-url/label.js b/packages/editor/src/components/post-url/label.js new file mode 100644 index 0000000000000..f34a640460561 --- /dev/null +++ b/packages/editor/src/components/post-url/label.js @@ -0,0 +1,18 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { filterURLForDisplay } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import { store as editorStore } from '../../store'; + +export default function PostURLLabel() { + const postLink = useSelect( + ( select ) => select( editorStore ).getCurrentPost().link, + [] + ); + return filterURLForDisplay( postLink ); +} diff --git a/packages/editor/src/components/post-url/style.scss b/packages/editor/src/components/post-url/style.scss new file mode 100644 index 0000000000000..bfdbf2a9697a7 --- /dev/null +++ b/packages/editor/src/components/post-url/style.scss @@ -0,0 +1,16 @@ +.editor-post-url__link-label { + font-size: $default-font-size; + font-weight: 400; + margin: 0; +} + +/* rtl:begin:ignore */ +.editor-post-url__link { + direction: ltr; + word-break: break-word; +} +/* rtl:end:ignore */ + +.editor-post-url__link-slug { + font-weight: 600; +} diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss index 88ed59125a939..e71f3b0261a23 100644 --- a/packages/editor/src/style.scss +++ b/packages/editor/src/style.scss @@ -14,6 +14,7 @@ @import "./components/post-saved-state/style.scss"; @import "./components/post-taxonomies/style.scss"; @import "./components/post-text-editor/style.scss"; +@import "./components/post-url/style.scss"; @import "./components/post-visibility/style.scss"; @import "./components/post-title/style.scss"; @import "./components/post-trash/style.scss";