* Separate primary and secondary menu components

* Simplify category mapping

* Map categories and items simultaneously to improve performance

* Sort added menu items

* Pre-sort menu items

* Create single mapped sorting on server-side

* Fix incorrect menu ID references

* Update tests

* Move methods to utils file

* Add in permissions check in client nav

* Fix server-side capability check

* Fix duplicate expectation and test name

* Add testing instructions
This commit is contained in:
Joshua T Flowers 2021-02-22 13:54:27 -05:00 committed by GitHub
parent 8e9c60b0b0
commit a8757648a1
9 changed files with 572 additions and 463 deletions

View File

@ -43,6 +43,16 @@ wp db query 'SELECT status FROM wp_wc_admin_notes WHERE name = "wc-admin-add-fir
- Run the cron again.
- The note's status should continue being `unactioned`.
### Refactor menu item mapping and sorting #6382
1. Enable the new navigation under WooCommerce -> Settings -> Advanced -> Features.
2. Navigate to a WooCommerce page.
3. Make sure all items and categories continue to work as expected.
4. Activate multiple extensions that register WooCommerce extension categories. (e.g., WooCommerce Bookings and WooCommerce Payments).
5. Favorite and unfavorite menu items.
6. Make sure the menu item order is correct after unfavoriting.
7. Create a user with permissions to see some but not all registered WooCommerce pages.
8. Check that a user without permission to access a menu item cannot see said menu item.
### Remove CES actions for adding and editing a product and editing an order #6355
1. Add a product. The customer effort score survey should not appear.

View File

@ -1,90 +1,25 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useEffect, useMemo, useState, useRef } from '@wordpress/element';
import classnames from 'classnames';
import { compose } from '@wordpress/compose';
import {
Navigation,
NavigationMenu,
NavigationGroup,
} from '@woocommerce/experimental';
import { NAVIGATION_STORE_NAME } from '@woocommerce/data';
import { Navigation } from '@woocommerce/experimental';
import { NAVIGATION_STORE_NAME, useUser } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
import { withSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import { addHistoryListener, getMatchingItem } from '../../utils';
import CategoryTitle from '../category-title';
import {
addHistoryListener,
getMappedItemsCategories,
getMatchingItem,
} from '../../utils';
import Header from '../header';
import Item from '../../components/Item';
/**
* Get a map of all categories, including the topmost WooCommerce parentCategory
*
* @param {Array} menuItems Array of menuItems
* @return {Object} Map of categories by id
*/
export const getCategoriesMap = ( menuItems ) => {
return menuItems.reduce(
( acc, item ) => {
if ( item.isCategory ) {
return { ...acc, [ item.id ]: item };
}
return acc;
},
{
woocommerce: {
capability: 'manage_woocommerce',
id: 'woocommerce',
isCategory: true,
menuId: 'primary',
migrate: true,
order: 10,
parent: '',
title: 'WooCommerce',
},
}
);
};
/**
* Get a flat tree structure of all Categories and thier children grouped by menuId
*
* @param {Object} categoriesMap Map of categories by id
* @param {Array} menuItems Array of menuItems
* @return {Object} a;dslkfj
*/
export const getMenuItemsByCategory = ( categoriesMap, menuItems ) => {
return menuItems.reduce( ( acc, item ) => {
// Set up the category if it doesn't yet exist.
if ( ! acc[ item.parent ] ) {
acc[ item.parent ] = {};
}
// Check if parent category is in the same menu.
if (
item.parent !== 'woocommerce' &&
categoriesMap[ item.parent ] &&
categoriesMap[ item.parent ].menuId !== item.menuId &&
// Allow favorites to exist under any menu.
categoriesMap[ item.parent ].menuId !== 'favorites'
) {
return acc;
}
// Create the menu object if it doesn't exist in this category.
if ( ! acc[ item.parent ][ item.menuId ] ) {
acc[ item.parent ][ item.menuId ] = [];
}
acc[ item.parent ][ item.menuId ].push( item );
return acc;
}, {} );
};
import { PrimaryMenu } from './primary-menu';
import { SecondaryMenu } from './secondary-menu';
const Container = ( { menuItems } ) => {
useEffect( () => {
@ -100,11 +35,6 @@ const Container = ( { menuItems } ) => {
adminMenu.classList.add( 'folded' );
}, [] );
const { rootBackLabel, rootBackUrl } = window.wcNavigation;
const categoriesMap = getCategoriesMap( menuItems );
const categories = Object.values( categoriesMap );
const [ activeItem, setActiveItem ] = useState( 'woocommerce-home' );
const [ activeLevel, setActiveLevel ] = useState( 'woocommerce' );
@ -127,21 +57,22 @@ const Container = ( { menuItems } ) => {
return removeListener;
}, [ menuItems ] );
const categorizedItems = useMemo(
() => getMenuItemsByCategory( categoriesMap, menuItems ),
[ categoriesMap, menuItems ]
const { currentUserCan } = useUser();
const { categories, items } = useMemo(
() => getMappedItemsCategories( menuItems, currentUserCan ),
[ menuItems, currentUserCan ]
);
const navDomRef = useRef( null );
const trackBackClick = ( id ) => {
const onBackClick = ( id ) => {
recordEvent( 'navigation_back_click', {
category: id,
} );
};
const isRoot = activeLevel === 'woocommerce';
const isRootBackVisible = isRoot && rootBackUrl;
const classes = classnames( 'woocommerce-navigation', {
'is-root': isRoot,
@ -162,121 +93,29 @@ const Container = ( { menuItems } ) => {
setActiveLevel( ...args );
} }
>
{ categories.map( ( category ) => {
const {
primary: primaryItems,
favorites: favoriteItems,
secondary: secondaryItems,
plugins: pluginItems,
} = categorizedItems[ category.id ] || {};
{ Object.values( categories ).map( ( category ) => {
const categoryItems = items[ category.id ];
const primaryAndFavoriteItems = [
...( primaryItems || [] ),
...( favoriteItems || [] ),
];
return [
( !! primaryAndFavoriteItems.length ||
!! pluginItems ) && (
<NavigationMenu
return (
!! categoryItems && [
<PrimaryMenu
key={ category.id }
title={
<CategoryTitle category={ category } />
}
menu={ category.id }
parentMenu={ category.parent }
backButtonLabel={
isRootBackVisible
? rootBackLabel
: category.backButtonLabel || null
}
onBackButtonClick={
isRootBackVisible
? () => {
trackBackClick(
'woocommerce'
);
window.location = rootBackUrl;
}
: () =>
trackBackClick(
category.id
)
}
>
{ !! primaryAndFavoriteItems.length && (
<NavigationGroup>
{ primaryAndFavoriteItems.map(
( item ) => (
<Item
key={ item.id }
item={ item }
/>
)
) }
</NavigationGroup>
) }
{ !! pluginItems && (
<NavigationGroup
title={
category.id === 'woocommerce'
? __(
'Extensions',
'woocommerce-admin'
)
: null
}
>
{ pluginItems.map( ( item ) => (
<Item
key={ item.id }
item={ item }
/>
) ) }
</NavigationGroup>
) }
</NavigationMenu>
),
!! secondaryItems && (
<NavigationMenu
className="components-navigation__menu-secondary"
key={ `secondary/${ category.id }` }
title={
! isRoot && (
<CategoryTitle
category={ category }
/>
)
}
menu={ category.id }
parentMenu={ category.parent }
backButtonLabel={
category.backButtonLabel || null
}
onBackButtonClick={
isRootBackVisible
? null
: () =>
trackBackClick(
category.id
)
}
>
<NavigationGroup
onBackButtonClick={ () =>
trackBackClick( category.id )
}
>
{ secondaryItems.map( ( item ) => (
<Item
key={ item.id }
item={ item }
/>
) ) }
</NavigationGroup>
</NavigationMenu>
),
];
onBackClick={ onBackClick }
primaryItems={ [
...categoryItems.primary,
...categoryItems.favorites,
] }
pluginItems={ categoryItems.plugins }
/>,
<SecondaryMenu
key={ `secondary/${ category.id }` }
category={ category }
onBackClick={ onBackClick }
items={ categoryItems.secondary }
/>,
]
);
} ) }
</Navigation>
</div>

View File

@ -0,0 +1,67 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { NavigationMenu, NavigationGroup } from '@woocommerce/experimental';
/**
* Internal dependencies
*/
import CategoryTitle from '../category-title';
import Item from '../../components/Item';
export const PrimaryMenu = ( {
category,
onBackClick,
pluginItems,
primaryItems,
} ) => {
if ( ! primaryItems.length && ! pluginItems.length ) {
return null;
}
const { rootBackLabel, rootBackUrl } = window.wcNavigation;
const isRootBackVisible = category.id === 'woocommerce' && rootBackUrl;
return (
<NavigationMenu
title={ <CategoryTitle category={ category } /> }
menu={ category.id }
parentMenu={ category.parent }
backButtonLabel={
isRootBackVisible
? rootBackLabel
: category.backButtonLabel || null
}
onBackButtonClick={
isRootBackVisible
? () => {
onBackClick( 'woocommerce' );
window.location = rootBackUrl;
}
: () => onBackClick( category.id )
}
>
{ !! primaryItems.length && (
<NavigationGroup>
{ primaryItems.map( ( item ) => (
<Item key={ item.id } item={ item } />
) ) }
</NavigationGroup>
) }
{ !! pluginItems.length && (
<NavigationGroup
title={
category.id === 'woocommerce'
? __( 'Extensions', 'woocommerce-admin' )
: null
}
>
{ pluginItems.map( ( item ) => (
<Item key={ item.id } item={ item } />
) ) }
</NavigationGroup>
) }
</NavigationMenu>
);
};

View File

@ -0,0 +1,39 @@
/**
* External dependencies
*/
import { NavigationMenu, NavigationGroup } from '@woocommerce/experimental';
/**
* Internal dependencies
*/
import CategoryTitle from '../category-title';
import Item from '../../components/Item';
export const SecondaryMenu = ( { category, items, onBackClick } ) => {
if ( ! items.length ) {
return null;
}
const isRoot = category.id === 'woocommerce';
return (
<NavigationMenu
className="components-navigation__menu-secondary"
title={ ! isRoot && <CategoryTitle category={ category } /> }
menu={ category.id }
parentMenu={ category.parent }
backButtonLabel={ category.backButtonLabel || null }
onBackButtonClick={
isRoot ? null : () => onBackClick( category.id )
}
>
<NavigationGroup
onBackButtonClick={ () => onBackClick( category.id ) }
>
{ items.map( ( item ) => (
<Item key={ item.id } item={ item } />
) ) }
</NavigationGroup>
</NavigationMenu>
);
};

View File

@ -1,204 +0,0 @@
/**
* Internal dependencies
*/
import { getCategoriesMap, getMenuItemsByCategory } from '../';
describe( 'getCategoriesMap', () => {
const menuItems = [
{ id: 'zero', title: 'zero', isCategory: true },
{ id: 'one', title: 'one', isCategory: true },
{ id: 'two', title: 'two', isCategory: true },
{ id: 'three', title: 'three', isCategory: false },
{ id: 'four', title: 'four', isCategory: false },
];
it( 'should get a map of all categories', () => {
const categoriesMap = getCategoriesMap( menuItems );
expect( categoriesMap.zero ).toMatchObject( menuItems[ 0 ] );
expect( categoriesMap.one ).toMatchObject( menuItems[ 1 ] );
expect( categoriesMap.two ).toMatchObject( menuItems[ 2 ] );
expect( categoriesMap.three ).toBeUndefined();
expect( categoriesMap.four ).toBeUndefined();
} );
it( 'should include the topmost WooCommerce parent category', () => {
const categoriesMap = getCategoriesMap( menuItems );
expect( categoriesMap.woocommerce ).toBeDefined();
} );
it( 'should have the correct number of values', () => {
const categoriesMap = getCategoriesMap( menuItems );
expect( Object.keys( categoriesMap ).length ).toBe( 4 );
} );
} );
describe( 'getMenuItemsByCategory', () => {
it( 'should get a map of all categories and child elements', () => {
const menuItems = [
{
id: 'child-one',
title: 'child-one',
isCategory: false,
parent: 'parent',
menuId: 'plugins',
},
{
id: 'child-two',
title: 'child-two',
isCategory: false,
parent: 'parent',
menuId: 'plugins',
},
{
id: 'parent',
title: 'parent',
isCategory: true,
parent: 'woocommerce',
menuId: 'plugins',
},
];
const categoriesMap = getCategoriesMap( menuItems );
const categorizedItems = getMenuItemsByCategory(
categoriesMap,
menuItems
);
expect( categorizedItems.woocommerce ).toBeDefined();
expect( categorizedItems.woocommerce.plugins ).toBeDefined();
expect( categorizedItems.woocommerce.plugins.length ).toBe( 1 );
expect( categorizedItems.parent ).toBeDefined();
expect( categorizedItems.parent.plugins ).toBeDefined();
expect( categorizedItems.parent.plugins.length ).toBe( 2 );
} );
it( 'should handle multiple depths', () => {
const menuItems = [
{
id: 'grand-child',
title: 'grand-child',
isCategory: false,
parent: 'child',
menuId: 'plugins',
},
{
id: 'child',
title: 'child',
isCategory: true,
parent: 'grand-parent',
menuId: 'plugins',
},
{
id: 'grand-parent',
title: 'grand-parent',
isCategory: true,
parent: 'woocommerce',
menuId: 'plugins',
},
];
const categoriesMap = getCategoriesMap( menuItems );
const categorizedItems = getMenuItemsByCategory(
categoriesMap,
menuItems
);
expect( categorizedItems[ 'grand-parent' ] ).toBeDefined();
expect( categorizedItems[ 'grand-parent' ] ).toBeDefined();
expect( categorizedItems[ 'grand-parent' ].plugins.length ).toBe( 1 );
expect( categorizedItems.child ).toBeDefined();
expect( categorizedItems.child ).toBeDefined();
expect( categorizedItems.child.plugins.length ).toBe( 1 );
expect( categorizedItems[ 'grand-child' ] ).not.toBeDefined();
} );
it( 'should group by menuId', () => {
const menuItems = [
{
id: 'parent',
title: 'parent',
isCategory: true,
parent: 'woocommerce',
menuId: 'primary',
},
{
id: 'primary-one',
title: 'primary-one',
isCategory: false,
parent: 'parent',
menuId: 'primary',
},
{
id: 'primary-two',
title: 'primary-two',
isCategory: false,
parent: 'parent',
menuId: 'primary',
},
];
const categoriesMap = getCategoriesMap( menuItems );
const categorizedItems = getMenuItemsByCategory(
categoriesMap,
menuItems
);
expect( categorizedItems.parent ).toBeDefined();
expect( categorizedItems.parent.primary ).toBeDefined();
expect( categorizedItems.parent.primary.length ).toBe( 2 );
} );
it( 'should group children only if their menuId matches parent', () => {
const menuItems = [
{
id: 'plugin-one',
title: 'plugin-one',
isCategory: false,
parent: 'parent',
menuId: 'plugins',
},
{
id: 'plugin-two',
title: 'plugin-two',
isCategory: false,
parent: 'parent',
menuId: 'plugins',
},
{
id: 'parent',
title: 'parent',
isCategory: true,
parent: 'woocommerce',
menuId: 'plugins',
},
{
id: 'primary-one',
title: 'primary-one',
isCategory: false,
parent: 'parent',
menuId: 'primary',
},
{
id: 'primary-two',
title: 'primary-two',
isCategory: false,
parent: 'parent',
menuId: 'primary',
},
];
const categoriesMap = getCategoriesMap( menuItems );
const categorizedItems = getMenuItemsByCategory(
categoriesMap,
menuItems
);
expect( categorizedItems.parent ).toBeDefined();
expect( categorizedItems.parent.plugins ).toBeDefined();
expect( categorizedItems.parent.plugins.length ).toBe( 2 );
expect( categorizedItems.primary ).not.toBeDefined();
} );
} );

View File

@ -10,8 +10,10 @@ import {
addHistoryListener,
getDefaultMatchExpression,
getFullUrl,
getMappedItemsCategories,
getMatchingItem,
getMatchScore,
sortMenuItems,
} from '../utils';
const originalLocation = window.location;
@ -309,3 +311,246 @@ describe( 'addHistoryListener', () => {
expect( mockCallback.mock.calls.length ).toBe( 2 );
} );
} );
describe( 'sortMenuItems', () => {
it( 'should return an array of items sorted by the order property', () => {
const menuItems = [
{ id: 'second', title: 'second', order: 2 },
{ id: 'first', title: 'three', order: 1 },
{ id: 'third', title: 'four', order: 3 },
];
const sortedItems = sortMenuItems( menuItems );
expect( sortedItems[ 0 ].id ).toBe( 'first' );
expect( sortedItems[ 1 ].id ).toBe( 'second' );
expect( sortedItems[ 2 ].id ).toBe( 'third' );
} );
it( 'should sort items alphabetically if order is the same', () => {
const menuItems = [
{ id: 'third', title: 'z', order: 2 },
{ id: 'first', title: 'first', order: 1 },
{ id: 'second', title: 'a', order: 2 },
];
const sortedItems = sortMenuItems( menuItems );
expect( sortedItems[ 0 ].id ).toBe( 'first' );
expect( sortedItems[ 1 ].id ).toBe( 'second' );
expect( sortedItems[ 2 ].id ).toBe( 'third' );
} );
} );
describe( 'getMappedItemsCategories', () => {
it( 'should get the default category when none are provided', () => {
const menuItems = [
{
id: 'child-one',
title: 'child-one',
isCategory: false,
parent: 'woocommerce',
menuId: 'plugins',
},
];
const { categories, items } = getMappedItemsCategories( menuItems );
expect( items.woocommerce ).toBeDefined();
expect( items.woocommerce.plugins ).toBeDefined();
expect( items.woocommerce.plugins.length ).toBe( 1 );
expect( Object.keys( categories ).length ).toBe( 1 );
expect( categories.woocommerce ).toBeDefined();
} );
it( 'should get a map of all items and categories', () => {
const menuItems = [
{
id: 'child-one',
title: 'child-one',
isCategory: false,
parent: 'parent',
menuId: 'plugins',
},
{
id: 'child-two',
title: 'child-two',
isCategory: false,
parent: 'parent',
menuId: 'plugins',
},
{
id: 'parent',
title: 'parent',
isCategory: true,
parent: 'woocommerce',
menuId: 'plugins',
},
];
const { categories, items } = getMappedItemsCategories( menuItems );
expect( items.woocommerce ).toBeDefined();
expect( items.woocommerce.plugins ).toBeDefined();
expect( items.woocommerce.plugins.length ).toBe( 1 );
expect( items.parent ).toBeDefined();
expect( items.parent.plugins ).toBeDefined();
expect( items.parent.plugins.length ).toBe( 2 );
expect( Object.keys( categories ).length ).toBe( 2 );
expect( categories.parent ).toBeDefined();
expect( categories.woocommerce ).toBeDefined();
} );
it( 'should handle multiple depths', () => {
const menuItems = [
{
id: 'grand-child',
title: 'grand-child',
isCategory: false,
parent: 'child',
menuId: 'plugins',
},
{
id: 'child',
title: 'child',
isCategory: true,
parent: 'grand-parent',
menuId: 'plugins',
},
{
id: 'grand-parent',
title: 'grand-parent',
isCategory: true,
parent: 'woocommerce',
menuId: 'plugins',
},
];
const { categories, items } = getMappedItemsCategories( menuItems );
expect( items[ 'grand-parent' ] ).toBeDefined();
expect( items[ 'grand-parent' ] ).toBeDefined();
expect( items[ 'grand-parent' ].plugins.length ).toBe( 1 );
expect( items.child ).toBeDefined();
expect( items.child.plugins.length ).toBe( 1 );
expect( items[ 'grand-child' ] ).not.toBeDefined();
expect( Object.keys( categories ).length ).toBe( 3 );
} );
it( 'should group by menuId', () => {
const menuItems = [
{
id: 'parent',
title: 'parent',
isCategory: true,
parent: 'woocommerce',
menuId: 'primary',
},
{
id: 'primary-one',
title: 'primary-one',
isCategory: false,
parent: 'parent',
menuId: 'primary',
},
{
id: 'primary-two',
title: 'primary-two',
isCategory: false,
parent: 'parent',
menuId: 'primary',
},
];
const { items } = getMappedItemsCategories( menuItems );
expect( items.parent ).toBeDefined();
expect( items.parent.primary ).toBeDefined();
expect( items.parent.primary.length ).toBe( 2 );
} );
it( 'should group children only if their menuId matches parent', () => {
const menuItems = [
{
id: 'plugin-one',
title: 'plugin-one',
isCategory: false,
parent: 'parent',
menuId: 'plugins',
},
{
id: 'plugin-two',
title: 'plugin-two',
isCategory: false,
parent: 'parent',
menuId: 'plugins',
},
{
id: 'parent',
title: 'parent',
isCategory: true,
parent: 'woocommerce',
menuId: 'plugins',
},
{
id: 'primary-one',
title: 'primary-one',
isCategory: false,
parent: 'parent',
menuId: 'primary',
},
{
id: 'primary-two',
title: 'primary-two',
isCategory: false,
parent: 'parent',
menuId: 'primary',
},
];
const { items } = getMappedItemsCategories( menuItems );
expect( items.parent ).toBeDefined();
expect( items.parent.plugins ).toBeDefined();
expect( items.parent.plugins.length ).toBe( 2 );
expect( items.primary ).not.toBeDefined();
} );
it( 'should ignore bad menu IDs', () => {
const menuItems = [
{
id: 'parent',
title: 'parent',
isCategory: false,
parent: 'woocommerce',
menuId: 'badId',
},
{
id: 'primary-one',
title: 'primary-one',
isCategory: false,
parent: 'woocommerce',
menuId: 'primary',
},
{
id: 'primary-two',
title: 'primary-two',
isCategory: false,
parent: 'woocommerce',
menuId: 'primary',
},
];
const { categories, items } = getMappedItemsCategories( menuItems );
expect( items.woocommerce ).toBeDefined();
expect( items.woocommerce.primary ).toBeDefined();
expect( items.woocommerce.primary.length ).toBe( 2 );
expect( items.woocommerce ).toBeDefined();
expect( items.woocommerce.badId ).not.toBeDefined();
expect( Object.keys( categories ).length ).toBe( 1 );
} );
} );

View File

@ -132,3 +132,90 @@ export const getMatchingItem = ( items ) => {
return matchedItem || null;
};
/**
* Available menu IDs.
*/
export const menuIds = [ 'primary', 'favorites', 'plugins', 'secondary' ];
/**
* Default categories for the menu.
*/
export const defaultCategories = {
woocommerce: {
id: 'woocommerce',
isCategory: true,
menuId: 'primary',
migrate: true,
order: 10,
parent: '',
title: 'WooCommerce',
},
};
/**
* Sort an array of menu items by their order property.
*
* @param {Array} menuItems Array of menu items.
* @return {Array} Sorted menu items.
*/
export const sortMenuItems = ( menuItems ) => {
return menuItems.sort( ( a, b ) => {
if ( a.order === b.order ) {
return a.title.localeCompare( b.title );
}
return a.order - b.order;
} );
};
/**
* Get a flat tree structure of all Categories and thier children grouped by menuId
*
* @param {Array} menuItems Array of menu items.
* @param {Function} currentUserCan Callback method passed the capability to determine if a menu item is visible.
* @return {Object} Mapped menu items and categories.
*/
export const getMappedItemsCategories = (
menuItems,
currentUserCan = null
) => {
const categories = { ...defaultCategories };
const items = sortMenuItems( menuItems ).reduce( ( acc, item ) => {
// Set up the category if it doesn't yet exist.
if ( ! acc[ item.parent ] ) {
acc[ item.parent ] = {};
menuIds.forEach( ( menuId ) => {
acc[ item.parent ][ menuId ] = [];
} );
}
// Incorrect menu ID.
if ( ! acc[ item.parent ][ item.menuId ] ) {
return acc;
}
// User does not have permission to view this item.
if (
currentUserCan &&
item.capability &&
! currentUserCan( item.capability )
) {
return acc;
}
// Add categories.
if ( item.isCategory ) {
categories[ item.id ] = item;
}
acc[ item.parent ][ item.menuId ].push( item );
return acc;
}, {} );
return {
items,
categories,
};
};

View File

@ -282,19 +282,22 @@ class CoreMenu {
*/
public function add_dashboard_menu_items() {
global $submenu, $menu;
$top_level_items = Menu::get_category_items( 'woocommerce' );
$mapped_items = Menu::get_mapped_menu_items();
$top_level = $mapped_items['woocommerce'];
// phpcs:disable
if ( ! isset( $submenu['woocommerce'] ) ) {
if ( ! isset( $submenu['woocommerce'] ) || empty( $top_level ) ) {
return;
}
foreach( $top_level_items as $item ) {
// Skip extensions.
if ( ! isset( $item['menuId'] ) || $item['menuId'] === 'plugins' ) {
continue;
}
$menuIds = array(
'primary',
'secondary',
'favorites',
);
foreach ( $menuIds as $menuId ) {
foreach( $top_level[ $menuId ] as $item ) {
// Skip specific categories.
if (
in_array(
@ -310,10 +313,13 @@ class CoreMenu {
// Use the link from the first item if it's a category.
if ( ! isset( $item['url'] ) ) {
$category_items = Menu::get_category_items( $item['id'] );
$categoryMenuId = $menuId === 'favorites' ? 'plugins' : $menuId;
$category_items = $mapped_items[ $item['id'] ][ $categoryMenuId ];
if ( ! empty( $category_items ) ) {
$first_item = $category_items[0];
$submenu['woocommerce'][] = array(
$item['title'],
$first_item['capability'],
@ -333,6 +339,7 @@ class CoreMenu {
$item['title'],
);
}
}
// phpcs:enable
}

View File

@ -50,6 +50,16 @@ class Menu {
*/
const CSS_CLASSES = 4;
/**
* Array of usable menu IDs.
*/
const MENU_IDS = array(
'primary',
'favorites',
'plugins',
'secondary',
);
/**
* Store menu items.
*
@ -675,8 +685,7 @@ class Menu {
* @return array
*/
public static function get_items() {
$menu_items = self::get_prepared_menu_item_data( self::$menu_items );
return apply_filters( 'woocommerce_navigation_menu_items', $menu_items );
return apply_filters( 'woocommerce_navigation_menu_items', self::$menu_items );
}
/**
@ -691,7 +700,6 @@ class Menu {
$menu_item_ids = self::$categories[ $category ];
$category_menu_items = array();
foreach ( $menu_item_ids as $id ) {
if ( isset( self::$menu_items[ $id ] ) ) {
@ -699,7 +707,6 @@ class Menu {
}
}
$category_menu_items = self::get_prepared_menu_item_data( $category_menu_items );
return apply_filters( 'woocommerce_navigation_menu_category_items', $category_menu_items );
}
@ -713,31 +720,43 @@ class Menu {
}
/**
* Gets the menu item data for use in the client.
* Gets the menu item data mapped by category and menu ID.
*
* @param array $menu_items Menu items to prepare.
* @return array
*/
public static function get_prepared_menu_item_data( $menu_items ) {
foreach ( $menu_items as $index => $menu_item ) {
if ( isset( $menu_item[ 'capability' ] ) && ! current_user_can( $menu_item[ 'capability' ] ) ) {
unset( $menu_items[ $index ] );
}
}
public static function get_mapped_menu_items() {
$menu_items = self::get_items();
$mapped_items = array();
// Sort the menu items.
$menuOrder = array(
'primary' => 0,
'secondary' => 1,
'plugins' => 2,
'favorites' => 3,
);
$menu = array_map( function( $item ) use( $menuOrder ) { return $menuOrder[ $item['menuId'] ]; }, $menu_items );
// Sort the items by order and title.
$order = array_column( $menu_items, 'order' );
$title = array_column( $menu_items, 'title' );
array_multisort( $menu, SORT_ASC, $order, SORT_ASC, $title, SORT_ASC, $menu_items );
array_multisort( $order, SORT_ASC, $title, SORT_ASC, $menu_items );
return $menu_items;
foreach ( $menu_items as $id => $menu_item ) {
$category_id = $menu_item[ 'parent' ];
$menu_id = $menu_item[ 'menuId' ];
if ( ! isset( $mapped_items[ $category_id ] ) ) {
$mapped_items[ $category_id ] = array();
foreach ( self::MENU_IDS as $available_menu_id ) {
$mapped_items[ $category_id ][ $available_menu_id ] = array();
}
}
// Incorrect menu ID.
if ( ! isset( $mapped_items[ $category_id ][ $menu_id ] ) ) {
continue;
}
// Remove the item if the user cannot access it.
if ( isset( $menu_item[ 'capability' ] ) && ! current_user_can( $menu_item[ 'capability' ] ) ) {
continue;
}
$mapped_items[ $category_id ][ $menu_id ][] = $menu_item;
}
return $mapped_items;
}
/**