Hook up leaderboards endpoint to dashboard (https://github.com/woocommerce/woocommerce-admin/pull/2004)
* Add allowed leaderboards endpoint to prefetch list * Hook up leadboards endpoint to dashboard * Skip data fetch for leaderboard if per page is less than 1 * Add tests to check for rendered html display
This commit is contained in:
parent
e190d513c2
commit
eb0b137020
|
@ -5,36 +5,50 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { Component } from '@wordpress/element';
|
||||
import { compose } from '@wordpress/compose';
|
||||
import { get } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { Card, EmptyTable, TableCard } from '@woocommerce/components';
|
||||
import { getPersistedQuery } from '@woocommerce/navigation';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getLeaderboard } from 'wc-api/items/utils';
|
||||
import ReportError from 'analytics/components/report-error';
|
||||
import { getReportTableData } from 'wc-api/reports/utils';
|
||||
import sanitizeHTML from 'lib/sanitize-html';
|
||||
import withSelect from 'wc-api/with-select';
|
||||
import './style.scss';
|
||||
|
||||
export class Leaderboard extends Component {
|
||||
getFormattedHeaders() {
|
||||
return this.props.headers.map( ( header, i ) => {
|
||||
return {
|
||||
isLeftAligned: 0 === i,
|
||||
hiddenByDefault: false,
|
||||
isSortable: false,
|
||||
key: header.label,
|
||||
label: header.label,
|
||||
};
|
||||
} );
|
||||
}
|
||||
|
||||
getFormattedRows() {
|
||||
return this.props.rows.map( row => {
|
||||
return row.map( column => {
|
||||
return {
|
||||
display: <div dangerouslySetInnerHTML={ sanitizeHTML( column.display ) } />,
|
||||
value: column.value,
|
||||
};
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
getHeadersContent,
|
||||
getRowsContent,
|
||||
isRequesting,
|
||||
isError,
|
||||
items,
|
||||
tableQuery,
|
||||
title,
|
||||
} = this.props;
|
||||
const data = get( items, [ 'data' ], [] );
|
||||
const rows = getRowsContent( data );
|
||||
const totalRows = tableQuery ? tableQuery.per_page : 5;
|
||||
const { isRequesting, isError, totalRows, title } = this.props;
|
||||
const rows = this.getFormattedRows();
|
||||
|
||||
if ( isError ) {
|
||||
return <ReportError className="woocommerce-leaderboard" isError />;
|
||||
|
@ -53,7 +67,7 @@ export class Leaderboard extends Component {
|
|||
return (
|
||||
<TableCard
|
||||
className="woocommerce-leaderboard"
|
||||
headers={ getHeadersContent() }
|
||||
headers={ this.getFormattedHeaders() }
|
||||
isLoading={ isRequesting }
|
||||
rows={ rows }
|
||||
rowsPerPage={ totalRows }
|
||||
|
@ -67,45 +81,60 @@ export class Leaderboard extends Component {
|
|||
|
||||
Leaderboard.propTypes = {
|
||||
/**
|
||||
* The endpoint to use in API calls to populate the table rows and summary.
|
||||
* For example, if `taxes` is provided, data will be fetched from the report
|
||||
* `taxes` endpoint (ie: `/wc/v4/reports/taxes` and `/wc/v4/reports/taxes/stats`).
|
||||
* If the provided endpoint doesn't exist, an error will be shown to the user
|
||||
* with `ReportError`.
|
||||
* An array of column headers.
|
||||
*/
|
||||
endpoint: PropTypes.string,
|
||||
headers: PropTypes.arrayOf(
|
||||
PropTypes.shape( {
|
||||
label: PropTypes.string,
|
||||
} )
|
||||
),
|
||||
/**
|
||||
* A function that returns the headers object to build the table.
|
||||
* String of leaderboard ID to display.
|
||||
*/
|
||||
getHeadersContent: PropTypes.func.isRequired,
|
||||
/**
|
||||
* A function that returns the rows array to build the table.
|
||||
*/
|
||||
getRowsContent: PropTypes.func.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
/**
|
||||
* Query args added to the report table endpoint request.
|
||||
*/
|
||||
query: PropTypes.object,
|
||||
/**
|
||||
* Properties to be added to the query sent to the report table endpoint.
|
||||
* Which column should be the row header, defaults to the first item (`0`) (see `Table` props).
|
||||
*/
|
||||
tableQuery: PropTypes.object,
|
||||
rows: PropTypes.arrayOf(
|
||||
PropTypes.arrayOf(
|
||||
PropTypes.shape( {
|
||||
display: PropTypes.node,
|
||||
value: PropTypes.oneOfType( [ PropTypes.string, PropTypes.number, PropTypes.bool ] ),
|
||||
} )
|
||||
)
|
||||
).isRequired,
|
||||
/**
|
||||
* String to display as the title of the table.
|
||||
*/
|
||||
title: PropTypes.string.isRequired,
|
||||
/**
|
||||
* Number of table rows.
|
||||
*/
|
||||
totalRows: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
Leaderboard.defaultProps = {
|
||||
rows: [],
|
||||
isError: false,
|
||||
isRequesting: false,
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withSelect( ( select, props ) => {
|
||||
const { endpoint, tableQuery, query } = props;
|
||||
const tableData = getReportTableData( {
|
||||
endpoint,
|
||||
const { id, query, totalRows } = props;
|
||||
const leaderboardQuery = {
|
||||
id,
|
||||
per_page: totalRows,
|
||||
persisted_query: getPersistedQuery( query ),
|
||||
query,
|
||||
select,
|
||||
tableQuery,
|
||||
} );
|
||||
};
|
||||
const leaderboardData = getLeaderboard( leaderboardQuery );
|
||||
|
||||
return { ...tableData };
|
||||
return leaderboardData;
|
||||
} )
|
||||
)( Leaderboard );
|
||||
|
|
|
@ -3,10 +3,7 @@
|
|||
*
|
||||
* @format
|
||||
*/
|
||||
import TestRenderer from 'react-test-renderer';
|
||||
import { map, noop } from 'lodash';
|
||||
import { shallow } from 'enzyme';
|
||||
import { createRegistry, RegistryProvider } from '@wordpress/data';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
|
@ -17,101 +14,72 @@ import { numberFormat } from '@woocommerce/number';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import LeaderboardWithSelect, { Leaderboard } from '../';
|
||||
import { NAMESPACE } from 'wc-api/constants';
|
||||
import { Leaderboard } from '../';
|
||||
import mockData from '../__mocks__/top-selling-products-mock-data';
|
||||
|
||||
// Mock <Table> to avoid tests failing due to it using DOM properties that
|
||||
// are not available on TestRenderer.
|
||||
jest.mock( '@woocommerce/components', () => ( {
|
||||
...jest.requireActual( '../../../../../packages/components' ),
|
||||
TableCard: () => null,
|
||||
} ) );
|
||||
const rows = mockData.map( row => {
|
||||
const { name, items_sold, net_revenue, orders_count } = row;
|
||||
return [
|
||||
{
|
||||
display: '<a href="#">' + name + '</a>',
|
||||
value: name,
|
||||
},
|
||||
{
|
||||
display: numberFormat( items_sold ),
|
||||
value: items_sold,
|
||||
},
|
||||
{
|
||||
display: numberFormat( orders_count ),
|
||||
value: orders_count,
|
||||
},
|
||||
{
|
||||
display: formatCurrency( net_revenue ),
|
||||
value: getCurrencyFormatDecimal( net_revenue ),
|
||||
},
|
||||
];
|
||||
} );
|
||||
|
||||
const getRowsContent = data => {
|
||||
return map( data, row => {
|
||||
const { name, items_sold, net_revenue, orders_count } = row;
|
||||
return [
|
||||
{
|
||||
display: name,
|
||||
value: name,
|
||||
},
|
||||
{
|
||||
display: numberFormat( items_sold ),
|
||||
value: items_sold,
|
||||
},
|
||||
{
|
||||
display: numberFormat( orders_count ),
|
||||
value: orders_count,
|
||||
},
|
||||
{
|
||||
display: formatCurrency( net_revenue ),
|
||||
value: getCurrencyFormatDecimal( net_revenue ),
|
||||
},
|
||||
];
|
||||
} );
|
||||
};
|
||||
const headers = [
|
||||
{
|
||||
label: 'Name',
|
||||
},
|
||||
{
|
||||
label: 'Items Sold',
|
||||
},
|
||||
{
|
||||
label: 'Orders Count',
|
||||
},
|
||||
{
|
||||
label: 'Net Revenue',
|
||||
},
|
||||
];
|
||||
|
||||
describe( 'Leaderboard', () => {
|
||||
test( 'should render empty message when there are no rows', () => {
|
||||
const leaderboard = shallow(
|
||||
<Leaderboard title={ '' } getHeadersContent={ noop } getRowsContent={ getRowsContent } />
|
||||
<Leaderboard id="products" title={ '' } headers={ [] } rows={ [] } totalRows={ 5 } />
|
||||
);
|
||||
|
||||
expect( leaderboard.find( 'EmptyTable' ).length ).toBe( 1 );
|
||||
} );
|
||||
|
||||
test( 'should render correct data in the table', () => {
|
||||
const leaderboard = shallow(
|
||||
<Leaderboard
|
||||
title={ '' }
|
||||
getHeadersContent={ noop }
|
||||
getRowsContent={ getRowsContent }
|
||||
items={ { data: { ...mockData } } }
|
||||
/>
|
||||
const leaderboard = mount(
|
||||
<Leaderboard id="products" title={ '' } headers={ headers } rows={ rows } totalRows={ 5 } />
|
||||
);
|
||||
const table = leaderboard.find( 'TableCard' );
|
||||
const firstRow = table.props().rows[ 0 ];
|
||||
const tableItems = leaderboard.find( '.woocommerce-table__item' );
|
||||
|
||||
expect( firstRow[ 0 ].value ).toBe( mockData[ 0 ].name );
|
||||
expect( firstRow[ 1 ].display ).toBe( numberFormat( mockData[ 0 ].items_sold ) );
|
||||
expect( firstRow[ 1 ].value ).toBe( mockData[ 0 ].items_sold );
|
||||
expect( firstRow[ 2 ].display ).toBe( numberFormat( mockData[ 0 ].orders_count ) );
|
||||
expect( firstRow[ 2 ].value ).toBe( mockData[ 0 ].orders_count );
|
||||
expect( firstRow[ 3 ].display ).toBe( formatCurrency( mockData[ 0 ].net_revenue ) );
|
||||
expect( firstRow[ 3 ].value ).toBe( getCurrencyFormatDecimal( mockData[ 0 ].net_revenue ) );
|
||||
} );
|
||||
|
||||
// @todo Since this now uses fresh-data / wc-api, the API testing needs to be revisted.
|
||||
xtest( 'should load report stats from API', () => {
|
||||
const getReportStatsMock = jest.fn().mockReturnValue( { data: mockData } );
|
||||
const isReportStatsRequestingMock = jest.fn().mockReturnValue( false );
|
||||
const isReportStatsErrorMock = jest.fn().mockReturnValue( false );
|
||||
const registry = createRegistry();
|
||||
registry.registerStore( 'wc-api', {
|
||||
reducer: () => {},
|
||||
selectors: {
|
||||
getReportStats: getReportStatsMock,
|
||||
isReportStatsRequesting: isReportStatsRequestingMock,
|
||||
isReportStatsError: isReportStatsErrorMock,
|
||||
},
|
||||
} );
|
||||
const leaderboardWrapper = TestRenderer.create(
|
||||
<RegistryProvider value={ registry }>
|
||||
<LeaderboardWithSelect />
|
||||
</RegistryProvider>
|
||||
);
|
||||
const leaderboard = leaderboardWrapper.root.findByType( Leaderboard );
|
||||
|
||||
const endpoint = NAMESPACE + '/reports/products';
|
||||
const query = { orderby: 'items_sold', per_page: 5, extended_info: 1 };
|
||||
|
||||
expect( getReportStatsMock.mock.calls[ 0 ][ 1 ] ).toBe( endpoint );
|
||||
expect( getReportStatsMock.mock.calls[ 0 ][ 2 ] ).toEqual( query );
|
||||
expect( isReportStatsRequestingMock.mock.calls[ 0 ][ 1 ] ).toBe( endpoint );
|
||||
expect( isReportStatsRequestingMock.mock.calls[ 0 ][ 2 ] ).toEqual( query );
|
||||
expect( isReportStatsErrorMock.mock.calls[ 0 ][ 1 ] ).toBe( endpoint );
|
||||
expect( isReportStatsErrorMock.mock.calls[ 0 ][ 2 ] ).toEqual( query );
|
||||
expect( leaderboard.props.data ).toBe( mockData );
|
||||
expect( leaderboard.render().find( '.woocommerce-table__item a' ).length ).toBe( 5 );
|
||||
expect( tableItems.at( 0 ).text() ).toBe( mockData[ 0 ].name );
|
||||
expect( tableItems.at( 1 ).text() ).toBe( numberFormat( mockData[ 0 ].items_sold ) );
|
||||
expect( tableItems.at( 2 ).text() ).toBe( numberFormat( mockData[ 0 ].orders_count ) );
|
||||
expect( tableItems.at( 3 ).text() ).toBe( formatCurrency( mockData[ 0 ].net_revenue ) );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -18,18 +18,15 @@ import { EllipsisMenu, MenuItem, MenuTitle, SectionHeader } from '@woocommerce/c
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Leaderboard from 'analytics/components/leaderboard';
|
||||
import withSelect from 'wc-api/with-select';
|
||||
import TopSellingCategories from './top-selling-categories';
|
||||
import TopSellingProducts from './top-selling-products';
|
||||
import TopCoupons from './top-coupons';
|
||||
import TopCustomers from './top-customers';
|
||||
import './style.scss';
|
||||
|
||||
class Leaderboards extends Component {
|
||||
constructor( props ) {
|
||||
super( ...arguments );
|
||||
this.state = {
|
||||
hiddenLeaderboardKeys: props.userPrefLeaderboards || [ 'top-coupons', 'top-customers' ],
|
||||
hiddenLeaderboardKeys: props.userPrefLeaderboards || [ 'coupons', 'customers' ],
|
||||
rowsPerTable: parseInt( props.userPrefLeaderboardRows ) || 5,
|
||||
};
|
||||
|
||||
|
@ -57,24 +54,8 @@ class Leaderboards extends Component {
|
|||
|
||||
renderMenu() {
|
||||
const { hiddenLeaderboardKeys, rowsPerTable } = this.state;
|
||||
const allLeaderboards = [
|
||||
{
|
||||
key: 'top-products',
|
||||
label: __( 'Top Products - Items Sold', 'woocommerce-admin' ),
|
||||
},
|
||||
{
|
||||
key: 'top-categories',
|
||||
label: __( 'Top Categories - Items Sold', 'woocommerce-admin' ),
|
||||
},
|
||||
{
|
||||
key: 'top-coupons',
|
||||
label: __( 'Top Coupons', 'woocommerce-admin' ),
|
||||
},
|
||||
{
|
||||
key: 'top-customers',
|
||||
label: __( 'Top Customers', 'woocommerce-admin' ),
|
||||
},
|
||||
];
|
||||
const { allLeaderboards } = this.props;
|
||||
|
||||
return (
|
||||
<EllipsisMenu
|
||||
label={ __(
|
||||
|
@ -87,11 +68,11 @@ class Leaderboards extends Component {
|
|||
{ allLeaderboards.map( leaderboard => {
|
||||
return (
|
||||
<MenuItem
|
||||
checked={ ! hiddenLeaderboardKeys.includes( leaderboard.key ) }
|
||||
checked={ ! hiddenLeaderboardKeys.includes( leaderboard.id ) }
|
||||
isCheckbox
|
||||
isClickable
|
||||
key={ leaderboard.key }
|
||||
onInvoke={ this.toggle( leaderboard.key ) }
|
||||
key={ leaderboard.id }
|
||||
onInvoke={ this.toggle( leaderboard.id ) }
|
||||
>
|
||||
{ leaderboard.label }
|
||||
</MenuItem>
|
||||
|
@ -112,9 +93,29 @@ class Leaderboards extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
renderLeaderboards() {
|
||||
const { hiddenLeaderboardKeys, rowsPerTable } = this.state;
|
||||
const { query } = this.props;
|
||||
const { allLeaderboards, query } = this.props;
|
||||
|
||||
return allLeaderboards.map( leaderboard => {
|
||||
if ( hiddenLeaderboardKeys.includes( leaderboard.id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<Leaderboard
|
||||
headers={ leaderboard.headers }
|
||||
id={ leaderboard.id }
|
||||
key={ leaderboard.id }
|
||||
query={ query }
|
||||
title={ leaderboard.label }
|
||||
totalRows={ rowsPerTable }
|
||||
/>
|
||||
);
|
||||
} );
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="woocommerce-dashboard__dashboard-leaderboards">
|
||||
|
@ -122,20 +123,7 @@ class Leaderboards extends Component {
|
|||
title={ __( 'Leaderboards', 'woocommerce-admin' ) }
|
||||
menu={ this.renderMenu() }
|
||||
/>
|
||||
<div className="woocommerce-dashboard__columns">
|
||||
{ ! hiddenLeaderboardKeys.includes( 'top-products' ) && (
|
||||
<TopSellingProducts query={ query } totalRows={ rowsPerTable } />
|
||||
) }
|
||||
{ ! hiddenLeaderboardKeys.includes( 'top-categories' ) && (
|
||||
<TopSellingCategories query={ query } totalRows={ rowsPerTable } />
|
||||
) }
|
||||
{ ! hiddenLeaderboardKeys.includes( 'top-coupons' ) && (
|
||||
<TopCoupons query={ query } totalRows={ rowsPerTable } />
|
||||
) }
|
||||
{ ! hiddenLeaderboardKeys.includes( 'top-customers' ) && (
|
||||
<TopCustomers query={ query } totalRows={ rowsPerTable } />
|
||||
) }
|
||||
</div>
|
||||
<div className="woocommerce-dashboard__columns">{ this.renderLeaderboards() }</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
|
@ -148,10 +136,17 @@ Leaderboards.propTypes = {
|
|||
|
||||
export default compose(
|
||||
withSelect( select => {
|
||||
const { getCurrentUserData } = select( 'wc-api' );
|
||||
const { getCurrentUserData, getItems, getItemsError, isGetItemsRequesting } = select(
|
||||
'wc-api'
|
||||
);
|
||||
const userData = getCurrentUserData();
|
||||
const allLeaderboards = wcSettings.dataEndpoints.leaderboards;
|
||||
|
||||
return {
|
||||
allLeaderboards,
|
||||
getItems,
|
||||
getItemsError,
|
||||
isGetItemsRequesting,
|
||||
userPrefLeaderboards: userData.dashboard_leaderboards,
|
||||
userPrefLeaderboardRows: userData.dashboard_leaderboard_rows,
|
||||
};
|
||||
|
|
|
@ -1,122 +0,0 @@
|
|||
/** @format */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Component } from '@wordpress/element';
|
||||
import { map } from 'lodash';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
|
||||
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
|
||||
import { Link } from '@woocommerce/components';
|
||||
import { numberFormat } from '@woocommerce/number';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Leaderboard from 'analytics/components/leaderboard';
|
||||
|
||||
export class TopCoupons extends Component {
|
||||
constructor( props ) {
|
||||
super( props );
|
||||
|
||||
this.getRowsContent = this.getRowsContent.bind( this );
|
||||
this.getHeadersContent = this.getHeadersContent.bind( this );
|
||||
}
|
||||
|
||||
getHeadersContent() {
|
||||
return [
|
||||
{
|
||||
label: __( 'Coupon Code', 'woocommerce-admin' ),
|
||||
key: 'code',
|
||||
required: true,
|
||||
isLeftAligned: true,
|
||||
isSortable: false,
|
||||
},
|
||||
{
|
||||
label: __( 'Orders', 'woocommerce-admin' ),
|
||||
key: 'orders_count',
|
||||
required: true,
|
||||
defaultSort: true,
|
||||
isSortable: false,
|
||||
isNumeric: true,
|
||||
},
|
||||
{
|
||||
label: __( 'Amount Discounted', 'woocommerce-admin' ),
|
||||
key: 'amount',
|
||||
isSortable: false,
|
||||
isNumeric: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
getRowsContent( data ) {
|
||||
const { query } = this.props;
|
||||
const persistedQuery = getPersistedQuery( query );
|
||||
return map( data, row => {
|
||||
const { amount, coupon_id, extended_info, orders_count } = row;
|
||||
const { code } = extended_info;
|
||||
|
||||
const couponUrl = getNewPath( persistedQuery, 'analytics/coupons', {
|
||||
filter: 'single_coupon',
|
||||
coupons: coupon_id,
|
||||
} );
|
||||
const couponLink = (
|
||||
<Link href={ couponUrl } type="wc-admin">
|
||||
{ code }
|
||||
</Link>
|
||||
);
|
||||
|
||||
const ordersUrl = getNewPath( persistedQuery, 'analytics/orders', {
|
||||
filter: 'advanced',
|
||||
coupon_includes: coupon_id,
|
||||
} );
|
||||
const ordersLink = (
|
||||
<Link href={ ordersUrl } type="wc-admin">
|
||||
{ numberFormat( orders_count ) }
|
||||
</Link>
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
display: couponLink,
|
||||
value: code,
|
||||
},
|
||||
{
|
||||
display: ordersLink,
|
||||
value: orders_count,
|
||||
},
|
||||
{
|
||||
display: formatCurrency( amount ),
|
||||
value: getCurrencyFormatDecimal( amount ),
|
||||
},
|
||||
];
|
||||
} );
|
||||
}
|
||||
|
||||
render() {
|
||||
const { query, totalRows } = this.props;
|
||||
const tableQuery = {
|
||||
orderby: 'orders_count',
|
||||
order: 'desc',
|
||||
per_page: totalRows,
|
||||
extended_info: true,
|
||||
};
|
||||
|
||||
return (
|
||||
<Leaderboard
|
||||
endpoint="coupons"
|
||||
getHeadersContent={ this.getHeadersContent }
|
||||
getRowsContent={ this.getRowsContent }
|
||||
query={ query }
|
||||
tableQuery={ tableQuery }
|
||||
title={ __( 'Top Coupons', 'woocommerce-admin' ) }
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TopCoupons;
|
|
@ -1,111 +0,0 @@
|
|||
/** @format */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Component } from '@wordpress/element';
|
||||
import { map } from 'lodash';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
|
||||
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
|
||||
import { Link } from '@woocommerce/components';
|
||||
import { numberFormat } from '@woocommerce/number';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Leaderboard from 'analytics/components/leaderboard';
|
||||
|
||||
export class TopCustomers extends Component {
|
||||
constructor( props ) {
|
||||
super( props );
|
||||
|
||||
this.getRowsContent = this.getRowsContent.bind( this );
|
||||
this.getHeadersContent = this.getHeadersContent.bind( this );
|
||||
}
|
||||
|
||||
getHeadersContent() {
|
||||
return [
|
||||
{
|
||||
label: __( 'Customer Name', 'woocommerce-admin' ),
|
||||
key: 'name',
|
||||
required: true,
|
||||
isLeftAligned: true,
|
||||
isSortable: false,
|
||||
},
|
||||
{
|
||||
label: __( 'Orders', 'woocommerce-admin' ),
|
||||
key: 'orders_count',
|
||||
required: true,
|
||||
defaultSort: true,
|
||||
isSortable: false,
|
||||
isNumeric: true,
|
||||
},
|
||||
{
|
||||
label: __( 'Total Spend', 'woocommerce-admin' ),
|
||||
key: 'total_spend',
|
||||
isSortable: false,
|
||||
isNumeric: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
getRowsContent( data ) {
|
||||
const { query } = this.props;
|
||||
const persistedQuery = getPersistedQuery( query );
|
||||
return map( data, row => {
|
||||
const { id, total_spend, name, orders_count } = row;
|
||||
|
||||
const customerUrl = getNewPath( persistedQuery, 'analytics/customers', {
|
||||
filter: 'single_customer',
|
||||
customers: id,
|
||||
} );
|
||||
const customerLink = (
|
||||
<Link href={ customerUrl } type="wc-admin">
|
||||
{ name }
|
||||
</Link>
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
display: customerLink,
|
||||
value: name,
|
||||
},
|
||||
{
|
||||
display: numberFormat( orders_count ),
|
||||
value: orders_count,
|
||||
},
|
||||
{
|
||||
display: formatCurrency( total_spend ),
|
||||
value: getCurrencyFormatDecimal( total_spend ),
|
||||
},
|
||||
];
|
||||
} );
|
||||
}
|
||||
|
||||
render() {
|
||||
const { query, totalRows } = this.props;
|
||||
const tableQuery = {
|
||||
orderby: 'total_spend',
|
||||
order: 'desc',
|
||||
per_page: totalRows,
|
||||
extended_info: false,
|
||||
};
|
||||
|
||||
return (
|
||||
<Leaderboard
|
||||
endpoint="customers"
|
||||
getHeadersContent={ this.getHeadersContent }
|
||||
getRowsContent={ this.getRowsContent }
|
||||
query={ query }
|
||||
tableQuery={ tableQuery }
|
||||
title={ __( 'Top Customers', 'woocommerce-admin' ) }
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TopCustomers;
|
|
@ -1,111 +0,0 @@
|
|||
/** @format */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Component } from '@wordpress/element';
|
||||
import { get, map } from 'lodash';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
|
||||
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
|
||||
import { Link } from '@woocommerce/components';
|
||||
import { numberFormat } from '@woocommerce/number';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Leaderboard from 'analytics/components/leaderboard';
|
||||
|
||||
export class TopSellingCategories extends Component {
|
||||
constructor( props ) {
|
||||
super( props );
|
||||
|
||||
this.getRowsContent = this.getRowsContent.bind( this );
|
||||
this.getHeadersContent = this.getHeadersContent.bind( this );
|
||||
}
|
||||
|
||||
getHeadersContent() {
|
||||
return [
|
||||
{
|
||||
label: __( 'Category', 'woocommerce-admin' ),
|
||||
key: 'category',
|
||||
required: true,
|
||||
isLeftAligned: true,
|
||||
isSortable: false,
|
||||
},
|
||||
{
|
||||
label: __( 'Items Sold', 'woocommerce-admin' ),
|
||||
key: 'items_sold',
|
||||
required: false,
|
||||
isSortable: false,
|
||||
isNumeric: true,
|
||||
},
|
||||
{
|
||||
label: __( 'Net Revenue', 'woocommerce-admin' ),
|
||||
key: 'net_revenue',
|
||||
required: true,
|
||||
isSortable: false,
|
||||
isNumeric: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
getRowsContent( data ) {
|
||||
const { query } = this.props;
|
||||
const persistedQuery = getPersistedQuery( query );
|
||||
return map( data, row => {
|
||||
const { category_id, items_sold, net_revenue } = row;
|
||||
const extended_info = row.extended_info || {};
|
||||
const name = get( extended_info, [ 'name' ] );
|
||||
const categoryUrl = getNewPath( persistedQuery, 'analytics/categories', {
|
||||
filter: 'single_category',
|
||||
categories: category_id,
|
||||
} );
|
||||
const categoryLink = (
|
||||
<Link href={ categoryUrl } type="wc-admin">
|
||||
{ name }
|
||||
</Link>
|
||||
);
|
||||
return [
|
||||
{
|
||||
display: categoryLink,
|
||||
value: name,
|
||||
},
|
||||
{
|
||||
display: numberFormat( items_sold ),
|
||||
value: items_sold,
|
||||
},
|
||||
{
|
||||
display: formatCurrency( net_revenue ),
|
||||
value: getCurrencyFormatDecimal( net_revenue ),
|
||||
},
|
||||
];
|
||||
} );
|
||||
}
|
||||
|
||||
render() {
|
||||
const { query, totalRows } = this.props;
|
||||
const tableQuery = {
|
||||
orderby: 'items_sold',
|
||||
order: 'desc',
|
||||
per_page: totalRows,
|
||||
extended_info: true,
|
||||
};
|
||||
|
||||
return (
|
||||
<Leaderboard
|
||||
endpoint="categories"
|
||||
getHeadersContent={ this.getHeadersContent }
|
||||
getRowsContent={ this.getRowsContent }
|
||||
query={ query }
|
||||
tableQuery={ tableQuery }
|
||||
title={ __( 'Top Categories - Items Sold', 'woocommerce-admin' ) }
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TopSellingCategories;
|
|
@ -1,113 +0,0 @@
|
|||
/** @format */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Component } from '@wordpress/element';
|
||||
import { get, map } from 'lodash';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
|
||||
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
|
||||
import { Link } from '@woocommerce/components';
|
||||
import { numberFormat } from '@woocommerce/number';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Leaderboard from 'analytics/components/leaderboard';
|
||||
|
||||
export class TopSellingProducts extends Component {
|
||||
constructor( props ) {
|
||||
super( props );
|
||||
|
||||
this.getRowsContent = this.getRowsContent.bind( this );
|
||||
this.getHeadersContent = this.getHeadersContent.bind( this );
|
||||
}
|
||||
|
||||
getHeadersContent() {
|
||||
return [
|
||||
{
|
||||
label: __( 'Product', 'woocommerce-admin' ),
|
||||
key: 'product',
|
||||
required: true,
|
||||
isLeftAligned: true,
|
||||
isSortable: false,
|
||||
},
|
||||
{
|
||||
label: __( 'Items Sold', 'woocommerce-admin' ),
|
||||
key: 'items_sold',
|
||||
required: false,
|
||||
isSortable: false,
|
||||
isNumeric: true,
|
||||
},
|
||||
{
|
||||
label: __( 'Net Revenue', 'woocommerce-admin' ),
|
||||
key: 'net_revenue',
|
||||
required: true,
|
||||
isSortable: false,
|
||||
isNumeric: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
getRowsContent( data ) {
|
||||
const { query } = this.props;
|
||||
const persistedQuery = getPersistedQuery( query );
|
||||
return map( data, row => {
|
||||
const { product_id, items_sold, net_revenue } = row;
|
||||
const extended_info = row.extended_info || {};
|
||||
const name = get( extended_info, [ 'name' ] );
|
||||
|
||||
const productUrl = getNewPath( persistedQuery, 'analytics/products', {
|
||||
filter: 'single_product',
|
||||
products: product_id,
|
||||
} );
|
||||
const productLink = (
|
||||
<Link href={ productUrl } type="wc-admin">
|
||||
{ name }
|
||||
</Link>
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
display: productLink,
|
||||
value: name,
|
||||
},
|
||||
{
|
||||
display: numberFormat( items_sold ),
|
||||
value: items_sold,
|
||||
},
|
||||
{
|
||||
display: formatCurrency( net_revenue ),
|
||||
value: getCurrencyFormatDecimal( net_revenue ),
|
||||
},
|
||||
];
|
||||
} );
|
||||
}
|
||||
|
||||
render() {
|
||||
const { query, totalRows } = this.props;
|
||||
const tableQuery = {
|
||||
orderby: 'items_sold',
|
||||
order: 'desc',
|
||||
per_page: totalRows,
|
||||
extended_info: true,
|
||||
};
|
||||
|
||||
return (
|
||||
<Leaderboard
|
||||
endpoint="products"
|
||||
getHeadersContent={ this.getHeadersContent }
|
||||
getRowsContent={ this.getRowsContent }
|
||||
query={ query }
|
||||
tableQuery={ tableQuery }
|
||||
title={ __( 'Top Products - Items Sold', 'woocommerce-admin' ) }
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TopSellingProducts;
|
|
@ -20,6 +20,7 @@ const typeEndpointMap = {
|
|||
'items-query-categories': 'products/categories',
|
||||
'items-query-customers': 'customers',
|
||||
'items-query-coupons': 'coupons',
|
||||
'items-query-leaderboards': 'leaderboards',
|
||||
'items-query-orders': 'orders',
|
||||
'items-query-products': 'products',
|
||||
'items-query-taxes': 'taxes',
|
||||
|
|
|
@ -4,6 +4,51 @@
|
|||
* External dependencies
|
||||
*/
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { appendTimestamp, getCurrentDates } from '@woocommerce/date';
|
||||
|
||||
/**
|
||||
* Returns leaderboard data to render a leaderboard table.
|
||||
*
|
||||
* @param {Objedt} options arguments
|
||||
* @param {String} options.id Leaderboard ID
|
||||
* @param {Integer} options.per_page Per page limit
|
||||
* @param {Object} options.persisted_query Persisted query passed to endpoint
|
||||
* @param {Object} options.query Query parameters in the url
|
||||
* @param {Object} options.select Instance of @wordpress/select
|
||||
* @return {Object} Object containing leaderboard responses.
|
||||
*/
|
||||
export function getLeaderboard( options ) {
|
||||
const endpoint = 'leaderboards';
|
||||
const { per_page, persisted_query, query, select } = options;
|
||||
const { getItems, getItemsError, isGetItemsRequesting } = select( 'wc-api' );
|
||||
const response = {
|
||||
isRequesting: false,
|
||||
isError: false,
|
||||
rows: [],
|
||||
};
|
||||
|
||||
const datesFromQuery = getCurrentDates( query );
|
||||
const leaderboardQuery = {
|
||||
after: appendTimestamp( datesFromQuery.primary.after, 'start' ),
|
||||
before: appendTimestamp( datesFromQuery.primary.before, 'end' ),
|
||||
per_page,
|
||||
persisted_query,
|
||||
};
|
||||
|
||||
const leaderboards = getItems( endpoint, leaderboardQuery );
|
||||
const leaderboard = leaderboards.get( options.id );
|
||||
if ( isGetItemsRequesting( endpoint, leaderboardQuery ) ) {
|
||||
return { ...response, isRequesting: true };
|
||||
} else if ( getItemsError( endpoint, leaderboardQuery ) ) {
|
||||
return { ...response, isError: true };
|
||||
}
|
||||
|
||||
return { ...response, rows: leaderboard.rows };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns items based on a search query.
|
||||
*
|
||||
|
|
|
@ -47,6 +47,19 @@ class WC_Admin_REST_Leaderboards_Controller extends WC_REST_Data_Controller {
|
|||
'schema' => array( $this, 'get_public_item_schema' ),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/allowed',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_allowed_items' ),
|
||||
'permission_callback' => array( $this, 'get_items_permissions_check' ),
|
||||
),
|
||||
'schema' => array( $this, 'get_public_allowed_item_schema' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,7 +72,7 @@ class WC_Admin_REST_Leaderboards_Controller extends WC_REST_Data_Controller {
|
|||
*/
|
||||
public function get_coupons_leaderboard( $per_page, $after, $before, $persisted_query ) {
|
||||
$coupons_data_store = new WC_Admin_Reports_Coupons_Data_Store();
|
||||
$coupons_data = $coupons_data_store->get_data(
|
||||
$coupons_data = $per_page > 0 ? $coupons_data_store->get_data(
|
||||
array(
|
||||
'orderby' => 'orders_count',
|
||||
'order' => 'desc',
|
||||
|
@ -68,10 +81,10 @@ class WC_Admin_REST_Leaderboards_Controller extends WC_REST_Data_Controller {
|
|||
'per_page' => $per_page,
|
||||
'extended_info' => true,
|
||||
)
|
||||
);
|
||||
)->data : array();
|
||||
|
||||
$rows = array();
|
||||
foreach ( $coupons_data->data as $coupon ) {
|
||||
foreach ( $coupons_data as $coupon ) {
|
||||
$url_query = wp_parse_args(
|
||||
array(
|
||||
'filter' => 'single_coupon',
|
||||
|
@ -125,7 +138,7 @@ class WC_Admin_REST_Leaderboards_Controller extends WC_REST_Data_Controller {
|
|||
*/
|
||||
public function get_categories_leaderboard( $per_page, $after, $before, $persisted_query ) {
|
||||
$categories_data_store = new WC_Admin_Reports_Categories_Data_Store();
|
||||
$categories_data = $categories_data_store->get_data(
|
||||
$categories_data = $per_page > 0 ? $categories_data_store->get_data(
|
||||
array(
|
||||
'orderby' => 'items_sold',
|
||||
'order' => 'desc',
|
||||
|
@ -134,10 +147,10 @@ class WC_Admin_REST_Leaderboards_Controller extends WC_REST_Data_Controller {
|
|||
'per_page' => $per_page,
|
||||
'extended_info' => true,
|
||||
)
|
||||
);
|
||||
)->data : array();
|
||||
|
||||
$rows = array();
|
||||
foreach ( $categories_data->data as $category ) {
|
||||
foreach ( $categories_data as $category ) {
|
||||
$url_query = wp_parse_args(
|
||||
array(
|
||||
'filter' => 'single_category',
|
||||
|
@ -191,16 +204,16 @@ class WC_Admin_REST_Leaderboards_Controller extends WC_REST_Data_Controller {
|
|||
*/
|
||||
public function get_customers_leaderboard( $per_page, $after, $before, $persisted_query ) {
|
||||
$customers_data_store = new WC_Admin_Reports_Customers_Data_Store();
|
||||
$customers_data = $customers_data_store->get_data(
|
||||
$customers_data = $per_page > 0 ? $customers_data_store->get_data(
|
||||
array(
|
||||
'orderby' => 'total_spend',
|
||||
'order' => 'desc',
|
||||
'per_page' => $per_page,
|
||||
)
|
||||
);
|
||||
)->data : array();
|
||||
|
||||
$rows = array();
|
||||
foreach ( $customers_data->data as $customer ) {
|
||||
foreach ( $customers_data as $customer ) {
|
||||
$url_query = wp_parse_args(
|
||||
array(
|
||||
'filter' => 'single_customer',
|
||||
|
@ -253,7 +266,7 @@ class WC_Admin_REST_Leaderboards_Controller extends WC_REST_Data_Controller {
|
|||
*/
|
||||
public function get_products_leaderboard( $per_page, $after, $before, $persisted_query ) {
|
||||
$products_data_store = new WC_Admin_Reports_Products_Data_Store();
|
||||
$products_data = $products_data_store->get_data(
|
||||
$products_data = $per_page > 0 ? $products_data_store->get_data(
|
||||
array(
|
||||
'orderby' => 'items_sold',
|
||||
'order' => 'desc',
|
||||
|
@ -262,10 +275,10 @@ class WC_Admin_REST_Leaderboards_Controller extends WC_REST_Data_Controller {
|
|||
'per_page' => $per_page,
|
||||
'extended_info' => true,
|
||||
)
|
||||
);
|
||||
)->data : array();
|
||||
|
||||
$rows = array();
|
||||
foreach ( $products_data->data as $product ) {
|
||||
foreach ( $products_data as $product ) {
|
||||
$url_query = wp_parse_args(
|
||||
array(
|
||||
'filter' => 'single_product',
|
||||
|
@ -351,6 +364,39 @@ class WC_Admin_REST_Leaderboards_Controller extends WC_REST_Data_Controller {
|
|||
return rest_ensure_response( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of allowed leaderboards.
|
||||
*
|
||||
* @param WP_REST_Request $request Request data.
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_allowed_items( $request ) {
|
||||
$leaderboards = $this->get_leaderboards( 0, null, null, null );
|
||||
|
||||
$data = array();
|
||||
foreach ( $leaderboards as $leaderboard ) {
|
||||
$data[] = (object) array(
|
||||
'id' => $leaderboard['id'],
|
||||
'label' => $leaderboard['label'],
|
||||
'headers' => $leaderboard['headers'],
|
||||
);
|
||||
}
|
||||
|
||||
$objects = array();
|
||||
foreach ( $data as $item ) {
|
||||
$prepared = $this->prepare_item_for_response( $item, $request );
|
||||
$objects[] = $this->prepare_response_for_collection( $prepared );
|
||||
}
|
||||
|
||||
$response = rest_ensure_response( $objects );
|
||||
$response->header( 'X-WP-Total', count( $data ) );
|
||||
$response->header( 'X-WP-TotalPages', 1 );
|
||||
|
||||
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data object for response.
|
||||
*
|
||||
|
|
|
@ -154,6 +154,7 @@ function wc_admin_print_script_settings() {
|
|||
$preload_data_endpoints = array(
|
||||
'countries' => '/wc/v4/data/countries',
|
||||
'performanceIndicators' => '/wc/v4/reports/performance-indicators/allowed',
|
||||
'leaderboards' => '/wc/v4/leaderboards/allowed',
|
||||
);
|
||||
|
||||
if ( function_exists( 'gutenberg_preload_api_request' ) ) {
|
||||
|
|
Loading…
Reference in New Issue