Reorganize the store management link section and provide extensibility (https://github.com/woocommerce/woocommerce-admin/pull/5476)

Rearrange the store management links under categories and expose extensibility via the `woocommerce_admin_homescreen_quicklinks` filter.
This commit is contained in:
Sam Seay 2020-11-04 13:33:04 +13:00 committed by GitHub
parent 17c149f66e
commit 06f9248572
16 changed files with 554 additions and 410 deletions

View File

@ -22,14 +22,15 @@ import {
/**
* Internal dependencies
*/
import QuickLinks from '../quick-links';
import StatsOverview from './stats-overview';
import './style.scss';
import '../dashboard/style.scss';
import TaskListPlaceholder from '../task-list/placeholder';
import InboxPanel from '../inbox-panel';
import { WelcomeModal } from './welcome-modal';
import './style.scss';
import '../dashboard/style.scss';
import { StoreManagementLinks } from '../store-management-links';
const TaskList = lazy( () =>
import( /* webpackChunkName: "task-list" */ '../task-list' )
);
@ -92,7 +93,7 @@ export const Layout = ( {
>
{ isTaskListEnabled && renderTaskList() }
<StatsOverview />
{ ! isTaskListEnabled && <QuickLinks /> }
{ ! isTaskListEnabled && <StoreManagementLinks /> }
</div>
</Fragment>
);

View File

@ -20,9 +20,6 @@ jest.mock( 'task-list', () => jest.fn().mockReturnValue( '[TaskList]' ) );
// We aren't testing the <InboxPanel /> component here.
jest.mock( 'inbox-panel', () => jest.fn().mockReturnValue( '[InboxPanel]' ) );
// We aren't testing the <QuickLinks /> component here.
jest.mock( 'quick-links', () => jest.fn().mockReturnValue( '[QuickLinks]' ) );
jest.mock( '@woocommerce/data', () => ( {
...jest.requireActual( '@woocommerce/data' ),
useUserPreferences: jest.fn().mockReturnValue( {} ),

View File

@ -1,197 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import {
Card,
CardBody,
CardHeader,
__experimentalText as Text,
} from '@wordpress/components';
import {
Icon,
megaphone,
box,
brush,
home,
shipping,
percent,
payment,
pencil,
lifesaver,
external,
} from '@wordpress/icons';
import { partial } from 'lodash';
import { getSetting } from '@woocommerce/wc-admin-settings';
import { List } from '@woocommerce/components';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import './style.scss';
function getItems( props ) {
return [
{
title: __( 'Market my store', 'woocommerce-admin' ),
type: 'wc-admin',
path: 'marketing',
icon: megaphone,
listItemTag: 'marketing',
},
{
title: __( 'Add products', 'woocommerce-admin' ),
type: 'wp-admin',
path: 'post-new.php?post_type=product',
icon: box,
listItemTag: 'add-products',
},
{
title: __( 'Personalize my store', 'woocommerce-admin' ),
type: 'wp-admin',
path: 'customize.php',
icon: brush,
listItemTag: 'personalize-store',
},
{
title: __( 'Shipping settings', 'woocommerce-admin' ),
type: 'wc-settings',
tab: 'shipping',
icon: shipping,
listItemTag: 'shipping-settings',
},
{
title: __( 'Tax settings', 'woocommerce-admin' ),
type: 'wc-settings',
tab: 'tax',
icon: percent,
listItemTag: 'tax-settings',
},
{
title: __( 'Payment settings', 'woocommerce-admin' ),
type: 'wc-settings',
tab: 'checkout',
icon: payment,
listItemTag: 'payment-settings',
},
{
title: __( 'Edit store details', 'woocommerce-admin' ),
type: 'wc-settings',
tab: 'general',
icon: pencil,
listItemTag: 'edit-store-details',
},
{
title: __( 'Get support', 'woocommerce-admin' ),
type: 'external',
href: 'https://woocommerce.com/my-account/create-a-ticket/',
icon: lifesaver,
after: <Icon icon={ external } />,
listItemTag: 'support',
},
{
title: __( 'View my store', 'woocommerce-admin' ),
type: 'external',
href: props.getSetting( 'siteUrl' ),
icon: home,
after: <Icon icon={ external } />,
listItemTag: 'view-store',
},
];
}
function handleOnItemClick( props, event ) {
const a = event.currentTarget;
const listItemTag = a.dataset.listItemTag;
if ( ! listItemTag ) {
return;
}
props.recordEvent( 'home_quick_links_click', {
task_name: listItemTag,
} );
if ( typeof props.onItemClick !== 'function' ) {
return;
}
if ( ! props.onItemClick( listItemTag ) ) {
event.preventDefault();
return false;
}
}
function getLinkTypeAndHref( item ) {
let linkType;
let href;
switch ( item.type ) {
case 'wc-admin':
linkType = 'wc-admin';
href = `admin.php?page=wc-admin&path=%2F${ item.path }`;
break;
case 'wp-admin':
linkType = 'wp-admin';
href = item.path;
break;
case 'wc-settings':
linkType = 'wp-admin';
href = `admin.php?page=wc-settings&tab=${ item.tab }`;
break;
default:
linkType = 'external';
href = item.href;
break;
}
return {
linkType,
href,
};
}
function getListItems( props ) {
return getItems( props ).map( ( item ) => {
return {
title: (
<Text as="div" variant="button">
{ item.title }
</Text>
),
before: <Icon icon={ item.icon } />,
after: item.after,
...getLinkTypeAndHref( item ),
listItemTag: item.listItemTag,
onClick: partial( handleOnItemClick, props ),
};
} );
}
const QuickLinks = ( props ) => {
const listItems = getListItems( props );
return (
<Card size="large" className="woocommerce-quick-links">
<CardHeader size="medium">
<Text variant="title.small">
{ __( 'Store management', 'woocommerce-admin' ) }
</Text>
</CardHeader>
<CardBody>
<List
items={ listItems }
className="woocommerce-quick-links__list"
/>
</CardBody>
</Card>
);
};
QuickLinks.defaultProps = {
getSetting,
recordEvent,
};
export default QuickLinks;

View File

@ -1,31 +0,0 @@
/**
* External dependencies
*/
import { withConsole } from '@storybook/addon-console';
/**
* Internal dependencies
*/
import QuickLinks from '..';
function logItemClick( listItemTag ) {
// eslint-disable-next-line no-console
console.log( `QuickLinks item with tag '${ listItemTag }' clicked` );
return false;
}
function getSetting() {
return 'https://example.com';
}
export default {
title: 'WooCommerce Admin/homescreen/QuickLinks',
component: QuickLinks,
decorators: [ ( storyFn, context ) => withConsole()( storyFn )( context ) ],
};
export const Default = () => {
return (
<QuickLinks getSetting={ getSetting } onItemClick={ logItemClick } />
);
};

View File

@ -1,9 +0,0 @@
.woocommerce-page .woocommerce-quick-links {
$background-color: $white;
background-color: $background-color;
.components-card__body.is-size-large {
padding: 0;
}
}

View File

@ -1,166 +0,0 @@
/**
* External dependencies
*/
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { toHaveAttribute } from '@testing-library/jest-dom/matchers';
/**
* Internal dependencies
*/
import QuickLinks from '../index';
expect.extend( { toHaveAttribute } );
describe( 'QuickLinks', () => {
it( 'should build href correctly for a `wc-admin` item', () => {
render( <QuickLinks getSetting={ () => {} } /> );
const marketingItem = screen.getByRole( 'menuitem', {
name: 'Market my store',
} );
expect( marketingItem ).toHaveAttribute(
'href',
'admin.php?page=wc-admin&path=%2Fmarketing'
);
} );
it( 'should build href correctly for a `wp-admin` item', () => {
render( <QuickLinks getSetting={ () => {} } /> );
const addProductsItem = screen.getByRole( 'menuitem', {
name: 'Add products',
} );
expect( addProductsItem ).toHaveAttribute(
'href',
'post-new.php?post_type=product'
);
} );
it( 'should build href correctly for a `wc-settings` item', () => {
render( <QuickLinks getSetting={ () => {} } /> );
const shippingSettingsItem = screen.getByRole( 'menuitem', {
name: 'Shipping settings',
} );
expect( shippingSettingsItem ).toHaveAttribute(
'href',
'admin.php?page=wc-settings&tab=shipping'
);
} );
it( 'should call `recordEvent` when a `wc-admin` item is clicked', () => {
const recordEvent = jest.fn();
render(
<QuickLinks
getSetting={ () => {} }
recordEvent={ recordEvent }
// Prevent jsdom "Error: Not implemented: navigation" in test output
onItemClick={ () => false }
/>
);
userEvent.click(
screen.getByRole( 'menuitem', { name: 'Market my store' } )
);
const homeQuickLinksClickEventName = 'home_quick_links_click';
const propsWithMarketingTaskName = { task_name: 'marketing' };
expect( recordEvent ).toHaveBeenCalledWith(
homeQuickLinksClickEventName,
propsWithMarketingTaskName
);
} );
it( 'should call `recordEvent` when a `wp-admin` item is clicked', () => {
const recordEvent = jest.fn();
render(
<QuickLinks
getSetting={ () => {} }
recordEvent={ recordEvent }
// Prevent jsdom "Error: Not implemented: navigation" in test output
onItemClick={ () => false }
/>
);
userEvent.click(
screen.getByRole( 'menuitem', { name: 'Add products' } )
);
const homeQuickLinksClickEventName = 'home_quick_links_click';
const propsWithAddProductsTaskName = { task_name: 'add-products' };
expect( recordEvent ).toHaveBeenCalledWith(
homeQuickLinksClickEventName,
propsWithAddProductsTaskName
);
} );
it( 'should call `recordEvent` when a `wc-settings` item is clicked', () => {
const recordEvent = jest.fn();
render(
<QuickLinks
getSetting={ () => {} }
recordEvent={ recordEvent }
// Prevent jsdom "Error: Not implemented: navigation" in test output
onItemClick={ () => false }
/>
);
userEvent.click(
screen.getByRole( 'menuitem', { name: 'Shipping settings' } )
);
const homeQuickLinksClickEventName = 'home_quick_links_click';
const propsWithShippingSettingsTaskName = {
task_name: 'shipping-settings',
};
expect( recordEvent ).toHaveBeenCalledWith(
homeQuickLinksClickEventName,
propsWithShippingSettingsTaskName
);
} );
it( 'should call `recordEvent` when an `external` item is clicked', () => {
const recordEvent = jest.fn();
render(
<QuickLinks
getSetting={ () => {} }
recordEvent={ recordEvent }
// Prevent jsdom "Error: Not implemented: navigation" in test output
onItemClick={ () => false }
/>
);
userEvent.click(
screen.getByRole( 'menuitem', { name: 'Get support' } )
);
const homeQuickLinksClickEventName = 'home_quick_links_click';
const propsWithSupportTaskName = { task_name: 'support' };
expect( recordEvent ).toHaveBeenCalledWith(
homeQuickLinksClickEventName,
propsWithSupportTaskName
);
} );
it( 'should call `getSetting` to determine the frontend url', () => {
const getSetting = jest.fn( () => 'https://example.com' );
render(
<QuickLinks getSetting={ getSetting } recordEvent={ () => {} } />
);
expect( getSetting ).toHaveBeenCalledWith( 'siteUrl' );
} );
} );

View File

@ -0,0 +1,227 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { applyFilters } from '@wordpress/hooks';
import {
Card,
CardBody,
CardHeader,
__experimentalText as Text,
} from '@wordpress/components';
import {
megaphone,
box,
brush,
home,
shipping,
percent,
payment,
pencil,
} from '@wordpress/icons';
import { getSetting } from '@woocommerce/wc-admin-settings';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import './style.scss';
import { QuickLinkCategory } from './quick-link-category';
import { QuickLink } from './quick-link';
export function getItemsByCategory( siteUrl ) {
return [
{
title: __( 'Marketing & Merchandising', 'woocommerce-admin' ),
items: [
{
title: __( 'Marketing', 'woocommerce-admin' ),
link: getLinkTypeAndHref( {
type: 'wc-admin',
path: 'marketing',
} ),
icon: megaphone,
listItemTag: 'marketing',
},
{
title: __( 'Add products', 'woocommerce-admin' ),
link: getLinkTypeAndHref( {
type: 'wp-admin',
path: 'post-new.php?post_type=product',
} ),
icon: box,
listItemTag: 'add-products',
},
{
title: __( 'Personalize my store', 'woocommerce-admin' ),
link: getLinkTypeAndHref( {
type: 'wp-admin',
path: 'customize.php',
} ),
icon: brush,
listItemTag: 'personalize-store',
},
{
title: __( 'View my store', 'woocommerce-admin' ),
link: getLinkTypeAndHref( {
type: 'external',
href: siteUrl,
} ),
icon: home,
listItemTag: 'view-store',
},
],
},
{
title: __( 'Settings', 'woocommerce-admin' ),
items: [
{
title: __( 'Store details', 'woocommerce-admin' ),
link: getLinkTypeAndHref( {
type: 'wc-settings',
tab: 'general',
} ),
icon: pencil,
listItemTag: 'edit-store-details',
},
{
title: __( 'Payments', 'woocommerce-admin' ),
link: getLinkTypeAndHref( {
type: 'wc-settings',
tab: 'checkout',
} ),
icon: payment,
listItemTag: 'payment-settings',
},
{
title: __( 'Tax', 'woocommerce-admin' ),
link: getLinkTypeAndHref( {
type: 'wc-settings',
tab: 'tax',
} ),
icon: percent,
listItemTag: 'tax-settings',
},
{
title: __( 'Shipping', 'woocommerce-admin' ),
link: getLinkTypeAndHref( {
type: 'wc-settings',
tab: 'shipping',
} ),
icon: shipping,
listItemTag: 'shipping-settings',
},
],
},
];
}
export function getLinkTypeAndHref( { path, tab = null, type, href = null } ) {
return (
{
'wc-admin': {
href: `admin.php?page=wc-admin&path=%2F${ path }`,
linkType: 'wc-admin',
},
'wp-admin': {
href: path,
linkType: 'wp-admin',
},
'wc-settings': {
href: `admin.php?page=wc-settings&tab=${ tab }`,
linkType: 'wp-admin',
},
}[ type ] || {
href,
linkType: 'external',
}
);
}
export const generateExtensionLinks = ( links ) => {
return links.reduce( ( acc, { icon, href, title } ) => {
const url = new URL( href, window.location.href );
// We do not support extension links that take users away from the host.
if ( url.origin === window.location.origin ) {
acc.push( {
icon,
link: {
href,
linkType: 'wp-admin',
},
title,
listItemTag: 'quick-links-extension-link',
} );
}
return acc;
}, [] );
};
export const StoreManagementLinks = () => {
const siteUrl = getSetting( 'siteUrl' );
const extensionQuickLinks = generateExtensionLinks(
applyFilters( 'woocommerce_admin_homescreen_quicklinks', [] )
);
const itemCategories = getItemsByCategory( siteUrl );
const extensionCategory = {
title: __( 'Extensions', 'woocommerce-admin' ),
items: extensionQuickLinks,
};
const categories = extensionQuickLinks.length
? [ ...itemCategories, extensionCategory ]
: itemCategories;
return (
<Card size="medium">
<CardHeader size="medium">
<Text variant="title.small">
{ __( 'Store management', 'woocommerce-admin' ) }
</Text>
</CardHeader>
<CardBody
size="custom"
className="woocommerce-store-management-links__card-body"
>
{ categories.map( ( category ) => {
return (
<QuickLinkCategory
key={ category.title }
title={ category.title }
>
{ category.items.map(
( {
icon,
listItemTag,
title,
link: { href, linkType },
} ) => (
<QuickLink
icon={ icon }
key={ `${ title }_${ listItemTag }_${ href }` }
title={ title }
linkType={ linkType }
href={ href }
onClick={ () => {
recordEvent(
'home_quick_links_click',
{
task_name: listItemTag,
}
);
} }
/>
)
) }
</QuickLinkCategory>
);
} ) }
</CardBody>
</Card>
);
};

View File

@ -0,0 +1,20 @@
/**
* External dependencies
*/
import React from '@wordpress/element';
/**
* Internal dependencies
*/
import './style.scss';
export const QuickLinkCategory = ( { title, children } ) => {
return (
<div className="woocommerce-quick-links__category">
<h3 className="woocommerce-quick-links__category-header">
{ title }
</h3>
{ children }
</div>
);
};

View File

@ -0,0 +1,14 @@
.woocommerce-quick-links__category {
display: flex;
flex-flow: row wrap;
margin-bottom: 8px;
.woocommerce-quick-links__category-header {
margin: 0 24px 8px 24px;
text-transform: uppercase;
color: $gray-700;
line-height: 16px;
font-size: 11px;
flex: 1 100%;
}
}

View File

@ -0,0 +1,22 @@
/**
* External dependencies
*/
import { render } from '@testing-library/react';
/**
* Internal dependencies
*/
import { QuickLinkCategory } from '..';
describe( 'QuickLinkCategory', () => {
it( 'displays the passed title and children', () => {
const { queryByText } = render(
<QuickLinkCategory title="hello world">
<div>Test</div>
</QuickLinkCategory>
);
expect( queryByText( 'hello world' ) ).not.toBeEmptyDOMElement();
expect( queryByText( 'Test' ) ).not.toBeEmptyDOMElement();
} );
} );

View File

@ -0,0 +1,40 @@
/**
* External dependencies
*/
import React from '@wordpress/element';
import { external, Icon } from '@wordpress/icons';
import { __experimentalText as Text } from '@wordpress/components';
import { Link } from '@woocommerce/components';
/**
* Internal dependencies
*/
import './style.scss';
export const QuickLink = ( { icon, title, href, linkType, onClick } ) => {
const isExternal = linkType === 'external';
return (
<div className="woocommerce-quick-links__item">
<Link
onClick={ onClick }
href={ href }
linkType={ linkType }
className="woocommerce-quick-links__item-link"
>
<Icon
className="woocommerce-quick-links__item-link__icon"
icon={ icon }
/>
<Text
className="woocommerce-quick-links__item-link__text"
as="div"
variant="button"
>
{ title }
</Text>
{ isExternal && <Icon icon={ external } /> }
</Link>
</div>
);
};

View File

@ -0,0 +1,34 @@
.woocommerce-quick-links__item {
display: flex;
flex: 1 50%;
&:hover {
background-color: $gray-100;
}
.woocommerce-quick-links__item-link {
width: 100%;
display: flex;
align-items: center;
text-decoration: none;
padding: 16px 27px 16px 24px;
.woocommerce-quick-links__item-link__icon {
fill: var(--wp-admin-theme-color);
}
.woocommerce-quick-links__item-link__text {
margin-left: 16px;
flex: 1;
line-height: 16px;
font-size: 13px;
}
}
}
// Modified styles for 2 column presentation.
.woocommerce-homescreen.two-columns {
.woocommerce-quick-links__item {
flex: 1 100%;
}
}

View File

@ -0,0 +1,49 @@
/**
* External dependencies
*/
import React from '@wordpress/element';
import { render } from '@testing-library/react';
import { brush } from '@wordpress/icons';
import userEvent from '@testing-library/user-event';
/**
* Internal dependencies
*/
import { QuickLink } from '../index';
describe( 'QuickLink', () => {
it( 'renders an title and href based on props passed', () => {
const { queryByText, queryByRole } = render(
<QuickLink
linkType="external"
title="hello world"
icon={ brush }
href="https://example.com"
/>
);
expect( queryByText( 'hello world' ) ).not.toBeEmptyDOMElement();
expect( queryByRole( 'link' ) ).toHaveAttribute(
'href',
'https://example.com'
);
} );
it( 'attaches a click handler to the link if it is passed', () => {
const clickHandler = jest.fn();
const { queryByRole } = render(
<QuickLink
linkType="external"
title="hello world"
icon={ brush }
href="https://example.com"
onClick={ clickHandler }
/>
);
const link = queryByRole( 'link' );
userEvent.click( link );
expect( clickHandler ).toHaveBeenCalled();
} );
} );

View File

@ -0,0 +1,5 @@
// Setting a custom size that is not recognised by the <CardBody> allows us to override the card's default padding sizes.
// We need to do this to allow item hover backgrounds to stretch the whole card body.
.woocommerce-store-management-links__card-body.is-size-custom {
padding: 24px 0 8px 0;
}

View File

@ -0,0 +1,134 @@
jest.mock( '@woocommerce/tracks', () => ( {
...jest.requireActual( '@woocommerce/tracks' ),
recordEvent: jest.fn(),
} ) );
jest.mock( '@woocommerce/wc-admin-settings', () => ( {
...jest.requireActual( '@woocommerce/wc-admin-settings' ),
getSetting: jest.fn( () => 'https://fake-site-url.com' ),
} ) );
/**
* External dependencies
*/
import { recordEvent } from '@woocommerce/tracks';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
/**
* Internal dependencies
*/
import {
StoreManagementLinks,
getLinkTypeAndHref,
getItemsByCategory,
generateExtensionLinks,
} from '..';
describe( 'getLinkTypeAndHref', () => {
it( 'generates the correct link for wc-admin links', () => {
const result = getLinkTypeAndHref( {
type: 'wc-admin',
path: 'foo/bar',
} );
expect( result.linkType ).toEqual( 'wc-admin' );
expect( result.href ).toEqual(
'admin.php?page=wc-admin&path=%2Ffoo/bar'
);
} );
it( 'generates the correct link for wp-admin links', () => {
const result = getLinkTypeAndHref( {
type: 'wp-admin',
path: '/foo/bar',
} );
expect( result.linkType ).toEqual( 'wp-admin' );
expect( result.href ).toEqual( '/foo/bar' );
} );
it( 'generates the correct link for wc-settings links', () => {
const result = getLinkTypeAndHref( {
type: 'wc-settings',
tab: 'foo',
} );
expect( result.linkType ).toEqual( 'wp-admin' );
expect( result.href ).toEqual( 'admin.php?page=wc-settings&tab=foo' );
} );
it( 'generates the an external link if there is no provided type', () => {
const result = getLinkTypeAndHref( {
href: 'http://example.com',
} );
expect( result.linkType ).toEqual( 'external' );
expect( result.href ).toEqual( 'http://example.com' );
} );
} );
describe( 'StoreManagementLinks', () => {
it( 'records a track when a link is clicked', () => {
const { queryByText } = render( <StoreManagementLinks /> );
const linkDetails = getItemsByCategory( 'fakeUrl' )[ 0 ].items[ 0 ];
userEvent.click( queryByText( linkDetails.title ) );
expect( recordEvent ).toHaveBeenCalledWith( 'home_quick_links_click', {
task_name: linkDetails.listItemTag,
} );
} );
} );
describe( 'generateExtensionLinks', () => {
it( 'filters out external links', () => {
expect(
generateExtensionLinks( [
{
href: 'https://example.com',
title: 'external link',
icon: <div>hi</div>,
},
] )
).toEqual( [] );
} );
it( 'generates a valid link for relative links', () => {
const validFullUrl = {
href: 'http://localhost/foo/bar',
title: 'external link',
icon: <div>hi</div>,
};
const validRelativeUrl = {
href: '/foo/bar',
title: 'external link',
icon: <div>hi</div>,
};
expect( generateExtensionLinks( [ validFullUrl ] ) ).toEqual( [
{
icon: validFullUrl.icon,
link: {
href: validFullUrl.href,
linkType: 'wp-admin',
},
title: validFullUrl.title,
listItemTag: 'quick-links-extension-link',
},
] );
expect( generateExtensionLinks( [ validRelativeUrl ] ) ).toEqual( [
{
icon: validRelativeUrl.icon,
link: {
href: validRelativeUrl.href,
linkType: 'wp-admin',
},
title: validRelativeUrl.title,
listItemTag: 'quick-links-extension-link',
},
] );
} );
} );

View File

@ -22,3 +22,7 @@ allows Woo themed components based on the config found in postcss.config.js
@import 'gutenberg-components/tab-panel/style.scss';
@import 'gutenberg-components/guide/style.scss';
@import 'gutenberg-components/animate/style.scss';
:root {
@include admin-scheme( #007cba );
}