diff --git a/plugins/woocommerce-admin/client/analytics/report/orders/index.js b/plugins/woocommerce-admin/client/analytics/report/orders/index.js index 3e976f69d7d..3d11b985419 100644 --- a/plugins/woocommerce-admin/client/analytics/report/orders/index.js +++ b/plugins/woocommerce-admin/client/analytics/report/orders/index.js @@ -4,34 +4,23 @@ */ import { Component, Fragment } from '@wordpress/element'; import { compose } from '@wordpress/compose'; -import { Button } from '@wordpress/components'; -import { withSelect, withDispatch } from '@wordpress/data'; -import moment from 'moment'; -import { partial } from 'lodash'; +import { withSelect } from '@wordpress/data'; /** * Internal dependencies */ -import { Card, ReportFilters } from '@woocommerce/components'; +import { ReportFilters } from '@woocommerce/components'; import { filters, advancedFilterConfig } from './config'; +import OrdersReportTable from './table'; class OrdersReport extends Component { constructor( props ) { super( props ); - - this.toggleStatus = this.toggleStatus.bind( this ); - } - - toggleStatus( order ) { - const { requestUpdateOrder } = this.props; - const updatedOrder = { ...order }; - const status = updatedOrder.status === 'completed' ? 'processing' : 'completed'; - updatedOrder.status = status; - requestUpdateOrder( updatedOrder ); } render() { - const { orders, orderIds, query, path } = this.props; + const { isRequesting, orders, path, query } = this.props; + return ( -

Below is a temporary example

- - - - - - - - - - - - - { orderIds && - orderIds.map( id => { - const order = orders[ id ]; - return ( - - - - - - - - ); - } ) } - -
IdDateTotalStatusAction
{ id }{ moment( order.date_created ).format( 'LL' ) }{ order.total }{ order.status } - -
-
+
); } @@ -80,17 +37,9 @@ class OrdersReport extends Component { export default compose( withSelect( select => { - const { getOrders, getOrderIds } = select( 'wc-admin' ); - return { - orders: getOrders(), - orderIds: getOrderIds(), - }; - } ), - withDispatch( dispatch => { - return { - requestUpdateOrder: function( updatedOrder ) { - dispatch( 'wc-admin' ).requestUpdateOrder( updatedOrder ); - }, - }; + const { getOrders } = select( 'wc-admin' ); + const orders = getOrders(); + const isRequesting = select( 'core/data' ).isResolving( 'wc-admin', 'getOrders' ); + return { isRequesting, orders }; } ) )( OrdersReport ); diff --git a/plugins/woocommerce-admin/client/analytics/report/orders/table.js b/plugins/woocommerce-admin/client/analytics/report/orders/table.js new file mode 100644 index 00000000000..7440b82731b --- /dev/null +++ b/plugins/woocommerce-admin/client/analytics/report/orders/table.js @@ -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: { id }, + value: id, + }, + { + display: , + 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 ) => ( + + { i > 0 ? ', ' : null } + 1 ? 'is-inline' : null } href={ item.href }> + { item.label } + + + ) ); + } + + renderPlaceholderTable() { + const headers = this.getHeadersContent(); + + return ( + + + + ); + } + + 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 ( + + ); + } + + render() { + const { isRequesting } = this.props; + + return isRequesting ? this.renderPlaceholderTable() : this.renderTable(); + } +} diff --git a/plugins/woocommerce-admin/client/analytics/report/products/index.js b/plugins/woocommerce-admin/client/analytics/report/products/index.js index 913cd7a7202..1f656494d4f 100644 --- a/plugins/woocommerce-admin/client/analytics/report/products/index.js +++ b/plugins/woocommerce-admin/client/analytics/report/products/index.js @@ -24,6 +24,7 @@ export default class extends Component { label: __( 'Product Title', 'wc-admin' ), key: 'name', required: true, + isLeftAligned: true, isSortable: true, }, { diff --git a/plugins/woocommerce-admin/client/analytics/report/revenue/index.js b/plugins/woocommerce-admin/client/analytics/report/revenue/index.js index 1258801552a..8ffdd0395fe 100644 --- a/plugins/woocommerce-admin/client/analytics/report/revenue/index.js +++ b/plugins/woocommerce-admin/client/analytics/report/revenue/index.js @@ -93,6 +93,7 @@ export class RevenueReport extends Component { key: 'date', required: true, defaultSort: true, + isLeftAligned: true, isSortable: true, }, { @@ -100,6 +101,7 @@ export class RevenueReport extends Component { key: 'orders_count', required: false, isSortable: true, + isNumeric: true, }, { label: __( 'Gross Revenue', 'wc-admin' ), diff --git a/plugins/woocommerce-admin/client/components/table/style.scss b/plugins/woocommerce-admin/client/components/table/style.scss index 67416a99d06..eeb4cadd24c 100644 --- a/plugins/woocommerce-admin/client/components/table/style.scss +++ b/plugins/woocommerce-admin/client/components/table/style.scss @@ -58,9 +58,11 @@ text-align: left; a { - display: block; - margin: ($gap*-1) ($gap-large*-1); - padding: $gap $gap-large; + &:not(.is-inline) { + display: block; + margin: ($gap*-1) ($gap-large*-1); + padding: $gap $gap-large; + } &:hover, &:focus { @@ -80,21 +82,28 @@ width: 80%; } - &.is-numeric { + &:not(.is-left-aligned) { text-align: right; + .rtl & { + text-align: left; + } + button { justify-content: flex-end; } + } - .is-placeholder { - max-width: 40px; - } + &.is-numeric .is-placeholder { + max-width: 40px; } } -.woocommerce-table__header, th.woocommerce-table__item { + font-weight: normal; +} + +.woocommerce-table__header { font-weight: bold; white-space: nowrap; } @@ -106,6 +115,11 @@ th.woocommerce-table__item { & + .woocommerce-table__header { border-left: 1px solid $core-grey-light-700; + + .rtl & { + border-left: 0; + border-right: 1px solid $core-grey-light-700; + } } .components-button.is-button { @@ -118,6 +132,10 @@ th.woocommerce-table__item { background: transparent; box-shadow: none !important; + .rtl & { + padding: $gap-smaller 0 $gap-smaller $gap-large; + } + // @todo Add interactive styles &:hover { box-shadow: none !important; diff --git a/plugins/woocommerce-admin/client/components/table/table.js b/plugins/woocommerce-admin/client/components/table/table.js index fe585864df9..f4d13a4b5c1 100644 --- a/plugins/woocommerce-admin/client/components/table/table.js +++ b/plugins/woocommerce-admin/client/components/table/table.js @@ -120,10 +120,11 @@ class Table extends Component { { headers.map( ( header, i ) => { - const { isSortable, isNumeric, key, label } = header; + const { isLeftAligned, isSortable, isNumeric, key, label } = header; const labelId = `header-${ instanceId } -${ i }`; const thProps = { className: classnames( 'woocommerce-table__header', { + 'is-left-aligned': isLeftAligned, 'is-sortable': isSortable, 'is-sorted': sortedBy === key, 'is-numeric': isNumeric, @@ -173,10 +174,11 @@ class Table extends Component { { rows.map( ( row, i ) => ( { row.map( ( cell, j ) => { - const { isNumeric } = headers[ j ]; + const { isLeftAligned, isNumeric } = headers[ j ]; const isHeader = rowHeader === j; const Cell = isHeader ? 'th' : 'td'; const cellClasses = classnames( 'woocommerce-table__item', { + 'is-left-aligned': isLeftAligned, 'is-numeric': isNumeric, } ); return ( @@ -217,6 +219,10 @@ Table.propTypes = { * Boolean, true if this column is the default for sorting. Only one column should have this set. */ 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. */ diff --git a/plugins/woocommerce-admin/client/dashboard/top-selling-products/index.js b/plugins/woocommerce-admin/client/dashboard/top-selling-products/index.js index 4232abe6719..ebcad1ff697 100644 --- a/plugins/woocommerce-admin/client/dashboard/top-selling-products/index.js +++ b/plugins/woocommerce-admin/client/dashboard/top-selling-products/index.js @@ -25,6 +25,7 @@ export class TopSellingProducts extends Component { label: __( 'Product', 'wc-admin' ), key: 'product', required: true, + isLeftAligned: true, isSortable: false, }, { diff --git a/plugins/woocommerce-admin/client/lib/csv/__mocks__/mock-csv-data.js b/plugins/woocommerce-admin/client/lib/csv/__mocks__/mock-csv-data.js index a0fa38d93ba..faaaa6156eb 100644 --- a/plugins/woocommerce-admin/client/lib/csv/__mocks__/mock-csv-data.js +++ b/plugins/woocommerce-admin/client/lib/csv/__mocks__/mock-csv-data.js @@ -1,4 +1,4 @@ /** @format */ -export default `Date,Orders,Gross Revenue,Refunds,Coupons,Taxes,Shipping,Net Revenue -2018-04-29T00:00:00,30,200,19,19,100,19,200`; +export default `Date,Orders,Description,Gross Revenue,Refunds,Coupons,Taxes,Shipping,Net Revenue +2018-04-29T00:00:00,30,lorem ipsum,200,19,19,100,19,200`; diff --git a/plugins/woocommerce-admin/client/lib/csv/__mocks__/mock-headers.js b/plugins/woocommerce-admin/client/lib/csv/__mocks__/mock-headers.js index 7b9a3fe6ec5..9a4a8f73593 100644 --- a/plugins/woocommerce-admin/client/lib/csv/__mocks__/mock-headers.js +++ b/plugins/woocommerce-admin/client/lib/csv/__mocks__/mock-headers.js @@ -9,6 +9,10 @@ export default [ label: 'Orders', key: 'orders_count', }, + { + label: 'Description', + key: 'description', + }, { label: 'Gross Revenue', key: 'gross_revenue', diff --git a/plugins/woocommerce-admin/client/lib/csv/__mocks__/mock-rows.js b/plugins/woocommerce-admin/client/lib/csv/__mocks__/mock-rows.js index 1e6ad6b08b7..41728c6b157 100644 --- a/plugins/woocommerce-admin/client/lib/csv/__mocks__/mock-rows.js +++ b/plugins/woocommerce-admin/client/lib/csv/__mocks__/mock-rows.js @@ -10,6 +10,10 @@ export default [ display: 'Product 30', value: '30', }, + { + display: 'Lorem, ipsum', + value: 'lorem, ipsum', + }, { display: '€200.00', value: 200, diff --git a/plugins/woocommerce-admin/client/lib/csv/index.js b/plugins/woocommerce-admin/client/lib/csv/index.js index 1a29b89affa..4a8a3ab3e49 100644 --- a/plugins/woocommerce-admin/client/lib/csv/index.js +++ b/plugins/woocommerce-admin/client/lib/csv/index.js @@ -11,7 +11,11 @@ function getCSVHeaders( headers ) { function getCSVRows( 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' ) : []; } diff --git a/plugins/woocommerce-admin/client/store/orders/reducer.js b/plugins/woocommerce-admin/client/store/orders/reducer.js index 5d7397f8ed3..2f6996ac27b 100644 --- a/plugins/woocommerce-admin/client/store/orders/reducer.js +++ b/plugins/woocommerce-admin/client/store/orders/reducer.js @@ -1,8 +1,4 @@ /** @format */ -/** - * External dependencies - */ -import { union } from 'lodash'; const DEFAULT_STATE = { orders: {}, @@ -13,7 +9,6 @@ export default function ordersReducer( state = DEFAULT_STATE, action ) { switch ( action.type ) { case 'SET_ORDERS': const { orders } = action; - const ids = orders.map( order => order.id ); const ordersMap = orders.reduce( ( map, order ) => { map[ order.id ] = order; return map; @@ -21,7 +16,6 @@ export default function ordersReducer( state = DEFAULT_STATE, action ) { return { ...state, orders: Object.assign( {}, state.orders, ordersMap ), - ids: union( state.ids, ids ), }; case 'UPDATE_ORDER': const updatedOrders = { ...state.orders }; diff --git a/plugins/woocommerce-admin/client/store/orders/selectors.js b/plugins/woocommerce-admin/client/store/orders/selectors.js index db392cf6846..7dd92c42949 100644 --- a/plugins/woocommerce-admin/client/store/orders/selectors.js +++ b/plugins/woocommerce-admin/client/store/orders/selectors.js @@ -4,7 +4,4 @@ export default { getOrders( state ) { return state.orders.orders; }, - getOrderIds( state ) { - return state.orders.ids; - }, };