-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Submenu nesting and saving new nested items. #21671
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
Changes from all commits
602c2ae
d71243b
eed638c
45c5d6a
8ad3ea8
817902a
51ff1a5
38f7f7e
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 |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| /** | ||
| * External dependencies | ||
| */ | ||
| import { isEqual, difference } from 'lodash'; | ||
| import { groupBy, isEqual, difference } from 'lodash'; | ||
|
|
||
| /** | ||
| * WordPress dependencies | ||
|
|
@@ -10,11 +10,15 @@ import { createBlock } from '@wordpress/blocks'; | |
| import { useSelect, useDispatch } from '@wordpress/data'; | ||
| import { useState, useRef, useEffect } from '@wordpress/element'; | ||
|
|
||
| function createBlockFromMenuItem( menuItem ) { | ||
| return createBlock( 'core/navigation-link', { | ||
| label: menuItem.title.raw, | ||
| url: menuItem.url, | ||
| } ); | ||
| function createBlockFromMenuItem( menuItem, innerBlocks = [] ) { | ||
| return createBlock( | ||
| 'core/navigation-link', | ||
| { | ||
| label: menuItem.title.rendered, | ||
| url: menuItem.url, | ||
| }, | ||
| innerBlocks | ||
| ); | ||
| } | ||
|
|
||
| function createMenuItemAttributesFromBlock( block ) { | ||
|
|
@@ -25,8 +29,10 @@ function createMenuItemAttributesFromBlock( block ) { | |
| } | ||
|
|
||
| export default function useNavigationBlocks( menuId ) { | ||
| // menuItems is an array of menu item objects. | ||
| const menuItems = useSelect( | ||
| ( select ) => select( 'core' ).getMenuItems( { menus: menuId } ), | ||
| ( select ) => | ||
| select( 'core' ).getMenuItems( { menus: menuId, per_page: -1 } ), | ||
| [ menuId ] | ||
| ); | ||
|
|
||
|
|
@@ -41,46 +47,76 @@ export default function useNavigationBlocks( menuId ) { | |
| return; | ||
| } | ||
|
|
||
| menuItemsRef.current = {}; | ||
| const itemsByParentID = groupBy( menuItems, 'parent' ); | ||
|
|
||
| const innerBlocks = []; | ||
| menuItemsRef.current = {}; | ||
|
|
||
| for ( const menuItem of menuItems ) { | ||
| const block = createBlockFromMenuItem( menuItem ); | ||
| menuItemsRef.current[ block.clientId ] = menuItem; | ||
| innerBlocks.push( block ); | ||
| } | ||
| const createMenuItemBlocks = ( items ) => { | ||
|
Member
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're going to want to unit test as much of this logic as possible. It's probably easiest to do that if we move the logic that turns a flat map of menu items into a tree of blocks out of the React hook and into a pure function that is name-exported. function createBlocksFromMenuItems( menuItems ) {
...
}
...
const { blocks, menuItemsByClientId } = createBlocksFromMenuItems( menuItems );
menuItemsRef.current = menuItemsByClientId;
setBlocks( [ createBlock( 'core/navigation', {}, blocks ) ] );This is kind of what I had in mind when I wrote Happy to defer this for when we do #21285. |
||
| const innerBlocks = []; | ||
| for ( const item of items ) { | ||
draganescu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| let menuItemInnerBlocks = []; | ||
| if ( itemsByParentID[ item.id ]?.length ) { | ||
| menuItemInnerBlocks = createMenuItemBlocks( | ||
| itemsByParentID[ item.id ] | ||
| ); | ||
| } | ||
| const block = createBlockFromMenuItem( | ||
| item, | ||
| menuItemInnerBlocks | ||
| ); | ||
| menuItemsRef.current[ block.clientId ] = item; | ||
| innerBlocks.push( block ); | ||
| } | ||
| return innerBlocks; | ||
| }; | ||
|
|
||
| // createMenuItemBlocks takes an array of top-level menu items and recursively creates all their innerBlocks | ||
| const innerBlocks = createMenuItemBlocks( itemsByParentID[ 0 ] ); | ||
| setBlocks( [ createBlock( 'core/navigation', {}, innerBlocks ) ] ); | ||
| }, [ menuItems ] ); | ||
|
|
||
| const saveBlocks = () => { | ||
| const { innerBlocks } = blocks[ 0 ]; | ||
|
|
||
| for ( const block of innerBlocks ) { | ||
| const menuItem = menuItemsRef.current[ block.clientId ]; | ||
|
|
||
| if ( ! menuItem ) { | ||
| saveMenuItem( { | ||
| ...createMenuItemAttributesFromBlock( block ), | ||
| menus: menuId, | ||
| } ); | ||
| continue; | ||
| const { clientId, innerBlocks } = blocks[ 0 ]; | ||
| const parentItemId = menuItemsRef.current[ clientId ]?.parent; | ||
|
|
||
| const saveNestedBlocks = async ( nestedBlocks, parentId = 0 ) => { | ||
| for ( const block of nestedBlocks ) { | ||
| const menuItem = menuItemsRef.current[ block.clientId ]; | ||
| let currentItemId = menuItem?.id || 0; | ||
|
|
||
| if ( ! menuItem ) { | ||
| const savedItem = await saveMenuItem( { | ||
| ...createMenuItemAttributesFromBlock( block ), | ||
| menus: menuId, | ||
| parent: parentId, | ||
| } ); | ||
| if ( block.innerBlocks.length ) { | ||
| currentItemId = savedItem.id; | ||
| } | ||
| } | ||
|
|
||
| if ( | ||
| menuItem && | ||
| ! isEqual( | ||
| block.attributes, | ||
| createBlockFromMenuItem( menuItem ).attributes | ||
| ) | ||
| ) { | ||
| saveMenuItem( { | ||
| ...menuItem, | ||
| ...createMenuItemAttributesFromBlock( block ), | ||
| menus: menuId, // Gotta do this because REST API doesn't like receiving an array here. Maybe a bug in the REST API? | ||
|
Member
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 we need to set
Contributor
Author
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. Good question; it doesn't appear to be possible to move items between nesting levels at the moment. Also, I just realised that nav items can be dragged by the handle from the top toolbar and the experience is... not delightful 🙀
Contributor
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 should still send parent even if our UI is missing proper nesting, I think! |
||
| parent: parentId, | ||
| } ); | ||
| } | ||
|
|
||
| if ( block.innerBlocks.length ) { | ||
| saveNestedBlocks( block.innerBlocks, currentItemId ); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| if ( | ||
| ! isEqual( | ||
| block.attributes, | ||
| createBlockFromMenuItem( menuItem ).attributes | ||
| ) | ||
| ) { | ||
| saveMenuItem( { | ||
| ...menuItem, | ||
| ...createMenuItemAttributesFromBlock( block ), | ||
| menus: menuId, // Gotta do this because REST API doesn't like receiving an array here. Maybe a bug in the REST API? | ||
| } ); | ||
| } | ||
| } | ||
| saveNestedBlocks( innerBlocks, parentItemId ); | ||
|
|
||
| const deletedClientIds = difference( | ||
| Object.keys( menuItemsRef.current ), | ||
|
|
@@ -89,7 +125,7 @@ export default function useNavigationBlocks( menuId ) { | |
|
|
||
| // Disable reason, this code will eventually be implemented. | ||
| // eslint-disable-next-line no-unused-vars | ||
| for ( const clientId of deletedClientIds ) { | ||
| for ( const deletedClientId of deletedClientIds ) { | ||
|
Contributor
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 revert this line as it will conflict with the PR that implements this ( or tries to :) )
Contributor
Author
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 I do that, it fails the lint check because |
||
| // TODO - delete menu items. | ||
| } | ||
| }; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.