Merge Commit

This commit is contained in:
Jonathan Belcher 2018-10-11 11:43:25 -04:00
commit 0a562e6e37
15 changed files with 332 additions and 83 deletions

View File

@ -5,16 +5,13 @@
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { Component, Fragment } from '@wordpress/element'; import { Component, Fragment } from '@wordpress/element';
import { compose } from '@wordpress/compose'; import { compose } from '@wordpress/compose';
import { Button } from '@wordpress/components'; import { withSelect } from '@wordpress/data';
import { withSelect, withDispatch } from '@wordpress/data'; import { map } from 'lodash';
import moment from 'moment';
import { map, partial } from 'lodash';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { import {
Card,
ReportFilters, ReportFilters,
SummaryList, SummaryList,
SummaryListPlaceholder, SummaryListPlaceholder,
@ -26,6 +23,7 @@ import { getNewPath } from 'lib/nav-utils';
import { getReportChartData } from 'store/reports/utils'; import { getReportChartData } from 'store/reports/utils';
import { getCurrentDates, getDateParamsFromQuery, getIntervalForQuery } from 'lib/date'; import { getCurrentDates, getDateParamsFromQuery, getIntervalForQuery } from 'lib/date';
import { MAX_PER_PAGE } from 'store/constants'; import { MAX_PER_PAGE } from 'store/constants';
import OrdersReportTable from './table';
class OrdersReport extends Component { class OrdersReport extends Component {
constructor( props ) { constructor( props ) {
@ -35,8 +33,6 @@ class OrdersReport extends Component {
primaryTotals: null, primaryTotals: null,
secondaryTotals: null, secondaryTotals: null,
}; };
this.toggleStatus = this.toggleStatus.bind( this );
} }
getCharts() { getCharts() {
@ -76,14 +72,6 @@ class OrdersReport extends Component {
return charts[ 0 ]; return charts[ 0 ];
} }
toggleStatus( order ) {
const { requestUpdateOrder } = this.props;
const updatedOrder = { ...order };
const status = updatedOrder.status === 'completed' ? 'processing' : 'completed';
updatedOrder.status = status;
requestUpdateOrder( updatedOrder );
}
renderChartSummaryNumbers() { renderChartSummaryNumbers() {
const selectedChart = this.getSelectedChart(); const selectedChart = this.getSelectedChart();
const charts = this.getCharts(); const charts = this.getCharts();
@ -109,7 +97,6 @@ class OrdersReport extends Component {
} }
switch ( type ) { switch ( type ) {
// TODO: implement other format handlers
case 'currency': case 'currency':
value = formatCurrency( value ); value = formatCurrency( value );
secondaryValue = secondaryValue && formatCurrency( secondaryValue ); secondaryValue = secondaryValue && formatCurrency( secondaryValue );
@ -144,7 +131,8 @@ class OrdersReport extends Component {
} }
render() { render() {
const { orders, orderIds, query, path } = this.props; const { isRequesting, orders, path, query } = this.props;
return ( return (
<Fragment> <Fragment>
<ReportFilters <ReportFilters
@ -154,39 +142,7 @@ class OrdersReport extends Component {
advancedConfig={ advancedFilterConfig } advancedConfig={ advancedFilterConfig }
/> />
{ this.renderChartSummaryNumbers() } { this.renderChartSummaryNumbers() }
<p>Below is a temporary example</p> <OrdersReportTable isRequesting={ isRequesting } orders={ orders } query={ query } />
<Card title="Orders">
<table style={ { width: '100%' } }>
<thead>
<tr style={ { textAlign: 'left' } }>
<th>Id</th>
<th>Date</th>
<th>Total</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{ orderIds &&
orderIds.map( id => {
const order = orders[ id ];
return (
<tr key={ id }>
<td>{ id }</td>
<td>{ moment( order.date_created ).format( 'LL' ) }</td>
<td>{ order.total }</td>
<td>{ order.status }</td>
<td>
<Button isPrimary onClick={ partial( this.toggleStatus, order ) }>
Toggle Status
</Button>
</td>
</tr>
);
} ) }
</tbody>
</table>
</Card>
</Fragment> </Fragment>
); );
} }
@ -194,7 +150,9 @@ class OrdersReport extends Component {
export default compose( export default compose(
withSelect( ( select, props ) => { withSelect( ( select, props ) => {
const { getOrders, getOrderIds } = select( 'wc-admin' ); 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 );
@ -224,17 +182,10 @@ export default compose(
select select
); );
return { return {
orders: getOrders(), isRequesting,
orderIds: getOrderIds(), orders,
primaryData, primaryData,
secondaryData, secondaryData,
}; };
} ),
withDispatch( dispatch => {
return {
requestUpdateOrder: function( updatedOrder ) {
dispatch( 'wc-admin' ).requestUpdateOrder( updatedOrder );
},
};
} ) } )
)( OrdersReport ); )( OrdersReport );

View File

@ -0,0 +1,265 @@
/** @format */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component, Fragment } from '@wordpress/element';
import { format as formatDate } from '@wordpress/date';
import { map, orderBy } from 'lodash';
/**
* Internal dependencies
*/
import { Card, OrderStatus, TableCard, TablePlaceholder } from '@woocommerce/components';
import { downloadCSVFile, generateCSVDataFromTable, generateCSVFileName } from 'lib/csv';
import { formatCurrency, getCurrencyFormatDecimal } from 'lib/currency';
import { getIntervalForQuery, getDateFormatsForInterval } from 'lib/date';
import { getAdminLink, onQueryChange } from 'lib/nav-utils';
export default class OrdersReportTable extends Component {
constructor( props ) {
super( props );
}
onDownload( headers, rows, query ) {
// @TODO The current implementation only downloads the contents displayed in the table.
// Another solution is required when the data set is larger (see #311).
return () => {
downloadCSVFile(
generateCSVFileName( 'orders', query ),
generateCSVDataFromTable( headers, rows )
);
};
}
getHeadersContent() {
return [
{
label: __( 'Date', 'wc-admin' ),
key: 'date_created',
required: true,
defaultSort: true,
isLeftAligned: true,
isSortable: true,
},
{
label: __( 'Order #', 'wc-admin' ),
key: 'id',
required: true,
isLeftAligned: true,
isSortable: true,
},
{
label: __( 'Status', 'wc-admin' ),
key: 'status',
required: false,
isSortable: true,
},
{
label: __( 'Customer', 'wc-admin' ),
key: 'customer_id',
required: false,
isSortable: true,
},
{
label: __( 'Product(s)', 'wc-admin' ),
key: 'products',
required: false,
isSortable: false,
},
{
label: __( 'Items Sold', 'wc-admin' ),
key: 'items_sold',
required: false,
isSortable: true,
isNumeric: true,
},
{
label: __( 'Coupon(s)', 'wc-admin' ),
key: 'coupons',
required: false,
isSortable: false,
},
{
label: __( 'N. Revenue', 'wc-admin' ),
key: 'net_revenue',
required: true,
isSortable: true,
isNumeric: true,
},
];
}
formatTableData( data ) {
return map( data, row => {
const {
date_created,
id,
status,
customer_id,
line_items,
coupon_lines,
currency,
total,
total_tax,
shipping_total,
discount_total,
} = row;
return {
date_created,
id,
status,
customer_id,
line_items,
items_sold: line_items.reduce( ( acc, item ) => item.quantity + acc, 0 ),
coupon_lines,
currency,
net_revenue: getCurrencyFormatDecimal(
total - total_tax - shipping_total - discount_total
),
};
} );
}
getRowsContent( tableData ) {
const { query } = this.props;
const currentInterval = getIntervalForQuery( query );
const { tableFormat } = getDateFormatsForInterval( currentInterval );
return map( tableData, row => {
const {
date_created,
id,
status,
customer_id,
line_items,
items_sold,
coupon_lines,
currency,
net_revenue,
} = row;
return [
{
display: formatDate( tableFormat, date_created ),
value: date_created,
},
{
display: <a href={ getAdminLink( 'post.php?post=' + id + '&action=edit' ) }>{ id }</a>,
value: id,
},
{
display: <OrderStatus order={ { status } } />,
value: status,
},
{
// @TODO This should display customer type (new/returning) once it's
// implemented in the API.
display: customer_id,
value: customer_id,
},
{
display: this.renderList(
line_items.map( item => ( {
href: getAdminLink( 'post.php?post=' + item.product_id + '&action=edit' ),
label: item.name,
} ) )
),
value: line_items
.map( item => item.name )
.join()
.toLowerCase(),
},
{
display: items_sold,
value: items_sold,
},
{
display: this.renderList(
coupon_lines.map( coupon => ( {
// @TODO It should link to the coupons report.
href: getAdminLink( 'edit.php?s=' + coupon.code + '&post_type=shop_coupon' ),
label: coupon.code,
} ) )
),
value: coupon_lines
.map( item => item.code )
.join()
.toLowerCase(),
},
{
display: formatCurrency( net_revenue, currency ),
value: net_revenue,
},
];
} );
}
renderList( items ) {
// @TODO Use ViewMore component if there are many items.
return items.map( ( item, i ) => (
<Fragment key={ i }>
{ i > 0 ? ', ' : null }
<a className={ items.length > 1 ? 'is-inline' : null } href={ item.href }>
{ item.label }
</a>
</Fragment>
) );
}
renderPlaceholderTable() {
const headers = this.getHeadersContent();
return (
<Card
title={ __( 'Orders', 'wc-admin' ) }
className="woocommerce-analytics__table-placeholder"
>
<TablePlaceholder caption={ __( 'Orders last week', 'wc-admin' ) } headers={ headers } />
</Card>
);
}
renderTable() {
const { orders, query } = 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 )
);
const headers = this.getHeadersContent();
const tableQuery = {
...query,
orderby: query.orderby || 'date_created',
order: query.order || 'asc',
};
return (
<TableCard
title={ __( 'Orders last week', 'wc-admin' ) }
rows={ rows }
totalRows={ Object.keys( orders ).length }
rowsPerPage={ rowsPerPage }
headers={ headers }
onClickDownload={ this.onDownload( headers, rows, tableQuery ) }
onQueryChange={ onQueryChange }
query={ tableQuery }
summary={ null }
/>
);
}
render() {
const { isRequesting } = this.props;
return isRequesting ? this.renderPlaceholderTable() : this.renderTable();
}
}

View File

@ -24,6 +24,7 @@ export default class extends Component {
label: __( 'Product Title', 'wc-admin' ), label: __( 'Product Title', 'wc-admin' ),
key: 'name', key: 'name',
required: true, required: true,
isLeftAligned: true,
isSortable: true, isSortable: true,
}, },
{ {

View File

@ -93,6 +93,7 @@ export class RevenueReport extends Component {
key: 'date', key: 'date',
required: true, required: true,
defaultSort: true, defaultSort: true,
isLeftAligned: true,
isSortable: true, isSortable: true,
}, },
{ {
@ -100,6 +101,7 @@ export class RevenueReport extends Component {
key: 'orders_count', key: 'orders_count',
required: false, required: false,
isSortable: true, isSortable: true,
isNumeric: true,
}, },
{ {
label: __( 'Gross Revenue', 'wc-admin' ), label: __( 'Gross Revenue', 'wc-admin' ),

View File

@ -62,7 +62,7 @@
} }
&:focus { &:focus {
outline: 2px solid lightseagreen; box-shadow: inset 0 -1px 0 $button-focus-inner, 0 0 0 2px $button-focus-outer;
} }
} }

View File

@ -58,9 +58,11 @@
text-align: left; text-align: left;
a { a {
&:not(.is-inline) {
display: block; display: block;
margin: ($gap*-1) ($gap-large*-1); margin: ($gap*-1) ($gap-large*-1);
padding: $gap $gap-large; padding: $gap $gap-large;
}
&:hover, &:hover,
&:focus { &:focus {
@ -80,21 +82,28 @@
width: 80%; width: 80%;
} }
&.is-numeric { &:not(.is-left-aligned) {
text-align: right; text-align: right;
.rtl & {
text-align: left;
}
button { button {
justify-content: flex-end; justify-content: flex-end;
} }
.is-placeholder {
max-width: 40px;
} }
&.is-numeric .is-placeholder {
max-width: 40px;
} }
} }
.woocommerce-table__header,
th.woocommerce-table__item { th.woocommerce-table__item {
font-weight: normal;
}
.woocommerce-table__header {
font-weight: bold; font-weight: bold;
white-space: nowrap; white-space: nowrap;
} }
@ -106,6 +115,11 @@ th.woocommerce-table__item {
& + .woocommerce-table__header { & + .woocommerce-table__header {
border-left: 1px solid $core-grey-light-700; border-left: 1px solid $core-grey-light-700;
.rtl & {
border-left: 0;
border-right: 1px solid $core-grey-light-700;
}
} }
.components-button.is-button { .components-button.is-button {
@ -118,6 +132,10 @@ th.woocommerce-table__item {
background: transparent; background: transparent;
box-shadow: none !important; box-shadow: none !important;
.rtl & {
padding: $gap-smaller 0 $gap-smaller $gap-large;
}
// @todo Add interactive styles // @todo Add interactive styles
&:hover { &:hover {
box-shadow: none !important; box-shadow: none !important;

View File

@ -120,10 +120,11 @@ class Table extends Component {
<tbody> <tbody>
<tr> <tr>
{ headers.map( ( header, i ) => { { headers.map( ( header, i ) => {
const { isSortable, isNumeric, key, label } = header; const { isLeftAligned, isSortable, isNumeric, key, label } = header;
const labelId = `header-${ instanceId } -${ i }`; const labelId = `header-${ instanceId } -${ i }`;
const thProps = { const thProps = {
className: classnames( 'woocommerce-table__header', { className: classnames( 'woocommerce-table__header', {
'is-left-aligned': isLeftAligned,
'is-sortable': isSortable, 'is-sortable': isSortable,
'is-sorted': sortedBy === key, 'is-sorted': sortedBy === key,
'is-numeric': isNumeric, 'is-numeric': isNumeric,
@ -173,10 +174,11 @@ class Table extends Component {
{ rows.map( ( row, i ) => ( { rows.map( ( row, i ) => (
<tr key={ i }> <tr key={ i }>
{ row.map( ( cell, j ) => { { row.map( ( cell, j ) => {
const { isNumeric } = headers[ j ]; const { isLeftAligned, isNumeric } = headers[ j ];
const isHeader = rowHeader === j; const isHeader = rowHeader === j;
const Cell = isHeader ? 'th' : 'td'; const Cell = isHeader ? 'th' : 'td';
const cellClasses = classnames( 'woocommerce-table__item', { const cellClasses = classnames( 'woocommerce-table__item', {
'is-left-aligned': isLeftAligned,
'is-numeric': isNumeric, 'is-numeric': isNumeric,
} ); } );
return ( return (
@ -217,6 +219,10 @@ Table.propTypes = {
* Boolean, true if this column is the default for sorting. Only one column should have this set. * Boolean, true if this column is the default for sorting. Only one column should have this set.
*/ */
defaultSort: PropTypes.bool, defaultSort: PropTypes.bool,
/**
* Boolean, true if this column should be aligned to the left.
*/
isLeftAligned: PropTypes.bool,
/** /**
* Boolean, true if this column is a number value. * Boolean, true if this column is a number value.
*/ */

View File

@ -25,6 +25,7 @@ export class TopSellingProducts extends Component {
label: __( 'Product', 'wc-admin' ), label: __( 'Product', 'wc-admin' ),
key: 'product', key: 'product',
required: true, required: true,
isLeftAligned: true,
isSortable: false, isSortable: false,
}, },
{ {

View File

@ -1,4 +1,4 @@
/** @format */ /** @format */
export default `Date,Orders,Gross Revenue,Refunds,Coupons,Taxes,Shipping,Net Revenue export default `Date,Orders,Description,Gross Revenue,Refunds,Coupons,Taxes,Shipping,Net Revenue
2018-04-29T00:00:00,30,200,19,19,100,19,200`; 2018-04-29T00:00:00,30,lorem ipsum,200,19,19,100,19,200`;

View File

@ -9,6 +9,10 @@ export default [
label: 'Orders', label: 'Orders',
key: 'orders_count', key: 'orders_count',
}, },
{
label: 'Description',
key: 'description',
},
{ {
label: 'Gross Revenue', label: 'Gross Revenue',
key: 'gross_revenue', key: 'gross_revenue',

View File

@ -10,6 +10,10 @@ export default [
display: 'Product 30', display: 'Product 30',
value: '30', value: '30',
}, },
{
display: 'Lorem, ipsum',
value: 'lorem, ipsum',
},
{ {
display: '€200.00', display: '€200.00',
value: 200, value: 200,

View File

@ -11,7 +11,11 @@ function getCSVHeaders( headers ) {
function getCSVRows( rows ) { function getCSVRows( rows ) {
return Array.isArray( rows ) return Array.isArray( rows )
? rows.map( row => row.map( rowItem => rowItem.value ).join( ',' ) ).join( '\n' ) ? rows
.map( row =>
row.map( rowItem => rowItem.value.toString().replace( /,/g, '' ) ).join( ',' )
)
.join( '\n' )
: []; : [];
} }

View File

@ -1,8 +1,4 @@
/** @format */ /** @format */
/**
* External dependencies
*/
import { union } from 'lodash';
const DEFAULT_STATE = { const DEFAULT_STATE = {
orders: {}, orders: {},
@ -13,7 +9,6 @@ export default function ordersReducer( state = DEFAULT_STATE, action ) {
switch ( action.type ) { switch ( action.type ) {
case 'SET_ORDERS': case 'SET_ORDERS':
const { orders } = action; const { orders } = action;
const ids = orders.map( order => order.id );
const ordersMap = orders.reduce( ( map, order ) => { const ordersMap = orders.reduce( ( map, order ) => {
map[ order.id ] = order; map[ order.id ] = order;
return map; return map;
@ -21,7 +16,6 @@ export default function ordersReducer( state = DEFAULT_STATE, action ) {
return { return {
...state, ...state,
orders: Object.assign( {}, state.orders, ordersMap ), orders: Object.assign( {}, state.orders, ordersMap ),
ids: union( state.ids, ids ),
}; };
case 'UPDATE_ORDER': case 'UPDATE_ORDER':
const updatedOrders = { ...state.orders }; const updatedOrders = { ...state.orders };

View File

@ -4,7 +4,4 @@ export default {
getOrders( state ) { getOrders( state ) {
return state.orders.orders; return state.orders.orders;
}, },
getOrderIds( state ) {
return state.orders.ids;
},
}; };

View File

@ -46,6 +46,8 @@ $woocommerce: $woocommerce-500;
// Gutenberg // Gutenberg
$button-hover: #fafafa; $button-hover: #fafafa;
$button-focus-inner: #00435d;
$button-focus-outer: #bfe7f3;
// wp-admin // wp-admin
$wp-admin-background: #f1f1f1; $wp-admin-background: #f1f1f1;