Refactor menu item mapping and sorting (https://github.com/woocommerce/woocommerce-admin/pull/6382)
* 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:
parent
8e9c60b0b0
commit
a8757648a1
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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();
|
||||
} );
|
||||
} );
|
|
@ -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 );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue