Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions packages/block-library/src/page-list/convert-to-links-modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* WordPress dependencies
*/
import { Button, Modal } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useDispatch, useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';
import { createBlock as create } from '@wordpress/blocks';
import { store as blockEditorStore } from '@wordpress/block-editor';

const PAGE_FIELDS = [ 'id', 'title', 'link', 'type', 'parent' ];
const MAX_PAGE_COUNT = 100;

export const convertSelectedBlockToNavigationLinks = ( {
pages,
clientId,
replaceBlock,
createBlock,
} ) => () => {
if ( ! pages ) {
return;
}

const linkMap = {};
const navigationLinks = [];
pages.forEach( ( { id, title, link: url, type, parent } ) => {
// See if a placeholder exists. This is created if children appear before parents in list
const innerBlocks = linkMap[ id ]?.innerBlocks ?? [];
linkMap[ id ] = createBlock(
'core/navigation-link',
{
id,
label: title.rendered,
url,
type,
kind: 'post-type',
},
innerBlocks
);

if ( ! parent ) {
navigationLinks.push( linkMap[ id ] );
} else {
if ( ! linkMap[ parent ] ) {
// Use a placeholder if the child appears before parent in list
linkMap[ parent ] = { innerBlocks: [] };
}
const parentLinkInnerBlocks = linkMap[ parent ].innerBlocks;
parentLinkInnerBlocks.push( linkMap[ id ] );
}
} );

replaceBlock( clientId, navigationLinks );
};

export default function ConvertToLinksModal( { onClose, clientId } ) {
const { pages, pagesFinished } = useSelect(
( select ) => {
const { getEntityRecords, hasFinishedResolution } = select(
coreDataStore
);
const query = [
'postType',
'page',
{
per_page: MAX_PAGE_COUNT,
_fields: PAGE_FIELDS,
// TODO: When https://core.trac.wordpress.org/ticket/39037 REST API support for multiple orderby
// values is resolved, update 'orderby' to [ 'menu_order', 'post_title' ] to provide a consistent
// sort.
orderby: 'menu_order',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Funny that this seems to return the pages in more or less oldest to most recent order, whereas the same parameter passed to get_pages returns a completely different order 😕

Copy link
Contributor Author

@gwwar gwwar Apr 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is because get_pages has a default order of asc and WP_Query (used by the REST API) has a default order of desc, I'll double check which one we need to respect for menu_order.

It looks like we can't expect a stable sort from using menu_order, when items are equal to each other (eg menu_order of 0), so I'll need another dimension to sort on in addition to menu_order. Do folks prefer title alphabetizing or something like post date?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For what it's worth, I don't think it's crucial to get this 100% right in this one PR. It's can be a nice enhancement in the future, but the value being able to convert the page list outweighs the downside that sorting might not be perfect, especially paired with the fact that you get immediate access to mover controls right after the conversion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it won't be possible to provide a consistent ordering until we can implement https://core.trac.wordpress.org/ticket/39037.

Fwiw, I implemented it that way initially because performance in the editor with >100 or so pages was pretty horrific

Good to know @tellthemachines! I'll look into adding multiple orderby support in the REST API next, then start on some general performance profiling for Navigation Links.

I've incorporated feedback and added a few explanations in e35f132

order: 'asc',
},
];
return {
pages: getEntityRecords( ...query ),
pagesFinished: hasFinishedResolution(
'getEntityRecords',
query
),
};
},
[ clientId ]
);
const { replaceBlock } = useDispatch( blockEditorStore );

return (
<Modal
closeLabel={ __( 'Close' ) }
onRequestClose={ onClose }
title={ __( 'Convert to links' ) }
className={ 'wp-block-page-list-modal' }
aria={ { describedby: 'wp-block-page-list-modal__description' } }
>
<p id={ 'wp-block-page-list-modal__description' }>
{ __(
'To edit this navigation menu, convert it to single page links. This allows you to add, re-order, remove items, or edit their labels.'
) }
</p>
<p>
{ __(
"Note: if you add new pages to your site, you'll need to add them to your navigation menu."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paragraph won't be read out as part of the description, is that ok?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify this? I'm seeing it in VoiceOver:

Screenshot 2021-04-08 at 07 04 45

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, it's read out if you're manually arrowing through the whole content of the modal, but not when you first land on it. So, if you press "Edit", the modal opens and "Convert to links" is read out, followed by the first paragraph of the content. If user just presses tab after that, they'll skip through the buttons and never reach the rest of the text.

I guess that might be ok though, as the note just relays complementary info and isn't essential to understand what the "convert" action does.

) }
</p>
<div className="wp-block-page-list-modal-buttons">
<Button isTertiary onClick={ onClose }>
{ __( 'Cancel' ) }
</Button>
<Button
isPrimary
disabled={ ! pagesFinished }
onClick={ convertSelectedBlockToNavigationLinks( {
pages,
replaceBlock,
clientId,
createBlock: create,
} ) }
>
{ __( 'Convert' ) }
</Button>
</div>
</Modal>
);
}
80 changes: 74 additions & 6 deletions packages/block-library/src/page-list/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,34 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/

import { useBlockProps } from '@wordpress/block-editor';
import {
BlockControls,
useBlockProps,
store as blockEditorStore,
} from '@wordpress/block-editor';
import ServerSideRender from '@wordpress/server-side-render';
import { ToolbarButton } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useEffect, useState } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';

/**
* Internal dependencies
*/
import ConvertToLinksModal from './convert-to-links-modal';

// We only show the edit option when page count is <= MAX_PAGE_COUNT
// Performance of Navigation Links is not good past this value.
const MAX_PAGE_COUNT = 100;

export default function PageListEdit( { context } ) {
export default function PageListEdit( { context, clientId } ) {
const { textColor, backgroundColor, showSubmenuIcon, style } =
context || {};

const [ allowConvertToLinks, setAllowConvertToLinks ] = useState( false );

const blockProps = useBlockProps( {
className: classnames( {
'has-text-color': !! textColor,
Expand All @@ -25,9 +45,57 @@ export default function PageListEdit( { context } ) {
style: { ...style?.color },
} );

const isParentNavigation = useSelect(
( select ) => {
const { getBlockParentsByBlockName } = select( blockEditorStore );
return (
getBlockParentsByBlockName( clientId, 'core/navigation' )
.length > 0
);
},
[ clientId ]
);

useEffect( () => {
if ( isParentNavigation ) {
apiFetch( {
path: addQueryArgs( '/wp/v2/pages', {
per_page: 1,
_fields: [ 'id' ],
} ),
parse: false,
} ).then( ( res ) => {
setAllowConvertToLinks(
res.headers.get( 'X-WP-Total' ) <= MAX_PAGE_COUNT
);
} );
} else {
setAllowConvertToLinks( false );
}
}, [ isParentNavigation ] );

const [ isOpen, setOpen ] = useState( false );
const openModal = () => setOpen( true );
const closeModal = () => setOpen( false );

return (
<div { ...blockProps }>
<ServerSideRender block="core/page-list" />
</div>
<>
{ allowConvertToLinks && (
<BlockControls group="other">
<ToolbarButton title={ __( 'Edit' ) } onClick={ openModal }>
{ __( 'Edit' ) }
</ToolbarButton>
</BlockControls>
) }
{ allowConvertToLinks && isOpen && (
<ConvertToLinksModal
onClose={ closeModal }
clientId={ clientId }
/>
) }
<div { ...blockProps }>
<ServerSideRender block="core/page-list" />
</div>
</>
);
}
14 changes: 14 additions & 0 deletions packages/block-library/src/page-list/editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,17 @@
margin: 0.5em;
}
}

// Modal that shows conversion option.
.wp-block-page-list-modal {
max-width: 400px;
}

.wp-block-page-list-modal-buttons {
display: flex;
justify-content: flex-end;

.components-button {
margin-left: $grid-unit-15;
}
}
12 changes: 11 additions & 1 deletion packages/block-library/src/page-list/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,17 @@ function render_block_core_page_list( $attributes, $content, $block ) {
static $block_id = 0;
$block_id++;

$all_pages = get_pages( array( 'sort_column' => 'menu_order' ) );
// TODO: When https://core.trac.wordpress.org/ticket/39037 REST API support for multiple orderby values is resolved,
// update 'sort_column' to 'menu_order, post_title'. Sorting by both menu_order and post_title ensures a stable sort.
// Otherwise with pages that have the same menu_order value, we can see different ordering depending on how DB
// queries are constructed internally. For example we might see a different order when a limit is set to <499
// versus >= 500.
$all_pages = get_pages(
array(
'sort_column' => 'menu_order',
'order' => 'asc',
)
);

$top_level_pages = array();

Expand Down
Loading