diff --git a/packages/block-library/src/navigation/create-data-tree.js b/packages/block-library/src/navigation/create-data-tree.js new file mode 100644 index 00000000000000..255c5a7b18da72 --- /dev/null +++ b/packages/block-library/src/navigation/create-data-tree.js @@ -0,0 +1,39 @@ +/** + * Creates a nested, hierarchical tree representation from unstructured data that + * has an inherent relationship defined between individual items. + * + * For example, by default, each element in the dataset should have an `id` and + * `parent` property where the `parent` property indicates a relationship between + * the current item and another item with a matching `id` properties. + * + * This is useful for building linked lists of data from flat data structures. + * + * @param {Array} dataset linked data to be rearranged into a hierarchical tree based on relational fields. + * @param {string} id the property which uniquely identifies each entry within the array. + * @param {*} relation the property which identifies how the current item is related to other items in the data (if at all). + * @return {Array} a nested array of parent/child relationships + */ +function createDataTree( dataset, id = 'id', relation = 'parent' ) { + const hashTable = Object.create( null ); + const dataTree = []; + + for ( const data of dataset ) { + hashTable[ data[ id ] ] = { + ...data, + children: [], + }; + } + for ( const data of dataset ) { + if ( data[ relation ] ) { + hashTable[ data[ relation ] ].children.push( + hashTable[ data[ id ] ] + ); + } else { + dataTree.push( hashTable[ data[ id ] ] ); + } + } + + return dataTree; +} + +export default createDataTree; diff --git a/packages/block-library/src/navigation/edit.js b/packages/block-library/src/navigation/edit.js index e252c1c0069ede..9019aec823f983 100644 --- a/packages/block-library/src/navigation/edit.js +++ b/packages/block-library/src/navigation/edit.js @@ -7,7 +7,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { useMemo, Fragment, useRef } from '@wordpress/element'; +import { useMemo, useState, useRef, useCallback } from '@wordpress/element'; import { InnerBlocks, InspectorControls, @@ -33,6 +33,7 @@ import { ToggleControl, Toolbar, ToolbarGroup, + CustomSelectControl, } from '@wordpress/components'; import { compose } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; @@ -44,6 +45,20 @@ import { navigation as icon } from '@wordpress/icons'; import useBlockNavigator from './use-block-navigator'; import BlockColorsStyleSelector from './block-colors-selector'; import * as navIcons from './icons'; +import createDataTree from './create-data-tree'; + +// Constants +const CREATE_EMPTY_OPTION_VALUE = '__CREATE_EMPTY__'; +const CREATE_FROM_PAGES_OPTION_VALUE = '__CREATE_FROM_PAGES__'; +const CREATE_PLACEHOLDER_VALUE = '__CREATE_PLACEHOLDER__'; + +function LoadingSpinner() { + return ( + <> + { __( 'Loading…' ) } + + ); +} function Navigation( { selectedBlockHasDescendants, @@ -54,8 +69,13 @@ function Navigation( { hasResolvedPages, isImmediateParentOfSelectedBlock, isRequestingPages, + getHasResolvedMenuItems, + hasResolvedMenus, + isRequestingMenus, isSelected, pages, + menus, + getMenuItems, setAttributes, setFontSize, updateNavItemBlocks, @@ -64,8 +84,11 @@ function Navigation( { // // HOOKS // - const ref = useRef(); + const [ + selectedCreateActionOption, + setSelectedCreateActionOption, + ] = useState( null ); const { selectBlock } = useDispatch( 'core/block-editor' ); const { TextColor, BackgroundColor, ColorPanel } = __experimentalUseColors( [ @@ -97,8 +120,11 @@ function Navigation( { clientId ); + const isRequestingEntities = isRequestingPages || isRequestingMenus; + const selectedCreateActionOptionKey = selectedCreateActionOption?.key; + // Builds navigation links from default Pages. - const defaultPagesNavigationItems = useMemo( () => { + const buildNavLinkBlocksFromPages = useMemo( () => { if ( ! pages ) { return null; } @@ -116,6 +142,47 @@ function Navigation( { ); }, [ pages ] ); + const menuItems = getMenuItems( selectedCreateActionOptionKey ); + + // Builds navigation links from selected Menu's items. + const buildNavLinkBlocksFromMenuItems = useMemo( () => { + if ( ! menuItems ) { + return null; + } + + function initialiseBlocks( nodes ) { + return nodes.map( ( { title, type, link: url, id, children } ) => { + const innerBlocks = + children && children.length + ? initialiseBlocks( children ) + : []; + + return createBlock( + 'core/navigation-link', + { + type, + id, + url, + label: ! title.rendered + ? __( '(no title)' ) + : escape( title.rendered ), + opensInNewTab: false, + }, + innerBlocks + ); + } ); + } + + const menuTree = createDataTree( menuItems ); + + const menuBlocksTree = initialiseBlocks( menuTree ); + + return menuBlocksTree; + }, [ menuItems ] ); + + const hasPages = !! ( hasResolvedPages && pages?.length ); + const hasMenus = !! ( hasResolvedMenus && menus?.length ); + // // HANDLERS // @@ -135,19 +202,141 @@ function Navigation( { } function handleCreateFromExistingPages() { - updateNavItemBlocks( defaultPagesNavigationItems ); + updateNavItemBlocks( buildNavLinkBlocksFromPages ); selectBlock( clientId ); } - const hasPages = hasResolvedPages && pages && pages.length; + function handleCreateFromExistingMenu() { + updateNavItemBlocks( buildNavLinkBlocksFromMenuItems ); + selectBlock( clientId ); + } - const blockInlineStyles = { - fontSize: fontSize.size ? fontSize.size + 'px' : undefined, - }; + function handleCreate() { + const { key } = selectedCreateActionOption; + + // Explicity request to create empty. + if ( key === CREATE_EMPTY_OPTION_VALUE ) { + return handleCreateEmpty(); + } + + // Create from Pages. + if ( hasPages && key === CREATE_FROM_PAGES_OPTION_VALUE ) { + return handleCreateFromExistingPages(); + } + + // Create from WP Menu (if exists and not empty). + if ( + hasMenus && + selectedCreateActionOption && + buildNavLinkBlocksFromMenuItems?.length + ) { + return handleCreateFromExistingMenu(); + } + + // Default to empty menu + return handleCreateEmpty(); + } + + const buildPlaceholderInstructionText = useCallback( () => { + if ( isRequestingEntities ) { + return ''; + } + + if ( hasMenus && hasPages ) { + return __( + 'Create a navigation from all existing pages, or choose a menu.' + ); + } + + if ( ! hasMenus && ! hasPages ) { + return __( 'Create an empty navigation.' ); + } + + if ( hasMenus && ! hasPages ) { + return __( 'Create a navigation from a menu or create empty.' ); + } + + if ( ! hasMenus && hasPages ) { + return __( + 'Create a navigation from all existing pages, or create empty.' + ); + } + }, [ isRequestingEntities, hasMenus, hasPages ] ); - // If we don't have existing items or the User hasn't - // indicated they want to automatically add top level Pages - // then show the Placeholder + const createActionOptions = useMemo( + () => [ + { + id: CREATE_PLACEHOLDER_VALUE, + name: __( 'Select where to start from…' ), + }, + ...( hasMenus ? menus : [] ), + { + id: CREATE_EMPTY_OPTION_VALUE, + name: __( 'Create empty menu' ), + className: 'is-create-empty-option', + }, + ...( hasPages + ? [ + { + id: CREATE_FROM_PAGES_OPTION_VALUE, + name: __( 'New from all top-level pages' ), + }, + ] + : [] ), + ], + [ + CREATE_PLACEHOLDER_VALUE, + CREATE_EMPTY_OPTION_VALUE, + CREATE_FROM_PAGES_OPTION_VALUE, + hasMenus, + menus, + hasPages, + ] + ); + + const shouldDisableCreateButton = useCallback( () => { + // If there is no key at all then disable. + if ( ! selectedCreateActionOptionKey ) { + return true; + } + + // Always disable if the default "placeholder" option is selected. + if ( selectedCreateActionOptionKey === CREATE_PLACEHOLDER_VALUE ) { + return true; + } + + // Always enable if Create Empty is selected. + if ( selectedCreateActionOptionKey === CREATE_EMPTY_OPTION_VALUE ) { + return false; + } + + // Enable if Pages option selected and we have Pages available. + if ( + selectedCreateActionOptionKey === CREATE_FROM_PAGES_OPTION_VALUE && + hasResolvedPages + ) { + return false; + } + + // Only "menu" options use an integer based key. + const selectedOptionIsMenu = Number.isInteger( + selectedCreateActionOptionKey + ); + + const menuItemsResolved = + selectedOptionIsMenu && + getHasResolvedMenuItems( selectedCreateActionOptionKey ); + + return ! menuItemsResolved; + }, [ + selectedCreateActionOptionKey, + hasResolvedPages, + CREATE_PLACEHOLDER_VALUE, + CREATE_EMPTY_OPTION_VALUE, + CREATE_FROM_PAGES_OPTION_VALUE, + ] ); + + // If we don't have existing items then show the Placeholder if ( ! hasExistingNavItems ) { return ( @@ -155,36 +344,75 @@ function Navigation( { className="wp-block-navigation-placeholder" icon={ icon } label={ __( 'Navigation' ) } - instructions={ __( - 'Create a Navigation from all existing pages, or create an empty one.' - ) } + instructions={ buildPlaceholderInstructionText() } > -
- - -
+ ) : ( +
- { __( 'Create empty' ) } - -
+ <> + { + if ( + selectedItem?.key === + selectedCreateActionOptionKey + ) { + return; + } + setSelectedCreateActionOption( + selectedItem + ); + } } + options={ createActionOptions.map( + ( option ) => { + return { + ...option, + key: option.id, + }; + } + ) } + /> + + + + ) }
); } + const blockInlineStyles = { + fontSize: fontSize.size ? fontSize.size + 'px' : undefined, + }; + const blockClassNames = classnames( className, { [ `items-justified-${ attributes.itemsJustification }` ]: attributes.itemsJustification, [ fontSize.class ]: fontSize.class, @@ -193,7 +421,7 @@ function Navigation( { // UI State: rendered Block UI return ( - + <> - { ! hasExistingNavItems && isRequestingPages && ( - <> - { __( 'Loading Navigation…' ) }{ ' ' } - + { ! hasExistingNavItems && isRequestingEntities && ( + ) } - + ); } @@ -334,6 +560,8 @@ export default compose( [ selectedBlockId, ] )?.length; + const menusQuery = { per_page: -1 }; + return { isImmediateParentOfSelectedBlock, selectedBlockHasDescendants, @@ -343,9 +571,50 @@ export default compose( [ 'page', filterDefaultPages ), + menus: select( 'core' ).getMenus( menusQuery ), + isRequestingMenus: select( 'core' ).isResolving( 'getMenus', [ + menusQuery, + ] ), + hasResolvedMenus: select( + 'core' + ).hasFinishedResolution( 'getMenus', [ menusQuery ] ), + getMenuItems: ( menuId ) => { + if ( ! menuId ) { + return false; + } + + // If the option is a placeholder or doesn't have a valid + // id then reject + if ( ! Number.isInteger( menuId ) ) { + return false; + } + + return select( 'core' ).getMenuItems( { + menus: menuId, + per_page: -1, + } ); + }, + getIsRequestingMenuItems: ( menuId ) => { + return select( 'core' ).isResolving( 'getMenuItems', [ + { + menus: menuId, + per_page: -1, + }, + ] ); + }, + getHasResolvedMenuItems: ( menuId ) => { + return select( 'core' ).hasFinishedResolution( 'getMenuItems', [ + { + menus: menuId, + per_page: -1, + }, + ] ); + }, + isRequestingPages: select( 'core/data' ).isResolving( ...pagesSelect ), + hasResolvedPages: select( 'core/data' ).hasFinishedResolution( ...pagesSelect ), @@ -354,6 +623,9 @@ export default compose( [ withDispatch( ( dispatch, { clientId } ) => { return { updateNavItemBlocks( blocks ) { + if ( blocks?.length === 0 ) { + return false; + } dispatch( 'core/block-editor' ).replaceInnerBlocks( clientId, blocks diff --git a/packages/block-library/src/navigation/style.scss b/packages/block-library/src/navigation/style.scss index c01d12d90e4377..a56689a123268d 100644 --- a/packages/block-library/src/navigation/style.scss +++ b/packages/block-library/src/navigation/style.scss @@ -169,3 +169,66 @@ .items-justified-right > ul { justify-content: flex-end; } + +.wp-block-navigation-placeholder__actions { + display: flex; + align-items: flex-start; + + .components-base-control { + margin-right: 5px; + } + + .components-select-control__input { + margin: 0; + min-height: 36px; // match button height + } + + .wp-block-navigation-placeholder__button { + height: 36px; + } +} + +.wp-block-navigation-placeholder { + + .components-spinner { + margin-top: -4px; + margin-left: 4px; + vertical-align: middle; + margin-right: 7px; + } + .components-custom-select-control__button { + height: auto; // overide default button styles + margin-bottom: 0; // cancel bottom margin + min-width: 220px; // avoids control size jump depending on selection + } + + // Styles for when there are Menus present in the dropdown. + .components-custom-select-control.has-menus .components-custom-select-control__item { + display: list-item; // remove flex to allow pseudo element to stack + padding: 10px 25px; + + // Creates a faux divider between the menu options if present + &.is-create-empty-option { + position: relative; // positioning context for pseudo + margin-top: 20px; + + &::before { + content: ""; + position: absolute; // take out of flow to retain size of list item + top: -10px; + left: 25px; // match list item padding + right: 25px; // match list item padding + display: list-item; + height: 15px; + border-top: 1px solid $medium-gray-text; + } + } + } + + .components-custom-select-control__menu { + margin-top: 3px; // match gap in design + font-family: $default-font; + font-size: $default-font-size; + } + +} diff --git a/packages/components/src/custom-select-control/style.scss b/packages/components/src/custom-select-control/style.scss index ad528085052f3e..73e7acdf117821 100644 --- a/packages/components/src/custom-select-control/style.scss +++ b/packages/components/src/custom-select-control/style.scss @@ -10,7 +10,6 @@ .components-custom-select-control__button { border: 1px solid $medium-gray-text; border-radius: $radius-block-ui; - display: inline; min-height: 30px; min-width: 130px; position: relative; diff --git a/packages/e2e-tests/specs/experiments/__snapshots__/navigation.test.js.snap b/packages/e2e-tests/specs/experiments/__snapshots__/navigation.test.js.snap index e41ca54dee0304..c575b3297d2a23 100644 --- a/packages/e2e-tests/specs/experiments/__snapshots__/navigation.test.js.snap +++ b/packages/e2e-tests/specs/experiments/__snapshots__/navigation.test.js.snap @@ -1,14 +1,50 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Navigation allows a navigation menu to be created from an empty menu using a mixture of internal and external links 1`] = ` +exports[`Navigation Creating from existing Menus allows a navigation block to be created from existing menus 1`] = ` " - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +" +`; + +exports[`Navigation Creating from existing Menus creates an empty navigation block when the selected existing menu is also empty 1`] = ` +" + " `; -exports[`Navigation allows a navigation menu to be created using existing pages 1`] = ` +exports[`Navigation Creating from existing Menus does not display option to create from existing menus if there are no menus 1`] = `""`; + +exports[`Navigation Creating from existing Pages allows a navigation block to be created using existing pages 1`] = ` " @@ -18,6 +54,16 @@ exports[`Navigation allows a navigation menu to be created using existing pages " `; +exports[`Navigation Creating from existing Pages does not display option to create from existing Pages if there are no Pages 1`] = `""`; + +exports[`Navigation allows an empty navigation block to be created and manually populated using a mixture of internal and external links 1`] = ` +" + + + +" +`; + exports[`Navigation allows pages to be created from the navigation block and their links added to menu 1`] = ` " diff --git a/packages/e2e-tests/specs/experiments/fixtures/menu-items-response-fixture.json b/packages/e2e-tests/specs/experiments/fixtures/menu-items-response-fixture.json new file mode 100644 index 00000000000000..5bf5baa7c228a3 --- /dev/null +++ b/packages/e2e-tests/specs/experiments/fixtures/menu-items-response-fixture.json @@ -0,0 +1,1357 @@ +[ + { + "id": 94, + "title": { + "raw": "Home", + "rendered": "Home" + }, + "status": "publish", + "url": "http:\/\/localhost:8889\/", + "attr_title": "", + "description": "", + "type": "custom", + "type_label": "Custom Link", + "object": "custom", + "object_id": 94, + "parent": 0, + "menu_order": 1, + "target": "", + "classes": [ + "" + ], + "xfn": [ + "" + ], + "meta": [], + "menus": [ + 23 + ], + "_links": { + "self": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/94" + } + ], + "collection": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items" + } + ], + "about": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/types\/nav_menu_item" + } + ], + "wp:term": [ + { + "taxonomy": "nav_menu", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=94" + } + ], + "wp:action-publish": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/94" + } + ], + "wp:action-unfiltered-html": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/94" + } + ], + "wp:action-create-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/94" + } + ], + "wp:action-assign-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/94" + } + ], + "curies": [ + { + "name": "wp", + "href": "https:\/\/api.w.org\/{rel}", + "templated": true + } + ] + } + }, + { + "id": 95, + "title": { + "raw": "", + "rendered": "Accusamus quo repellat illum magnam quas" + }, + "status": "publish", + "url": "http:\/\/localhost:8889\/?page_id=41", + "attr_title": "", + "description": "", + "type": "post_type", + "type_label": "Page", + "object": "page", + "object_id": 41, + "parent": 0, + "menu_order": 2, + "target": "", + "classes": [ + "" + ], + "xfn": [ + "" + ], + "meta": [], + "menus": [ + 23 + ], + "_links": { + "self": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/95" + } + ], + "collection": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items" + } + ], + "about": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/types\/nav_menu_item" + } + ], + "wp:term": [ + { + "taxonomy": "nav_menu", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=95" + } + ], + "wp:object": [ + { + "post_type": "post_type", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/pages\/41" + } + ], + "wp:action-publish": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/95" + } + ], + "wp:action-unfiltered-html": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/95" + } + ], + "wp:action-create-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/95" + } + ], + "wp:action-assign-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/95" + } + ], + "curies": [ + { + "name": "wp", + "href": "https:\/\/api.w.org\/{rel}", + "templated": true + } + ] + } + }, + { + "id": 96, + "title": { + "raw": "", + "rendered": "Debitis cum consequatur sit doloremque" + }, + "status": "publish", + "url": "http:\/\/localhost:8889\/?page_id=51", + "attr_title": "", + "description": "", + "type": "post_type", + "type_label": "Page", + "object": "page", + "object_id": 51, + "parent": 95, + "menu_order": 3, + "target": "", + "classes": [ + "" + ], + "xfn": [ + "" + ], + "meta": [], + "menus": [ + 23 + ], + "_links": { + "self": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/96" + } + ], + "collection": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items" + } + ], + "about": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/types\/nav_menu_item" + } + ], + "wp:term": [ + { + "taxonomy": "nav_menu", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=96" + } + ], + "wp:object": [ + { + "post_type": "post_type", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/pages\/51" + } + ], + "wp:action-publish": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/96" + } + ], + "wp:action-unfiltered-html": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/96" + } + ], + "wp:action-create-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/96" + } + ], + "wp:action-assign-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/96" + } + ], + "curies": [ + { + "name": "wp", + "href": "https:\/\/api.w.org\/{rel}", + "templated": true + } + ] + } + }, + { + "id": 97, + "title": { + "raw": "", + "rendered": "Est ea vero non nihil officiis in" + }, + "status": "publish", + "url": "http:\/\/localhost:8889\/?page_id=53", + "attr_title": "", + "description": "", + "type": "post_type", + "type_label": "Page", + "object": "page", + "object_id": 53, + "parent": 0, + "menu_order": 4, + "target": "", + "classes": [ + "" + ], + "xfn": [ + "" + ], + "meta": [], + "menus": [ + 23 + ], + "_links": { + "self": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/97" + } + ], + "collection": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items" + } + ], + "about": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/types\/nav_menu_item" + } + ], + "wp:term": [ + { + "taxonomy": "nav_menu", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=97" + } + ], + "wp:object": [ + { + "post_type": "post_type", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/pages\/53" + } + ], + "wp:action-publish": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/97" + } + ], + "wp:action-unfiltered-html": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/97" + } + ], + "wp:action-create-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/97" + } + ], + "wp:action-assign-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/97" + } + ], + "curies": [ + { + "name": "wp", + "href": "https:\/\/api.w.org\/{rel}", + "templated": true + } + ] + } + }, + { + "id": 98, + "title": { + "raw": "", + "rendered": "Fuga odio quis tempora" + }, + "status": "publish", + "url": "http:\/\/localhost:8889\/?page_id=56", + "attr_title": "", + "description": "", + "type": "post_type", + "type_label": "Page", + "object": "page", + "object_id": 56, + "parent": 97, + "menu_order": 5, + "target": "", + "classes": [ + "" + ], + "xfn": [ + "" + ], + "meta": [], + "menus": [ + 23 + ], + "_links": { + "self": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/98" + } + ], + "collection": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items" + } + ], + "about": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/types\/nav_menu_item" + } + ], + "wp:term": [ + { + "taxonomy": "nav_menu", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=98" + } + ], + "wp:object": [ + { + "post_type": "post_type", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/pages\/56" + } + ], + "wp:action-publish": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/98" + } + ], + "wp:action-unfiltered-html": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/98" + } + ], + "wp:action-create-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/98" + } + ], + "wp:action-assign-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/98" + } + ], + "curies": [ + { + "name": "wp", + "href": "https:\/\/api.w.org\/{rel}", + "templated": true + } + ] + } + }, + { + "id": 99, + "title": { + "raw": "", + "rendered": "In consectetur repellendus eveniet maiores aperiam" + }, + "status": "publish", + "url": "http:\/\/localhost:8889\/?page_id=15", + "attr_title": "", + "description": "", + "type": "post_type", + "type_label": "Page", + "object": "page", + "object_id": 15, + "parent": 98, + "menu_order": 6, + "target": "", + "classes": [ + "" + ], + "xfn": [ + "" + ], + "meta": [], + "menus": [ + 23 + ], + "_links": { + "self": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/99" + } + ], + "collection": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items" + } + ], + "about": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/types\/nav_menu_item" + } + ], + "wp:term": [ + { + "taxonomy": "nav_menu", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=99" + } + ], + "wp:object": [ + { + "post_type": "post_type", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/pages\/15" + } + ], + "wp:action-publish": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/99" + } + ], + "wp:action-unfiltered-html": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/99" + } + ], + "wp:action-create-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/99" + } + ], + "wp:action-assign-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/99" + } + ], + "curies": [ + { + "name": "wp", + "href": "https:\/\/api.w.org\/{rel}", + "templated": true + } + ] + } + }, + { + "id": 100, + "title": { + "raw": "", + "rendered": "Mollitia maiores consequatur ea dolorem blanditiis" + }, + "status": "publish", + "url": "http:\/\/localhost:8889\/?page_id=45", + "attr_title": "", + "description": "", + "type": "post_type", + "type_label": "Page", + "object": "page", + "object_id": 45, + "parent": 99, + "menu_order": 7, + "target": "", + "classes": [ + "" + ], + "xfn": [ + "" + ], + "meta": [], + "menus": [ + 23 + ], + "_links": { + "self": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/100" + } + ], + "collection": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items" + } + ], + "about": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/types\/nav_menu_item" + } + ], + "wp:term": [ + { + "taxonomy": "nav_menu", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=100" + } + ], + "wp:object": [ + { + "post_type": "post_type", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/pages\/45" + } + ], + "wp:action-publish": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/100" + } + ], + "wp:action-unfiltered-html": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/100" + } + ], + "wp:action-create-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/100" + } + ], + "wp:action-assign-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/100" + } + ], + "curies": [ + { + "name": "wp", + "href": "https:\/\/api.w.org\/{rel}", + "templated": true + } + ] + } + }, + { + "id": 101, + "title": { + "raw": "", + "rendered": "Necessitatibus nisi qui qui necessitatibus quaerat possimus" + }, + "status": "publish", + "url": "http:\/\/localhost:8889\/?page_id=27", + "attr_title": "", + "description": "", + "type": "post_type", + "type_label": "Page", + "object": "page", + "object_id": 27, + "parent": 100, + "menu_order": 8, + "target": "", + "classes": [ + "" + ], + "xfn": [ + "" + ], + "meta": [], + "menus": [ + 23 + ], + "_links": { + "self": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/101" + } + ], + "collection": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items" + } + ], + "about": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/types\/nav_menu_item" + } + ], + "wp:term": [ + { + "taxonomy": "nav_menu", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=101" + } + ], + "wp:object": [ + { + "post_type": "post_type", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/pages\/27" + } + ], + "wp:action-publish": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/101" + } + ], + "wp:action-unfiltered-html": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/101" + } + ], + "wp:action-create-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/101" + } + ], + "wp:action-assign-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/101" + } + ], + "curies": [ + { + "name": "wp", + "href": "https:\/\/api.w.org\/{rel}", + "templated": true + } + ] + } + }, + { + "id": 102, + "title": { + "raw": "", + "rendered": "Nulla omnis autem dolores eligendi" + }, + "status": "publish", + "url": "http:\/\/localhost:8889\/?page_id=43", + "attr_title": "", + "description": "", + "type": "post_type", + "type_label": "Page", + "object": "page", + "object_id": 43, + "parent": 0, + "menu_order": 9, + "target": "", + "classes": [ + "" + ], + "xfn": [ + "" + ], + "meta": [], + "menus": [ + 23 + ], + "_links": { + "self": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/102" + } + ], + "collection": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items" + } + ], + "about": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/types\/nav_menu_item" + } + ], + "wp:term": [ + { + "taxonomy": "nav_menu", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=102" + } + ], + "wp:object": [ + { + "post_type": "post_type", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/pages\/43" + } + ], + "wp:action-publish": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/102" + } + ], + "wp:action-unfiltered-html": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/102" + } + ], + "wp:action-create-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/102" + } + ], + "wp:action-assign-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/102" + } + ], + "curies": [ + { + "name": "wp", + "href": "https:\/\/api.w.org\/{rel}", + "templated": true + } + ] + } + }, + { + "id": 103, + "title": { + "raw": "", + "rendered": "Sample Page" + }, + "status": "publish", + "url": "http:\/\/localhost:8889\/?page_id=2", + "attr_title": "", + "description": "", + "type": "post_type", + "type_label": "Page", + "object": "page", + "object_id": 2, + "parent": 0, + "menu_order": 10, + "target": "", + "classes": [ + "" + ], + "xfn": [ + "" + ], + "meta": [], + "menus": [ + 23 + ], + "_links": { + "self": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/103" + } + ], + "collection": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items" + } + ], + "about": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/types\/nav_menu_item" + } + ], + "wp:term": [ + { + "taxonomy": "nav_menu", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=103" + } + ], + "wp:object": [ + { + "post_type": "post_type", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/pages\/2" + } + ], + "wp:action-publish": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/103" + } + ], + "wp:action-unfiltered-html": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/103" + } + ], + "wp:action-create-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/103" + } + ], + "wp:action-assign-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/103" + } + ], + "curies": [ + { + "name": "wp", + "href": "https:\/\/api.w.org\/{rel}", + "templated": true + } + ] + } + }, + { + "id": 104, + "title": { + "raw": "", + "rendered": "Beatae qui labore voluptas eveniet officia quia voluptas qui porro sequi et aut est" + }, + "status": "publish", + "url": "http:\/\/localhost:8889\/?cat=7", + "attr_title": "", + "description": "Ratione nemo ut aut ullam sed assumenda quis est exercitationem", + "type": "taxonomy", + "type_label": "Category", + "object": "category", + "object_id": 7, + "parent": 0, + "menu_order": 11, + "target": "", + "classes": [ + "" + ], + "xfn": [ + "" + ], + "meta": [], + "menus": [ + 23 + ], + "_links": { + "self": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/104" + } + ], + "collection": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items" + } + ], + "about": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/types\/nav_menu_item" + } + ], + "wp:term": [ + { + "taxonomy": "nav_menu", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=104" + } + ], + "wp:object": [ + { + "taxonomy": "taxonomy", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/categories\/7" + } + ], + "wp:action-publish": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/104" + } + ], + "wp:action-unfiltered-html": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/104" + } + ], + "wp:action-create-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/104" + } + ], + "wp:action-assign-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/104" + } + ], + "curies": [ + { + "name": "wp", + "href": "https:\/\/api.w.org\/{rel}", + "templated": true + } + ] + } + }, + { + "id": 105, + "title": { + "raw": "", + "rendered": "Et minus itaque velit tempore hic quisquam saepe quas asperiores" + }, + "status": "publish", + "url": "http:\/\/localhost:8889\/?cat=19", + "attr_title": "", + "description": "Vel fuga enim rerum perspiciatis sapiente mollitia magni ut molestiae labore quae quia quia libero perspiciatis voluptatem quidem deleniti eveniet laboriosam doloribus dolor laborum accusantium modi ducimus itaque rerum cum nostrum", + "type": "taxonomy", + "type_label": "Category", + "object": "category", + "object_id": 19, + "parent": 104, + "menu_order": 12, + "target": "", + "classes": [ + "" + ], + "xfn": [ + "" + ], + "meta": [], + "menus": [ + 23 + ], + "_links": { + "self": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/105" + } + ], + "collection": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items" + } + ], + "about": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/types\/nav_menu_item" + } + ], + "wp:term": [ + { + "taxonomy": "nav_menu", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=105" + } + ], + "wp:object": [ + { + "taxonomy": "taxonomy", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/categories\/19" + } + ], + "wp:action-publish": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/105" + } + ], + "wp:action-unfiltered-html": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/105" + } + ], + "wp:action-create-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/105" + } + ], + "wp:action-assign-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/105" + } + ], + "curies": [ + { + "name": "wp", + "href": "https:\/\/api.w.org\/{rel}", + "templated": true + } + ] + } + }, + { + "id": 106, + "title": { + "raw": "", + "rendered": "Et quas a et mollitia et voluptas optio voluptate quia quo unde aut in nostrum iste impedit quisquam id aut" + }, + "status": "publish", + "url": "http:\/\/localhost:8889\/?cat=6", + "attr_title": "", + "description": "Quas sit labore earum omnis eos sint iste est possimus harum aut soluta sint optio quos distinctio inventore voluptate non ut aliquam ad ut voluptates fugiat numquam magnam modi repellendus modi laudantium et debitis officia est voluptatum quidem unde molestiae animi vero fuga accusamus nam", + "type": "taxonomy", + "type_label": "Category", + "object": "category", + "object_id": 6, + "parent": 105, + "menu_order": 13, + "target": "", + "classes": [ + "" + ], + "xfn": [ + "" + ], + "meta": [], + "menus": [ + 23 + ], + "_links": { + "self": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/106" + } + ], + "collection": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items" + } + ], + "about": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/types\/nav_menu_item" + } + ], + "wp:term": [ + { + "taxonomy": "nav_menu", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=106" + } + ], + "wp:object": [ + { + "taxonomy": "taxonomy", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/categories\/6" + } + ], + "wp:action-publish": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/106" + } + ], + "wp:action-unfiltered-html": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/106" + } + ], + "wp:action-create-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/106" + } + ], + "wp:action-assign-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/106" + } + ], + "curies": [ + { + "name": "wp", + "href": "https:\/\/api.w.org\/{rel}", + "templated": true + } + ] + } + }, + { + "id": 107, + "title": { + "raw": "", + "rendered": "Illo quis sit impedit itaque expedita earum deserunt magni doloremque velit eum id error" + }, + "status": "publish", + "url": "http:\/\/localhost:8889\/?cat=16", + "attr_title": "", + "description": "Doloremque vero sunt officiis iste voluptatibus voluptas molestiae sint asperiores recusandae amet praesentium et explicabo nesciunt similique voluptatum laudantium amet officiis quas distinctio quis enim nihil tempora", + "type": "taxonomy", + "type_label": "Category", + "object": "category", + "object_id": 16, + "parent": 106, + "menu_order": 14, + "target": "", + "classes": [ + "" + ], + "xfn": [ + "" + ], + "meta": [], + "menus": [ + 23 + ], + "_links": { + "self": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/107" + } + ], + "collection": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items" + } + ], + "about": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/types\/nav_menu_item" + } + ], + "wp:term": [ + { + "taxonomy": "nav_menu", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=107" + } + ], + "wp:object": [ + { + "taxonomy": "taxonomy", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/categories\/16" + } + ], + "wp:action-publish": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/107" + } + ], + "wp:action-unfiltered-html": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/107" + } + ], + "wp:action-create-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/107" + } + ], + "wp:action-assign-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/107" + } + ], + "curies": [ + { + "name": "wp", + "href": "https:\/\/api.w.org\/{rel}", + "templated": true + } + ] + } + }, + { + "id": 108, + "title": { + "raw": "WordPress.org", + "rendered": "WordPress.org" + }, + "status": "publish", + "url": "https:\/\/wordpress.org", + "attr_title": "", + "description": "", + "type": "custom", + "type_label": "Custom Link", + "object": "custom", + "object_id": 108, + "parent": 0, + "menu_order": 15, + "target": "", + "classes": [ + "" + ], + "xfn": [ + "" + ], + "meta": [], + "menus": [ + 23 + ], + "_links": { + "self": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/108" + } + ], + "collection": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items" + } + ], + "about": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/types\/nav_menu_item" + } + ], + "wp:term": [ + { + "taxonomy": "nav_menu", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=108" + } + ], + "wp:action-publish": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/108" + } + ], + "wp:action-unfiltered-html": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/108" + } + ], + "wp:action-create-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/108" + } + ], + "wp:action-assign-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/108" + } + ], + "curies": [ + { + "name": "wp", + "href": "https:\/\/api.w.org\/{rel}", + "templated": true + } + ] + } + }, + { + "id": 109, + "title": { + "raw": "Google", + "rendered": "Google" + }, + "status": "publish", + "url": "https:\/\/google.com", + "attr_title": "", + "description": "", + "type": "custom", + "type_label": "Custom Link", + "object": "custom", + "object_id": 109, + "parent": 108, + "menu_order": 16, + "target": "", + "classes": [ + "" + ], + "xfn": [ + "" + ], + "meta": [], + "menus": [ + 23 + ], + "_links": { + "self": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/109" + } + ], + "collection": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items" + } + ], + "about": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/wp\/v2\/types\/nav_menu_item" + } + ], + "wp:term": [ + { + "taxonomy": "nav_menu", + "embeddable": true, + "href": "http:\/\/localhost:8889\/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=109" + } + ], + "wp:action-publish": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/109" + } + ], + "wp:action-unfiltered-html": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/109" + } + ], + "wp:action-create-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/109" + } + ], + "wp:action-assign-menus": [ + { + "href": "http:\/\/localhost:8889\/index.php?rest_route=\/__experimental\/menu-items\/109" + } + ], + "curies": [ + { + "name": "wp", + "href": "https:\/\/api.w.org\/{rel}", + "templated": true + } + ] + } + } +] \ No newline at end of file diff --git a/packages/e2e-tests/specs/experiments/navigation.test.js b/packages/e2e-tests/specs/experiments/navigation.test.js index 948158e30dbd4c..e21557108f6ebb 100644 --- a/packages/e2e-tests/specs/experiments/navigation.test.js +++ b/packages/e2e-tests/specs/experiments/navigation.test.js @@ -11,6 +11,53 @@ import { pressKeyWithModifier, } from '@wordpress/e2e-test-utils'; +/** + * Internal dependencies + */ +import menuItemsFixture from './fixtures/menu-items-response-fixture.json'; + +const menusFixture = [ + { + name: 'Test Menu 1', + slug: 'test-menu-1', + }, + { + name: 'Test Menu 2', + slug: 'test-menu-2', + }, + { + name: 'Test Menu 3', + slug: 'test-menu-3', + }, +]; + +// Matching against variations of the same URL encoded and non-encoded +// produces the most reliable mocking. +const REST_MENUS_ROUTES = [ + '/__experimental/menus', + `rest_route=${ encodeURIComponent( '/__experimental/menus' ) }`, +]; +const REST_MENU_ITEMS_ROUTES = [ + '/__experimental/menu-items', + `rest_route=${ encodeURIComponent( '/__experimental/menu-items' ) }`, +]; + +const REST_PAGES_ROUTES = [ + '/wp/v2/pages', + `rest_route=${ encodeURIComponent( '/wp/v2/pages' ) }`, +]; + +/** + * Determines if a given URL matches any of a given collection of + * routes (extressed as substrings). + * + * @param {string} reqUrl the full URL to be tested for matches. + * @param {Array} routes array of strings to match against the URL. + */ +function matchUrlToRoute( reqUrl, routes ) { + return routes.some( ( route ) => reqUrl.includes( route ) ); +} + async function mockPagesResponse( pages ) { const mappedPages = pages.map( ( { title, slug }, index ) => ( { id: index + 1, @@ -25,11 +72,7 @@ async function mockPagesResponse( pages ) { await setUpResponseMocking( [ { match: ( request ) => - request - .url() - .includes( - `rest_route=${ encodeURIComponent( '/wp/v2/pages' ) }` - ), + matchUrlToRoute( request.url(), REST_PAGES_ROUTES ), onRequestMatch: createJSONResponse( mappedPages ), }, ] ); @@ -54,6 +97,56 @@ async function mockSearchResponse( items ) { ] ); } +/** + * Creates mocked REST API responses for calls to menus and menu-items + * endpoints. + * Note: this needs to be within a single call to + * `setUpResponseMocking` as you can only setup response mocking once per test run. + * + * @param {Array} menus menus to provide as mocked responses to menus entity API requests. + * @param {Array} menuItems menu items to provide as mocked responses to menu-items entity API requests. + */ +async function mockAllMenusResponses( + menus = menusFixture, + menuItems = menuItemsFixture +) { + const mappedMenus = menus.length + ? menus.map( ( menu, index ) => ( { + ...menu, + id: index + 1, + } ) ) + : []; + + await setUpResponseMocking( [ + { + match: ( request ) => + matchUrlToRoute( request.url(), REST_MENUS_ROUTES ), + onRequestMatch: createJSONResponse( mappedMenus ), + }, + { + match: ( request ) => + matchUrlToRoute( request.url(), REST_MENU_ITEMS_ROUTES ), + onRequestMatch: createJSONResponse( menuItems ), + }, + ] ); +} + +async function mockEmptyMenusAndPagesResponses() { + const emptyResponse = []; + await setUpResponseMocking( [ + { + match: ( request ) => + matchUrlToRoute( request.url(), REST_MENUS_ROUTES ), + onRequestMatch: createJSONResponse( emptyResponse ), + }, + { + match: ( request ) => + matchUrlToRoute( request.url(), REST_PAGES_ROUTES ), + onRequestMatch: createJSONResponse( emptyResponse ), + }, + ] ); +} + async function mockCreatePageResponse( title, slug ) { const page = { id: 1, @@ -123,60 +216,209 @@ async function updateActiveNavigationLink( { url, label, type } ) { } } +async function selectDropDownOption( optionText ) { + const buttonText = 'Select where to start from…'; + await page.waitForXPath( + `//button[text()="${ buttonText }"][not(@disabled)]` + ); + const [ dropdownToggle ] = await page.$x( + `//button[text()="${ buttonText }"][not(@disabled)]` + ); + await dropdownToggle.click(); + + const [ theOption ] = await page.$x( `//li[text()="${ optionText }"]` ); + + await theOption.click(); +} + +async function clickCreateButton() { + const buttonText = 'Create'; + // Wait for button to become available + await page.waitForXPath( + `//button[text()="${ buttonText }"][not(@disabled)]` + ); + + // Then locate... + const [ createNavigationButton ] = await page.$x( + `//button[text()="${ buttonText }"][not(@disabled)]` + ); + + // Then click + await createNavigationButton.click(); +} + +async function createEmptyNavBlock() { + await selectDropDownOption( 'Create empty menu' ); + await clickCreateButton(); +} + +beforeEach( async () => { + await createNewPost(); +} ); + +afterEach( async () => { + await setUpResponseMocking( [] ); +} ); describe( 'Navigation', () => { - beforeEach( async () => { - await createNewPost(); - } ); + describe( 'Creating from existing Pages', () => { + it( 'allows a navigation block to be created using existing pages', async () => { + // Mock the response from the Pages endpoint. This is done so that the pages returned are always + // consistent and to test the feature more rigorously than the single default sample page. + await mockPagesResponse( [ + { + title: 'Home', + slug: 'home', + }, + { + title: 'About', + slug: 'about', + }, + { + title: 'Contact Us', + slug: 'contact', + }, + ] ); + + // Add the navigation block. + await insertBlock( 'Navigation' ); + + await selectDropDownOption( 'New from all top-level pages' ); + + await clickCreateButton(); + + // Snapshot should contain the mocked pages. + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'does not display option to create from existing Pages if there are no Pages', async () => { + // Force no Pages or Menus to be returned by API responses. + await mockEmptyMenusAndPagesResponses(); + + // Add the navigation block. + await insertBlock( 'Navigation' ); + + const dropdownButtonText = 'Select where to start from…'; + await page.waitForXPath( + `//button[text()="${ dropdownButtonText }"][not(@disabled)]` + ); + const [ dropdownToggle ] = await page.$x( + `//button[text()="${ dropdownButtonText }"][not(@disabled)]` + ); + + await dropdownToggle.click(); + + const dropDownItemsLength = await page.$$eval( + 'ul[role="listbox"] li[role="option"]', + ( els ) => els.length + ); + + // Should only be showing + // 1. Placeholder value. + // 2. Create empty menu. + expect( dropDownItemsLength ).toEqual( 2 ); + + await page.waitForXPath( '//li[text()="Create empty menu"]' ); - afterEach( async () => { - await setUpResponseMocking( [] ); + // Snapshot should contain the mocked menu items. + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); } ); - it( 'allows a navigation menu to be created using existing pages', async () => { - // Mock the response from the Pages endpoint. This is done so that the pages returned are always - // consistent and to test the feature more rigorously than the single default sample page. - await mockPagesResponse( [ - { - title: 'Home', - slug: 'home', - }, - { - title: 'About', - slug: 'about', - }, - { - title: 'Contact Us', - slug: 'contact', - }, - ] ); + describe( 'Creating from existing Menus', () => { + it( 'allows a navigation block to be created from existing menus', async () => { + await mockAllMenusResponses(); - // Add the navigation block. - await insertBlock( 'Navigation' ); + // Add the navigation block. + await insertBlock( 'Navigation' ); - // Create an empty nav block. The 'create' button is disabled until pages are loaded, - // so we must wait for it to become not-disabled. - await page.waitForXPath( - '//button[text()="Create from all top-level pages"][not(@disabled)]' - ); - const [ createFromExistingButton ] = await page.$x( - '//button[text()="Create from all top-level pages"][not(@disabled)]' - ); - await createFromExistingButton.click(); + await selectDropDownOption( 'Test Menu 2' ); - // Snapshot should contain the mocked pages. - expect( await getEditedPostContent() ).toMatchSnapshot(); + await clickCreateButton(); + + // await page.waitFor( 50000000 ); + + // Scope element selector to the Editor's "Content" region as otherwise it picks up on + // block previews. + const navBlockItemsLength = await page.$$eval( + '[aria-label="Content"][role="region"] li[aria-label="Block: Navigation Link"]', + ( els ) => els.length + ); + + // Assert the correct number of Nav Link blocks were inserted. + expect( navBlockItemsLength ).toEqual( menuItemsFixture.length ); + + // Snapshot should contain the mocked menu items. + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'creates an empty navigation block when the selected existing menu is also empty', async () => { + // Force mock to return no Menus Items (empty menu) + const emptyMenuItems = []; + await mockAllMenusResponses( menusFixture, emptyMenuItems ); + + // Add the navigation block. + await insertBlock( 'Navigation' ); + + await selectDropDownOption( 'Test Menu 1' ); + + await clickCreateButton(); + + // Scope element selector to the "Editor content" as otherwise it picks up on + // Block Style live previews. + const navBlockItemsLength = await page.$$eval( + '[aria-label="Content"][role="region"] li[aria-label="Block: Navigation Link"]', + ( els ) => els.length + ); + + // Assert an empty Nav Block is created. + // We expect 1 here because a "placeholder" Nav Item Block is automatically inserted + expect( navBlockItemsLength ).toEqual( 1 ); + + // Snapshot should contain the mocked menu items. + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'does not display option to create from existing menus if there are no menus', async () => { + // Force no Menus to be returned by API response. + await mockEmptyMenusAndPagesResponses(); + + // Add the navigation block. + await insertBlock( 'Navigation' ); + + const dropdownButtonText = 'Select where to start from…'; + await page.waitForXPath( + `//button[text()="${ dropdownButtonText }"][not(@disabled)]` + ); + const [ dropdownToggle ] = await page.$x( + `//button[text()="${ dropdownButtonText }"][not(@disabled)]` + ); + await dropdownToggle.click(); + + const dropDownItemsLength = await page.$$eval( + 'ul[role="listbox"] li[role="option"]', + ( els ) => els.length + ); + + // Should only be showing + // 1. Placeholder. + // 2. Create from Empty. + expect( dropDownItemsLength ).toEqual( 2 ); + + await page.waitForXPath( '//li[text()="Create empty menu"]' ); + + // Snapshot should contain the mocked menu items. + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); } ); - it( 'allows a navigation menu to be created from an empty menu using a mixture of internal and external links', async () => { + it( 'allows an empty navigation block to be created and manually populated using a mixture of internal and external links', async () => { // Add the navigation block. await insertBlock( 'Navigation' ); // Create an empty nav block. await page.waitForSelector( '.wp-block-navigation-placeholder' ); - const [ createEmptyButton ] = await page.$x( - '//button[text()="Create empty"]' - ); - await createEmptyButton.click(); + + await createEmptyNavBlock(); // Add a link to the default Navigation Link block. await updateActiveNavigationLink( { @@ -257,11 +499,7 @@ describe( 'Navigation', () => { await insertBlock( 'Navigation' ); // Create an empty nav block. - await page.waitForSelector( '.wp-block-navigation-placeholder' ); - const [ createEmptyButton ] = await page.$x( - '//button[text()="Create empty"]' - ); - await createEmptyButton.click(); + await createEmptyNavBlock(); // Wait for URL input to be focused await page.waitForSelector(