* Send params with Orders table API calls

* Add onError case and caching for Orders calls

* Remove unused actions

* Load only 'processing', 'on-hold' and 'completed' orders

* Use NAMESPACE constant instead of hard-coded value

* Comment typos

* Add tests to Orders reducer, resolvers and selectors

* Typos

* Fix JSDoc mismatch
This commit is contained in:
Albert Juhé Lluveras 2018-10-16 10:50:07 +02:00 committed by GitHub
parent 43037baff2
commit ae6652b26c
10 changed files with 348 additions and 90 deletions

View File

@ -8,7 +8,7 @@ import { compose } from '@wordpress/compose';
import { format as formatDate } from '@wordpress/date'; import { format as formatDate } from '@wordpress/date';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withSelect } from '@wordpress/data'; import { withSelect } from '@wordpress/data';
import { map, find } from 'lodash'; import { find, get, map } from 'lodash';
/** /**
* Internal dependencies * Internal dependencies
@ -203,7 +203,8 @@ class OrdersReport extends Component {
render() { render() {
const { const {
isRequesting, isTableDataError,
isTableDataRequesting,
orders, orders,
path, path,
query, query,
@ -212,9 +213,14 @@ class OrdersReport extends Component {
summaryNumbers, summaryNumbers,
} = this.props; } = this.props;
if ( primaryData.isError || secondaryData.isError || summaryNumbers.isError ) { if (
primaryData.isError ||
secondaryData.isError ||
isTableDataError ||
summaryNumbers.isError
) {
let title, actionLabel, actionURL, actionCallback; let title, actionLabel, actionURL, actionCallback;
if ( primaryData.isError || secondaryData.isError ) { if ( primaryData.isError || secondaryData.isError || isTableDataError ) {
title = __( 'There was an error getting your stats. Please try again.', 'wc-admin' ); title = __( 'There was an error getting your stats. Please try again.', 'wc-admin' );
actionLabel = __( 'Reload', 'wc-admin' ); actionLabel = __( 'Reload', 'wc-admin' );
actionCallback = () => { actionCallback = () => {
@ -250,7 +256,16 @@ class OrdersReport extends Component {
/> />
{ this.renderChartSummaryNumbers() } { this.renderChartSummaryNumbers() }
{ this.renderChart() } { this.renderChart() }
<OrdersReportTable isRequesting={ isRequesting } orders={ orders } query={ query } /> <OrdersReportTable
isRequesting={ isTableDataRequesting }
orders={ orders }
query={ query }
totalRows={ get(
primaryData,
[ 'data', 'totals', 'orders_count' ],
Object.keys( orders ).length
) }
/>
</Fragment> </Fragment>
); );
} }
@ -264,9 +279,6 @@ OrdersReport.propTypes = {
export default compose( export default compose(
withSelect( ( select, props ) => { withSelect( ( select, props ) => {
const { getOrders } = select( 'wc-admin' );
const orders = getOrders();
const isRequesting = select( 'core/data' ).isResolving( 'wc-admin', 'getOrders' );
const { query } = props; const { query } = props;
const interval = getIntervalForQuery( query ); const interval = getIntervalForQuery( query );
const datesFromQuery = getCurrentDates( query ); const datesFromQuery = getCurrentDates( query );
@ -304,8 +316,24 @@ export default compose(
}, },
select select
); );
const { getOrders, isGetOrdersError, isGetOrdersRequesting } = select( 'wc-admin' );
const tableQuery = {
orderby: query.orderby || 'date',
order: query.order || 'asc',
page: query.page || 1,
per_page: query.per_page || 25,
after: datesFromQuery.primary.after + 'T00:00:00+00:00',
before: datesFromQuery.primary.before + 'T23:59:59+00:00',
status: [ 'processing', 'on-hold', 'completed' ],
};
const orders = getOrders( tableQuery );
const isTableDataError = isGetOrdersError( tableQuery );
const isTableDataRequesting = isGetOrdersRequesting( tableQuery );
return { return {
isRequesting, isTableDataError,
isTableDataRequesting,
orders, orders,
primaryData, primaryData,
secondaryData, secondaryData,

View File

@ -43,7 +43,7 @@ export default class OrdersReportTable extends Component {
return [ return [
{ {
label: __( 'Date', 'wc-admin' ), label: __( 'Date', 'wc-admin' ),
key: 'date_created', key: 'date',
required: true, required: true,
defaultSort: true, defaultSort: true,
isLeftAligned: true, isLeftAligned: true,
@ -60,13 +60,13 @@ export default class OrdersReportTable extends Component {
label: __( 'Status', 'wc-admin' ), label: __( 'Status', 'wc-admin' ),
key: 'status', key: 'status',
required: false, required: false,
isSortable: true, isSortable: false,
}, },
{ {
label: __( 'Customer', 'wc-admin' ), label: __( 'Customer', 'wc-admin' ),
key: 'customer_id', key: 'customer_id',
required: false, required: false,
isSortable: true, isSortable: false,
}, },
{ {
label: __( 'Product(s)', 'wc-admin' ), label: __( 'Product(s)', 'wc-admin' ),
@ -78,7 +78,7 @@ export default class OrdersReportTable extends Component {
label: __( 'Items Sold', 'wc-admin' ), label: __( 'Items Sold', 'wc-admin' ),
key: 'items_sold', key: 'items_sold',
required: false, required: false,
isSortable: true, isSortable: false,
isNumeric: true, isNumeric: true,
}, },
{ {
@ -91,7 +91,7 @@ export default class OrdersReportTable extends Component {
label: __( 'N. Revenue', 'wc-admin' ), label: __( 'N. Revenue', 'wc-admin' ),
key: 'net_revenue', key: 'net_revenue',
required: true, required: true,
isSortable: true, isSortable: false,
isNumeric: true, isNumeric: true,
}, },
]; ];
@ -114,7 +114,7 @@ export default class OrdersReportTable extends Component {
} = row; } = row;
return { return {
date_created, date: date_created,
id, id,
status, status,
customer_id, customer_id,
@ -136,7 +136,7 @@ export default class OrdersReportTable extends Component {
return map( tableData, row => { return map( tableData, row => {
const { const {
date_created, date,
id, id,
status, status,
customer_id, customer_id,
@ -163,8 +163,8 @@ export default class OrdersReportTable extends Component {
return [ return [
{ {
display: formatDate( tableFormat, date_created ), display: formatDate( tableFormat, date ),
value: date_created, value: date,
}, },
{ {
display: <a href={ getAdminLink( 'post.php?post=' + id + '&action=edit' ) }>{ id }</a>, display: <a href={ getAdminLink( 'post.php?post=' + id + '&action=edit' ) }>{ id }</a>,
@ -231,37 +231,32 @@ export default class OrdersReportTable extends Component {
title={ __( 'Orders', 'wc-admin' ) } title={ __( 'Orders', 'wc-admin' ) }
className="woocommerce-analytics__table-placeholder" className="woocommerce-analytics__table-placeholder"
> >
<TablePlaceholder caption={ __( 'Orders last week', 'wc-admin' ) } headers={ headers } /> <TablePlaceholder caption={ __( 'Orders', 'wc-admin' ) } headers={ headers } />
</Card> </Card>
); );
} }
renderTable() { renderTable() {
const { orders, query } = this.props; const { orders, query, totalRows } = this.props;
const page = parseInt( query.page ) || 1;
const rowsPerPage = parseInt( query.per_page ) || 25; const rowsPerPage = parseInt( query.per_page ) || 25;
const rows = this.getRowsContent( const rows = this.getRowsContent(
orderBy( orderBy( this.formatTableData( orders ), query.orderby || 'date', query.order || 'asc' )
this.formatTableData( orders ),
query.orderby || 'date_created',
query.order || 'asc'
).slice( ( page - 1 ) * rowsPerPage, page * rowsPerPage )
); );
const headers = this.getHeadersContent(); const headers = this.getHeadersContent();
const tableQuery = { const tableQuery = {
...query, ...query,
orderby: query.orderby || 'date_created', orderby: query.orderby || 'date',
order: query.order || 'asc', order: query.order || 'asc',
}; };
return ( return (
<TableCard <TableCard
title={ __( 'Orders last week', 'wc-admin' ) } title={ __( 'Orders', 'wc-admin' ) }
rows={ rows } rows={ rows }
totalRows={ Object.keys( orders ).length } totalRows={ totalRows }
rowsPerPage={ rowsPerPage } rowsPerPage={ rowsPerPage }
headers={ headers } headers={ headers }
onClickDownload={ this.onDownload( headers, rows, tableQuery ) } onClickDownload={ this.onDownload( headers, rows, tableQuery ) }

View File

@ -1,42 +1,17 @@
/** @format */ /** @format */
/**
* External dependencies
*/
import { dispatch } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch';
export default { export default {
setOrders( orders ) { setOrders( orders, query ) {
return { return {
type: 'SET_ORDERS', type: 'SET_ORDERS',
orders, orders,
query: query || {},
}; };
}, },
updateOrder( order ) { setOrdersError( query ) {
return { return {
type: 'UPDATE_ORDER', type: 'SET_ORDERS_ERROR',
order, query: query || {},
};
},
requestUpdateOrder( order ) {
return async () => {
// Lets be optimistic
dispatch( 'wc-admin' ).updateOrder( order );
try {
const updatedOrder = await apiFetch( {
path: '/wc/v3/orders/' + order.id,
method: 'PUT',
data: order,
} );
dispatch( 'wc-admin' ).updateOrder( updatedOrder );
} catch ( error ) {
if ( error && error.responseJSON ) {
alert( error.responseJSON.message );
} else {
alert( error );
}
}
}; };
}, },
}; };

View File

@ -1,29 +1,31 @@
/** @format */ /** @format */
const DEFAULT_STATE = { /**
orders: {}, * External dependencies
ids: [], */
}; import { merge } from 'lodash';
/**
* Internal dependencies
*/
import { ERROR } from 'store/constants';
import { getJsonString } from 'store/utils';
const DEFAULT_STATE = {};
export default function ordersReducer( state = DEFAULT_STATE, action ) { export default function ordersReducer( state = DEFAULT_STATE, action ) {
const queryKey = getJsonString( action.query );
switch ( action.type ) { switch ( action.type ) {
case 'SET_ORDERS': case 'SET_ORDERS':
const { orders } = action; return merge( {}, state, {
const ordersMap = orders.reduce( ( map, order ) => { [ queryKey ]: action.orders,
map[ order.id ] = order; } );
return map;
}, {} ); case 'SET_ORDERS_ERROR':
return { return merge( {}, state, {
...state, [ queryKey ]: ERROR,
orders: Object.assign( {}, state.orders, ordersMap ), } );
};
case 'UPDATE_ORDER':
const updatedOrders = { ...state.orders };
updatedOrders[ action.order.id ] = action.order;
return {
...state,
orders: updatedOrders,
};
} }
return state; return state;

View File

@ -5,17 +5,19 @@
import { dispatch } from '@wordpress/data'; import { dispatch } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch'; import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import { stringifyQuery } from 'lib/nav-utils';
import { NAMESPACE } from 'store/constants';
export default { export default {
async getOrders() { async getOrders( state, query ) {
try { try {
const orders = await apiFetch( { path: '/wc/v3/orders' } ); const orders = await apiFetch( { path: NAMESPACE + 'orders' + stringifyQuery( query ) } );
dispatch( 'wc-admin' ).setOrders( orders ); dispatch( 'wc-admin' ).setOrders( orders, query );
} catch ( error ) { } catch ( error ) {
if ( error && error.responseJSON ) { dispatch( 'wc-admin' ).setOrdersError( query );
alert( error.responseJSON.message );
} else {
alert( error );
}
} }
}, },
}; };

View File

@ -1,7 +1,49 @@
/** @format */ /** @format */
/**
* External dependencies
*/
import { get } from 'lodash';
import { select } from '@wordpress/data';
/**
* Internal dependencies
*/
import { getJsonString } from 'store/utils';
import { ERROR } from 'store/constants';
/**
* Returns orders for a specific query.
*
* @param {Object} state Current state
* @param {Object} query Report query paremters
* @return {Array} Report details
*/
function getOrders( state, query = {} ) {
return get( state, [ 'orders', getJsonString( query ) ], [] );
}
export default { export default {
getOrders( state ) { getOrders,
return state.orders.orders;
/**
* Returns true if a query is pending.
*
* @param {Object} state Current state
* @return {Boolean} True if the `getOrders` request is pending, false otherwise
*/
isGetOrdersRequesting( state, ...args ) {
return select( 'core/data' ).isResolving( 'wc-admin', 'getOrders', args );
},
/**
* Returns true if a get orders request has returned an error.
*
* @param {Object} state Current state
* @param {Object} query Query parameters
* @return {Boolean} True if the `getOrders` request has failed, false otherwise
*/
isGetOrdersError( state, query ) {
return ERROR === getOrders( state, query );
}, },
}; };

View File

@ -0,0 +1,80 @@
/**
* @format
*/
/**
* External dependencies
*/
import deepFreeze from 'deep-freeze';
/**
* Internal dependencies
*/
import { ERROR } from 'store/constants';
import ordersReducer from '../reducer';
import { getJsonString } from 'store/utils';
describe( 'ordersReducer()', () => {
it( 'returns an empty data object by default', () => {
const state = ordersReducer( undefined, {} );
expect( state ).toEqual( {} );
} );
it( 'returns with received orders data', () => {
const originalState = deepFreeze( {} );
const query = {
orderby: 'date',
};
const orders = [ { id: 1214 }, { id: 1215 }, { id: 1216 } ];
const state = ordersReducer( originalState, {
type: 'SET_ORDERS',
query,
orders,
} );
const queryKey = getJsonString( query );
expect( state[ queryKey ] ).toEqual( orders );
} );
it( 'tracks multiple queries in orders data', () => {
const otherQuery = {
orderby: 'id',
};
const otherQueryKey = getJsonString( otherQuery );
const otherOrders = [ { id: 1 }, { id: 2 }, { id: 3 } ];
const otherQueryState = {
[ otherQueryKey ]: otherOrders,
};
const originalState = deepFreeze( otherQueryState );
const query = {
orderby: 'date',
};
const orders = [ { id: 1214 }, { id: 1215 }, { id: 1216 } ];
const state = ordersReducer( originalState, {
type: 'SET_ORDERS',
query,
orders,
} );
const queryKey = getJsonString( query );
expect( state[ queryKey ] ).toEqual( orders );
expect( state[ otherQueryKey ] ).toEqual( otherOrders );
} );
it( 'returns with received error data', () => {
const originalState = deepFreeze( {} );
const query = {
orderby: 'date',
};
const state = ordersReducer( originalState, {
type: 'SET_ORDERS_ERROR',
query,
} );
const queryKey = getJsonString( query );
expect( state[ queryKey ] ).toEqual( ERROR );
} );
} );

View File

@ -0,0 +1,42 @@
/*
* @format
*/
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import resolvers from '../resolvers';
const { getOrders } = resolvers;
jest.mock( '@wordpress/api-fetch', () => jest.fn() );
describe( 'getOrders', () => {
const ORDERS_1 = [ { id: 1214 }, { id: 1215 }, { id: 1216 } ];
const ORDERS_2 = [ { id: 1 }, { id: 2 }, { id: 3 } ];
beforeAll( () => {
apiFetch.mockImplementation( options => {
if ( options.path === '/wc/v3/orders' ) {
return Promise.resolve( ORDERS_1 );
}
if ( options.path === '/wc/v3/orders&orderby=id' ) {
return Promise.resolve( ORDERS_2 );
}
} );
} );
it( 'returns requested report data', async () => {
getOrders().then( data => expect( data ).toEqual( ORDERS_1 ) );
} );
it( 'returns requested report data for a specific query', async () => {
getOrders( { orderby: 'id' } ).then( data => expect( data ).toEqual( ORDERS_2 ) );
} );
} );

View File

@ -0,0 +1,92 @@
/*
* @format
*/
/**
* External dependencies
*/
import deepFreeze from 'deep-freeze';
/**
* Internal dependencies
*/
import { ERROR } from 'store/constants';
import selectors from '../selectors';
import { select } from '@wordpress/data';
import { getJsonString } from 'store/utils';
const { getOrders, isGetOrdersRequesting, isGetOrdersError } = selectors;
jest.mock( '@wordpress/data', () => ( {
...require.requireActual( '@wordpress/data' ),
select: jest.fn().mockReturnValue( {} ),
} ) );
const query = { orderby: 'date' };
const queryKey = getJsonString( query );
describe( 'getOrders()', () => {
it( 'returns an empty array when no orders are available', () => {
const state = deepFreeze( {} );
expect( getOrders( state, query ) ).toEqual( [] );
} );
it( 'returns stored orders for current query', () => {
const orders = [ { id: 1214 }, { id: 1215 }, { id: 1216 } ];
const state = deepFreeze( {
orders: {
[ queryKey ]: orders,
},
} );
expect( getOrders( state, query ) ).toEqual( orders );
} );
} );
describe( 'isGetOrdersRequesting()', () => {
beforeAll( () => {
select( 'core/data' ).isResolving = jest.fn().mockReturnValue( false );
} );
afterAll( () => {
select( 'core/data' ).isResolving.mockRestore();
} );
function setIsResolving( isResolving ) {
select( 'core/data' ).isResolving.mockImplementation(
( reducerKey, selectorName ) =>
isResolving && reducerKey === 'wc-admin' && selectorName === 'getOrders'
);
}
it( 'returns false if never requested', () => {
const result = isGetOrdersRequesting( query );
expect( result ).toBe( false );
} );
it( 'returns false if request finished', () => {
setIsResolving( false );
const result = isGetOrdersRequesting( query );
expect( result ).toBe( false );
} );
it( 'returns true if requesting', () => {
setIsResolving( true );
const result = isGetOrdersRequesting( query );
expect( result ).toBe( true );
} );
} );
describe( 'isGetOrdersError()', () => {
it( 'returns false by default', () => {
const state = deepFreeze( {} );
expect( isGetOrdersError( state, query ) ).toEqual( false );
} );
it( 'returns true if ERROR constant is found', () => {
const state = deepFreeze( {
orders: {
[ queryKey ]: ERROR,
},
} );
expect( isGetOrdersError( state, query ) ).toEqual( true );
} );
} );

View File

@ -43,7 +43,7 @@ export default {
* *
* @param {Object} state Current state * @param {Object} state Current state
* @param {String} endpoint Stats endpoint * @param {String} endpoint Stats endpoint
* @param {Object} query Report query paremters * @param {Object} query Report query parameters
* @return {Boolean} True if the `getReportStats` request has failed, false otherwise * @return {Boolean} True if the `getReportStats` request has failed, false otherwise
*/ */
isReportStatsError( state, endpoint, query ) { isReportStatsError( state, endpoint, query ) {