-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Off Canvas Navigation Editor: Add Convert To Links Modal #45984
Changes from 25 commits
42f6a2c
0a29db5
6f9434d
430fe3e
d3db5d3
2560b0d
29331c4
865b4fa
076e9e7
47d7381
23f61be
3061b43
a8e93cc
6aebd4c
d9ff62e
77f1643
1c60ea0
0136c51
c8efdb3
5bd0cfc
411a961
d30e4a5
a7410e0
d970deb
8e44721
ba00268
997186f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,26 +2,208 @@ | |
* WordPress dependencies | ||
*/ | ||
import { edit } from '@wordpress/icons'; | ||
import { Button } from '@wordpress/components'; | ||
import { useDispatch } from '@wordpress/data'; | ||
import { forwardRef } from '@wordpress/element'; | ||
import { __ } from '@wordpress/i18n'; | ||
import { forwardRef, useMemo, useState } from '@wordpress/element'; | ||
import { Button, Modal } from '@wordpress/components'; | ||
import { useSelect, useDispatch } from '@wordpress/data'; | ||
import { createBlock as create } from '@wordpress/blocks'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { store as blockEditorStore } from '../../store'; | ||
|
||
// copied from packages/block-library/src/page-list/edit.js | ||
|
||
// 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; | ||
|
||
const usePageData = () => { | ||
// 1. Grab editor settings | ||
// 2. Call the selector when we need it | ||
const { pages } = useSelect( ( select ) => { | ||
const { getSettings } = select( blockEditorStore ); | ||
|
||
return { | ||
pages: getSettings().__experimentalFetchPageEntities( { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll need to test whether There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
orderby: 'menu_order', | ||
order: 'asc', | ||
_fields: [ 'id', 'link', 'parent', 'title', 'menu_order' ], | ||
per_page: -1, | ||
context: 'view', | ||
} ), | ||
}; | ||
}, [] ); | ||
|
||
return useMemo( () => { | ||
// TODO: Once the REST API supports passing multiple values to | ||
// 'orderby', this can be removed. | ||
// https://core.trac.wordpress.org/ticket/39037 | ||
const sortedPages = [ ...( pages ?? [] ) ].sort( ( a, b ) => { | ||
if ( a.menu_order === b.menu_order ) { | ||
return a.title.rendered.localeCompare( b.title.rendered ); | ||
} | ||
return a.menu_order - b.menu_order; | ||
} ); | ||
const pagesByParentId = sortedPages.reduce( ( accumulator, page ) => { | ||
const { parent } = page; | ||
if ( accumulator.has( parent ) ) { | ||
accumulator.get( parent ).push( page ); | ||
} else { | ||
accumulator.set( parent, [ page ] ); | ||
} | ||
return accumulator; | ||
}, new Map() ); | ||
|
||
return { | ||
pages, // necessary for access outside the hook | ||
pagesByParentId, | ||
totalPages: pages?.length ?? null, | ||
}; | ||
}, [ pages ] ); | ||
}; | ||
|
||
// copied from convert-to-links-modal.js | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please can we have the full file path? 🙏 |
||
const convertSelectedBlockToNavigationLinks = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's move this to it's own file. All the more reason to do so in order to keep in sync with |
||
( { pages, clientId, replaceBlock, createBlock } ) => | ||
() => { | ||
if ( ! pages?.length ) { | ||
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 ] ); | ||
} | ||
} ); | ||
|
||
// Transform all links with innerBlocks into Submenus. This can't be done | ||
// sooner because page objects have no information on their children. | ||
|
||
const transformSubmenus = ( listOfLinks ) => { | ||
listOfLinks.forEach( ( block, index, listOfLinksArray ) => { | ||
const { attributes, innerBlocks } = block; | ||
if ( innerBlocks.length !== 0 ) { | ||
transformSubmenus( innerBlocks ); | ||
const transformedBlock = createBlock( | ||
'core/navigation-submenu', | ||
attributes, | ||
innerBlocks | ||
); | ||
listOfLinksArray[ index ] = transformedBlock; | ||
} | ||
} ); | ||
}; | ||
|
||
transformSubmenus( navigationLinks ); | ||
|
||
replaceBlock( clientId, navigationLinks ); | ||
}; | ||
|
||
const ConvertToLinksModal = ( { onClose, clientId, pages } ) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this is copied from somewhere can we have a comment to specific exactly where? 🙏 |
||
const hasPages = !! pages?.length; | ||
|
||
const { replaceBlock } = useDispatch( blockEditorStore ); | ||
|
||
return ( | ||
<Modal | ||
closeLabel={ __( 'Close' ) } | ||
onRequestClose={ onClose } | ||
title={ __( 'Customize this menu' ) } | ||
className={ 'wp-block-page-list-modal' } | ||
aria={ { describedby: 'wp-block-page-list-modal__description' } } | ||
> | ||
<p id={ 'wp-block-page-list-modal__description' }> | ||
{ __( | ||
'This menu is automatically kept in sync with pages on your site. You can manage the menu yourself by clicking customize below.' | ||
) } | ||
</p> | ||
<div className="wp-block-page-list-modal-buttons"> | ||
<Button variant="tertiary" onClick={ onClose }> | ||
{ __( 'Cancel' ) } | ||
</Button> | ||
<Button | ||
variant="primary" | ||
disabled={ ! hasPages } | ||
onClick={ convertSelectedBlockToNavigationLinks( { | ||
pages, | ||
replaceBlock, | ||
clientId, | ||
createBlock: create, | ||
} ) } | ||
> | ||
{ __( 'Customize' ) } | ||
</Button> | ||
</div> | ||
</Modal> | ||
); | ||
}; | ||
|
||
export default forwardRef( function BlockEditButton( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In general I feel the Ideally these components need to stay as close to those in the canonical To this end I think we should consider
We'll still be coupling concepts like Pages to the block editor package (not good) but at least it will be easier to extract and refactor in future. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please take a look at the refactored version and let me know what you think. Moving |
||
{ clientId, ...props }, | ||
ref | ||
) { | ||
const { selectBlock } = useDispatch( blockEditorStore ); | ||
const [ convertModalOpen, setConvertModalOpen ] = useState( false ); | ||
const { pages, totalPages } = usePageData(); | ||
|
||
const block = useSelect( | ||
( select ) => { | ||
return select( blockEditorStore ).getBlock( clientId ); | ||
}, | ||
[ clientId ] | ||
); | ||
|
||
const allowConvertToLinks = | ||
'core/page-list' === block.name && totalPages <= MAX_PAGE_COUNT; | ||
getdave marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const onClick = () => { | ||
selectBlock( clientId ); | ||
georgeh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if ( allowConvertToLinks ) { | ||
setConvertModalOpen( ! convertModalOpen ); | ||
} else { | ||
selectBlock( clientId ); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this component should know anything about this concept. Lets lift this up and pass down as a prop. Perhaps if the |
||
}; | ||
|
||
return ( | ||
<Button { ...props } ref={ ref } icon={ edit } onClick={ onClick } /> | ||
<> | ||
{ convertModalOpen && ( | ||
<ConvertToLinksModal | ||
onClose={ () => setConvertModalOpen( false ) } | ||
clientId={ clientId } | ||
pages={ pages } | ||
/> | ||
) } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be lifted up. I don't think it needs to "live" with the edit button. |
||
<Button | ||
{ ...props } | ||
ref={ ref } | ||
icon={ edit } | ||
onClick={ onClick } | ||
/> | ||
</> | ||
); | ||
} ); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -126,6 +126,16 @@ export default function BlockEditor( { setIsInserterOpen } ) { | |
[ settingsBlockPatternCategories, restBlockPatternCategories ] | ||
); | ||
|
||
const { fetchPagesEntities } = useSelect( ( select ) => { | ||
const { getEntityRecords } = select( coreStore ); | ||
|
||
return { | ||
fetchPagesEntities: ( options = {} ) => { | ||
return getEntityRecords( 'postType', 'page', options ); | ||
}, | ||
}; | ||
}, [] ); | ||
|
||
const settings = useMemo( () => { | ||
const { | ||
__experimentalAdditionalBlockPatterns, | ||
|
@@ -138,8 +148,14 @@ export default function BlockEditor( { setIsInserterOpen } ) { | |
__unstableFetchMedia: fetchMedia, | ||
__experimentalBlockPatterns: blockPatterns, | ||
__experimentalBlockPatternCategories: blockPatternCategories, | ||
__experimentalFetchPageEntities: fetchPagesEntities, | ||
georgeh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}; | ||
}, [ storedSettings, blockPatterns, blockPatternCategories ] ); | ||
}, [ | ||
storedSettings, | ||
blockPatterns, | ||
blockPatternCategories, | ||
fetchPagesEntities, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think there's a chance this might cause a perf problem because I'm hoping the fact that |
||
] ); | ||
|
||
const [ blocks, onInput, onChange ] = useEntityBlockEditor( | ||
'postType', | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's move this to its own file.