[Navigation] Prepare to remove feature (#50190)

This commit is contained in:
Paul Sealock 2024-08-09 14:18:39 +12:00 committed by GitHub
parent 47e4d918c9
commit 9d12459c44
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 54 additions and 3983 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Remove WooCommerce Navigation client side feature and deprecate PHP classes.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Remove deprecated Navigation SlotFill

View File

@ -1,17 +1,11 @@
/**
* External dependencies
*/
import {
createElement,
useState,
useEffect,
useLayoutEffect,
} from '@wordpress/element';
import { useState, useEffect, useLayoutEffect } from '@wordpress/element';
import { addQueryArgs } from '@wordpress/url';
import { parse } from 'qs';
import { pick } from 'lodash';
import { applyFilters } from '@wordpress/hooks';
import { Slot, Fill } from '@wordpress/components';
import { getAdminLink } from '@woocommerce/settings';
/**
@ -343,19 +337,3 @@ export const navigateTo = ( { url } ) => {
window.location.href = String( parsedUrl );
};
/**
* A Fill for extensions to add client facing custom Navigation Items.
*
* @slotFill WooNavigationItem
* @scope woocommerce-navigation
* @param {Object} props React props.
* @param {Array} props.children Node children.
* @param {string} props.item Navigation item slug.
*/
export const WooNavigationItem = ( { children, item } ) => {
return <Fill name={ 'woocommerce_navigation_' + item }>{ children }</Fill>;
};
WooNavigationItem.Slot = ( { name } ) => (
<Slot name={ 'woocommerce_navigation_' + name } />
);

View File

@ -27,7 +27,6 @@ import ActivityHeader from '~/activity-panel/activity-header';
import { ActivityPanel } from './activity-panel';
import { Column } from './column';
import InboxPanel from '../inbox-panel';
import { IntroModal as NavigationIntroModal } from '../navigation/components/intro-modal';
import StatsOverview from './stats-overview';
import { StoreManagementLinks } from '../store-management-links';
import {
@ -162,9 +161,6 @@ export const Layout = ( {
>
{ isDashboardShown ? renderColumns() : renderTaskList() }
{ shouldShowMobileAppModal && <MobileAppModal /> }
{ window.wcAdminFeatures.navigation && (
<NavigationIntroModal />
) }
</div>
</>
);

View File

@ -53,7 +53,6 @@ import { getAdminSetting } from '~/utils/admin-settings';
import { usePageClasses } from './hooks/use-page-classes';
import '~/activity-panel';
import '~/mobile-banner';
import './navigation';
const StoreAlerts = lazy( () =>
import( /* webpackChunkName: "store-alerts" */ './store-alerts' )
@ -125,15 +124,10 @@ function _Layout( {
usePageClasses( page );
function recordPageViewTrack() {
const navigationFlag = {
has_navigation: !! window.wcNavigation,
};
if ( isEmbedded ) {
const path = document.location.pathname + document.location.search;
recordPageView( path, {
is_embedded: true,
...navigationFlag,
} );
return;
}
@ -155,7 +149,6 @@ function _Layout( {
jetpack_installed: installedPlugins.includes( 'jetpack' ),
jetpack_active: activePlugins.includes( 'jetpack' ),
jetpack_connected: isJetpackConnected,
...navigationFlag,
} );
}
@ -256,9 +249,6 @@ function _Layout( {
{ showPluginArea && (
<>
<PluginArea scope="woocommerce-admin" />
{ window.wcAdminFeatures.navigation && (
<PluginArea scope="woocommerce-navigation" />
) }
<PluginArea scope="woocommerce-tasks" />
</>
) }

View File

@ -1,110 +0,0 @@
/**
* External dependencies
*/
import { registerPlugin } from '@wordpress/plugins';
import { WooHeaderNavigationItem } from '@woocommerce/admin-layout';
import {
WooNavigationItem,
getNewPath,
pathIsExcluded,
isWCAdmin,
} from '@woocommerce/navigation';
import { Link } from '@woocommerce/components';
import { __ } from '@wordpress/i18n';
import { useSelect } from '@wordpress/data';
import { NAVIGATION_STORE_NAME } from '@woocommerce/data';
/**
* Internal dependencies
*/
import getReports from '../analytics/report/get-reports';
import { getPages } from './controller';
import Navigation from '~/navigation';
const NavigationPlugin = () => {
const { persistedQuery } = useSelect( ( select ) => {
return {
persistedQuery: select( NAVIGATION_STORE_NAME ).getPersistedQuery(),
};
} );
if ( ! window.wcAdminFeatures.navigation ) {
return null;
}
/**
* If the current page is embedded, stay with the default urls
* provided by Navigation because the router isn't present to
* respond to <Link /> component's manipulation of the url.
*/
if ( ! isWCAdmin() ) {
return (
<WooHeaderNavigationItem order={ -100 }>
<Navigation />
</WooHeaderNavigationItem>
);
}
const reports = getReports().filter( ( item ) => item.navArgs );
const pages = getPages()
.filter( ( page ) => page.navArgs )
.map( ( page ) => {
if ( page.path === '/analytics/settings' ) {
return {
...page,
breadcrumbs: [ __( 'Analytics', 'woocommerce' ) ],
};
}
return page;
} );
return (
<>
<WooHeaderNavigationItem order={ -100 }>
<Navigation />
</WooHeaderNavigationItem>
{ pages.map( ( page ) => (
<WooNavigationItem
item={ page.navArgs.id }
key={ page.navArgs.id }
>
<Link
className="components-button"
href={ getNewPath(
pathIsExcluded( page.path ) ? {} : persistedQuery,
page.path,
{}
) }
type="wc-admin"
>
{ page.breadcrumbs[ page.breadcrumbs.length - 1 ] }
</Link>
</WooNavigationItem>
) ) }
{ reports.map( ( item ) => (
<WooNavigationItem
item={ item.navArgs.id }
key={ item.navArgs.id }
>
<Link
className="components-button"
href={ getNewPath(
pathIsExcluded( item.report ) ? {} : persistedQuery,
`/analytics/${ item.report }`,
{}
) }
type="wc-admin"
>
{ item.title }
</Link>
</WooNavigationItem>
) ) }
</>
);
};
registerPlugin( 'wc-admin-navigation', {
render: NavigationPlugin,
scope: 'woocommerce-navigation',
} );

View File

@ -34,8 +34,6 @@ jest.mock( '@woocommerce/components', () => ( {
jest.mock( '~/activity-panel', () => null );
jest.mock( '../navigation', () => null );
jest.mock( '~/utils/admin-settings', () => {
const adminSetting = jest.requireActual( '~/utils/admin-settings' );
return {
@ -136,7 +134,6 @@ describe( 'EmbedLayout', () => {
window.history.pushState( {}, 'Page Title', '/url?search' );
render( <EmbedLayout /> );
expect( recordPageView ).toHaveBeenCalledWith( '/url?search', {
has_navigation: true,
is_embedded: true,
} );
} );
@ -168,7 +165,6 @@ describe( 'PageLayout', () => {
mockPath( '/analytics/overview' );
render( <PageLayout /> );
expect( recordPageView ).toHaveBeenCalledWith( 'analytics_overview', {
has_navigation: true,
jetpack_active: false,
jetpack_connected: false,
jetpack_installed: false,

View File

@ -1,5 +1,3 @@
@import "../../navigation/stylesheets/variables.scss";
.woocommerce-transient-notices {
position: absolute;
left: $gap;

View File

@ -1,48 +0,0 @@
/**
* External dependencies
*/
import { NavigationItem, useSlot } from '@woocommerce/experimental';
import { recordEvent } from '@woocommerce/tracks';
import { WooNavigationItem } from '@woocommerce/navigation';
const Item = ( { item } ) => {
const slot = useSlot( 'woocommerce_navigation_' + item.id );
const hasFills = Boolean( slot?.fills?.length );
const trackClick = ( id ) => {
recordEvent( 'navigation_click', {
menu_item: id,
} );
};
// Disable reason: The div wrapping the slot item is used for tracking purposes
// and should not be a tabbable element.
/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
// Only render a slot if a corresponding Fill exists and the item is not a category
if ( hasFills && ! item.isCategory ) {
return (
<NavigationItem key={ item.id } item={ item.id }>
<div onClick={ () => trackClick( item.id ) }>
<WooNavigationItem.Slot name={ item.id } />
</div>
</NavigationItem>
);
}
return (
<NavigationItem
key={ item.id }
item={ item.id }
title={ item.title }
badge={ item.badge ? item.badge : null }
href={ item.url }
navigateToMenu={ ! item.url && item.id }
onClick={ () => trackClick( item.id ) }
hideIfTargetMenuEmpty
/>
);
/* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
};
export default Item;

View File

@ -1,26 +0,0 @@
/**
* Internal dependencies
*/
import './style.scss';
import { FavoriteButton } from '../favorite-button';
import { FavoritesTooltip } from '../favorites-tooltip';
export const CategoryTitle = ( { category } ) => {
const { id, menuId, title } = category;
const className = 'woocommerce-navigation-category-title';
if ( [ 'plugins', 'favorites' ].includes( menuId ) ) {
return (
<span className={ className }>
<span className={ `${ className }__text` }>{ title }</span>
<FavoriteButton id={ id } />
<FavoritesTooltip />
</span>
);
}
return <span className={ className }>{ title }</span>;
};
export default CategoryTitle;

View File

@ -1,10 +0,0 @@
.woocommerce-navigation-category-title {
display: flex;
align-items: center;
font-size: 20px;
line-height: 28px;
.woocommerce-navigation-favorite-button {
margin-left: auto;
}
}

View File

@ -1,80 +0,0 @@
/**
* External dependencies
*/
import { render } from '@testing-library/react';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import CategoryTitle from '../';
describe( 'CategoryTitle', () => {
test( 'should render the category title without the option to favorite for the primary menu', () => {
const { container, queryByText } = render(
<CategoryTitle
category={ {
id: 'my-category',
menuId: 'primary',
title: 'Category Title',
} }
/>
);
expect(
container.querySelector( '.woocommerce-navigation-favorite-button' )
).toBeNull();
expect( queryByText( 'Category Title' ) ).not.toBeNull();
} );
test( 'should render the category title without the option to favorite for any other menus', () => {
const { container, queryByText } = render(
<CategoryTitle
category={ {
id: 'my-category',
menuId: 'new-menu',
title: 'Category Title',
} }
/>
);
expect(
container.querySelector( '.woocommerce-navigation-favorite-button' )
).toBeNull();
expect( queryByText( 'Category Title' ) ).not.toBeNull();
} );
test( 'should render the category title and favorite button for plugins', () => {
const { container, queryByText } = render(
<CategoryTitle
category={ {
id: 'my-category',
menuId: 'plugins',
title: 'Category Title',
} }
/>
);
expect(
container.querySelector( '.woocommerce-navigation-favorite-button' )
).not.toBeNull();
expect( queryByText( 'Category Title' ) ).not.toBeNull();
} );
test( 'should render the category title and unfavorite button for favorites', () => {
const { container, queryByText } = render(
<CategoryTitle
category={ {
id: 'my-category',
menuId: 'favorites',
title: 'Category Title',
} }
/>
);
expect(
container.querySelector( '.woocommerce-navigation-favorite-button' )
).not.toBeNull();
expect( queryByText( 'Category Title' ) ).not.toBeNull();
} );
} );

View File

@ -1,129 +0,0 @@
/**
* External dependencies
*/
import { useEffect, useMemo, useState, useRef } from '@wordpress/element';
import clsx from 'clsx';
import { Navigation } from '@woocommerce/experimental';
import { NAVIGATION_STORE_NAME, useUser } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
import { useSelect } from '@wordpress/data';
import { addHistoryListener } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { getMappedItemsCategories, getMatchingItem } from '../../utils';
import Header from '../header';
import { PrimaryMenu } from './primary-menu';
import { SecondaryMenu } from './secondary-menu';
const Container = () => {
const { menuItems } = useSelect( ( select ) => {
return {
menuItems: select( NAVIGATION_STORE_NAME ).getMenuItems(),
};
} );
useEffect( () => {
// Collapse the original WP Menu.
document.documentElement.classList.remove( 'wp-toolbar' );
document.body.classList.add( 'has-woocommerce-navigation' );
const adminMenu = document.getElementById( 'adminmenumain' );
if ( ! adminMenu ) {
return;
}
adminMenu.classList.add( 'folded' );
}, [] );
const [ activeItem, setActiveItem ] = useState( 'woocommerce-home' );
const [ activeLevel, setActiveLevel ] = useState( 'woocommerce' );
useEffect( () => {
const initialMatchedItem = getMatchingItem( menuItems );
if ( initialMatchedItem && activeItem !== initialMatchedItem ) {
setActiveItem( initialMatchedItem );
setActiveLevel( initialMatchedItem.parent );
}
const removeListener = addHistoryListener( () => {
setTimeout( () => {
const matchedItem = getMatchingItem( menuItems );
if ( matchedItem ) {
setActiveItem( matchedItem );
setActiveLevel( matchedItem.parent );
}
}, 0 );
} );
return removeListener;
}, [ menuItems ] );
const { currentUserCan } = useUser();
const { categories, items } = useMemo(
() => getMappedItemsCategories( menuItems, currentUserCan ),
[ menuItems, currentUserCan ]
);
const navDomRef = useRef( null );
const onBackClick = ( id ) => {
recordEvent( 'navigation_back_click', {
category: id,
} );
};
const isRoot = activeLevel === 'woocommerce';
const classes = clsx( 'woocommerce-navigation', {
'is-root': isRoot,
} );
return (
<div className={ classes }>
<Header />
<div className="woocommerce-navigation__wrapper" ref={ navDomRef }>
<Navigation
activeItem={ activeItem ? activeItem.id : null }
activeMenu={ activeLevel }
onActivateMenu={ ( ...args ) => {
if ( navDomRef && navDomRef.current ) {
navDomRef.current.scrollTop = 0;
}
setActiveLevel( ...args );
} }
>
{ Object.values( categories ).map( ( category ) => {
const categoryItems = items[ category.id ];
return (
!! categoryItems && [
<PrimaryMenu
key={ category.id }
category={ category }
onBackClick={ onBackClick }
primaryItems={ [
...categoryItems.primary,
...categoryItems.favorites,
] }
pluginItems={ categoryItems.plugins }
/>,
<SecondaryMenu
key={ `secondary/${ category.id }` }
category={ category }
onBackClick={ onBackClick }
items={ categoryItems.secondary }
/>,
]
);
} ) }
</Navigation>
</div>
</div>
);
};
export default Container;

View File

@ -1,89 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { NavigationMenu, NavigationGroup } from '@woocommerce/experimental';
import { applyFilters } from '@wordpress/hooks';
/**
* 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;
}
/**
* Navigation's exit button WooCommerce label.
*
* @filter woocommerce_navigation_root_back_label
* @param {string} label Back button label.
*/
const rootBackLabel = applyFilters(
'woocommerce_navigation_root_back_label',
__( 'WordPress Dashboard', 'woocommerce' )
);
/**
* Navigation's exit button url.
*
* @filter woocommerce_navigation_root_back_url
* @param {string} url Back button url.
*/
const rootBackUrl = applyFilters(
'woocommerce_navigation_root_back_url',
window.wcNavigation.rootBackUrl
);
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' )
: null
}
>
{ pluginItems.map( ( item ) => (
<Item key={ item.id } item={ item } />
) ) }
</NavigationGroup>
) }
</NavigationMenu>
);
};

View File

@ -1,39 +0,0 @@
/**
* 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,118 +0,0 @@
.woocommerce-navigation {
display: grid;
grid-template-rows: min-content 1fr;
height: 100%;
}
.woocommerce-navigation .woocommerce-navigation__wrapper {
h2 > span {
width: 100%;
}
.components-navigation__menu {
overflow-y: auto;
margin-bottom: 0;
padding-bottom: $gap-large;
}
.components-navigation__group + .components-navigation__group {
margin-top: 24px;
}
.components-navigation__item {
margin-bottom: 0;
.components-button {
opacity: 1;
}
&:not(:hover) {
.components-button {
color: $gray-400;
}
}
&:hover {
.components-button {
color: $white;
}
}
&.is-active {
.components-button {
color: #fff;
}
}
/*
* <-------- Start Temporary Code -------->
*
* A change to Gutenberg base Navigation component in version 9.8.3
* requires these overrides. As of this comment, the problematic code
* was published to .org but not shipped as part of WP 5.7.
*
* A fix in Gutenberg is https://github.com/WordPress/gutenberg/pull/28619
*
* Criteria for removal
* 1. https://github.com/WordPress/gutenberg/pull/28619 is merged and deployed to .org
* 2. https://github.com/WordPress/gutenberg/pull/28619 is included in the Gutenberg version associated with WP 5.7
* 3. If not part of WP 5.7, this code will be required until WP 6.0 is released and WC Admin no longer supports 5.7
*/
a.components-button {
padding: 6px 16px;
}
&:not(:hover) {
a.components-button {
// ${ G2.lightGray.ui };
color: $gray-400;
}
}
&.is-active {
a.components-button {
// ${ UI.textDark };
color: #fff;
}
}
/*
* <-------- End Temporary Code -------->
*/
}
.components-navigation {
height: 100%;
}
.components-navigation > div {
height: 100%;
display: grid;
grid-template-rows: 1fr min-content;
}
&.is-root {
.components-navigation__menu-secondary {
border-top: 1px solid $studio-gray-80;
margin: 0 -#{$gap-smaller};
padding: $gap $gap-smaller $gap-small $gap-smaller;
}
}
.components-navigation__menu-title,
.components-navigation__group-title {
color: #f0f0f0;
opacity: 1;
}
.components-navigation__back-button {
color: $gray-400;
opacity: 1;
&,
span {
font-size: 13px;
line-height: normal;
}
&:hover,
&:hover:not(:disabled) {
color: #ddd;
}
}
}

View File

@ -1,253 +0,0 @@
/**
* External dependencies
*/
import { act, render, waitFor } from '@testing-library/react';
import { getAdminLink } from '@woocommerce/settings';
import { getHistory } from '@woocommerce/navigation';
import { useSelect } from '@wordpress/data';
import userEvent from '@testing-library/user-event';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import Container from '../';
jest.mock( '@wordpress/data', () => {
// Require the original module to not be mocked...
const originalModule = jest.requireActual( '@wordpress/data' );
return {
__esModule: true, // Use it when dealing with esModules
...originalModule,
useSelect: jest.fn().mockReturnValue( {} ),
};
} );
const originalLocation = window.location;
global.window = Object.create( window );
window.wcNavigation = {
rootBackLabel: 'Root Back',
rootBackUrl: 'http://custombackbutton.com',
};
const menuItems = [
{
id: 'woocommerce-home',
title: 'Home',
parent: 'woocommerce',
menuId: 'primary',
url: 'admin.php?page=wc-admin',
},
{
id: 'primary-category',
title: 'Primary Category',
isCategory: true,
parent: 'woocommerce',
backButtonLabel: 'Custom Back Label',
menuId: 'primary',
},
{
id: 'primary-child',
title: 'Primary Child',
parent: 'primary-category',
menuId: 'primary',
url: 'admin.php?page=wc-admin&path=/child',
},
{
id: 'favorite-category',
title: 'Favorite Category',
isCategory: true,
parent: 'woocommerce',
menuId: 'favorites',
},
{
id: 'favorite-child',
title: 'Favorite Child',
parent: 'favorite-category',
menuId: 'plugins',
},
{
id: 'plugin-category',
title: 'Plugin Category',
isCategory: true,
parent: 'woocommerce',
menuId: 'plugins',
},
{
id: 'plugin-child',
title: 'Plugin Child',
parent: 'plugin-category',
menuId: 'plugins',
url: 'admin.php?page=my-plugin',
},
{
id: 'secondary-category',
title: 'Secondary Category',
isCategory: true,
parent: 'woocommerce',
menuId: 'secondary',
},
{
id: 'secondary-child',
title: 'Secondary Child',
parent: 'secondary-category',
menuId: 'secondary',
},
];
describe( 'Container', () => {
beforeEach( () => {
delete window.location;
window.location = new URL( getAdminLink( 'admin.php?page=wc-admin' ) );
useSelect.mockImplementation( () => ( {
menuItems,
favorites: [ 'favorite-category' ],
} ) );
} );
afterAll( () => {
window.location = originalLocation;
} );
test( 'should show the woocommerce root items initially', () => {
const { container } = render( <Container /> );
expect(
container.querySelectorAll( '.components-navigation__item' ).length
).toBe( 5 );
} );
test( 'should set the active item based on initial location', async () => {
const { queryByText } = render( <Container /> );
expect(
queryByText( 'Home' ).parentElement.parentElement.classList
).toContain( 'is-active' );
} );
test( 'should set the initial active item based on current location', () => {
window.location = new URL( getAdminLink( 'admin.php?page=my-plugin' ) );
const { container, queryByText } = render( <Container /> );
expect(
container.querySelector( '.woocommerce-navigation-category-title' )
.textContent
).toBe( 'Plugin Category' );
expect(
queryByText( 'Plugin Child' ).parentElement.parentElement.classList
).toContain( 'is-active' );
} );
test( 'should update the active item and level when location changes', async () => {
window.location = new URL( getAdminLink( 'admin.php?page=wc-admin' ) );
const { container, queryByText } = render( <Container /> );
expect( queryByText( 'Primary Child' ) ).toBeNull();
// Trigger and wait for history change.
await act( async () => {
delete window.location;
window.location = new URL(
getAdminLink( 'admin.php?page=wc-admin&path=/child' )
);
getHistory().push( new URL( getAdminLink( '/child' ) ) );
} );
await waitFor( () =>
expect(
container.querySelector(
'.woocommerce-navigation-category-title'
).textContent
).toBe( 'Primary Category' )
);
await waitFor( () =>
expect(
queryByText( 'Primary Child' ).parentElement.parentElement
.classList
).toContain( 'is-active' )
);
} );
test( 'should update the active level when a category is clicked', () => {
const { container, queryByText } = render( <Container /> );
userEvent.click( queryByText( 'Secondary Category' ) );
expect(
container.querySelector( '.woocommerce-navigation-category-title' )
.textContent
).toBe( 'Secondary Category' );
} );
test( 'should show the back button in each category', () => {
const { container, queryByText } = render( <Container /> );
userEvent.click( queryByText( 'Primary Category' ) );
const backButton = container.querySelector(
'.components-navigation__back-button'
);
expect( backButton.textContent ).toBe( 'Custom Back Label' );
} );
test( 'should go up a level on back button click', () => {
const { container, queryByText } = render( <Container /> );
userEvent.click( queryByText( 'Primary Category' ) );
const backButton = container.querySelector(
'.components-navigation__back-button'
);
userEvent.click( backButton );
expect(
container.querySelector( '.woocommerce-navigation-category-title' )
.textContent
).toBe( 'WooCommerce' );
} );
test( 'should show the favorite items after the primary items', () => {
const { container } = render( <Container /> );
const navigationGroups = container.querySelectorAll(
'.components-navigation__group'
);
expect(
navigationGroups[ 0 ].querySelector( 'li:nth-child(1)' ).textContent
).toBe( 'Home' );
expect(
navigationGroups[ 0 ].querySelector( 'li:nth-child(2)' ).textContent
).toBe( 'Primary Category' );
expect(
navigationGroups[ 0 ].querySelector( 'li:nth-child(3)' ).textContent
).toBe( 'Favorite Category' );
expect(
navigationGroups[ 1 ].querySelector( 'li:nth-child(1)' ).textContent
).toBe( 'Plugin Category' );
} );
test( 'should not show multiple menus outside of the root category', () => {
const { container, queryByText } = render( <Container /> );
const rootNavigationGroups = container.querySelectorAll(
'.components-navigation__group'
);
expect( rootNavigationGroups.length ).toBe( 3 );
userEvent.click( queryByText( 'Primary Category' ) );
const categoryNavigationGroups = container.querySelectorAll(
'.components-navigation__group'
);
expect( categoryNavigationGroups.length ).toBe( 1 );
} );
} );

View File

@ -1,68 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { NAVIGATION_STORE_NAME } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
import { useDispatch, useSelect } from '@wordpress/data';
import { Icon, starEmpty, starFilled } from '@wordpress/icons';
/**
* Internal dependencies
*/
import './style.scss';
export const FavoriteButton = ( { id } ) => {
const { favorites, isResolving } = useSelect( ( select ) => {
return {
favorites: select( NAVIGATION_STORE_NAME ).getFavorites(),
isResolving: select( NAVIGATION_STORE_NAME ).isResolving(
'getFavorites'
),
};
} );
const { addFavorite, removeFavorite } = useDispatch(
NAVIGATION_STORE_NAME
);
const isFavorited = favorites.includes( id );
const toggleFavorite = () => {
const toggle = isFavorited ? removeFavorite : addFavorite;
toggle( id );
recordEvent( 'navigation_favorite', {
id,
action: isFavorited ? 'unfavorite' : 'favorite',
} );
};
if ( isResolving ) {
return null;
}
return (
<Button
id="woocommerce-navigation-favorite-button"
className="woocommerce-navigation-favorite-button"
isTertiary
onClick={ toggleFavorite }
aria-label={
isFavorited
? __( 'Add this item to your favorites.', 'woocommerce' )
: __(
'Remove this item from your favorites.',
'woocommerce'
)
}
>
<Icon
icon={ isFavorited ? starFilled : starEmpty }
className={ `star-${ isFavorited ? 'filled' : 'empty' }-icon` }
/>
</Button>
);
};
export default FavoriteButton;

View File

@ -1,9 +0,0 @@
.woocommerce-navigation-favorite-button.components-button {
.star-empty-icon {
color: $gray-600;
}
.star-filled-icon {
color: $alert-yellow;
}
}

View File

@ -1,109 +0,0 @@
/**
* External dependencies
*/
import { render } from '@testing-library/react';
import { useDispatch, useSelect } from '@wordpress/data';
import userEvent from '@testing-library/user-event';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import FavoriteButton from '../';
jest.mock( '@wordpress/data', () => {
// Require the original module to not be mocked...
const originalModule = jest.requireActual( '@wordpress/data' );
return {
__esModule: true, // Use it when dealing with esModules
...originalModule,
useDispatch: jest.fn().mockReturnValue( {} ),
useSelect: jest.fn().mockReturnValue( {} ),
};
} );
describe( 'FavoriteButton', () => {
test( 'should not show when favorites are still resolving', () => {
useSelect.mockImplementation( () => ( {
favorites: [],
isResolving: true,
} ) );
const { container } = render( <FavoriteButton id="my-item" /> );
expect(
container.querySelector( '.woocommerce-navigation-favorite-button' )
).toBeNull();
} );
test( 'should show the empty star when item is not favorited', () => {
useSelect.mockImplementation( () => ( {
favorites: [],
isResolving: false,
} ) );
const { container } = render( <FavoriteButton id="my-item" /> );
expect( container.querySelector( '.star-empty-icon' ) ).not.toBeNull();
} );
test( 'should show the filled star when item is favorited', () => {
useSelect.mockImplementation( () => ( {
favorites: [ 'my-item' ],
isResolving: false,
} ) );
const { container } = render( <FavoriteButton id="my-item" /> );
expect( container.querySelector( '.star-filled-icon' ) ).not.toBeNull();
} );
test( 'should remove the favorite when toggling a favorited item', () => {
useSelect.mockImplementation( () => ( {
favorites: [ 'my-item' ],
isResolving: false,
} ) );
const addFavorite = jest.fn();
const removeFavorite = jest.fn();
useDispatch.mockReturnValue( {
addFavorite,
removeFavorite,
} );
const { container } = render( <FavoriteButton id="my-item" /> );
userEvent.click(
container.querySelector( '.woocommerce-navigation-favorite-button' )
);
expect( addFavorite ).not.toHaveBeenCalled();
expect( removeFavorite ).toHaveBeenCalled();
} );
test( 'should add the favorite when toggling a unfavorited item', () => {
useSelect.mockImplementation( () => ( {
favorites: [],
isResolving: false,
} ) );
const addFavorite = jest.fn();
const removeFavorite = jest.fn();
useDispatch.mockReturnValue( {
addFavorite,
removeFavorite,
} );
const { container } = render( <FavoriteButton id="my-item" /> );
userEvent.click(
container.querySelector( '.woocommerce-navigation-favorite-button' )
);
expect( addFavorite ).toHaveBeenCalled();
expect( removeFavorite ).not.toHaveBeenCalled();
} );
} );

View File

@ -1,60 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { NAVIGATION_STORE_NAME, OPTIONS_STORE_NAME } from '@woocommerce/data';
import { useDispatch, useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import { HighlightTooltip } from '~/activity-panel/highlight-tooltip';
const tooltipHiddenOption = 'woocommerce_navigation_favorites_tooltip_hidden';
export const FavoritesTooltip = () => {
const { isFavoritesResolving, isOptionResolving, isTooltipHidden } =
useSelect( ( select ) => {
const { getOption, isResolving } = select( OPTIONS_STORE_NAME );
return {
isFavoritesResolving: select(
NAVIGATION_STORE_NAME
).isResolving( 'getFavorites' ),
isOptionResolving: isResolving( 'getOption', [
tooltipHiddenOption,
] ),
isTooltipHidden: getOption( tooltipHiddenOption ) === 'yes',
};
} );
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
if ( isFavoritesResolving || isTooltipHidden || isOptionResolving ) {
return null;
}
if ( document.body.classList.contains( 'is-wc-nav-folded' ) ) {
return null;
}
return (
<HighlightTooltip
delay={ 1000 }
title={ __( 'Introducing favorites', 'woocommerce' ) }
content={ __(
'You can now favorite your extensions to pin them in the top level of the navigation.',
'woocommerce'
) }
closeButtonText={ __( 'Got it', 'woocommerce' ) }
id="woocommerce-navigation-favorite-button"
onClose={ () =>
updateOptions( {
[ tooltipHiddenOption ]: 'yes',
} )
}
useAnchor
/>
);
};
export default FavoritesTooltip;

View File

@ -1,133 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { decodeEntities } from '@wordpress/html-entities';
import { Icon, wordpress } from '@wordpress/icons';
import { getSetting } from '@woocommerce/settings';
import { useSelect } from '@wordpress/data';
import { useEffect, useState } from '@wordpress/element';
import clsx from 'clsx';
import { debounce } from 'lodash';
import { addHistoryListener } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import useIsScrolled from '../../../hooks/useIsScrolled';
const Header = () => {
const siteTitle = getSetting( 'siteTitle', '' );
const homeUrl = getSetting( 'homeUrl', '' );
const { isScrolled } = useIsScrolled();
const [ isFolded, setIsFolded ] = useState(
document.body.classList.contains( false )
);
const navClasses = {
folded: 'is-wc-nav-folded',
expanded: 'is-wc-nav-expanded',
};
const foldNav = () => {
document.body.classList.add( navClasses.folded );
document.body.classList.remove( navClasses.expanded );
setIsFolded( true );
};
const expandNav = () => {
document.body.classList.remove( navClasses.folded );
document.body.classList.add( navClasses.expanded );
setIsFolded( false );
};
const toggleFolded = () => {
if ( document.body.classList.contains( navClasses.folded ) ) {
expandNav();
} else {
foldNav();
}
};
const foldOnMobile = ( screenWidth = document.body.clientWidth ) => {
if ( screenWidth <= 960 ) {
foldNav();
} else {
expandNav();
}
};
useEffect( () => {
foldOnMobile();
const foldEvents = [
{
eventName: 'orientationchange',
handler: ( e ) => foldOnMobile( e.target.screen.availWidth ),
},
{
eventName: 'resize',
handler: debounce( () => foldOnMobile(), 200 ),
},
];
for ( const { eventName, handler } of foldEvents ) {
window.addEventListener( eventName, handler, false );
}
addHistoryListener( () => foldOnMobile() );
}, [] );
let buttonIcon = <Icon size="36px" icon={ wordpress } />;
const { isRequestingSiteIcon, siteIconUrl } = useSelect( ( select ) => {
const { isResolving } = select( 'core/data' );
const { getEntityRecord } = select( 'core' );
const siteData =
getEntityRecord( 'root', '__unstableBase', undefined ) || {};
return {
isRequestingSiteIcon: isResolving( 'core', 'getEntityRecord', [
'root',
'__unstableBase',
undefined,
] ),
siteIconUrl: siteData.siteIconUrl,
};
} );
if ( siteIconUrl ) {
buttonIcon = (
<img alt={ __( 'Site Icon', 'woocommerce' ) } src={ siteIconUrl } />
);
} else if ( isRequestingSiteIcon ) {
buttonIcon = null;
}
const className = clsx( 'woocommerce-navigation-header', {
'is-scrolled': isScrolled,
} );
return (
<div className={ className }>
<Button
onClick={ () => toggleFolded() }
className="woocommerce-navigation-header__site-icon"
aria-label="Fold navigation"
role="switch"
aria-checked={ isFolded ? 'true' : 'false' }
>
{ buttonIcon }
</Button>
<Button
title={ siteTitle }
href={ homeUrl }
className="woocommerce-navigation-header__site-title"
as="span"
>
{ decodeEntities( siteTitle ) }
</Button>
</div>
);
};
export default Header;

View File

@ -1,39 +0,0 @@
.woocommerce-navigation-header {
display: flex;
align-items: center;
border: none;
border-radius: 0;
height: auto;
.woocommerce-navigation-header__site-icon.components-button {
padding: 12px;
height: 60px;
color: #fff;
&:hover,
&:focus,
&:not([aria-disabled="true"]):active {
color: #fff;
}
}
.woocommerce-navigation-header__site-title.components-button {
padding-left: 0;
color: $gray-400;
font-weight: 600;
&:hover,
&:focus,
&:active {
color: $gray-200;
}
}
.woocommerce-navigation-header__site-title {
padding-top: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}

View File

@ -1,95 +0,0 @@
/**
* External dependencies
*/
import { render } from '@testing-library/react';
import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import Header from '../';
global.window.wcNavigation = {};
jest.mock( '@woocommerce/settings', () => ( {
...jest.requireActual( '@woocommerce/settings' ),
getSetting: jest.fn( ( setting ) => {
const settings = {
homeUrl: 'https://fake-site-url.com',
siteTitle: 'Fake &amp; Title &lt;3',
};
return settings[ setting ];
} ),
} ) );
jest.mock( '@wordpress/data', () => {
// Require the original module to not be mocked...
const originalModule = jest.requireActual( '@wordpress/data' );
return {
__esModule: true, // Use it when dealing with esModules
...originalModule,
useDispatch: jest.fn().mockReturnValue( {} ),
useSelect: jest.fn().mockReturnValue( {} ),
};
} );
describe( 'Header', () => {
test( 'should not show the button when the site icon is requesting', () => {
useSelect.mockImplementation( () => ( {
isRequestingSiteIcon: true,
siteIconUrl: null,
} ) );
const { container } = render( <Header /> );
expect( container.querySelector( 'img' ) ).toBeNull();
} );
test( 'should show the button when the site icon has resolved', () => {
useSelect.mockImplementation( () => ( {
isRequestingSiteIcon: false,
siteIconUrl: '#',
} ) );
const { container } = render( <Header /> );
expect( container.querySelector( 'img' ) ).not.toBeNull();
} );
test( 'should start with the nav expanded in larger viewports', () => {
Object.defineProperty( window.HTMLElement.prototype, 'clientWidth', {
configurable: true,
value: 2000,
} );
render( <Header /> );
expect( Object.values( document.body.classList ) ).toContain(
'is-wc-nav-expanded'
);
} );
test( 'should start with the nav folded when the viewport is smaller', () => {
Object.defineProperty( window.HTMLElement.prototype, 'clientWidth', {
configurable: true,
value: 480,
} );
render( <Header /> );
expect( Object.values( document.body.classList ) ).toContain(
'is-wc-nav-folded'
);
} );
test( 'should decode site titles', () => {
const { container } = render( <Header /> );
expect(
container.querySelector(
'.woocommerce-navigation-header__site-title'
).textContent
).toBe( 'Fake & Title <3' );
} );
} );

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,120 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Guide } from '@wordpress/components';
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
import { Text } from '@woocommerce/experimental';
import { useState } from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import './style.scss';
import NavInto1 from './images/nav-intro-1.png';
import NavInto2 from './images/nav-intro-2.png';
import NavInto3 from './images/nav-intro-3.png';
export const INTRO_MODAL_DISMISSED_OPTION_NAME =
'woocommerce_navigation_intro_modal_dismissed';
export const IntroModal = () => {
const [ isOpen, setOpen ] = useState( true );
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
const { isDismissed, isResolving } = useSelect( ( select ) => {
const { getOption, isResolving: isOptionResolving } =
select( OPTIONS_STORE_NAME );
const dismissedOption = getOption( INTRO_MODAL_DISMISSED_OPTION_NAME );
return {
isDismissed: dismissedOption === 'yes',
isResolving:
typeof dismissedOption === 'undefined' ||
isOptionResolving( 'getOption', [
INTRO_MODAL_DISMISSED_OPTION_NAME,
] ),
};
} );
const dismissModal = () => {
updateOptions( {
[ INTRO_MODAL_DISMISSED_OPTION_NAME ]: 'yes',
} );
recordEvent( 'navigation_intro_modal_close', {} );
setOpen( false );
};
if ( ! isOpen || isDismissed || isResolving ) {
return null;
}
const getPage = ( title, description, imageUrl ) => {
return {
content: (
<div className="woocommerce-navigation-intro-modal__page-wrapper">
<div className="woocommerce-navigation-intro-modal__page-text">
<Text
variant="title.large"
as="h2"
size="32"
lineHeight="40px"
>
{ title }
</Text>
<Text
as="p"
variant="body.large"
size="16"
lineHeight="24px"
>
{ description }
</Text>
</div>
<div className="woocommerce-navigation-intro-modal__image-wrapper">
<img alt={ title } src={ imageUrl } />
</div>
</div>
),
};
};
return (
<Guide
className="woocommerce-navigation-intro-modal"
onFinish={ dismissModal }
pages={ [
getPage(
__( 'A new navigation for WooCommerce', 'woocommerce' ),
__(
'All of your store management features in one place',
'woocommerce'
),
NavInto1
),
getPage(
__( 'Focus on managing your store', 'woocommerce' ),
__(
'Give your attention to key areas of WooCommerce with little distraction',
'woocommerce'
),
NavInto2
),
getPage(
__(
'Easily find and favorite your extensions',
'woocommerce'
),
__(
"They'll appear in the top level of the navigation for quick access",
'woocommerce'
),
NavInto3
),
] }
/>
);
};

View File

@ -1,76 +0,0 @@
.woocommerce-navigation-intro-modal {
width: 670px;
@include breakpoint( "<782px" ) {
width: 350px;
}
.components-guide__page-control {
order: 3;
margin: $gap 0;
li {
margin: 0;
}
}
.components-modal__header {
display: none;
}
.components-guide__container {
margin-top: 0;
}
.components-guide__footer {
box-sizing: border-box;
margin: 0;
height: 0;
overflow: visible;
.components-button {
position: absolute;
bottom: 100%;
margin-bottom: $gap;
}
.components-guide__back-button {
display: none;
}
}
&.components-modal__frame.components-guide {
height: auto;
}
.woocommerce-navigation-intro-modal__page-wrapper {
display: grid;
grid-template-columns: 1fr 1fr;
img {
max-width: 100%;
}
@include breakpoint( "<782px" ) {
grid-template-columns: 1fr;
.woocommerce-navigation-intro-modal__image-wrapper {
grid-row: 1;
max-height: 328px;
overflow: hidden;
}
}
}
.woocommerce-navigation-intro-modal__page-text {
padding: $gap-large;
display: flex;
flex-direction: column;
justify-content: center;
h2 {
font-weight: bold;
margin-bottom: $gap-smaller;
}
}
}

View File

@ -1,74 +0,0 @@
/**
* External dependencies
*/
import { fireEvent, render, screen } from '@testing-library/react';
import { useDispatch, useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import { IntroModal, INTRO_MODAL_DISMISSED_OPTION_NAME } from '../';
global.window.wcNavigation = {};
jest.mock( '@wordpress/data', () => {
// Require the original module to not be mocked...
const originalModule = jest.requireActual( '@wordpress/data' );
return {
__esModule: true, // Use it when dealing with esModules
...originalModule,
useDispatch: jest.fn().mockReturnValue( {} ),
useSelect: jest.fn().mockReturnValue( {} ),
};
} );
describe( 'IntroModal', () => {
test( 'should not show when modal options are resolving', () => {
useSelect.mockImplementation( () => ( {
isResolving: true,
} ) );
const { container } = render( <IntroModal /> );
expect( container ).toBeEmptyDOMElement();
} );
test( 'should not dismiss when the modal has already been dismissed', () => {
const updateOptions = jest.fn();
useSelect.mockImplementation( () => ( {
isDismissed: true,
isResolving: false,
} ) );
useDispatch.mockImplementation( () => ( {
updateOptions,
} ) );
const { container } = render( <IntroModal /> );
expect( container ).toBeEmptyDOMElement();
expect( updateOptions ).not.toHaveBeenCalled();
} );
test( 'should hide and update the dismissal option when closing the modal', () => {
const updateOptions = jest.fn();
useSelect.mockImplementation( () => ( {
isResolving: false,
isDismissed: false,
} ) );
useDispatch.mockImplementation( () => ( {
updateOptions,
} ) );
render( <IntroModal /> );
fireEvent.click( screen.queryByLabelText( 'Close dialog' ) );
expect(
screen.queryByText( 'A new navigation for WooCommerce' )
).toBeNull();
expect( updateOptions ).toHaveBeenCalledWith( {
[ INTRO_MODAL_DISMISSED_OPTION_NAME ]: 'yes',
} );
} );
} );

View File

@ -1,16 +0,0 @@
/**
* External dependencies
*/
import { withNavigationHydration } from '@woocommerce/data';
/**
* Internal dependencies
*/
import './style.scss';
import Container from './components/container';
const HydratedNavigation = withNavigationHydration( window.wcNavigation )(
Container
);
export default HydratedNavigation;

View File

@ -1,167 +0,0 @@
@import "./stylesheets/variables.scss";
@import "./components/container/style.scss";
@import "./components/header/style.scss";
.woocommerce-navigation {
position: relative;
width: $navigation-width;
box-sizing: border-box;
background-color: $gray-900;
z-index: 1100; //Must be greater than z-index on .woocommerce-layout__header
@media ( max-width: 960px ) {
width: $header-height;
height: $header-height;
}
.components-navigation {
box-sizing: border-box;
}
.components-navigation__menu-title {
overflow: visible;
}
.components-navigation__menu {
scrollbar-color: $gray-700 $gray-900;
scrollbar-width: thin;
&::-webkit-scrollbar-thumb {
border-radius: 10px;
background-color: $gray-700;
}
&::-webkit-scrollbar-thumb:hover {
background-color: $gray-700;
width: 8px;
height: 8px;
}
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
}
}
.woocommerce-navigation__wrapper {
background-color: $gray-900;
position: absolute;
top: $header-height;
width: 100%;
height: calc(100vh - #{$header-height + $adminbar-height});
overflow-y: auto;
}
.is-wp-toolbar-disabled .woocommerce-navigation__wrapper {
height: calc(100vh - #{$header-height});
}
body.is-wc-nav-expanded {
.woocommerce-navigation {
width: $navigation-width;
height: 100%;
}
font > .xdebug-error {
margin-left: calc(#{$navigation-width} + #{$gap});
}
}
body.is-wc-nav-folded {
.woocommerce-navigation {
width: $header-height;
height: $header-height;
overflow: hidden;
.woocommerce-navigation-header {
> * {
display: none;
}
}
.woocommerce-navigation-header__site-icon {
display: block;
}
.components-navigation {
display: none;
}
}
.woocommerce-transient-notices {
left: $gap;
}
#wpbody {
padding-left: 0;
}
}
.has-woocommerce-navigation {
#adminmenuwrap,
#adminmenuback {
display: none !important;
}
&.woocommerce_page_wc-reports,
&.woocommerce_page_wc-settings,
&.woocommerce_page_wc-status {
.woo-nav-tab-wrapper {
display: none;
}
.woocommerce .subsubsub {
font-size: 14px;
margin: 5px 0;
}
}
#wpcontent,
#wpfooter {
margin-left: 0;
@media ( max-width: 960px ) {
margin-left: 0;
}
}
#wpbody {
padding-left: $navigation-width;
@media ( max-width: 960px ) {
padding-left: 0;
}
}
.woocommerce-layout__header.is-embed-loading {
&::before {
content: "";
position: fixed;
width: $navigation-width;
height: 100%;
background: $gray-900;
@include breakpoint( "<960px" ) {
width: $header-height;
height: $header-height;
}
}
}
#woocommerce-embedded-root.is-embed-loading {
margin-bottom: -$adminbar-height;
}
&:not(.is-wp-toolbar-disabled) {
#wpbody-content {
margin-top: $adminbar-height;
}
}
font > .xdebug-error {
margin-top: $header-height;
}
}

View File

@ -1,22 +0,0 @@
// WooCommerce Navigation
$navigation-width: 240px;
$navigation-x-padding: 30px;
// WordPress defaults.
$admin-menu-width: 160px;
$admin-bar-height: 32px;
$admin-bar-height-mobile: 46px;
// Gaps and gutters.
$fallback-gutter: 24px;
$fallback-gutter-large: 40px;
$gutter: var(--main-gap);
$gutter-large: var(--large-gap);
$gap-largest: 40px;
$gap-larger: 36px;
$gap-large: 24px;
$gap: 16px;
$gap-small: 12px;
$gap-smaller: 8px;
$gap-smallest: 4px;

View File

@ -1,5 +0,0 @@
describe( 'Sample test', () => {
it( 'should pass', () => {
expect( true ).not.toBeNull();
} );
} );

View File

@ -1,521 +0,0 @@
/**
* External dependencies
*/
import { getAdminLink } from '@woocommerce/settings';
/**
* Internal dependencies
*/
import {
getDefaultMatchExpression,
getFullUrl,
getMappedItemsCategories,
getMatchingItem,
getMatchScore,
sortMenuItems,
} from '../utils';
const originalLocation = window.location;
global.window = Object.create( window );
global.window.wcNavigation = {};
const sampleMenuItems = [
{
id: 'main',
title: 'Main page',
url: 'admin.php?page=wc-admin',
},
{
id: 'path',
title: 'Page with Path',
url: 'admin.php?page=wc-admin&path=/test-path',
},
{
id: 'hash',
title: 'Page with Hash',
url: 'admin.php?page=wc-admin&path=/test-path#anchor',
},
{
id: 'multiple-args',
title: 'Page with multiple arguments',
url: 'admin.php?page=wc-admin&path=/test-path&section=section-name',
},
{
id: 'multiple-args-plus-one',
title: 'Page with same multiple arguments plus an additional one',
url: 'admin.php?page=wc-admin&path=/test-path&section=section-name&version=22',
},
{
id: 'hash-and-multiple-args',
title: 'Page with multiple arguments and a hash',
url: 'admin.php?page=wc-admin&path=/test-path&section=section-name#anchor',
},
];
const runGetMatchingItemTests = ( items ) => {
it( 'should get the closest matched item', () => {
window.location = new URL( getAdminLink( 'admin.php?page=wc-admin' ) );
const matchingItem = getMatchingItem( items );
expect( matchingItem.id ).toBe( 'main' );
} );
it( 'should match the item without hash if a better match does not exist', () => {
window.location = new URL(
getAdminLink( 'admin.php?page=wc-admin#hash' )
);
const matchingItem = getMatchingItem( items );
expect( matchingItem.id ).toBe( 'main' );
} );
it( 'should exactly match the item with a hash if it exists', () => {
window.location = new URL(
getAdminLink( 'admin.php?page=wc-admin&path=/test-path#anchor' )
);
const matchingItem = getMatchingItem( items );
expect( matchingItem.id ).toBe( 'hash' );
} );
it( 'should roughly match the item if all menu item arguments exist', () => {
window.location = new URL(
getAdminLink(
'admin.php?page=wc-admin&path=/test-path&section=section-name'
)
);
const matchingItem = getMatchingItem( items );
expect( matchingItem.id ).toBe( 'multiple-args' );
} );
it( 'should match an item with irrelevant query parameters', () => {
window.location = new URL(
getAdminLink(
'admin.php?page=wc-admin&path=/test-path&section=section-name&foo=bar'
)
);
const matchingItem = getMatchingItem( items );
expect( matchingItem.id ).toBe( 'multiple-args' );
} );
it( 'should match an item with similar query args plus one additional arg', () => {
window.location = new URL(
getAdminLink(
'admin.php?page=wc-admin&path=/test-path&section=section-name&version=22'
)
);
const matchingItem = getMatchingItem( items );
expect( matchingItem.id ).toBe( 'multiple-args-plus-one' );
} );
it( 'should match an item with query parameters in mixed order', () => {
window.location = new URL(
getAdminLink(
'admin.php?foo=bar&page=wc-admin&path=/test-path&section=section-name'
)
);
const matchingItem = getMatchingItem( items );
expect( matchingItem.id ).toBe( 'multiple-args' );
} );
it( 'should match an item with query parameters and a hash', () => {
window.location = new URL(
getAdminLink(
'admin.php?foo=bar&page=wc-admin&path=/test-path&section=section-name#anchor'
)
);
const matchingItem = getMatchingItem( items );
expect( matchingItem.id ).toBe( 'hash-and-multiple-args' );
} );
};
describe( 'getMatchingItem', () => {
beforeAll( () => {
delete window.location;
} );
afterAll( () => {
window.location = originalLocation;
} );
runGetMatchingItemTests( sampleMenuItems );
// re-run the tests with sampleMenuItems in reverse order.
runGetMatchingItemTests( sampleMenuItems.reverse() );
} );
describe( 'getDefaultMatchExpression', () => {
it( 'should return the regex for the path without query args', () => {
expect( getDefaultMatchExpression( 'http://wordpress.org' ) ).toBe(
'^http:\\/\\/wordpress\\.org'
);
} );
it( 'should return the regex for the path and query args', () => {
expect(
getDefaultMatchExpression(
'http://wordpress.org?param1=a&param2=b'
)
).toBe(
'^http:\\/\\/wordpress\\.org(?=.*[?|&]param1=a(&|$|#))(?=.*[?|&]param2=b(&|$|#))'
);
} );
it( 'should return the regex with hash if present', () => {
expect(
getDefaultMatchExpression(
'http://wordpress.org?param1=a&param2=b#hash'
)
).toBe(
'^http:\\/\\/wordpress\\.org(?=.*[?|&]param1=a(&|$|#))(?=.*[?|&]param2=b(&|$|#))(.*#hash$)'
);
} );
} );
describe( 'getMatchScore', () => {
beforeAll( () => {
delete window.location;
window.location = new URL( getAdminLink( '/' ) );
} );
afterAll( () => {
window.location = originalLocation;
} );
it( 'should return max safe integer if the url is an exact match', () => {
expect(
getMatchScore(
new URL( getAdminLink( 'admin.php?page=testpage' ) ),
getAdminLink( 'admin.php?page=testpage' )
)
).toBe( Number.MAX_SAFE_INTEGER );
} );
it( 'should return matching path and parameter count', () => {
expect(
getMatchScore(
new URL(
getFullUrl(
'/wp-admin/admin.php?page=testpage&extra_param=a'
)
),
'/wp-admin/admin.php?page=testpage'
)
).toBe( 2 );
} );
it( 'should return 0 if the URL does not meet match criteria', () => {
expect(
getMatchScore(
new URL( getAdminLink( 'admin.php?page=different-page' ) ),
getAdminLink( 'admin.php?page=testpage' )
)
).toBe( 0 );
} );
it( 'should return match count for a custom match expression', () => {
expect(
getMatchScore(
new URL(
getAdminLink( 'admin.php?page=different-page&param1=a' )
),
getAdminLink( 'admin.php?page=testpage' ),
'param1=a'
)
).toBe( 1 );
} );
it( 'should return 0 for custom match expression that does not match', () => {
expect(
getMatchScore(
new URL(
getAdminLink( 'admin.php?page=different-page&param1=b' )
),
getAdminLink( 'admin.php?page=testpage' ),
'param1=a'
)
).toBe( 0 );
} );
it( 'should return match count if params match but are out of order', () => {
expect(
getMatchScore(
new URL( getAdminLink( 'admin.php?param1=a&page=testpage' ) ),
getAdminLink( 'admin.php?page=testpage' )
)
).toBe( 2 );
} );
it( 'should return match count if multiple params match but are out of order', () => {
expect(
getMatchScore(
new URL(
getAdminLink( 'admin.php?param1=a&page=testpage&param2=b' )
),
getAdminLink( 'admin.php?page=testpage&param1=a' )
)
).toBe( 3 );
} );
} );
describe( 'getFullUrl', () => {
beforeAll( () => {
delete window.location;
window.location = new URL( getAdminLink( '/' ) );
} );
afterAll( () => {
window.location = originalLocation;
} );
it( 'should get the full admin URL from a path', () => {
expect( getFullUrl( 'admin.php?page=testpage' ) ).toBe(
getAdminLink( 'admin.php?page=testpage' )
);
} );
it( 'should return the same URL from an already complete URL', () => {
expect( getFullUrl( getAdminLink( 'admin.php?page=testpage' ) ) ).toBe(
getAdminLink( 'admin.php?page=testpage' )
);
} );
} );
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

@ -1,259 +0,0 @@
/**
* External dependencies
*/
import { getAdminLink } from '@woocommerce/settings';
type MenuId = 'primary' | 'favorites' | 'plugins' | 'secondary';
interface Item {
id: string;
matchExpression: string;
url: string;
order: number;
title: string;
parent: string;
menuId: MenuId;
capability: string;
isCategory: boolean;
}
interface Category {
id: string;
isCategory: boolean;
menuId: MenuId;
migrate: boolean;
order: number;
parent: string;
title: string;
primary?: Item[];
favorites?: Item[];
plugins?: Item[];
secondary?: Item[];
}
/**
* Get the full URL if a relative path is passed.
*/
export const getFullUrl = ( url: string ): string => {
if ( url.indexOf( 'http' ) === 0 ) {
return url;
}
return getAdminLink( url );
};
/**
* Get a default regex expression to match the path and provided params.
*/
export const getDefaultMatchExpression = ( url: string ): string => {
const escapedUrl = url.replace( /[-\/\\^$*+?.()|[\]{}]/gi, '\\$&' );
const [ path, args, hash ] = escapedUrl.split( /\\\?|#/ );
const hashExpression = hash ? `(.*#${ hash }$)` : '';
const argsExpression = args
? args.split( '&' ).reduce( ( acc, param ) => {
return `${ acc }(?=.*[?|&]${ param }(&|$|#))`;
}, '' )
: '';
return '^' + path + argsExpression + hashExpression;
};
/**
* Get a match score for a menu item given a location.
*/
export const getMatchScore = (
location: Location,
itemUrl: string,
itemExpression: string | null = null
): number => {
if ( ! itemUrl ) {
return 0;
}
const fullUrl = getFullUrl( itemUrl );
const { href } = location;
// Return highest possible score for exact match.
if ( fullUrl === href ) {
return Number.MAX_SAFE_INTEGER;
}
const defaultExpression = getDefaultMatchExpression( fullUrl );
const regexp = new RegExp( itemExpression || defaultExpression, 'i' );
return ( decodeURIComponent( href ).match( regexp ) || [] ).length;
};
interface wcNavigation {
menuItems: Item[];
rootBackLabel: string;
rootBackUrl: string;
historyPatched: boolean;
}
declare global {
interface Window {
wcNavigation: wcNavigation;
}
}
interface wcNavigation {
menuItems: Item[];
rootBackLabel: string;
rootBackUrl: string;
historyPatched: boolean;
}
declare global {
interface Window {
wcNavigation: wcNavigation;
}
}
/**
* Get the closest matching item.
*
* @param {Array} items An array of items to match against.
*/
export const getMatchingItem = ( items: Item[] ): Item | null => {
let matchedItem = null;
let highestMatchScore = 0;
items.forEach( ( item ) => {
const score = getMatchScore(
window.location,
item.url,
item.matchExpression
);
if ( score > 0 && score >= highestMatchScore ) {
highestMatchScore = score;
matchedItem = item;
}
} );
return matchedItem || null;
};
/**
* Available menu IDs.
*/
export const menuIds: MenuId[] = [
'primary',
'favorites',
'plugins',
'secondary',
];
interface Category {
id: string;
isCategory: boolean;
menuId: MenuId;
migrate: boolean;
order: number;
parent: string;
title: string;
primary?: Item[];
favorites?: Item[];
plugins?: Item[];
secondary?: Item[];
}
/**
* Default categories for the menu.
*/
export const defaultCategories: {
[ key: string ]: Category;
} = {
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: Item[] ): Item[] => {
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 their 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: Item[],
currentUserCan: ( capability: string ) => boolean
): {
items: Record< string, Category | Record< string, Item[] > >;
categories: Record< string, Category | Item >;
} => {
const categories: {
[ key: string ]: Category | Item;
} = { ...defaultCategories };
const items = sortMenuItems( menuItems ).reduce(
(
acc: {
[ key: string ]: Category | { [ key: string ]: Item[] };
},
item: 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;
}
const menuIdArray = acc[ item.parent ][ item.menuId ];
if ( menuIdArray ) {
menuIdArray.push( item );
}
return acc;
},
{}
);
return {
items,
categories,
};
};

View File

@ -1,63 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { Button, Modal } from '@wordpress/components';
export class NavigationOptOutContainer extends Component {
constructor( props ) {
super( props );
this.state = {
isModalOpen: true,
};
}
render() {
const { isModalOpen } = this.state;
if ( ! isModalOpen ) {
return null;
}
if ( ! window.surveyData || ! window.surveyData.url ) {
return null;
}
return (
<Modal
title={ __( 'Help us improve', 'woocommerce' ) }
onRequestClose={ () => this.setState( { isModalOpen: false } ) }
className="woocommerce-navigation-opt-out-modal"
>
<p>
{ __(
"Take this 2-minute survey to share why you're opting out of the new navigation",
'woocommerce'
) }
</p>
<div className="woocommerce-navigation-opt-out-modal__actions">
<Button
isDefault
onClick={ () =>
this.setState( { isModalOpen: false } )
}
>
{ __( 'No thanks', 'woocommerce' ) }
</Button>
<Button
isPrimary
target="_blank"
href={ window.surveyData.url }
onClick={ () =>
this.setState( { isModalOpen: false } )
}
>
{ __( 'Share feedback', 'woocommerce' ) }
</Button>
</div>
</Modal>
);
}
}

View File

@ -1,18 +0,0 @@
/**
* External dependencies
*/
import { render } from '@wordpress/element';
/**
* Internal dependencies
*/
import { NavigationOptOutContainer } from './container';
import './style.scss';
const navigationOptOutRoot = document.createElement( 'div' );
navigationOptOutRoot.setAttribute( 'id', 'navigation-opt-out-root' );
render(
<NavigationOptOutContainer />,
document.body.appendChild( navigationOptOutRoot )
);

View File

@ -1,8 +0,0 @@
.woocommerce-navigation-opt-out-modal__actions {
text-align: right;
margin-top: $gap-large;
.components-button.is-primary {
margin-left: $gap;
}
}

View File

@ -1,26 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { registerPlugin } from '@wordpress/plugins';
import { WooNavigationItem } from '@woocommerce/navigation';
const MyPlugin = () => {
const handleClick = () => {
alert( 'Menu item clicked!' );
};
return (
<WooNavigationItem item="example-category-child-2">
<Button onClick={ handleClick }>
{ __( 'JavaScript Example', 'plugin-domain' ) }
</Button>
</WooNavigationItem>
);
};
registerPlugin( 'my-plugin', {
render: MyPlugin,
scope: 'woocommerce-navigation',
} );

View File

@ -1,77 +0,0 @@
<?php
/**
* Plugin Name: WooCommerce Admin Add Navigation Items Example
*
* @package WooCommerce\Admin
*/
/**
* Register the JS.
*/
function add_navigation_items_register_script() {
if ( ! class_exists( '\Automattic\WooCommerce\Admin\Features\Navigation\Screen' ) || ! \Automattic\WooCommerce\Admin\Features\Navigation\Screen::is_woocommerce_page() ) {
return;
}
$asset_file = require __DIR__ . '/dist/index.asset.php';
wp_register_script(
'add-navigation-items',
plugins_url( '/dist/index.js', __FILE__ ),
$asset_file['dependencies'],
$asset_file['version'],
true
);
wp_enqueue_script( 'add-navigation-items' );
}
add_action( 'admin_enqueue_scripts', 'add_navigation_items_register_script' );
/**
* Add Example items to WooCommerce Navigation
*/
function add_navigation_items_register_items() {
if (
! method_exists( '\Automattic\WooCommerce\Admin\Features\Navigation\Menu', 'add_plugin_category' ) ||
! method_exists( '\Automattic\WooCommerce\Admin\Features\Navigation\Menu', 'add_plugin_item' )
) {
return;
}
\Automattic\WooCommerce\Admin\Features\Navigation\Menu::add_plugin_item(
array(
'id' => 'example-plugin',
'title' => 'Example Plugin',
'capability' => 'view_woocommerce_reports',
'url' => 'https://www.google.com',
)
);
\Automattic\WooCommerce\Admin\Features\Navigation\Menu::add_plugin_category(
array(
'id' => 'example-category',
'title' => 'Example Category',
'capability' => 'view_woocommerce_reports',
)
);
\Automattic\WooCommerce\Admin\Features\Navigation\Menu::add_plugin_item(
array(
'id' => 'example-category-child-1',
'parent' => 'example-category',
'title' => 'Sub Menu Child 1',
'capability' => 'view_woocommerce_reports',
'url' => 'https://www.google.com',
)
);
\Automattic\WooCommerce\Admin\Features\Navigation\Menu::add_plugin_item(
array(
'id' => 'example-category-child-2',
'parent' => 'example-category',
'title' => 'Sub Menu Child 2',
'capability' => 'view_woocommerce_reports',
'url' => 'https://www.google.com',
)
);
}
add_filter( 'admin_menu', 'add_navigation_items_register_items' );

View File

@ -1,125 +0,0 @@
# WooCommerce Navigation
The WooCommerce Navigation feature is a navigational project designed to create a more intuitive and functional WooCommerce specific navigation.
This API will allow you to add in your own items to the navigation and register pages with the new navigation screens.
### Getting started
This feature is hidden behind a feature flag and can be turned on or off by visiting WooCommerce -> Settngs -> Advanced -> Features and checking the box next to the `Navigation` option. It can also by controlled programmatically by setting the option `woocommerce_navigation_enable` to `yes` or `no`.
The fastest way to get started is by creating an example plugin from WooCommerce Admin. Enter the following command:
`WC_EXT=add-navigation-items pnpm example --filter=@woocommerce/admin-library`
This will create a new plugin that covers various features of the navigation and helps to register some initial items and categories within the new navigation menu. After running the command above, you can make edits directly to the files at `docs/examples/extensions/add-navigation-items` and they will be built and copied to your `wp-content/add-navigation-items` folder on save.
If you need to enable the WP Toolbar for debugging purposes in the new navigation, you can add the following filter to do so:
`add_filter( 'woocommerce_navigation_wp_toolbar_disabled', '__return_false' );`
### Adding a menu category
Categories in the new navigation are menu items that house child menu items.
Clicking on a category will not navigate to a new page, but instead open the child menu. Note that categories without menu items will not be shown in the menu.
* `id` - (string) The unique ID of the menu item. Required.
* `title` - (string) Title of the menu item. Required.
* `parent` - (string) Parent menu item ID.
* `capability` - (string) Capability to view this menu item.
```php
\Automattic\WooCommerce\Admin\Features\Navigation\Menu::add_plugin_category(
array(
'id' => 'example-category',
'title' => 'Example Category',
)
);
```
Categories can also contain more categories by specifying the `parent` property for the child category. There is no limit on the level of nesting.
```php
\Automattic\WooCommerce\Admin\Features\Navigation\Menu::add_plugin_category(
array(
'id' => 'example-nested-category',
'parent' => 'example-category',
'title' => 'Example Nested Category',
)
);
```
### Adding a menu item
Adding an item, much like a category, can be added directly to the menu or to an existing category. Typically this will create a link to the specified URL or callback unless overridden by JavaScript using the slot/fill approach described below.
* `id` - (string) The unique ID of the menu item. Required.
* `title` - (string) Title of the menu item. Required.
* `parent` - (string) Parent menu item ID.
* `capability` - (string) Capability to view this menu item.
* `url` - (string) URL or callback to be used. Required.
* `migrate` - (bool) Whether or not to hide the item in the wp admin menu.
* `matchExpression` - (string) An optional regex string to compare against the current location and mark the item active.
```php
\Automattic\WooCommerce\Admin\Features\Navigation\Menu::add_plugin_item(
array(
'id' => 'example-plugin',
'title' => 'Example Plugin',
'capability' => 'view_woocommerce_reports',
'url' => 'https://www.google.com',
)
);
```
### Registering plugin screens
In order to show the new navigation in place of the traditional WordPress menu on a given page, the screen ID must be registered to identify a page as supporting the new WooCommerce navigation.
When adding items, the navigation will automatically add support for the screen via the URL or callback provided with an item. However, custom post types and taxonomies need to be registered with the navigation to work on the custom post type page.
```php
\Automattic\WooCommerce\Admin\Features\Navigation\Screen::register_post_type( 'my-custom-post-type' );
\Automattic\WooCommerce\Admin\Features\Navigation\Screen::register_taxonomy( 'my-custom-taxonomy' );
```
You can also manually add a screen without registering an item.
```php
\Automattic\WooCommerce\Admin\Features\Navigation\Screen::add_screen( 'my-plugin-page' );
```
### Slot/fill items
Using slot fill we can update items on the front-end of the site using JavaScript. This is useful for modern JavaScript routing, more intricate interactions with menu items, or updating URLs and hyperlink text without reloading the page.
In order to use slot fill, you can import the `WooNavigationItem` component from `@woocommerce/navigation` and match the `item` prop with the ID of the item you'd like to modify the behavior of.
```js
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { registerPlugin } from "@wordpress/plugins";
import { useHistory } from "react-router-dom";
import { WooNavigationItem } from "@woocommerce/navigation";
const MyPlugin = () => {
const history = useHistory();
const handleClick = () => {
history.push( '/my-plugin-path' );
}
return (
<WooNavigationItem item="example-plugin">
<Button onClick={ handleClick }>
{ __( 'My Link', 'plugin-domain' ) }
</Button>
</WooNavigationItem>
);
};
registerPlugin( 'my-plugin', { render: MyPlugin, scope: 'woocommerce-navigation' } );
```

View File

@ -48,7 +48,6 @@ const wcAdminPackages = [
// See ./client/wp-admin-scripts/README.md for more details
const wpAdminScripts = [
'marketing-coupons',
'navigation-opt-out',
'onboarding-homepage-notice',
'onboarding-product-notice',
'onboarding-product-import-notice',

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Remove WooCommerce Navigation client side feature and deprecate PHP classes.

View File

@ -2,6 +2,7 @@
/**
* WooCommerce Navigation Core Menu
*
* @deprecated 9.3.0 Navigation is no longer a feature and its classes will be removed in WooCommerce 9.4.
* @package Woocommerce Admin
*/

View File

@ -2,6 +2,7 @@
/**
* WooCommerce Navigation Favorite
*
* @deprecated 9.3.0 Navigation is no longer a feature and its classes will be removed in WooCommerce 9.4.
* @package Woocommerce Navigation
*/

View File

@ -2,17 +2,17 @@
/**
* Navigation Experience
*
* @deprecated 9.3.0 Navigation is no longer a feature and its classes will be removed in WooCommerce 9.4.
* @package Woocommerce Admin
*/
namespace Automattic\WooCommerce\Admin\Features\Navigation;
use Automattic\WooCommerce\Internal\Admin\Survey;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\Navigation\Screen;
use Automattic\WooCommerce\Admin\Features\Navigation\Menu;
use Automattic\WooCommerce\Admin\Features\Navigation\CoreMenu;
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;
use WC_Tracks;
/**
* Contains logic for the Navigation
@ -23,115 +23,27 @@ class Init {
*/
const TOGGLE_OPTION_NAME = 'woocommerce_navigation_enabled';
/**
* Determines if the feature has been toggled on or off.
*
* @var boolean
*/
protected static $is_updated = false;
/**
* Hook into WooCommerce.
*/
public function __construct() {
add_action( 'update_option_' . self::TOGGLE_OPTION_NAME, array( $this, 'reload_page_on_toggle' ), 10, 2 );
add_action( 'woocommerce_settings_saved', array( $this, 'maybe_reload_page' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'maybe_enqueue_opt_out_scripts' ) );
if ( Features::is_enabled( 'navigation' ) ) {
Menu::instance()->init();
CoreMenu::instance()->init();
Screen::instance()->init();
// Disable the option to turn off the feature.
update_option( self::TOGGLE_OPTION_NAME, 'no' );
if ( class_exists( 'WC_Tracks' ) ) {
WC_Tracks::record_event( 'deprecated_navigation_in_use' );
}
}
}
/**
* Add the feature toggle to the features settings.
* Create a deprecation notice.
*
* @deprecated 7.0 The WooCommerce Admin features are now handled by the WooCommerce features engine (see the FeaturesController class).
*
* @param array $features Feature sections.
* @return array
* @param string $fcn The function that is deprecated.
*/
public static function add_feature_toggle( $features ) {
return $features;
}
/**
* Determine if sufficient versions are present to support Navigation feature
*/
public function is_nav_compatible() {
include_once ABSPATH . 'wp-admin/includes/plugin.php';
$gutenberg_minimum_version = '9.0.0'; // https://github.com/WordPress/gutenberg/releases/tag/v9.0.0.
$wp_minimum_version = '5.6';
$has_gutenberg = is_plugin_active( 'gutenberg/gutenberg.php' );
$gutenberg_version = $has_gutenberg ? get_plugin_data( WP_PLUGIN_DIR . '/gutenberg/gutenberg.php' )['Version'] : false;
if ( $gutenberg_version && version_compare( $gutenberg_version, $gutenberg_minimum_version, '>=' ) ) {
return true;
}
// Get unmodified $wp_version.
include ABSPATH . WPINC . '/version.php';
// Strip '-src' from the version string. Messes up version_compare().
$wp_version = str_replace( '-src', '', $wp_version );
if ( version_compare( $wp_version, $wp_minimum_version, '>=' ) ) {
return true;
}
return false;
}
/**
* Reloads the page when the option is toggled to make sure all nav features are loaded.
*
* @param string $old_value Old value.
* @param string $value New value.
*/
public static function reload_page_on_toggle( $old_value, $value ) {
if ( $old_value === $value ) {
return;
}
if ( 'yes' !== $value ) {
update_option( 'woocommerce_navigation_show_opt_out', 'yes' );
}
self::$is_updated = true;
}
/**
* Reload the page if the setting has been updated.
*/
public static function maybe_reload_page() {
if ( ! isset( $_SERVER['REQUEST_URI'] ) || ! self::$is_updated ) {
return;
}
wp_safe_redirect( wp_unslash( $_SERVER['REQUEST_URI'] ) );
exit();
}
/**
* Enqueue the opt out scripts.
*/
public function maybe_enqueue_opt_out_scripts() {
if ( get_option( 'woocommerce_navigation_show_opt_out', 'no' ) !== 'yes' ) {
return;
}
WCAdminAssets::register_style( 'navigation-opt-out', 'style', array( 'wp-components' ) );
WCAdminAssets::register_script( 'wp-admin-scripts', 'navigation-opt-out', true );
wp_localize_script(
'wc-admin-navigation-opt-out',
'surveyData',
array(
'url' => Survey::get_url( '/new-navigation-opt-out' ),
)
);
delete_option( 'woocommerce_navigation_show_opt_out' );
public static function deprecation_notice( $fcn ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
error_log( 'Automattic\WooCommerce\Admin\Features\Navigation\\' . $fcn . ' is deprecated since 9.3 with no alternative. Navigation classes will be removed in WooCommerce 9.4' );
}
}

View File

@ -2,6 +2,7 @@
/**
* WooCommerce Navigation Menu
*
* @deprecated 9.3.0 Navigation is no longer a feature and its classes will be removed in WooCommerce 9.4.
* @package Woocommerce Navigation
*/
@ -10,6 +11,7 @@ namespace Automattic\WooCommerce\Admin\Features\Navigation;
use Automattic\WooCommerce\Admin\Features\Navigation\Favorites;
use Automattic\WooCommerce\Admin\Features\Navigation\Screen;
use Automattic\WooCommerce\Admin\Features\Navigation\CoreMenu;
use Automattic\WooCommerce\Admin\Features\Navigation\Init;
/**
* Contains logic for the WooCommerce Navigation menu.
@ -95,172 +97,33 @@ class Menu {
/**
* Init.
*
* @internal
*/
public function init() {
add_action( 'admin_menu', array( $this, 'add_core_items' ), 100 );
add_filter( 'admin_enqueue_scripts', array( $this, 'enqueue_data' ), 20 );
add_filter( 'admin_menu', array( $this, 'migrate_core_child_items' ), PHP_INT_MAX - 1 );
add_filter( 'admin_menu', array( $this, 'migrate_menu_items' ), PHP_INT_MAX - 2 );
}
final public function init() {}
/**
* Convert a WordPress menu callback to a URL.
*
* @param string $callback Menu callback.
* @return string
*/
public static function get_callback_url( $callback ) {
// Return the full URL.
if ( strpos( $callback, 'http' ) === 0 ) {
return $callback;
}
$pos = strpos( $callback, '?' );
$file = $pos > 0 ? substr( $callback, 0, $pos ) : $callback;
if ( file_exists( ABSPATH . "/wp-admin/$file" ) ) {
return $callback;
}
return 'admin.php?page=' . $callback;
}
public static function get_callback_url() {}
/**
* Get the parent key if one exists.
*
* @param string $callback Callback or URL.
* @return string|null
*/
public static function get_parent_key( $callback ) {
global $submenu;
if ( ! $submenu ) {
return null;
}
// This is already a parent item.
if ( isset( $submenu[ $callback ] ) ) {
return null;
}
foreach ( $submenu as $key => $menu ) {
foreach ( $menu as $item ) {
if ( $item[ self::CALLBACK ] === $callback ) {
return $key;
}
}
}
return null;
}
public static function get_parent_key() {}
/**
* Adds a top level menu item to the navigation.
*
* @param array $args Array containing the necessary arguments.
* $args = array(
* 'id' => (string) The unique ID of the menu item. Required.
* 'title' => (string) Title of the menu item. Required.
* 'url' => (string) URL or callback to be used. Required.
* 'order' => (int) Menu item order.
* 'migrate' => (bool) Whether or not to hide the item in the wp admin menu.
* 'menuId' => (string) The ID of the menu to add the category to.
* ).
*/
private static function add_category( $args ) {
if ( ! isset( $args['id'] ) || isset( self::$menu_items[ $args['id'] ] ) ) {
return;
}
$defaults = array(
'id' => '',
'title' => '',
'order' => 100,
'migrate' => true,
'menuId' => 'primary',
'isCategory' => true,
);
$menu_item = wp_parse_args( $args, $defaults );
$menu_item['title'] = wp_strip_all_tags( wp_specialchars_decode( $menu_item['title'] ) );
unset( $menu_item['url'] );
unset( $menu_item['capability'] );
if ( ! isset( $menu_item['parent'] ) ) {
$menu_item['parent'] = 'woocommerce';
$menu_item['backButtonLabel'] = __(
'WooCommerce Home',
'woocommerce'
);
}
self::$menu_items[ $menu_item['id'] ] = $menu_item;
self::$categories[ $menu_item['id'] ] = array();
self::$categories[ $menu_item['parent'] ][] = $menu_item['id'];
if ( isset( $args['url'] ) ) {
self::$callbacks[ $args['url'] ] = $menu_item['migrate'];
}
private static function add_category() {
Init::deprecation_notice( 'Menu::add_category' );
}
/**
* Adds a child menu item to the navigation.
*
* @param array $args Array containing the necessary arguments.
* $args = array(
* 'id' => (string) The unique ID of the menu item. Required.
* 'title' => (string) Title of the menu item. Required.
* 'parent' => (string) Parent menu item ID.
* 'capability' => (string) Capability to view this menu item.
* 'url' => (string) URL or callback to be used. Required.
* 'order' => (int) Menu item order.
* 'migrate' => (bool) Whether or not to hide the item in the wp admin menu.
* 'menuId' => (string) The ID of the menu to add the item to.
* 'matchExpression' => (string) A regular expression used to identify if the menu item is active.
* ).
*/
private static function add_item( $args ) {
if ( ! isset( $args['id'] ) ) {
return;
}
if ( isset( self::$menu_items[ $args['id'] ] ) ) {
wc_doing_it_wrong(
__METHOD__,
sprintf(
/* translators: 1: Duplicate menu item path. */
esc_html__( 'You have attempted to register a duplicate item with WooCommerce Navigation: %1$s', 'woocommerce' ),
'`' . $args['id'] . '`'
),
'6.5.0'
);
return;
}
$defaults = array(
'id' => '',
'title' => '',
'capability' => 'manage_woocommerce',
'url' => '',
'order' => 100,
'migrate' => true,
'menuId' => 'primary',
);
$menu_item = wp_parse_args( $args, $defaults );
$menu_item['title'] = wp_strip_all_tags( wp_specialchars_decode( $menu_item['title'] ) );
$menu_item['url'] = self::get_callback_url( $menu_item['url'] );
if ( ! isset( $menu_item['parent'] ) ) {
$menu_item['parent'] = 'woocommerce';
}
$menu_item['menuId'] = self::get_item_menu_id( $menu_item );
self::$menu_items[ $menu_item['id'] ] = $menu_item;
self::$categories[ $menu_item['parent'] ][] = $menu_item['id'];
if ( isset( $args['url'] ) ) {
self::$callbacks[ $args['url'] ] = $menu_item['migrate'];
}
private static function add_item() {
Init::deprecation_notice( 'Menu::add_item' );
}
/**
@ -287,112 +150,25 @@ class Menu {
/**
* Adds a plugin category.
*
* @param array $args Array containing the necessary arguments.
* $args = array(
* 'id' => (string) The unique ID of the menu item. Required.
* 'title' => (string) Title of the menu item. Required.
* 'url' => (string) URL or callback to be used. Required.
* 'migrate' => (bool) Whether or not to hide the item in the wp admin menu.
* 'order' => (int) Menu item order.
* ).
*/
public static function add_plugin_category( $args ) {
$category_args = array_merge(
$args,
array(
'menuId' => 'plugins',
)
);
if ( ! isset( $category_args['parent'] ) ) {
unset( $category_args['order'] );
}
$menu_id = self::get_item_menu_id( $category_args );
if ( ! in_array( $menu_id, array( 'plugins', 'favorites' ), true ) ) {
return;
}
$category_args['menuId'] = $menu_id;
self::add_category( $category_args );
public static function add_plugin_category() {
Init::deprecation_notice( 'Menu::add_plugin_category' );
}
/**
* Adds a plugin item.
*
* @param array $args Array containing the necessary arguments.
* $args = array(
* 'id' => (string) The unique ID of the menu item. Required.
* 'title' => (string) Title of the menu item. Required.
* 'parent' => (string) Parent menu item ID.
* 'capability' => (string) Capability to view this menu item.
* 'url' => (string) URL or callback to be used. Required.
* 'migrate' => (bool) Whether or not to hide the item in the wp admin menu.
* 'order' => (int) Menu item order.
* 'matchExpression' => (string) A regular expression used to identify if the menu item is active.
* ).
*/
public static function add_plugin_item( $args ) {
if ( ! isset( $args['parent'] ) ) {
unset( $args['order'] );
}
$item_args = array_merge(
$args,
array(
'menuId' => 'plugins',
)
);
$menu_id = self::get_item_menu_id( $item_args );
if ( 'plugins' !== $menu_id ) {
return;
}
self::add_item( $item_args );
public static function add_plugin_item() {
Init::deprecation_notice( 'Menu::add_plugin_item' );
}
/**
* Adds a plugin setting item.
*
* @param array $args Array containing the necessary arguments.
* $args = array(
* 'id' => (string) The unique ID of the menu item. Required.
* 'title' => (string) Title of the menu item. Required.
* 'capability' => (string) Capability to view this menu item.
* 'url' => (string) URL or callback to be used. Required.
* 'migrate' => (bool) Whether or not to hide the item in the wp admin menu.
* ).
*/
public static function add_setting_item( $args ) {
unset( $args['order'] );
if ( isset( $args['parent'] ) || isset( $args['menuId'] ) ) {
error_log( // phpcs:ignore
sprintf(
/* translators: 1: Duplicate menu item path. */
esc_html__( 'The item ID %1$s attempted to register using an invalid option. The arguments `menuId` and `parent` are not allowed for add_setting_item()', 'woocommerce' ),
'`' . $args['id'] . '`'
)
);
}
$item_args = array_merge(
$args,
array(
'menuId' => 'secondary',
'parent' => 'woocommerce-settings',
)
);
self::add_item( $item_args );
public static function add_setting_item() {
Init::deprecation_notice( 'Menu::add_setting_item' );
}
/**
* Get menu item templates for a given post type.
*

View File

@ -2,12 +2,14 @@
/**
* WooCommerce Navigation Screen
*
* @deprecated 9.3.0 Navigation is no longer a feature and its classes will be removed in WooCommerce 9.4.
* @package Woocommerce Navigation
*/
namespace Automattic\WooCommerce\Admin\Features\Navigation;
use Automattic\WooCommerce\Admin\Features\Navigation\Menu;
use Automattic\WooCommerce\Admin\Features\Navigation\Init;
/**
* Contains logic for the WooCommerce Navigation menu.
@ -85,6 +87,8 @@ class Screen {
* @return bool
*/
public static function is_woocommerce_page() {
Init::deprecation_notice( 'Screen::is_woocommerce_page' );
global $pagenow;
// Get taxonomy if on a taxonomy screen.
@ -218,23 +222,15 @@ class Screen {
/**
* Register post type for use in WooCommerce Navigation screens.
*
* @param string $post_type Post type to add.
*/
public static function register_post_type( $post_type ) {
if ( ! in_array( $post_type, self::$post_types, true ) ) {
self::$post_types[] = $post_type;
}
public static function register_post_type() {
Init::deprecation_notice( 'Screen::register_post_type' );
}
/**
* Register taxonomy for use in WooCommerce Navigation screens.
*
* @param string $taxonomy Taxonomy to add.
*/
public static function register_taxonomy( $taxonomy ) {
if ( ! in_array( $taxonomy, self::$taxonomies, true ) ) {
self::$taxonomies[] = $taxonomy;
}
public static function register_taxonomy() {
Init::deprecation_notice( 'Screen::register_taxonomy' );
}
}

View File

@ -574,6 +574,6 @@ class PageController {
* TODO: See usage in `admin.php`. This needs refactored and implemented properly in core.
*/
public static function is_embed_page() {
return wc_admin_is_connected_page() || ( ! self::is_admin_page() && class_exists( 'Automattic\WooCommerce\Admin\Features\Navigation\Screen' ) && Screen::is_woocommerce_page() );
return wc_admin_is_connected_page();
}
}

View File

@ -6,7 +6,6 @@
namespace Automattic\WooCommerce\Internal\Features;
use Automattic\WooCommerce\Internal\Admin\Analytics;
use Automattic\WooCommerce\Admin\Features\Navigation\Init;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Proxies\LegacyProxy;
@ -174,17 +173,6 @@ class FeaturesController {
'disable_ui' => false,
'is_legacy' => true,
),
'new_navigation' => array(
'name' => __( 'Navigation', 'woocommerce' ),
'description' => __(
'Add the new WooCommerce navigation experience to the dashboard',
'woocommerce'
),
'option_key' => Init::TOGGLE_OPTION_NAME,
'is_experimental' => false,
'disable_ui' => false,
'is_legacy' => true,
),
'product_block_editor' => array(
'name' => __( 'New product editor', 'woocommerce' ),
'description' => __( 'Try the new product editor (Beta)', 'woocommerce' ),

View File

@ -1,89 +0,0 @@
<?php
/**
* Favorites tests
*
* @package WooCommerce\Admin\Tests\Navigation
*/
use Automattic\WooCommerce\Admin\Features\Navigation\Favorites;
/**
* Class WC_Admin_Tests_Navigation_Favorites
*/
class WC_Admin_Tests_Navigation_Favorites extends WC_Unit_Test_Case {
/**
* @var Favorites
*/
private $instance;
/**
* setUp
*/
public function setUp(): void {
parent::setUp();
$this->instance = new Favorites();
$this->user = $this->factory->user->create(
array(
'role' => 'administrator',
)
);
}
/**
* Test that favorites can be added.
*/
public function test_add_favorites() {
wp_set_current_user( $this->user );
$result = $this->instance->add_item( 'menu-item', get_current_user_id() );
$this->assertTrue( $result );
$result = $this->instance->add_item( 'menu-item2', get_current_user_id() );
$this->assertTrue( $result );
$favorites = $this->instance->get_all( get_current_user_id() );
$this->assertContains( 'menu-item', $favorites );
$this->assertContains( 'menu-item2', $favorites );
}
/**
* Test that favorites can be removed.
*/
public function test_remove_favorites() {
wp_set_current_user( $this->user );
$result = $this->instance->add_item( 'item-to-remove', get_current_user_id() );
$this->assertTrue( $result );
$favorites = $this->instance->get_all( get_current_user_id() );
$this->assertContains( 'item-to-remove', $favorites );
$result = $this->instance->remove_item( 'item-to-remove', get_current_user_id() );
$favorites = $this->instance->get_all( get_current_user_id() );
$this->assertNotContains( 'item-to-remove', $favorites );
}
/**
* Test that existing favorites can not be added again.
*/
public function test_add_previously_added_favorite() {
wp_set_current_user( $this->user );
$result = $this->instance->add_item( 'duplicate-item', get_current_user_id() );
$this->assertTrue( $result );
$result = $this->instance->add_item( 'duplicate-item', get_current_user_id() );
$this->assertInstanceOf( 'WP_Error', $result );
}
/**
* Test removing a favorite that does not exist.
*/
public function test_remove_invalid_favorite() {
wp_set_current_user( $this->user );
$result = $this->instance->remove_item( 'does-not-exist', get_current_user_id() );
$this->assertInstanceOf( 'WP_Error', $result );
}
}

View File

@ -1,293 +0,0 @@
<?php
/**
* Menu tests
*
* @package WooCommerce\Admin\Tests\Navigation
*/
use Automattic\WooCommerce\Admin\Features\Navigation\Menu;
/**
* Class WC_Admin_Tests_Navigation_Menu
*/
class WC_Admin_Tests_Navigation_Menu extends WC_Unit_Test_Case {
/**
* @var Menu
*/
private $instance;
/**
* setUp
*/
public function setUp(): void {
parent::setUp();
$this->instance = new Menu();
$this->user = $this->factory->user->create(
array(
'role' => 'administrator',
)
);
wp_set_current_user( $this->user );
}
/**
* Test that the correct callback is returned given a string.
*/
public function test_get_callback_url() {
// Full URLs should return the same full URL.
$full_url = 'http://mycustomurl.com';
$callback = $this->instance->get_callback_url( $full_url );
$this->assertEquals( $full_url, $callback );
// Files that exist should return the same callback.
$callback = $this->instance->get_callback_url( 'edit.php' );
$this->assertEquals( 'edit.php', $callback );
$callback = $this->instance->get_callback_url( 'edit.php?custom_arg=1' );
$this->assertEquals( 'edit.php?custom_arg=1', $callback );
// Custom callbacks should return the callback as an admin page.
$callback = $this->instance->get_callback_url( 'my-page' );
$this->assertEquals( 'admin.php?page=my-page', $callback );
}
/**
* Test the ability to retrieve a parent key.
*/
public function test_get_parent_key() {
global $submenu;
$submenu['my-parent-page'] = array( // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
array(
'my-child-item',
'manage_woocommerce',
'my-child-item',
),
);
// Items that are already parents should not return parent keys.
$parent_key = $this->instance->get_parent_key( 'my-parent-page' );
$this->assertNull( $parent_key );
// Fake items should not return parent keys.
$parent_key = $this->instance->get_parent_key( 'not-a-page' );
$this->assertNull( $parent_key );
// Children should return their parent's key.
$parent_key = $this->instance->get_parent_key( 'my-child-item' );
$this->assertEquals( 'my-parent-page', $parent_key );
}
/**
* Test adding a menu item.
*/
public function test_add_plugin_items() {
$item = array(
'id' => 'test-plugin-item',
'title' => 'Test Plugin Item',
'capability' => 'manage_woocommerce',
'url' => 'my-test-page',
);
$this->instance->add_plugin_item( $item );
$items = $this->instance->get_items();
$this->assertEquals( $item['id'], $items['test-plugin-item']['id'] );
$this->assertEquals( 'Test Plugin Item', $items['test-plugin-item']['title'] );
}
/**
* Test adding an existing menu ID.
*/
public function test_add_duplicate_plugin_items() {
$this->setExpectedIncorrectUsage( 'Automattic\WooCommerce\Admin\Features\Navigation\Menu::add_item' );
$item = array(
'id' => 'test-duplicate-item',
'title' => 'Test Duplicate Item',
'capability' => 'manage_woocommerce',
'url' => 'my-duplicate-page',
);
$this->instance->add_plugin_item( $item );
// Test that the duplicate ID does not replace the item.
$item['title'] = 'Test Updated Title';
$this->instance->add_plugin_item( $item );
$items = $this->instance->get_items();
$this->assertEquals( 'Test Duplicate Item', $items['test-duplicate-item']['title'] );
}
/**
* Test adding a plugin category.
*/
public function test_add_plugin_category() {
$this->instance->add_plugin_category(
array(
'id' => 'test-plugin-category',
'title' => 'Test Plugin Category',
'capability' => 'manage_woocommerce',
)
);
$this->instance->add_plugin_item(
array(
'id' => 'test-plugin-child',
'title' => 'Test Plugin Child',
'parent' => 'test-plugin-category',
'capability' => 'manage_woocommerce',
'url' => 'my-test-child',
)
);
$items = $this->instance->get_items();
$this->assertEquals( 'test-plugin-category', $items['test-plugin-category']['id'] );
$this->assertEquals( 'test-plugin-child', $items['test-plugin-child']['id'] );
}
/**
* Test that a plugin item's menu ID gets properly set.
*/
public function test_plugin_menus() {
$this->instance->add_plugin_item(
array(
'id' => 'test-plugin-menu',
'title' => 'Test Plugin Category',
'capability' => 'manage_woocommerce',
)
);
$items = $this->instance->get_items();
$this->assertEquals( 'plugins', $items['test-plugin-menu']['menuId'] );
$this->instance->add_plugin_item(
array(
'id' => 'test-plugin-bad-menu',
'title' => 'Test Plugin Category',
'capability' => 'manage_woocommerce',
'menuId' => 'primary',
)
);
$items = $this->instance->get_items();
$this->assertEquals( 'plugins', $items['test-plugin-bad-menu']['menuId'] );
}
/**
* Test that menu mapping by category works as expected.
*/
public function test_get_mapped_menu_items() {
$this->instance->add_plugin_category(
array(
'id' => 'test-mapped-category',
'title' => 'Test Mapped Category',
'capability' => 'manage_woocommerce',
)
);
$this->instance->add_plugin_item(
array(
'id' => 'test-mapped-item-c',
'title' => 'Test Mapped Item C',
'parent' => 'test-mapped-category',
'capability' => 'manage_woocommerce',
'order' => 2,
)
);
$this->instance->add_plugin_item(
array(
'id' => 'test-mapped-item-b',
'title' => 'Test Mapped Item B',
'parent' => 'test-mapped-category',
'capability' => 'manage_woocommerce',
'order' => 1,
)
);
$this->instance->add_plugin_item(
array(
'id' => 'test-mapped-item-a',
'title' => 'Test Mapped Item A',
'parent' => 'test-mapped-category',
'capability' => 'manage_woocommerce',
'order' => 1,
)
);
$this->instance->add_plugin_item(
array(
'id' => 'test-mapped-permission',
'title' => 'Should not be included',
'parent' => 'test-mapped-category',
'capability' => 'no_permission',
)
);
$map = $this->instance->get_mapped_menu_items();
$this->assertCount( count( $this->instance::MENU_IDS ), $map['test-mapped-category'] );
foreach ( $this->instance::MENU_IDS as $menu_id ) {
$this->assertArrayHasKey( $menu_id, $map['test-mapped-category'] );
}
$this->assertEquals( 'test-mapped-item-a', $map['test-mapped-category']['plugins'][0]['id'] );
$this->assertEquals( 'test-mapped-item-b', $map['test-mapped-category']['plugins'][1]['id'] );
$this->assertEquals( 'test-mapped-item-c', $map['test-mapped-category']['plugins'][2]['id'] );
}
/**
* Test adding a setting item.
*/
public function add_setting_item() {
$this->instance->add_setting_item(
array(
'id' => 'test-setting-item',
'title' => 'Test Setting Item',
)
);
$this->instance->add_setting_item(
array(
'id' => 'test-setting-item-bad-parent',
'title' => 'Test Bad Parent',
'parent' => 'woocommerce',
)
);
$this->instance->add_setting_item(
array(
'id' => 'test-setting-item-bad-menu',
'title' => 'Test Bad Menu',
'menuId' => 'primary',
)
);
$items = $this->instance->get_items();
$this->assertArrayHasKey( $menu_id, $map['test-setting-item'] );
$this->assertArrayNotHasKey( $menu_id, $map['test-setting-item-bad-parent'] );
$this->assertArrayNotHasKey( $menu_id, $map['test-setting-item-bad-menu'] );
}
/**
* Test if adding a menu item can be checked via the callback.
*/
public function test_has_callback() {
$item = array(
'test-callback-item',
'manage_woocommerce',
'test-callback-item',
);
$this->assertFalse( $this->instance->has_callback( $item ) );
$this->instance->add_plugin_item(
array(
'id' => 'test-callback-item',
'title' => 'Test Callback Item',
'capability' => 'manage_woocommerce',
'url' => 'test-callback-item',
)
);
$this->assertTrue( $this->instance->has_callback( $item ) );
}
}

View File

@ -1,198 +0,0 @@
<?php
/**
* Screen tests
*
* @package WooCommerce\Admin\Tests\Navigation
*/
use Automattic\WooCommerce\Admin\Features\Navigation\Screen;
/*
* This is required to allow manually setting of the screen during testing.
* phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited
*/
/**
* Class WC_Admin_Tests_Navigation_Screen
*/
class WC_Admin_Tests_Navigation_Screen extends WC_Unit_Test_Case {
/**
* @var Screen
*/
private $instance;
/**
* setUp
*/
public function setUp(): void {
parent::setUp();
$this->instance = new Screen();
// Store globals for reset.
global $pagenow, $current_screen;
$this->_current_screen = $current_screen;
$this->_pagenow = $pagenow;
/* phpcs:disable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash */
$this->_post = isset( $_GET['post'] ) ? $_GET['post'] : null;
$this->_post_type = isset( $_GET['post_type'] ) ? $_GET['post_type'] : null;
$this->_taxonomy = isset( $_GET['taxonomy'] ) ? $_GET['taxonomy'] : null;
/* phpcs:enable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash */
// Register a post type.
register_post_type( 'my-post-type' );
}
/**
* Reset globals.
*/
public function reset_globals() {
global $pagenow, $current_screen;
$current_screen = $this->_current_screen;
$pagenow = $this->_pagenow;
$_GET['post'] = $this->_post;
$_GET['post_type'] = $this->_post_type;
$_GET['taxonomy'] = $this->_taxonomy;
}
/**
* Test that screen IDs are correctly registered.
*/
public function test_add_screen() {
$this->instance->add_screen( 'my-screen' );
// Test adding a duplicate screen ID.
$this->instance->add_screen( 'my-screen' );
$this->instance->add_screen( 'plugins.php' );
$screen_ids = $this->instance->get_screen_ids();
$this->assertCount( 2, $screen_ids );
$this->assertContains( 'admin_page_my-screen', $screen_ids );
$this->assertContains( 'admin_page_plugins', $screen_ids );
}
/**
* Test that taxonomies are correctly registered.
*/
public function test_register_taxonomy() {
$this->instance->register_taxonomy( 'my-taxonomy' );
// Test adding a duplicate taxonomy.
$this->instance->register_taxonomy( 'my-taxonomy' );
$taxonomies = $this->instance->get_taxonomies();
$this->assertCount( 1, $taxonomies );
$this->assertContains( 'my-taxonomy', $taxonomies );
}
/**
* Test that post types are correctly registered.
*/
public function test_register_post_type() {
$this->instance->register_post_type( 'my-post-type' );
// Test adding a duplicate post type.
$this->instance->register_post_type( 'my-post-type' );
$post_types = $this->instance->get_post_types();
$this->assertCount( 1, $post_types );
$this->assertContains( 'my-post-type', $post_types );
}
/**
* Test that screens can be detected.
*/
public function test_is_woocommerce_page_screens() {
$this->instance->add_screen( 'my-screen' );
global $pagenow, $current_screen;
$this->assertFalse( $this->instance->is_woocommerce_page() );
$current_screen = (object) array( 'id' => 'admin_page_my-screen' );
$this->assertTrue( $this->instance->is_woocommerce_page() );
$current_screen = (object) array( 'id' => 'admin_page_different-screen' );
$this->assertFalse( $this->instance->is_woocommerce_page() );
$this->reset_globals();
}
/**
* Test that taxonomies can be detected.
*/
public function test_is_woocommerce_page_taxonomies() {
global $pagenow, $current_screen;
$this->instance->register_taxonomy( 'my-taxonomy' );
$pagenow = 'edit-tags.php';
$_GET['taxonomy'] = 'my-taxonomy';
$this->assertTrue( $this->instance->is_woocommerce_page() );
$pagenow = 'term.php';
$this->assertTrue( $this->instance->is_woocommerce_page() );
$_GET['taxonomy'] = 'different-taxonomy';
$this->assertFalse( $this->instance->is_woocommerce_page() );
$pagenow = 'edit.php';
$_GET['taxonomy'] = 'my-taxonomy';
$this->assertFalse( $this->instance->is_woocommerce_page() );
// Core taxonomies.
$pagenow = 'edit-tags.php';
$_GET['taxonomy'] = 'product_cat';
$this->assertTrue( $this->instance->is_woocommerce_page() );
$pagenow = 'edit-tags.php';
$_GET['taxonomy'] = 'product_tag';
$this->assertTrue( $this->instance->is_woocommerce_page() );
$this->reset_globals();
}
/**
* Test that post types can be detected.
*/
public function test_is_woocommerce_page_post_types() {
global $pagenow, $current_screen;
$this->instance->register_post_type( 'my-post-type' );
$pagenow = 'edit.php';
$_GET['taxonomy'] = null;
$_GET['post_type'] = 'my-post-type';
$this->assertTrue( $this->instance->is_woocommerce_page() );
$pagenow = 'post.php';
$this->assertTrue( $this->instance->is_woocommerce_page() );
$pagenow = 'post-new.php';
$this->assertTrue( $this->instance->is_woocommerce_page() );
$_GET['post_type'] = 'different-post-type';
$this->assertFalse( $this->instance->is_woocommerce_page() );
$pagenow = 'edit-tags.php';
$_GET['post_type'] = 'my-post-type';
$this->assertFalse( $this->instance->is_woocommerce_page() );
$my_post_id = wp_insert_post(
array(
'post_title' => 'Test post type',
'post_type' => 'my-post-type',
)
);
$different_post_id = wp_insert_post(
array(
'post_title' => 'Different post type',
'post_type' => 'post',
)
);
$_GET['post'] = $my_post_id;
$pagenow = 'edit.php';
$this->assertTrue( $this->instance->is_woocommerce_page() );
$_GET['post'] = $different_post_id;
$pagenow = 'edit.php';
$this->assertFalse( $this->instance->is_woocommerce_page() );
$this->reset_globals();
}
}
/* phpcs:enable WordPress.WP.GlobalVariablesOverride.Prohibited */