* 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 PropTypes from 'prop-types';
import { withSelect } from '@wordpress/data';
import { map, find } from 'lodash';
import { find, get, map } from 'lodash';
/**
* Internal dependencies
@ -203,7 +203,8 @@ class OrdersReport extends Component {
render() {
const {
isRequesting,
isTableDataError,
isTableDataRequesting,
orders,
path,
query,
@ -212,9 +213,14 @@ class OrdersReport extends Component {
summaryNumbers,
} = this.props;
if ( primaryData.isError || secondaryData.isError || summaryNumbers.isError ) {
if (
primaryData.isError ||
secondaryData.isError ||
isTableDataError ||
summaryNumbers.isError
) {
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' );
actionLabel = __( 'Reload', 'wc-admin' );
actionCallback = () => {
@ -250,7 +256,16 @@ class OrdersReport extends Component {
/>
{ this.renderChartSummaryNumbers() }
{ 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>
);
}
@ -264,9 +279,6 @@ OrdersReport.propTypes = {
export default compose(
withSelect( ( select, props ) => {
const { getOrders } = select( 'wc-admin' );
const orders = getOrders();
const isRequesting = select( 'core/data' ).isResolving( 'wc-admin', 'getOrders' );
const { query } = props;
const interval = getIntervalForQuery( query );
const datesFromQuery = getCurrentDates( query );
@ -304,8 +316,24 @@ export default compose(
},
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 {
isRequesting,
isTableDataError,
isTableDataRequesting,
orders,
primaryData,
secondaryData,

View File

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

View File

@ -1,42 +1,17 @@
/** @format */
/**
* External dependencies
*/
import { dispatch } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch';
export default {
setOrders( orders ) {
setOrders( orders, query ) {
return {
type: 'SET_ORDERS',
orders,
query: query || {},
};
},
updateOrder( order ) {
setOrdersError( query ) {
return {
type: 'UPDATE_ORDER',
order,
};
},
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 );
}
}
type: 'SET_ORDERS_ERROR',
query: query || {},
};
},
};

View File

@ -1,29 +1,31 @@
/** @format */
const DEFAULT_STATE = {
orders: {},
ids: [],
};
/**
* External dependencies
*/
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 ) {
const queryKey = getJsonString( action.query );
switch ( action.type ) {
case 'SET_ORDERS':
const { orders } = action;
const ordersMap = orders.reduce( ( map, order ) => {
map[ order.id ] = order;
return map;
}, {} );
return {
...state,
orders: Object.assign( {}, state.orders, ordersMap ),
};
case 'UPDATE_ORDER':
const updatedOrders = { ...state.orders };
updatedOrders[ action.order.id ] = action.order;
return {
...state,
orders: updatedOrders,
};
return merge( {}, state, {
[ queryKey ]: action.orders,
} );
case 'SET_ORDERS_ERROR':
return merge( {}, state, {
[ queryKey ]: ERROR,
} );
}
return state;

View File

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

View File

@ -1,7 +1,49 @@
/** @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 {
getOrders( state ) {
return state.orders.orders;
getOrders,
/**
* 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 {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
*/
isReportStatsError( state, endpoint, query ) {