From 3718692ad63e614160f7d5fc2b26a8c6600ca18a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" Date: Thu, 20 Dec 2018 09:56:02 -0600 Subject: [PATCH 01/53] Update babel monorepo (https://github.com/woocommerce/woocommerce-admin/pull/1124) --- plugins/woocommerce-admin/package-lock.json | 37 ++++++++++++--------- plugins/woocommerce-admin/package.json | 4 +-- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/plugins/woocommerce-admin/package-lock.json b/plugins/woocommerce-admin/package-lock.json index 9caabdd71da..2a87c1c8ac1 100644 --- a/plugins/woocommerce-admin/package-lock.json +++ b/plugins/woocommerce-admin/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "@babel/cli": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.2.0.tgz", - "integrity": "sha512-FLteTkEoony0DX8NbnT51CmwmLBzINdlXmiJCSqCLmqWCDA/xk8EITPWqwDnVLbuK0bsZONt/grqHnQzQ15j0Q==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.2.3.tgz", + "integrity": "sha512-bfna97nmJV6nDJhXNPeEfxyMjWnt6+IjUAaDPiYRTBlm8L41n8nvw6UAqUCbvpFfU246gHPxW7sfWwqtF4FcYA==", "dev": true, "requires": { "chokidar": "^2.0.3", @@ -39,18 +39,18 @@ } }, "@babel/core": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.2.0.tgz", - "integrity": "sha512-7pvAdC4B+iKjFFp9Ztj0QgBndJ++qaMeonT185wAqUnhipw8idm9Rv1UMyBuKtYjfl6ORNkgEgcsYLfHX/GpLw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.2.2.tgz", + "integrity": "sha512-59vB0RWt09cAct5EIe58+NzGP4TFSD3Bz//2/ELy3ZeTeKF6VTD1AXlH8BGGbCX0PuobZBsIzO7IAI9PH67eKw==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.2.0", + "@babel/generator": "^7.2.2", "@babel/helpers": "^7.2.0", - "@babel/parser": "^7.2.0", - "@babel/template": "^7.1.2", - "@babel/traverse": "^7.1.6", - "@babel/types": "^7.2.0", + "@babel/parser": "^7.2.2", + "@babel/template": "^7.2.2", + "@babel/traverse": "^7.2.2", + "@babel/types": "^7.2.2", "convert-source-map": "^1.1.0", "debug": "^4.1.0", "json5": "^2.1.0", @@ -71,7 +71,7 @@ }, "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true } @@ -8718,7 +8718,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -9083,7 +9084,8 @@ }, "safe-buffer": { "version": "5.1.1", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -9131,6 +9133,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -9169,11 +9172,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.2", - "bundled": true + "bundled": true, + "optional": true } } }, diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index c0744926f00..61b21d88413 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -45,8 +45,8 @@ "publish:prod": "npm run build:packages && lerna publish from-package" }, "devDependencies": { - "@babel/cli": "7.2.0", - "@babel/core": "7.2.0", + "@babel/cli": "7.2.3", + "@babel/core": "7.2.2", "@babel/plugin-transform-async-to-generator": "7.2.0", "@babel/plugin-transform-react-jsx": "7.2.0", "@babel/runtime-corejs2": "7.2.0", From 5bab0767b6420ea33f93d63c1b8de123b6a76ffd Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Fri, 21 Dec 2018 02:40:44 +0800 Subject: [PATCH 02/53] Fix item refunds report data (https://github.com/woocommerce/woocommerce-admin/pull/1059) * Fix product report data issue when issuing item refund * Modify product order lookup stats when refund is given * Remove products where orders are refunded * Remove unused var and add comment about early return * Reduce product revenue if quantity still remains after refund * Fix subtotal array key and abs value for float * Change product_gross_revenue to product_net_revenue --- .../includes/wc-admin-order-functions.php | 98 ++++++++++++++----- 1 file changed, 73 insertions(+), 25 deletions(-) diff --git a/plugins/woocommerce-admin/includes/wc-admin-order-functions.php b/plugins/woocommerce-admin/includes/wc-admin-order-functions.php index c1dd7981545..a9eedb98485 100644 --- a/plugins/woocommerce-admin/includes/wc-admin-order-functions.php +++ b/plugins/woocommerce-admin/includes/wc-admin-order-functions.php @@ -16,39 +16,87 @@ function wc_admin_order_product_lookup_entry( $order_id ) { global $wpdb; $order = wc_get_order( $order_id ); - if ( ! $order ) { + + // This hook gets called on refunds as well, so return early to avoid errors. + if ( ! $order || 'shop_order_refund' === $order->get_type() ) { return; } - foreach ( $order->get_items() as $order_item ) { - $wpdb->replace( + if ( 'refunded' === $order->get_status() ) { + $wpdb->delete( $wpdb->prefix . 'wc_order_product_lookup', - array( - 'order_item_id' => $order_item->get_id(), - 'order_id' => $order->get_id(), - 'product_id' => $order_item->get_product_id( 'edit' ), - 'variation_id' => $order_item->get_variation_id( 'edit' ), - 'customer_id' => ( 0 < $order->get_customer_id( 'edit' ) ) ? $order->get_customer_id( 'edit' ) : null, - 'product_qty' => $order_item->get_quantity( 'edit' ), - 'product_net_revenue' => $order_item->get_subtotal( 'edit' ), - 'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ), - ), - array( - '%d', - '%d', - '%d', - '%d', - '%d', - '%d', - '%f', - '%s', - ) + array( 'order_id' => $order->get_id() ), + array( '%d' ) ); + return; + } + + $refunds = wc_admin_get_order_refund_items( $order ); + + foreach ( $order->get_items() as $order_item ) { + $order_item_id = $order_item->get_id(); + $quantity_refunded = isset( $refunds[ $order_item_id ] ) ? $refunds[ $order_item_id ]['quantity'] : 0; + $amount_refunded = isset( $refunds[ $order_item_id ] ) ? $refunds[ $order_item_id ]['subtotal'] : 0; + if ( $quantity_refunded >= $order_item->get_quantity( 'edit' ) ) { + $wpdb->delete( + $wpdb->prefix . 'wc_order_product_lookup', + array( 'order_item_id' => $order_item_id ), + array( '%d' ) + ); + } else { + $wpdb->replace( + $wpdb->prefix . 'wc_order_product_lookup', + array( + 'order_item_id' => $order_item_id, + 'order_id' => $order->get_id(), + 'product_id' => $order_item->get_product_id( 'edit' ), + 'variation_id' => $order_item->get_variation_id( 'edit' ), + 'customer_id' => ( 0 < $order->get_customer_id( 'edit' ) ) ? $order->get_customer_id( 'edit' ) : null, + 'product_qty' => $order_item->get_quantity( 'edit' ) - $quantity_refunded, + 'product_net_revenue' => $order_item->get_subtotal( 'edit' ) - $amount_refunded, + 'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ), + ), + array( + '%d', + '%d', + '%d', + '%d', + '%d', + '%d', + '%f', + '%s', + ) + ); + } } } // TODO: maybe replace these with woocommerce_create_order, woocommerce_update_order, woocommerce_trash_order, woocommerce_delete_order, as clean_post_cache might be called in other circumstances and trigger too many updates? -add_action( 'save_post', 'wc_admin_order_product_lookup_entry', 10, 1 ); -add_action( 'clean_post_cache', 'wc_admin_order_product_lookup_entry', 10, 1 ); +add_action( 'save_post', 'wc_admin_order_product_lookup_entry' ); +add_action( 'woocommerce_order_refunded', 'wc_admin_order_product_lookup_entry' ); +add_action( 'clean_post_cache', 'wc_admin_order_product_lookup_entry' ); + +/** + * Get total refund amount and line items refunded. + * + * @param object $order WC_Order. + * @return array Refunded line items with line item ID as key. + */ +function wc_admin_get_order_refund_items( $order ) { + $refunds = $order->get_refunds(); + $refunded_line_items = array(); + foreach ( $refunds as $refund ) { + foreach ( $refund->get_items() as $refunded_item ) { + $line_item_id = wc_get_order_item_meta( $refunded_item->get_id(), '_refunded_item_id', true ); + if ( ! isset( $refunded_line_items[ $line_item_id ] ) ) { + $refunded_line_items[ $line_item_id ]['quantity'] = 0; + $refunded_line_items[ $line_item_id ]['subtotal'] = 0; + } + $refunded_line_items[ $line_item_id ]['quantity'] += absint( $refunded_item['quantity'] ); + $refunded_line_items[ $line_item_id ]['subtotal'] += abs( $refunded_item['subtotal'] ); + } + } + return $refunded_line_items; +} /** * Make an entry in the wc_order_tax_lookup table for an order. From 4e68b56fdc08464d49fbf0c8691b05b55646dd7b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" Date: Thu, 20 Dec 2018 13:25:36 -0600 Subject: [PATCH 03/53] Update dependency css-loader to v2.0.1 (https://github.com/woocommerce/woocommerce-admin/pull/1129) * Update dependency css-loader to v2.0.1 * http -> https --- plugins/woocommerce-admin/package-lock.json | 6 +++--- plugins/woocommerce-admin/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/woocommerce-admin/package-lock.json b/plugins/woocommerce-admin/package-lock.json index 2a87c1c8ac1..9fc9d8709c3 100644 --- a/plugins/woocommerce-admin/package-lock.json +++ b/plugins/woocommerce-admin/package-lock.json @@ -6131,9 +6131,9 @@ } }, "css-loader": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-2.0.0.tgz", - "integrity": "sha512-3Fq8HJYs7ruBiDpJA/w2ZROtivA769ePuH3/vgPdOB+FQiotErJ7VJYRZq86SPRVFaccn1wEktUnaaUyf+Uslw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-2.0.1.tgz", + "integrity": "sha512-XIVwoIOzSFRVsafOKa060GJ/A70c0IP/C1oVPHEX4eHIFF39z0Jl7j8Kua1SUTiqWDupUnbY3/yQx9r7EUB35w==", "dev": true, "requires": { "icss-utils": "^4.0.0", diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index 61b21d88413..199f46097ae 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -68,7 +68,7 @@ "concurrently": "4.1.0", "copy-webpack-plugin": "4.6.0", "cross-env": "5.2.0", - "css-loader": "2.0.0", + "css-loader": "2.0.1", "deasync": "0.1.14", "deep-freeze": "0.0.1", "docsify-cli": "4.3.0", From 46c313cdc7fb6f3dab49074e52de0c1fbe90da31 Mon Sep 17 00:00:00 2001 From: Paul Sealock Date: Thu, 20 Dec 2018 13:50:30 +1300 Subject: [PATCH 04/53] REST API: Categories: optimize query by removing get_the_terms --- ...wc-admin-reports-categories-data-store.php | 48 ++++++------------- 1 file changed, 15 insertions(+), 33 deletions(-) diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-categories-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-categories-data-store.php index 85a8db789a4..b0119e54b8b 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-categories-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-categories-data-store.php @@ -53,10 +53,10 @@ class WC_Admin_Reports_Categories_Data_Store extends WC_Admin_Reports_Data_Store * @var array */ protected $report_columns = array( - 'items_sold' => 'SUM(product_qty) as items_sold', - 'net_revenue' => 'SUM(product_net_revenue) AS net_revenue', - 'orders_count' => 'COUNT(DISTINCT order_id) as orders_count', - // 'products_count' is not a SQL column at the moment, see below. + 'items_sold' => 'SUM(product_qty) as items_sold', + 'net_revenue' => 'SUM(product_net_revenue) AS net_revenue', + 'orders_count' => 'COUNT(DISTINCT order_id) as orders_count', + 'products_count' => 'COUNT(DISTINCT product_id) as products_count', ); /** @@ -73,6 +73,11 @@ class WC_Admin_Reports_Categories_Data_Store extends WC_Admin_Reports_Data_Store // Limit is left out here so that the grouping in code by PHP can be applied correctly. $sql_query_params = array_merge( $sql_query_params, $this->get_order_by_sql_params( $query_args ) ); + // join wp_order_product_lookup_table with relationships and taxonomies + // @TODO: How to handle custom product tables? + $sql_query_params['from_clause'] .= " LEFT JOIN {$wpdb->prefix}term_relationships ON {$order_product_lookup_table}.product_id = {$wpdb->prefix}term_relationships.object_id"; + $sql_query_params['from_clause'] .= " LEFT JOIN {$wpdb->prefix}term_taxonomy ON {$wpdb->prefix}term_relationships.term_taxonomy_id = {$wpdb->prefix}term_taxonomy.term_taxonomy_id"; + // TODO: only products in the category C or orders with products from category C (and, possibly others?). $included_products = $this->get_included_products( $query_args ); if ( $included_products ) { @@ -85,6 +90,8 @@ class WC_Admin_Reports_Categories_Data_Store extends WC_Admin_Reports_Data_Store $sql_query_params['where_clause'] .= " AND ( {$order_status_filter} )"; } + $sql_query_params['where_clause'] .= " AND taxonomy = 'product_cat' "; + return $sql_query_params; } @@ -208,10 +215,9 @@ class WC_Admin_Reports_Categories_Data_Store extends WC_Admin_Reports_Data_Store $selections = $this->selected_columns( $query_args ); $sql_query_params = $this->get_sql_query_params( $query_args ); - $products_data = $wpdb->get_results( + $categories_data = $wpdb->get_results( "SELECT - product_id, - date_created, + term_id as category_id, {$selections} FROM {$table_name} @@ -221,39 +227,15 @@ class WC_Admin_Reports_Categories_Data_Store extends WC_Admin_Reports_Data_Store {$sql_query_params['where_time_clause']} {$sql_query_params['where_clause']} GROUP BY - product_id + category_id ", ARRAY_A ); // WPCS: cache ok, DB call ok, unprepared SQL ok. - if ( null === $products_data ) { + if ( null === $categories_data ) { return new WP_Error( 'woocommerce_reports_categories_result_failed', __( 'Sorry, fetching revenue data failed.', 'wc-admin' ), array( 'status' => 500 ) ); } - // Group by category without a helper table, worst case we add it and change the SQL afterwards. - // Other option would be a join with wp_post and taxonomies, but a) performance might be bad, b) how to handle custom product tables? - $categories_data = array(); - foreach ( $products_data as $product_data ) { - $categories = get_the_terms( $product_data['product_id'], 'product_cat' ); - foreach ( $categories as $category ) { - $cat_id = $category->term_id; - if ( ! key_exists( $cat_id, $categories_data ) ) { - $categories_data[ $cat_id ] = array( - 'category_id' => 0, - 'items_sold' => 0, - 'net_revenue' => 0.0, - 'orders_count' => 0, - 'products_count' => 0, - ); - } - - $categories_data[ $cat_id ]['category_id'] = $cat_id; - $categories_data[ $cat_id ]['items_sold'] += $product_data['items_sold']; - $categories_data[ $cat_id ]['net_revenue'] += $product_data['net_revenue']; - $categories_data[ $cat_id ]['orders_count'] += $product_data['orders_count']; - $categories_data[ $cat_id ]['products_count'] ++; - } - } $record_count = count( $categories_data ); $total_pages = (int) ceil( $record_count / $query_args['per_page'] ); if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) { From 524bb0c96f12972e985d4729ca6aa23ea9688d0a Mon Sep 17 00:00:00 2001 From: Paul Sealock Date: Tue, 11 Dec 2018 14:50:26 +1300 Subject: [PATCH 05/53] DateRangeFilterPicker --- .../docs/components/calendar.md | 53 +++++++ .../docs/components/filters.md | 4 +- .../docs/components/search.md | 4 +- .../woocommerce-admin/docs/components/tag.md | 2 +- .../components/src/calendar/date-picker.js | 141 ++++++++++++++++++ .../src/calendar/{index.js => date-range.js} | 27 +--- .../components/src/calendar/example.md | 68 ++++++--- .../packages/components/src/calendar/input.js | 23 ++- .../components/src/calendar/style.scss | 86 +++++++---- .../packages/components/src/calendar/utils.js | 20 +++ .../components/src/filters/date/content.js | 2 +- .../components/src/filters/date/index.js | 8 +- .../packages/components/src/filters/index.js | 4 +- .../components/src/filters/style.scss | 4 + .../packages/components/src/index.js | 5 +- 15 files changed, 367 insertions(+), 84 deletions(-) create mode 100644 plugins/woocommerce-admin/packages/components/src/calendar/date-picker.js rename plugins/woocommerce-admin/packages/components/src/calendar/{index.js => date-range.js} (89%) create mode 100644 plugins/woocommerce-admin/packages/components/src/calendar/utils.js diff --git a/plugins/woocommerce-admin/docs/components/calendar.md b/plugins/woocommerce-admin/docs/components/calendar.md index 7140f0f13ca..95fecb2f318 100644 --- a/plugins/woocommerce-admin/docs/components/calendar.md +++ b/plugins/woocommerce-admin/docs/components/calendar.md @@ -1,3 +1,56 @@ +`DatePicker` (component) +======================== + + + +Props +----- + +### `date` + +- Type: Object +- Default: null + +A moment date object representing the selected date. `null` for no selection. + +### `text` + +- Type: String +- Default: null + +The date in human-readable format. Displayed in the text input. + +### `error` + +- Type: String +- Default: null + +A string error message, shown to the user. + +### `invalidDays` + +- Type: One of type: enum, func +- Default: null + +(Coming Soon) Optionally invalidate certain days. `past`, `future`, `none`, or function are accepted. +A function will be passed to react-dates' `isOutsideRange` prop + +### `onUpdate` + +- **Required** +- Type: Function +- Default: null + +A function called upon selection of a date or input change. + +### `dateFormat` + +- **Required** +- Type: String +- Default: null + +The date format in moment.js-style tokens. + `DateRange` (component) ======================= diff --git a/plugins/woocommerce-admin/docs/components/filters.md b/plugins/woocommerce-admin/docs/components/filters.md index eb92acc7e01..6312ecbbfb3 100644 --- a/plugins/woocommerce-admin/docs/components/filters.md +++ b/plugins/woocommerce-admin/docs/components/filters.md @@ -144,8 +144,8 @@ The query string represented in object form Which type of autocompleter should be used in the Search -`DatePicker` (component) -======================== +`DateRangeFilterPicker` (component) +=================================== Select a range of dates or single dates. diff --git a/plugins/woocommerce-admin/docs/components/search.md b/plugins/woocommerce-admin/docs/components/search.md index 75e3385aafc..0408f6b2c8b 100644 --- a/plugins/woocommerce-admin/docs/components/search.md +++ b/plugins/woocommerce-admin/docs/components/search.md @@ -24,7 +24,7 @@ Function called when selected results change, passed result list. ### `type` - **Required** -- Type: One of: 'products', 'product_cats', 'orders', 'customers', 'coupons', 'taxes', 'variations' +- Type: One of: 'countries', 'coupons', 'customers', 'emails', 'orders', 'products', 'product_cats', 'taxes', 'usernames', 'variations' - Default: null The object type to be used in searching. @@ -39,7 +39,7 @@ A placeholder for the search input. ### `selected` - Type: Array - - id: Number + - id: One of type: number, string - label: String - Default: `[]` diff --git a/plugins/woocommerce-admin/docs/components/tag.md b/plugins/woocommerce-admin/docs/components/tag.md index f11fd5a67ab..c60cd369f5d 100644 --- a/plugins/woocommerce-admin/docs/components/tag.md +++ b/plugins/woocommerce-admin/docs/components/tag.md @@ -11,7 +11,7 @@ Props ### `id` -- Type: Number +- Type: One of type: number, string - Default: null The ID for this item, used in the remove function. diff --git a/plugins/woocommerce-admin/packages/components/src/calendar/date-picker.js b/plugins/woocommerce-admin/packages/components/src/calendar/date-picker.js new file mode 100644 index 00000000000..4781d3e49f5 --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/calendar/date-picker.js @@ -0,0 +1,141 @@ +/** @format */ +/** + * External dependencies + */ +import 'core-js/fn/object/assign'; +import 'core-js/fn/array/from'; +import { __, sprintf } from '@wordpress/i18n'; +import { Component } from '@wordpress/element'; +import { Dropdown, DatePicker as WpDatePicker } from '@wordpress/components'; +import { partial } from 'lodash'; +import { TAB } from '@wordpress/keycodes'; +import moment from 'moment'; + +/** + * Internal dependencies + */ +import DateInput from './input'; +import { toMoment } from '@woocommerce/date'; +import { H, Section } from '../section'; +import PropTypes from 'prop-types'; + +class DatePicker extends Component { + constructor( props ) { + super( props ); + + this.onDateChange = this.onDateChange.bind( this ); + this.onInputChange = this.onInputChange.bind( this ); + } + + handleKeyDown( isOpen, onToggle, { keyCode } ) { + if ( TAB === keyCode && isOpen ) { + onToggle(); + } + } + + handleFocus( isOpen, onToggle ) { + if ( ! isOpen ) { + onToggle(); + } + } + + onDateChange( onToggle, dateString ) { + const { onUpdate, dateFormat } = this.props; + const date = moment( dateString ); + onUpdate( { + date, + text: dateString ? date.format( dateFormat ) : '', + error: null, + } ); + onToggle(); + } + + onInputChange( event ) { + const value = event.target.value; + const { dateFormat } = this.props; + const date = toMoment( dateFormat, value ); + const error = date ? null : __( 'Invalid date', 'wc-admin' ); + + this.props.onUpdate( { + date, + text: value, + error: value.length > 0 ? error : null, + } ); + } + + render() { + const { date, text, dateFormat, error } = this.props; + // @TODO: make upstream Gutenberg change to invalidate certain days. + // const isOutsideRange = getOutsideRange( invalidDays ); + return ( + ( + + ) } + renderContent={ ( { onToggle } ) => ( +
+ + { __( 'select a date', 'wc-admin' ) } + +
+ +
+
+ ) } + /> + ); + } +} + +DatePicker.propTypes = { + /** + * A moment date object representing the selected date. `null` for no selection. + */ + date: PropTypes.object, + /** + * The date in human-readable format. Displayed in the text input. + */ + text: PropTypes.string, + /** + * A string error message, shown to the user. + */ + error: PropTypes.string, + /** + * (Coming Soon) Optionally invalidate certain days. `past`, `future`, `none`, or function are accepted. + * A function will be passed to react-dates' `isOutsideRange` prop + */ + invalidDays: PropTypes.oneOfType( [ + PropTypes.oneOf( [ 'past', 'future', 'none' ] ), + PropTypes.func, + ] ), + /** + * A function called upon selection of a date or input change. + */ + onUpdate: PropTypes.func.isRequired, + /** + * The date format in moment.js-style tokens. + */ + dateFormat: PropTypes.string.isRequired, +}; + +export default DatePicker; diff --git a/plugins/woocommerce-admin/packages/components/src/calendar/index.js b/plugins/woocommerce-admin/packages/components/src/calendar/date-range.js similarity index 89% rename from plugins/woocommerce-admin/packages/components/src/calendar/index.js rename to plugins/woocommerce-admin/packages/components/src/calendar/date-range.js index 1711ffff7a8..9d8deb3b7e4 100644 --- a/plugins/woocommerce-admin/packages/components/src/calendar/index.js +++ b/plugins/woocommerce-admin/packages/components/src/calendar/date-range.js @@ -7,11 +7,7 @@ import 'core-js/fn/array/from'; import { __, sprintf } from '@wordpress/i18n'; import classnames from 'classnames'; import { Component } from '@wordpress/element'; -import { - DayPickerRangeController, - isInclusivelyAfterDay, - isInclusivelyBeforeDay, -} from 'react-dates'; +import { DayPickerRangeController } from 'react-dates'; import moment from 'moment'; import { partial } from 'lodash'; import PropTypes from 'prop-types'; @@ -27,6 +23,7 @@ import { validateDateInputForRange } from '@woocommerce/date'; */ import DateInput from './input'; import phrases from './phrases'; +import { getOutsideRange } from './utils'; /** * This is wrapper for a [react-dates](https://github.com/airbnb/react-dates) powered calendar. @@ -38,7 +35,6 @@ class DateRange extends Component { this.onDatesChange = this.onDatesChange.bind( this ); this.onFocusChange = this.onFocusChange.bind( this ); this.onInputChange = this.onInputChange.bind( this ); - this.getOutsideRange = this.getOutsideRange.bind( this ); } onDatesChange( { startDate, endDate } ) { @@ -76,22 +72,6 @@ class DateRange extends Component { } ); } - getOutsideRange() { - const { invalidDays } = this.props; - if ( 'string' === typeof invalidDays ) { - switch ( invalidDays ) { - case 'past': - return day => isInclusivelyBeforeDay( day, moment() ); - case 'future': - return day => isInclusivelyAfterDay( day, moment() ); - case 'none': - default: - return undefined; - } - } - return 'function' === typeof invalidDays ? invalidDays : undefined; - } - setTnitialVisibleMonth( isDoubleCalendar, before ) { return () => { const visibleDate = before || moment(); @@ -114,8 +94,9 @@ class DateRange extends Component { shortDateFormat, isViewportMobile, isViewportSmall, + invalidDays, } = this.props; - const isOutsideRange = this.getOutsideRange(); + const isOutsideRange = getOutsideRange( invalidDays ); const isDoubleCalendar = isViewportMobile && ! isViewportSmall; return (
{ - function onUpdate( { after, afterText, before, beforeText } ) { - setState( { after, afterText, before, beforeText } ); + after: null, + afterText: '', + before: null, + beforeText: '', + afterError: null, + beforeError: null, + focusedInput: 'startDate', +} )( ( { after, afterText, before, beforeText, afterError, beforeError, focusedInput, setState } ) => { + function onRangeUpdate( update ) { + setState( update ); } - + + function onDatePickerUpdate( { date, text, error } ) { + setState( { + after: date, + afterText: text, + afterError: error, + } ); + } + return ( - +
+ Date Range Picker +
+ +
+ + Date Picker +
+ +
+
) } ); ``` diff --git a/plugins/woocommerce-admin/packages/components/src/calendar/input.js b/plugins/woocommerce-admin/packages/components/src/calendar/input.js index 0c88e299077..330b08d5987 100644 --- a/plugins/woocommerce-admin/packages/components/src/calendar/input.js +++ b/plugins/woocommerce-admin/packages/components/src/calendar/input.js @@ -7,7 +7,17 @@ import classnames from 'classnames'; import { uniqueId } from 'lodash'; import PropTypes from 'prop-types'; -const DateInput = ( { value, onChange, dateFormat, label, describedBy, error } ) => { +const DateInput = ( { + value, + onChange, + dateFormat, + label, + describedBy, + error, + onFocus, + onKeyDown, + errorPosition, +} ) => { const classes = classnames( 'woocommerce-calendar__input', { 'is-empty': value.length === 0, 'is-error': error, @@ -24,12 +34,14 @@ const DateInput = ( { value, onChange, dateFormat, label, describedBy, error } ) id={ id } aria-describedby={ `${ id }-message` } placeholder={ dateFormat.toLowerCase() } + onFocus={ onFocus } + onKeyDown={ onKeyDown } /> { error && ( { error } @@ -49,6 +61,13 @@ DateInput.propTypes = { label: PropTypes.string.isRequired, describedBy: PropTypes.string.isRequired, error: PropTypes.string, + errorPosition: PropTypes.string, + onFocus: PropTypes.func, +}; + +DateInput.defaultProps = { + onFocus: () => {}, + errorPosition: 'bottom center', }; export default DateInput; diff --git a/plugins/woocommerce-admin/packages/components/src/calendar/style.scss b/plugins/woocommerce-admin/packages/components/src/calendar/style.scss index 8f85f829cfa..246d94ecef8 100644 --- a/plugins/woocommerce-admin/packages/components/src/calendar/style.scss +++ b/plugins/woocommerce-admin/packages/components/src/calendar/style.scss @@ -60,7 +60,23 @@ outline: 2px solid #bfe7f3; } } -} + + // Make exceptions for wp Core DatePicker. + &.is-core-datepicker { + .components-datetime__date { + padding-left: 0; + } + + .CalendarDay__default { + background-color: transparent; + } + + .CalendarDay__selected { + background: $woocommerce-700; + border: none; + } + } + } .woocommerce-calendar__inputs { padding: 1em; @@ -143,32 +159,50 @@ } .woocommerce-filters-date__content { - .woocommerce-calendar__input-error { - display: none; - - .components-popover__content { - background-color: $core-grey-dark-400; - color: $white; - padding: 0.5em; - border: none; - } - - &.components-popover { - .components-popover__content { - min-width: 100px; - width: 100px; - text-align: center; - } - - &:not(.no-arrow):not(.is-mobile).is-bottom::before { - border-bottom-color: $core-grey-dark-400; - z-index: 1; - top: -6px; - } - } - } - &.is-mobile .woocommerce-calendar__input-error .components-popover__content { height: initial; } } + +.woocommerce-calendar__input-error { + display: none; + + .components-popover__content { + background-color: $core-grey-dark-400; + color: $white; + padding: 0.5em; + border: none; + } + + &.components-popover { + .components-popover__content { + min-width: 100px; + width: 100px; + text-align: center; + } + + &:not(.no-arrow):not(.is-mobile).is-bottom::before { + border-bottom-color: $core-grey-dark-400; + z-index: 1; + top: -6px; + } + + &:not(.no-arrow):not(.is-mobile).is-top::after { + border-top-color: $core-grey-dark-400; + z-index: 1; + top: 0px; + } + } +} + +.woocommerce-calendar__date-picker-title { + @include font-size( 12 ); + font-weight: 100; + text-transform: uppercase; + text-align: center; + color: $core-grey-dark-300; + width: 100%; + margin: 0; + padding: 1em; + background-color: $white; +} diff --git a/plugins/woocommerce-admin/packages/components/src/calendar/utils.js b/plugins/woocommerce-admin/packages/components/src/calendar/utils.js new file mode 100644 index 00000000000..206d110e573 --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/calendar/utils.js @@ -0,0 +1,20 @@ +/** @format */ +/** + * External dependencies + */ +import moment from 'moment'; + +export function getOutsideRange( invalidDays ) { + if ( 'string' === typeof invalidDays ) { + switch ( invalidDays ) { + case 'past': + return day => moment().isAfter( day, 'day' ); + case 'future': + return day => moment().isBefore( day, 'day' ); + case 'none': + default: + return undefined; + } + } + return 'function' === typeof invalidDays ? invalidDays : undefined; +} diff --git a/plugins/woocommerce-admin/packages/components/src/filters/date/content.js b/plugins/woocommerce-admin/packages/components/src/filters/date/content.js index a0534e517ba..af4f1164a04 100644 --- a/plugins/woocommerce-admin/packages/components/src/filters/date/content.js +++ b/plugins/woocommerce-admin/packages/components/src/filters/date/content.js @@ -12,7 +12,7 @@ import classnames from 'classnames'; * Internal dependencies */ import ComparePeriods from './compare-periods'; -import DateRange from '../../calendar'; +import DateRange from '../../calendar/date-range'; import { H, Section } from '../../section'; import PresetPeriods from './preset-periods'; diff --git a/plugins/woocommerce-admin/packages/components/src/filters/date/index.js b/plugins/woocommerce-admin/packages/components/src/filters/date/index.js index 4d3806430ba..4afa779c932 100644 --- a/plugins/woocommerce-admin/packages/components/src/filters/date/index.js +++ b/plugins/woocommerce-admin/packages/components/src/filters/date/index.js @@ -24,7 +24,7 @@ const shortDateFormat = __( 'MM/DD/YYYY', 'wc-admin' ); /** * Select a range of dates or single dates. */ -class DatePicker extends Component { +class DateRangeFilterPicker extends Component { constructor( props ) { super( props ); this.state = this.getResetState(); @@ -156,7 +156,7 @@ class DatePicker extends Component { } } -DatePicker.propTypes = { +DateRangeFilterPicker.propTypes = { /** * The `path` parameter supplied by React-Router. */ @@ -167,8 +167,8 @@ DatePicker.propTypes = { query: PropTypes.object, }; -DatePicker.defaultProps = { +DateRangeFilterPicker.defaultProps = { query: {}, }; -export default DatePicker; +export default DateRangeFilterPicker; diff --git a/plugins/woocommerce-admin/packages/components/src/filters/index.js b/plugins/woocommerce-admin/packages/components/src/filters/index.js index 8e69ae966e4..791e45eaa78 100644 --- a/plugins/woocommerce-admin/packages/components/src/filters/index.js +++ b/plugins/woocommerce-admin/packages/components/src/filters/index.js @@ -12,7 +12,7 @@ import PropTypes from 'prop-types'; */ import AdvancedFilters from './advanced'; import CompareFilter from './compare'; -import DatePicker from './date'; +import DateRangeFilterPicker from './date'; import FilterPicker from './filter'; import { H, Section } from '../section'; @@ -64,7 +64,7 @@ class ReportFilters extends Component {
{ showDatePicker && ( - + ) } { filters.map( config => { if ( config.showFilters( query ) ) { diff --git a/plugins/woocommerce-admin/packages/components/src/filters/style.scss b/plugins/woocommerce-admin/packages/components/src/filters/style.scss index 41f8c0c6113..ff6131517a0 100644 --- a/plugins/woocommerce-admin/packages/components/src/filters/style.scss +++ b/plugins/woocommerce-admin/packages/components/src/filters/style.scss @@ -59,6 +59,10 @@ background-color: $white; } + .woocommerce-calendar__input-error .components-popover__content { + background-color: $core-grey-dark-400; + } + &.is-mobile { .components-popover__content { width: 100%; diff --git a/plugins/woocommerce-admin/packages/components/src/index.js b/plugins/woocommerce-admin/packages/components/src/index.js index af501d1acdf..96806ae7117 100644 --- a/plugins/woocommerce-admin/packages/components/src/index.js +++ b/plugins/woocommerce-admin/packages/components/src/index.js @@ -12,8 +12,9 @@ export { default as ChartPlaceholder } from './chart/placeholder'; export { default as Card } from './card'; export { default as Count } from './count'; export { default as CompareFilter } from './filters/compare'; -export { default as DatePicker } from './filters/date'; -export { default as DateRange } from './calendar'; +export { default as DateRangeFilterPicker } from './filters/date'; +export { default as DateRange } from './calendar/date-range'; +export { default as DatePicker } from './calendar/date-picker'; export { default as DropdownButton } from './dropdown-button'; export { default as EllipsisMenu } from './ellipsis-menu'; export { default as EmptyContent } from './empty-content'; From 716ebc0658eb68640714e495442a170175f5e4a2 Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Fri, 21 Dec 2018 10:13:57 +0800 Subject: [PATCH 06/53] Hook up coupons to REST API (https://github.com/woocommerce/woocommerce-admin/pull/1112) --- .../client/analytics/report/coupons/config.js | 8 +-- .../client/analytics/report/coupons/index.js | 4 +- .../client/analytics/report/coupons/table.js | 55 ++++++++++--------- .../client/store/reports/items/resolvers.js | 16 ------ .../client/store/reports/stats/resolvers.js | 2 +- .../client/wc-api/reports/items/operations.js | 2 +- .../client/wc-api/reports/stats/operations.js | 4 +- 7 files changed, 40 insertions(+), 51 deletions(-) diff --git a/plugins/woocommerce-admin/client/analytics/report/coupons/config.js b/plugins/woocommerce-admin/client/analytics/report/coupons/config.js index 6f532325e0e..fdc957146cc 100644 --- a/plugins/woocommerce-admin/client/analytics/report/coupons/config.js +++ b/plugins/woocommerce-admin/client/analytics/report/coupons/config.js @@ -12,13 +12,13 @@ import { NAMESPACE } from 'store/constants'; export const charts = [ { - key: 'discounted_orders', + key: 'orders_count', label: __( 'Discounted Orders', 'wc-admin' ), type: 'number', }, { - key: 'coupons', - label: __( 'Gross Discounted', 'wc-admin' ), + key: 'amount', + label: __( 'Amount', 'wc-admin' ), type: 'currency', }, ]; @@ -48,7 +48,7 @@ export const filters = [ }, }, { label: __( 'Top Coupons by Discounted Orders', 'wc-admin' ), value: 'top_orders' }, - { label: __( 'Top Coupons by Gross Discounted', 'wc-admin' ), value: 'top_discount' }, + { label: __( 'Top Coupons by Amount Discounted', 'wc-admin' ), value: 'top_discount' }, ], }, ]; diff --git a/plugins/woocommerce-admin/client/analytics/report/coupons/index.js b/plugins/woocommerce-admin/client/analytics/report/coupons/index.js index 0078c34c5a0..113c2c45457 100644 --- a/plugins/woocommerce-admin/client/analytics/report/coupons/index.js +++ b/plugins/woocommerce-admin/client/analytics/report/coupons/index.js @@ -28,13 +28,13 @@ export default class CouponsReport extends Component { { - const { coupon_id, gross_discount, orders_count } = coupon; + const { amount, coupon_id, extended_info, orders_count } = coupon; + const { code, date_created, date_expires, discount_type } = extended_info; // @TODO must link to the coupon detail report const couponLink = ( - { coupon_id } + { code } ); @@ -97,33 +92,29 @@ export default class CouponsReportTable extends Component { ); return [ - // @TODO it should be the coupon code, not the coupon ID { display: couponLink, - value: coupon_id, + value: code, }, { display: ordersLink, value: orders_count, }, { - display: formatCurrency( gross_discount ), - value: getCurrencyFormatDecimal( gross_discount ), + display: formatCurrency( amount ), + value: getCurrencyFormatDecimal( amount ), }, { - // @TODO - display: formatDate( tableFormat, '' ), - value: '', + display: formatDate( tableFormat, date_created ), + value: date_created, }, { - // @TODO - display: formatDate( tableFormat, '' ), - value: '', + display: date_expires ? formatDate( tableFormat, date_expires ) : __( 'N/A', 'wc-admin' ), + value: date_expires, }, { - // @TODO - display: '', - value: '', + display: this.getCouponType( discount_type ), + value: discount_type, }, ]; } ); @@ -143,12 +134,21 @@ export default class CouponsReportTable extends Component { value: numberFormat( totals.orders_count ), }, { - label: __( 'gross discounted', 'wc-admin' ), - value: formatCurrency( totals.gross_discount ), + label: __( 'amount discounted', 'wc-admin' ), + value: formatCurrency( totals.amount ), }, ]; } + getCouponType( discount_type ) { + const couponTypes = { + percent: __( 'Percentage', 'wc-admin' ), + fixed_cart: __( 'Fixed cart', 'wc-admin' ), + fixed_product: __( 'Fixed product', 'wc-admin' ), + }; + return couponTypes[ discount_type ]; + } + render() { const { query } = this.props; @@ -161,6 +161,11 @@ export default class CouponsReportTable extends Component { getSummary={ this.getSummary } itemIdField="coupon_id" query={ query } + tableQuery={ { + orderby: query.orderby || 'coupon_id', + order: query.order || 'asc', + extended_info: true, + } } title={ __( 'Coupons', 'wc-admin' ) } columnPrefsKey="coupons_report_columns" /> diff --git a/plugins/woocommerce-admin/client/store/reports/items/resolvers.js b/plugins/woocommerce-admin/client/store/reports/items/resolvers.js index 13dca9b6979..612810d6707 100644 --- a/plugins/woocommerce-admin/client/store/reports/items/resolvers.js +++ b/plugins/woocommerce-admin/client/store/reports/items/resolvers.js @@ -20,22 +20,6 @@ export default { async getReportItems( ...args ) { const [ endpoint, query ] = args.slice( -2 ); - const swaggerEndpoints = [ 'coupons' ]; - if ( swaggerEndpoints.indexOf( endpoint ) >= 0 ) { - try { - const response = await fetch( - SWAGGERNAMESPACE + 'reports/' + endpoint + stringifyQuery( query ) - ); - const itemsData = await response.json(); - - dispatch( 'wc-admin' ).setReportItems( endpoint, query, itemsData ); - } catch ( error ) { - dispatch( 'wc-admin' ).setReportItemsError( endpoint, query ); - } - - return; - } - try { const response = await apiFetch( { parse: false, diff --git a/plugins/woocommerce-admin/client/store/reports/stats/resolvers.js b/plugins/woocommerce-admin/client/store/reports/stats/resolvers.js index 0cde00310d2..83f35864089 100644 --- a/plugins/woocommerce-admin/client/store/reports/stats/resolvers.js +++ b/plugins/woocommerce-admin/client/store/reports/stats/resolvers.js @@ -28,7 +28,7 @@ export default { let apiPath = endpoint + stringifyQuery( query ); // TODO: Remove once swagger endpoints are phased out. - const swaggerEndpoints = [ 'categories', 'coupons' ]; + const swaggerEndpoints = [ 'categories' ]; if ( swaggerEndpoints.indexOf( endpoint ) >= 0 ) { apiPath = SWAGGERNAMESPACE + 'reports/' + endpoint + '/stats' + stringifyQuery( query ); try { diff --git a/plugins/woocommerce-admin/client/wc-api/reports/items/operations.js b/plugins/woocommerce-admin/client/wc-api/reports/items/operations.js index 2c21f040530..9ec07af3caa 100644 --- a/plugins/woocommerce-admin/client/wc-api/reports/items/operations.js +++ b/plugins/woocommerce-admin/client/wc-api/reports/items/operations.js @@ -17,7 +17,7 @@ import { NAMESPACE } from '../../constants'; import { SWAGGERNAMESPACE } from 'store/constants'; // TODO: Remove once swagger endpoints are phased out. -const swaggerEndpoints = [ 'coupons', 'customers', 'downloads' ]; +const swaggerEndpoints = [ 'customers', 'downloads' ]; const typeEndpointMap = { 'report-items-query-orders': 'orders', diff --git a/plugins/woocommerce-admin/client/wc-api/reports/stats/operations.js b/plugins/woocommerce-admin/client/wc-api/reports/stats/operations.js index dd3d06cc7d8..133fff60eac 100644 --- a/plugins/woocommerce-admin/client/wc-api/reports/stats/operations.js +++ b/plugins/woocommerce-admin/client/wc-api/reports/stats/operations.js @@ -16,9 +16,9 @@ import { getResourceIdentifier, getResourcePrefix } from '../../utils'; import { NAMESPACE } from '../../constants'; import { SWAGGERNAMESPACE } from 'store/constants'; -const statEndpoints = [ 'orders', 'revenue', 'products', 'taxes' ]; +const statEndpoints = [ 'orders', 'revenue', 'products', 'taxes', 'coupons' ]; // TODO: Remove once swagger endpoints are phased out. -const swaggerEndpoints = [ 'categories', 'coupons', 'downloads' ]; +const swaggerEndpoints = [ 'categories', 'downloads' ]; const typeEndpointMap = { 'report-stats-query-orders': 'orders', From c76ceb25915fff785913214d7e77f9f8eb9ac378 Mon Sep 17 00:00:00 2001 From: Paul Sealock Date: Fri, 21 Dec 2018 15:19:10 +1300 Subject: [PATCH 07/53] DatePicker: cleanup example, propTypes --- .../packages/components/src/calendar/example.md | 2 -- .../packages/components/src/calendar/input.js | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/woocommerce-admin/packages/components/src/calendar/example.md b/plugins/woocommerce-admin/packages/components/src/calendar/example.md index a95d04e0a1a..17f9e8858c6 100644 --- a/plugins/woocommerce-admin/packages/components/src/calendar/example.md +++ b/plugins/woocommerce-admin/packages/components/src/calendar/example.md @@ -50,8 +50,6 @@ const MyDateRange = withState( { onUpdate={ onDatePickerUpdate } dateFormat={ dateFormat } invalidDays="none" - onUpdate={ onDatePickerUpdate } - invalidDays="future" />
diff --git a/plugins/woocommerce-admin/packages/components/src/calendar/input.js b/plugins/woocommerce-admin/packages/components/src/calendar/input.js index 330b08d5987..02844dd4c33 100644 --- a/plugins/woocommerce-admin/packages/components/src/calendar/input.js +++ b/plugins/woocommerce-admin/packages/components/src/calendar/input.js @@ -4,7 +4,7 @@ */ import { Dashicon, Popover } from '@wordpress/components'; import classnames from 'classnames'; -import { uniqueId } from 'lodash'; +import { uniqueId, noop } from 'lodash'; import PropTypes from 'prop-types'; const DateInput = ( { @@ -63,11 +63,13 @@ DateInput.propTypes = { error: PropTypes.string, errorPosition: PropTypes.string, onFocus: PropTypes.func, + onKeyDown: PropTypes.func, }; DateInput.defaultProps = { onFocus: () => {}, errorPosition: 'bottom center', + onKeyDown: noop, }; export default DateInput; From 69a11b50e435a3748dde95257b69fb8191326192 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" Date: Thu, 20 Dec 2018 20:20:05 -0600 Subject: [PATCH 08/53] Update dependency autoprefixer to v9.4.3 (https://github.com/woocommerce/woocommerce-admin/pull/1125) --- plugins/woocommerce-admin/package-lock.json | 10 +++++----- plugins/woocommerce-admin/package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/woocommerce-admin/package-lock.json b/plugins/woocommerce-admin/package-lock.json index 9fc9d8709c3..bc0254dfff8 100644 --- a/plugins/woocommerce-admin/package-lock.json +++ b/plugins/woocommerce-admin/package-lock.json @@ -3705,13 +3705,13 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" }, "autoprefixer": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.4.2.tgz", - "integrity": "sha512-tYQYJvZvqlJCzF+BLC//uAcdT/Yy4ik9bwZRXr/EehUJ/bjjpTthsWTy8dpowdoIE1sLCDf1ch4Eb2cOSzZC9w==", + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.4.3.tgz", + "integrity": "sha512-/XSnzDepRkAU//xLcXA/lUWxpsBuw0WiriAHOqnxkuCtzLhaz+fL4it4gp20BQ8n5SyLzK/FOc7A0+u/rti2FQ==", "dev": true, "requires": { - "browserslist": "^4.3.5", - "caniuse-lite": "^1.0.30000914", + "browserslist": "^4.3.6", + "caniuse-lite": "^1.0.30000921", "normalize-range": "^0.1.2", "num2fraction": "^1.2.2", "postcss": "^7.0.6", diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index 199f46097ae..9e94b125934 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -58,7 +58,7 @@ "@wordpress/jest-preset-default": "2.0.6", "@wordpress/postcss-themes": "1.0.4", "ast-types": "0.11.7", - "autoprefixer": "9.4.2", + "autoprefixer": "9.4.3", "babel-core": "7.0.0-bridge.0", "babel-eslint": "10.0.1", "babel-loader": "8.0.4", From f83b0e06152e71c0f6f963d7f93374d726632db1 Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Fri, 21 Dec 2018 10:44:27 +0800 Subject: [PATCH 09/53] Add product categories wc-api and breadcrumbs (https://github.com/woocommerce/woocommerce-admin/pull/1131) * Add support for product cats in wp rest api * Add categories to WC API * Add category breadcrumbs component * Increase per_page limit size for product cat API * Use wc/v3 API to pull product categories * Return category IDs in API * Add categories to product reports * Add category IDs test for REST API * Switch to getResource instead of require for total count selector --- .../report/categories/breadcrumbs.js | 67 +++++++++++++++++++ .../analytics/report/categories/table.js | 47 ++++++------- .../analytics/report/products/style.scss | 16 +++++ .../client/analytics/report/products/table.js | 58 +++++++++++++--- .../client/wc-api/categories/index.js | 11 +++ .../client/wc-api/categories/operations.js | 54 +++++++++++++++ .../client/wc-api/categories/selectors.js | 56 ++++++++++++++++ .../client/wc-api/wc-api-spec.js | 3 + .../includes/class-wc-admin-api-init.php | 26 +++++++ ...s-wc-admin-reports-products-data-store.php | 25 ++++--- .../class-wc-tests-reports-products.php | 10 ++- 11 files changed, 327 insertions(+), 46 deletions(-) create mode 100644 plugins/woocommerce-admin/client/analytics/report/categories/breadcrumbs.js create mode 100644 plugins/woocommerce-admin/client/analytics/report/products/style.scss create mode 100644 plugins/woocommerce-admin/client/wc-api/categories/index.js create mode 100644 plugins/woocommerce-admin/client/wc-api/categories/operations.js create mode 100644 plugins/woocommerce-admin/client/wc-api/categories/selectors.js diff --git a/plugins/woocommerce-admin/client/analytics/report/categories/breadcrumbs.js b/plugins/woocommerce-admin/client/analytics/report/categories/breadcrumbs.js new file mode 100644 index 00000000000..544434dc4a1 --- /dev/null +++ b/plugins/woocommerce-admin/client/analytics/report/categories/breadcrumbs.js @@ -0,0 +1,67 @@ +/** @format */ +/** + * External dependencies + */ +import { Component } from '@wordpress/element'; +import { first, last } from 'lodash'; +import { Spinner } from '@wordpress/components'; + +/** + * WooCommerce dependencies + */ +import { Link } from '@woocommerce/components'; + +export default class CategoryBreadcrumbs extends Component { + getCategoryAncestorIds( category, categories ) { + const ancestors = []; + let parent = category.parent; + while ( parent ) { + ancestors.unshift( parent ); + parent = categories[ parent ].parent; + } + return ancestors; + } + + getCategoryAncestors( category, categories ) { + const ancestorIds = this.getCategoryAncestorIds( category, categories ); + + if ( ! ancestorIds.length ) { + return; + } + if ( ancestorIds.length === 1 ) { + return categories[ first( ancestorIds ) ].name + ' › '; + } + if ( ancestorIds.length === 2 ) { + return ( + categories[ first( ancestorIds ) ].name + + ' › ' + + categories[ last( ancestorIds ) ].name + + ' › ' + ); + } + return ( + categories[ first( ancestorIds ) ].name + + ' … ' + + categories[ last( ancestorIds ) ].name + + ' › ' + ); + } + + render() { + const { categories, category } = this.props; + + return category ? ( +
+ { this.getCategoryAncestors( category, categories ) } + + { category.name } + +
+ ) : ( + + ); + } +} diff --git a/plugins/woocommerce-admin/client/analytics/report/categories/table.js b/plugins/woocommerce-admin/client/analytics/report/categories/table.js index 8eb3632b8ce..30ab59dd82c 100644 --- a/plugins/woocommerce-admin/client/analytics/report/categories/table.js +++ b/plugins/woocommerce-admin/client/analytics/report/categories/table.js @@ -4,21 +4,23 @@ */ import { __, _n } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; +import { compose } from '@wordpress/compose'; import { map } from 'lodash'; /** * WooCommerce dependencies */ -import { Link } from '@woocommerce/components'; import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency'; /** * Internal dependencies */ -import ReportTable from 'analytics/components/report-table'; +import CategoryBreacrumbs from './breadcrumbs'; import { numberFormat } from 'lib/number'; +import ReportTable from 'analytics/components/report-table'; +import withSelect from 'wc-api/with-select'; -export default class CategoriesReportTable extends Component { +class CategoriesReportTable extends Component { constructor( props ) { super( props ); @@ -64,29 +66,16 @@ export default class CategoriesReportTable extends Component { ]; } - getRowsContent( categories ) { - return map( categories, category => { - const { - category_id, - items_sold, - net_revenue, - products_count, - orders_count, - extended_info, - } = category; - const { name } = extended_info; + getRowsContent( categoryStats ) { + return map( categoryStats, categoryStat => { + const { category_id, items_sold, net_revenue, products_count, orders_count } = categoryStat; + const categories = this.props.categories; + const category = categories[ category_id ]; return [ { - display: ( - - { name } - - ), - value: name, + display: , + value: category && category.name, }, { display: numberFormat( items_sold ), @@ -156,3 +145,15 @@ export default class CategoriesReportTable extends Component { ); } } + +export default compose( + withSelect( select => { + const { getCategories, getCategoriesError, isGetCategoriesRequesting } = select( 'wc-api' ); + + const categories = getCategories(); + const isError = Boolean( getCategoriesError() ); + const isRequesting = isGetCategoriesRequesting(); + + return { categories, isError, isRequesting }; + } ) +)( CategoriesReportTable ); diff --git a/plugins/woocommerce-admin/client/analytics/report/products/style.scss b/plugins/woocommerce-admin/client/analytics/report/products/style.scss new file mode 100644 index 00000000000..d2a80f105fa --- /dev/null +++ b/plugins/woocommerce-admin/client/analytics/report/products/style.scss @@ -0,0 +1,16 @@ +/** @format */ + +.woocommerce-table__product-categories { + > .woocommerce-table__breadcrumbs { + display: inline-block; + margin-right: $gap-small; + } + .components-popover__content { + padding: 0 $gap; + text-align: left; + } + .components-popover__content .woocommerce-table__breadcrumbs { + margin-top: $gap-small; + margin-bottom: $gap-small; + } +} diff --git a/plugins/woocommerce-admin/client/analytics/report/products/table.js b/plugins/woocommerce-admin/client/analytics/report/products/table.js index 007875a96a3..9c9b4aa856e 100644 --- a/plugins/woocommerce-admin/client/analytics/report/products/table.js +++ b/plugins/woocommerce-admin/client/analytics/report/products/table.js @@ -2,25 +2,29 @@ /** * External dependencies */ -import { __, _n, _x } from '@wordpress/i18n'; +import { __, _n, _x, sprintf } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; +import { compose } from '@wordpress/compose'; import { map } from 'lodash'; /** * WooCommerce dependencies */ -import { Link } from '@woocommerce/components'; import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency'; import { getNewPath, getPersistedQuery } from '@woocommerce/navigation'; +import { Link, Tag } from '@woocommerce/components'; /** * Internal dependencies */ -import ReportTable from 'analytics/components/report-table'; -import { numberFormat } from 'lib/number'; +import CategoryBreacrumbs from '../categories/breadcrumbs'; import { isLowStock } from './utils'; +import { numberFormat } from 'lib/number'; +import ReportTable from 'analytics/components/report-table'; +import withSelect from 'wc-api/with-select'; +import './style.scss'; -export default class ProductsReportTable extends Component { +class ProductsReportTable extends Component { constructor() { super(); @@ -96,10 +100,9 @@ export default class ProductsReportTable extends Component { items_sold, net_revenue, orders_count, - categories = [], // @TODO variations = [], // @TODO } = row; - const { name, stock_status, stock_quantity, low_stock_amount } = extended_info; + const { category_ids, low_stock_amount, name, stock_status, stock_quantity } = extended_info; const ordersLink = getNewPath( persistedQuery, 'orders', { filter: 'advanced', product_includes: product_id, @@ -108,6 +111,8 @@ export default class ProductsReportTable extends Component { filter: 'single_product', products: product_id, } ); + const categories = this.props.categories; + const productCategories = category_ids.map( category_id => categories[ category_id ] ); return [ { @@ -139,10 +144,29 @@ export default class ProductsReportTable extends Component { value: orders_count, }, { - display: Array.isArray( categories ) - ? categories.map( cat => cat.name ).join( ', ' ) - : '', - value: Array.isArray( categories ) ? categories.map( cat => cat.name ).join( ', ' ) : '', + display: ( +
+ { productCategories[ 0 ] && ( + + ) } + { productCategories.length > 1 && ( + ( + + ) ) } + /> + ) } +
+ ), + value: productCategories.map( category => category.name ).join( ', ' ), }, { display: numberFormat( variations.length ), @@ -219,3 +243,15 @@ export default class ProductsReportTable extends Component { ); } } + +export default compose( + withSelect( select => { + const { getCategories, getCategoriesError, isGetCategoriesRequesting } = select( 'wc-api' ); + + const categories = getCategories(); + const isError = Boolean( getCategoriesError() ); + const isRequesting = isGetCategoriesRequesting(); + + return { categories, isError, isRequesting }; + } ) +)( ProductsReportTable ); diff --git a/plugins/woocommerce-admin/client/wc-api/categories/index.js b/plugins/woocommerce-admin/client/wc-api/categories/index.js new file mode 100644 index 00000000000..cd0cff2f807 --- /dev/null +++ b/plugins/woocommerce-admin/client/wc-api/categories/index.js @@ -0,0 +1,11 @@ +/** @format */ +/** + * Internal dependencies + */ +import operations from './operations'; +import selectors from './selectors'; + +export default { + operations, + selectors, +}; diff --git a/plugins/woocommerce-admin/client/wc-api/categories/operations.js b/plugins/woocommerce-admin/client/wc-api/categories/operations.js new file mode 100644 index 00000000000..bcf891917ef --- /dev/null +++ b/plugins/woocommerce-admin/client/wc-api/categories/operations.js @@ -0,0 +1,54 @@ +/** @format */ + +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; + +/** + * WooCommerce dependencies + */ +import { stringifyQuery } from '@woocommerce/navigation'; + +/** + * Internal dependencies + */ +import { isResourcePrefix, getResourceIdentifier, getResourceName } from '../utils'; + +function read( resourceNames, fetch = apiFetch ) { + const filteredNames = resourceNames.filter( name => isResourcePrefix( name, 'category-query' ) ); + + return filteredNames.map( async resourceName => { + const query = getResourceIdentifier( resourceName ); + const url = `/wc/v3/products/categories${ stringifyQuery( query ) }`; + + try { + const response = await fetch( { + parse: false, + path: url, + } ); + + const categories = await response.json(); + const totalCount = parseInt( response.headers.get( 'x-wp-total' ) ); + const ids = categories.map( category => category.id ); + const categoryResources = categories.reduce( ( resources, category ) => { + resources[ getResourceName( 'category', category.id ) ] = { data: category }; + return resources; + }, {} ); + + return { + [ resourceName ]: { + data: ids, + totalCount, + }, + ...categoryResources, + }; + } catch ( error ) { + return { [ resourceName ]: { error } }; + } + } ); +} + +export default { + read, +}; diff --git a/plugins/woocommerce-admin/client/wc-api/categories/selectors.js b/plugins/woocommerce-admin/client/wc-api/categories/selectors.js new file mode 100644 index 00000000000..b5d0257d7b7 --- /dev/null +++ b/plugins/woocommerce-admin/client/wc-api/categories/selectors.js @@ -0,0 +1,56 @@ +/** @format */ + +/** + * External dependencies + */ +import { isNil } from 'lodash'; + +/** + * Internal dependencies + */ +import { getResourceName } from '../utils'; +import { DEFAULT_REQUIREMENT } from '../constants'; + +const getCategories = ( getResource, requireResource ) => ( + query = {}, + requirement = DEFAULT_REQUIREMENT +) => { + const resourceName = getResourceName( 'category-query', query ); + const ids = requireResource( requirement, resourceName ).data || []; + const categories = ids.reduce( + ( acc, id ) => ( { + ...acc, + [ id ]: getResource( getResourceName( 'category', id ) ).data || {}, + } ), + {} + ); + return categories; +}; + +const getCategoriesTotalCount = getResource => ( query = {} ) => { + const resourceName = getResourceName( 'category-query', query ); + return getResource( resourceName ).totalCount || 0; +}; + +const getCategoriesError = getResource => ( query = {} ) => { + const resourceName = getResourceName( 'category-query', query ); + return getResource( resourceName ).error; +}; + +const isGetCategoriesRequesting = getResource => ( query = {} ) => { + const resourceName = getResourceName( 'category-query', query ); + const { lastRequested, lastReceived } = getResource( resourceName ); + + if ( isNil( lastRequested ) || isNil( lastReceived ) ) { + return true; + } + + return lastRequested > lastReceived; +}; + +export default { + getCategories, + getCategoriesError, + getCategoriesTotalCount, + isGetCategoriesRequesting, +}; diff --git a/plugins/woocommerce-admin/client/wc-api/wc-api-spec.js b/plugins/woocommerce-admin/client/wc-api/wc-api-spec.js index 6bc9b887a9c..b14f3a5ef07 100644 --- a/plugins/woocommerce-admin/client/wc-api/wc-api-spec.js +++ b/plugins/woocommerce-admin/client/wc-api/wc-api-spec.js @@ -3,6 +3,7 @@ /** * Internal dependencies */ +import categories from './categories'; import customers from './customers'; import notes from './notes'; import orders from './orders'; @@ -17,6 +18,7 @@ function createWcApiSpec() { ...user.mutations, }, selectors: { + ...categories.selectors, ...customers.selectors, ...notes.selectors, ...orders.selectors, @@ -28,6 +30,7 @@ function createWcApiSpec() { operations: { read( resourceNames ) { return [ + ...categories.operations.read( resourceNames ), ...customers.operations.read( resourceNames ), ...notes.operations.read( resourceNames ), ...orders.operations.read( resourceNames ), diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php index 95c2bd2d242..85823327952 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -29,6 +29,10 @@ class WC_Admin_Api_Init { // Initialize Orders data store class's static vars. add_action( 'woocommerce_after_register_post_type', array( 'WC_Admin_Api_Init', 'orders_data_store_init' ), 20 ); + // Add taxonomy support for product categories. + add_filter( 'woocommerce_taxonomy_args_product_cat', array( 'WC_Admin_Api_Init', 'show_product_categories_in_rest' ) ); + // Increase per_page limit in REST response. + add_filter( 'woocommerce_rest_product_cat_query', array( 'WC_Admin_Api_Init', 'increase_per_page_limit' ) ); } /** @@ -470,6 +474,28 @@ class WC_Admin_Api_Init { add_action( 'woocommerce_after_register_post_type', array( 'WC_Admin_Api_Init', 'order_product_lookup_store_init' ), 20 ); } + /** + * Enables the WP REST API for product categories + * + * @param array $args Default arguments for product_cat taxonomy. + * @return array + */ + public static function show_product_categories_in_rest( $args ) { + $args['show_in_rest'] = true; + return $args; + } + + /** + * Increase per page limit for product categories + * + * @param array $prepared_args Prepared arguments for query. + * @return array + */ + public static function increase_per_page_limit( $prepared_args ) { + $prepared_args['number'] = PHP_INT_MAX; + return $prepared_args; + } + } new WC_Admin_Api_Init(); diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-products-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-products-data-store.php index 32ef3da268b..747073ef37f 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-products-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-products-data-store.php @@ -25,17 +25,21 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i * @var array */ protected $column_types = array( - 'date_start' => 'strval', - 'date_end' => 'strval', - 'product_id' => 'intval', - 'items_sold' => 'intval', - 'net_revenue' => 'floatval', - 'orders_count' => 'intval', + 'date_start' => 'strval', + 'date_end' => 'strval', + 'product_id' => 'intval', + 'items_sold' => 'intval', + 'net_revenue' => 'floatval', + 'orders_count' => 'intval', // Extended attributes. - 'name' => 'strval', - 'price' => 'floatval', - 'image' => 'strval', - 'permalink' => 'strval', + 'name' => 'strval', + 'price' => 'floatval', + 'image' => 'strval', + 'permalink' => 'strval', + 'stock_status' => 'strval', + 'stock_quantity' => 'intval', + 'low_stock_amount' => 'intval', + 'category_ids' => 'array_values', ); /** @@ -63,6 +67,7 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i 'stock_status', 'stock_quantity', 'low_stock_amount', + 'category_ids', ); /** diff --git a/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-products.php b/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-products.php index d8036795063..d329f24593a 100644 --- a/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-products.php +++ b/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-products.php @@ -199,6 +199,9 @@ class WC_Tests_Reports_Products extends WC_Unit_Test_Case { $product->set_low_stock_amount( 5 ); $product->save(); + $term = wp_insert_term( 'Test Category', 'product_cat' ); + wp_set_object_terms( $product->get_id(), $term['term_id'], 'product_cat' ); + $order = WC_Helper_Order::create_order( 1, $product ); $order->set_status( 'completed' ); $order->set_shipping_total( 10 ); @@ -217,7 +220,9 @@ class WC_Tests_Reports_Products extends WC_Unit_Test_Case { 'extended_info' => 1, ); // Test retrieving the stats through the data store. - $data = $data_store->get_data( $args ); + $data = $data_store->get_data( $args ); + // Get updated product data. + $product = wc_get_product( $product->get_id() ); $expected_data = (object) array( 'total' => 1, 'pages' => 1, @@ -234,8 +239,9 @@ class WC_Tests_Reports_Products extends WC_Unit_Test_Case { 'permalink' => $product->get_permalink(), 'price' => (float) $product->get_price(), 'stock_status' => $product->get_stock_status(), - 'stock_quantity' => $product->get_stock_quantity() - 4, // subtract the ones purchased. + 'stock_quantity' => $product->get_stock_quantity(), 'low_stock_amount' => $product->get_low_stock_amount(), + 'category_ids' => array_values( $product->get_category_ids() ), ), ), ), From 56577101c4077d9d5e6e04a9ee223282feb20e96 Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Fri, 21 Dec 2018 12:07:54 +0800 Subject: [PATCH 10/53] Add customer numeric filters (https://github.com/woocommerce/woocommerce-admin/pull/1116) * Add customer numeric filters * Add between params to customers controller * Escape strings for translation and change inputs to currency * Add Currency shorthand filter --- .../analytics/report/customers/config.js | 85 ++++++++++++++++++ ...dmin-rest-reports-customers-controller.php | 88 +++++++++++-------- .../components/src/filters/advanced/index.js | 9 ++ 3 files changed, 147 insertions(+), 35 deletions(-) diff --git a/plugins/woocommerce-admin/client/analytics/report/customers/config.js b/plugins/woocommerce-admin/client/analytics/report/customers/config.js index 5f5055234c7..181f776b911 100644 --- a/plugins/woocommerce-admin/client/analytics/report/customers/config.js +++ b/plugins/woocommerce-admin/client/analytics/report/customers/config.js @@ -166,6 +166,91 @@ export const advancedFilters = { } ) ), }, }, + order_count: { + labels: { + add: __( 'No. of Orders', 'wc-admin' ), + remove: __( 'Remove order filter', 'wc-admin' ), + rule: __( 'Select an order count filter match', 'wc-admin' ), + title: __( 'No. of Orders {{rule /}} {{filter /}}', 'wc-admin' ), + }, + rules: [ + { + value: 'max', + /* translators: Sentence fragment, logical, "Less Than" refers to number of orders a customer has placed, less than a given amount. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */ + label: _x( 'Less Than', 'number of orders', 'wc-admin' ), + }, + { + value: 'min', + /* translators: Sentence fragment, logical, "More Than" refers to number of orders a customer has placed, more than a given amount. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */ + label: _x( 'More Than', 'number of orders', 'wc-admin' ), + }, + { + value: 'between', + /* translators: Sentence fragment, logical, "Between" refers to number of orders a customer has placed, between two given integers. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */ + label: _x( 'Between', 'number of orders', 'wc-admin' ), + }, + ], + input: { + component: 'Number', + }, + }, + total_spend: { + labels: { + add: __( 'Total Spend', 'wc-admin' ), + remove: __( 'Remove total spend filter', 'wc-admin' ), + rule: __( 'Select a total spend filter match', 'wc-admin' ), + title: __( 'Total Spend {{rule /}} {{filter /}}', 'wc-admin' ), + }, + rules: [ + { + value: 'max', + /* translators: Sentence fragment, logical, "Less Than" refers to total spending by a customer, less than a given amount. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */ + label: _x( 'Less Than', 'total spend by customer', 'wc-admin' ), + }, + { + value: 'min', + /* translators: Sentence fragment, logical, "Less Than" refers to total spending by a customer, more than a given amount. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */ + label: _x( 'More Than', 'total spend by customer', 'wc-admin' ), + }, + { + value: 'between', + /* translators: Sentence fragment, logical, "Between" refers to total spending by a customer, between two given amounts. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */ + label: _x( 'Between', 'total spend by customer', 'wc-admin' ), + }, + ], + input: { + component: 'Currency', + }, + }, + avg_order_value: { + labels: { + add: __( 'AOV', 'wc-admin' ), + remove: __( 'Remove average older value filter', 'wc-admin' ), + rule: __( 'Select an average order value filter match', 'wc-admin' ), + title: __( 'AOV {{rule /}} {{filter /}}', 'wc-admin' ), + }, + rules: [ + { + value: 'max', + /* translators: Sentence fragment, logical, "Less Than" refers to average order value of a customer, more than a given amount. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */ + label: _x( 'Less Than', 'average order value of customer', 'wc-admin' ), + }, + { + value: 'min', + /* translators: Sentence fragment, logical, "Less Than" refers to average order value of a customer, less than a given amount. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */ + + label: _x( 'More Than', 'average order value of customer', 'wc-admin' ), + }, + { + value: 'between', + /* translators: Sentence fragment, logical, "Between" refers to average order value of a customer, between two given amounts. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */ + label: _x( 'Between', 'average order value of customer', 'wc-admin' ), + }, + ], + input: { + component: 'Currency', + }, + }, }, }; /*eslint-enable max-len*/ diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php index b9d7821cdd4..6165b3a97f6 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php @@ -38,23 +38,26 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control * @return array */ protected function prepare_reports_query( $request ) { - $args = array(); - $args['before'] = $request['before']; - $args['after'] = $request['after']; - $args['page'] = $request['page']; - $args['per_page'] = $request['per_page']; - $args['name'] = $request['name']; - $args['username'] = $request['username']; - $args['email'] = $request['email']; - $args['country'] = $request['country']; - $args['last_active_before'] = $request['last_active_before']; - $args['last_active_after'] = $request['last_active_after']; - $args['order_count_min'] = $request['order_count_min']; - $args['order_count_max'] = $request['order_count_max']; - $args['total_spend_min'] = $request['total_spend_min']; - $args['total_spend_max'] = $request['total_spend_max']; - $args['avg_order_value_min'] = $request['avg_order_value_min']; - $args['avg_order_value_max'] = $request['avg_order_value_max']; + $args = array(); + $args['before'] = $request['before']; + $args['after'] = $request['after']; + $args['page'] = $request['page']; + $args['per_page'] = $request['per_page']; + $args['name'] = $request['name']; + $args['username'] = $request['username']; + $args['email'] = $request['email']; + $args['country'] = $request['country']; + $args['last_active_before'] = $request['last_active_before']; + $args['last_active_after'] = $request['last_active_after']; + $args['order_count_min'] = $request['order_count_min']; + $args['order_count_max'] = $request['order_count_max']; + $args['order_count_between'] = $request['order_count_between']; + $args['total_spend_min'] = $request['total_spend_min']; + $args['total_spend_max'] = $request['total_spend_max']; + $args['total_spend_between'] = $request['total_spend_between']; + $args['avg_order_value_min'] = $request['avg_order_value_min']; + $args['avg_order_value_max'] = $request['avg_order_value_max']; + $args['avg_order_value_between'] = $request['avg_order_value_between']; return $args; } @@ -204,21 +207,21 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control * @return array */ public function get_collection_params() { - $params = array(); - $params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); - $params['before'] = array( + $params = array(); + $params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); + $params['before'] = array( 'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'wc-admin' ), 'type' => 'string', 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); - $params['after'] = array( + $params['after'] = array( 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'wc-admin' ), 'type' => 'string', 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); - $params['page'] = array( + $params['page'] = array( 'description' => __( 'Current page of the collection.', 'wc-admin' ), 'type' => 'integer', 'default' => 1, @@ -226,7 +229,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control 'validate_callback' => 'rest_validate_request_arg', 'minimum' => 1, ); - $params['per_page'] = array( + $params['per_page'] = array( 'description' => __( 'Maximum number of items to be returned in result set.', 'wc-admin' ), 'type' => 'integer', 'default' => 10, @@ -235,70 +238,85 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); - $params['name'] = array( + $params['name'] = array( 'description' => __( 'Limit response to objects with a specfic customer name.', 'wc-admin' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); - $params['username'] = array( + $params['username'] = array( 'description' => __( 'Limit response to objects with a specfic username.', 'wc-admin' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); - $params['email'] = array( + $params['email'] = array( 'description' => __( 'Limit response to objects equal to an email.', 'wc-admin' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); - $params['country'] = array( + $params['country'] = array( 'description' => __( 'Limit response to objects with a specfic country.', 'wc-admin' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); - $params['last_active_before'] = array( + $params['last_active_before'] = array( 'description' => __( 'Limit response to objects last active before (or at) a given ISO8601 compliant datetime.', 'wc-admin' ), 'type' => 'string', 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); - $params['last_active_after'] = array( + $params['last_active_after'] = array( 'description' => __( 'Limit response to objects last active after (or at) a given ISO8601 compliant datetime.', 'wc-admin' ), 'type' => 'string', 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); - $params['order_count_min'] = array( + $params['order_count_min'] = array( 'description' => __( 'Limit response to objects with an order count greater than or equal to given integer.', 'wc-admin' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); - $params['order_count_max'] = array( + $params['order_count_max'] = array( 'description' => __( 'Limit response to objects with an order count less than or equal to given integer.', 'wc-admin' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); - $params['total_spend_min'] = array( + $params['order_count_between'] = array( + 'description' => __( 'Limit response to objects with an order count between two given integers.', 'wc-admin' ), + 'type' => 'array', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['total_spend_min'] = array( 'description' => __( 'Limit response to objects with a total order spend greater than or equal to given number.', 'wc-admin' ), 'type' => 'number', 'validate_callback' => 'rest_validate_request_arg', ); - $params['total_spend_max'] = array( + $params['total_spend_max'] = array( 'description' => __( 'Limit response to objects with a total order spend less than or equal to given number.', 'wc-admin' ), 'type' => 'number', 'validate_callback' => 'rest_validate_request_arg', ); - $params['avg_order_value_min'] = array( + $params['total_spend_between'] = array( + 'description' => __( 'Limit response to objects with a total order spend between two given numbers.', 'wc-admin' ), + 'type' => 'array', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['avg_order_value_min'] = array( 'description' => __( 'Limit response to objects with an average order spend greater than or equal to given number.', 'wc-admin' ), 'type' => 'number', 'validate_callback' => 'rest_validate_request_arg', ); - $params['avg_order_value_max'] = array( + $params['avg_order_value_max'] = array( 'description' => __( 'Limit response to objects with an average order spend less than or equal to given number.', 'wc-admin' ), 'type' => 'number', 'validate_callback' => 'rest_validate_request_arg', ); + $params['avg_order_value_between'] = array( + 'description' => __( 'Limit response to objects with an average order spend between two given numbers.', 'wc-admin' ), + 'type' => 'array', + 'validate_callback' => 'rest_validate_request_arg', + ); return $params; } } diff --git a/plugins/woocommerce-admin/packages/components/src/filters/advanced/index.js b/plugins/woocommerce-admin/packages/components/src/filters/advanced/index.js index e52566c8958..4b39460a513 100644 --- a/plugins/woocommerce-admin/packages/components/src/filters/advanced/index.js +++ b/plugins/woocommerce-admin/packages/components/src/filters/advanced/index.js @@ -201,6 +201,15 @@ class AdvancedFilters extends Component { query={ query } /> ) } + { 'Currency' === input.component && ( + + ) } Date: Fri, 21 Dec 2018 17:57:46 -0500 Subject: [PATCH 11/53] Add `reports/downloads` REST API endpoint. (https://github.com/woocommerce/woocommerce-admin/pull/1122) * First pass at downloads REST API * Handle PR feedback --- ...dmin-rest-reports-downloads-controller.php | 330 ++++++++++++++ .../includes/class-wc-admin-api-init.php | 5 + ...class-wc-admin-reports-downloads-query.php | 47 ++ .../class-wc-admin-reports-data-store.php | 59 +++ ...-wc-admin-reports-downloads-data-store.php | 383 +++++++++++++++++ .../tests/api/reports-downloads.php | 401 ++++++++++++++++++ 6 files changed, 1225 insertions(+) create mode 100644 plugins/woocommerce-admin/includes/class-wc-admin-reports-downloads-query.php create mode 100644 plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-downloads-data-store.php create mode 100644 plugins/woocommerce-admin/tests/api/reports-downloads.php diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-downloads-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-downloads-controller.php index e70a3538c30..3dc6aaf8bf9 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-downloads-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-downloads-controller.php @@ -30,4 +30,334 @@ class WC_Admin_REST_Reports_Downloads_Controller extends WC_REST_Reports_Control * @var string */ protected $rest_base = 'reports/downloads'; + + /** + * Get items. + * + * @param WP_REST_Request $request Request data. + * + * @return array|WP_Error + */ + public function get_items( $request ) { + $args = array(); + $registered = array_keys( $this->get_collection_params() ); + foreach ( $registered as $param_name ) { + if ( isset( $request[ $param_name ] ) ) { + $args[ $param_name ] = $request[ $param_name ]; + } + } + + $reports = new WC_Admin_Reports_Downloads_Query( $args ); + $downloads_data = $reports->get_data(); + + $data = array(); + + foreach ( $downloads_data->data as $download_data ) { + $item = $this->prepare_item_for_response( $download_data, $request ); + $data[] = $this->prepare_response_for_collection( $item ); + } + + $response = rest_ensure_response( $data ); + + $response->header( 'X-WP-Total', (int) $downloads_data->total ); + $response->header( 'X-WP-TotalPages', (int) $downloads_data->pages ); + + $page = $downloads_data->page_no; + $max_pages = $downloads_data->pages; + $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Prepare a report object for serialization. + * + * @param Array $report Report data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_item_for_response( $report, $request ) { + $data = $report; + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $report ) ); + + $response->data['date'] = get_date_from_gmt( $data['date_gmt'], 'Y-m-d H:i:s' ); + + // Figure out file name. + // Matches https://github.com/woocommerce/woocommerce/blob/4be0018c092e617c5d2b8c46b800eb71ece9ddef/includes/class-wc-download-handler.php#L197. + $product_id = intval( $data['product_id'] ); + $_product = wc_get_product( $product_id ); + $file_path = $_product->get_file_download_path( $data['download_id'] ); + $filename = basename( $file_path ); + $response->data['file_name'] = apply_filters( 'woocommerce_file_download_filename', $filename, $product_id ); + + /** + * Filter a report returned from the API. + * + * Allows modification of the report data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $report The original report object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_report_downloads', $response, $report, $request ); + } + + /** + * Prepare links for the request. + * + * @param Array $object Object data. + * @return array Links for the given post. + */ + protected function prepare_links( $object ) { + $links = array( + 'product' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, 'products', $object['product_id'] ) ), + 'embeddable' => true, + ), + 'user' => array( + 'href' => rest_url( 'wp/v2/users/' . $object['user_id'] ), + 'embeddable' => true, + ), + ); + + return $links; + } + + /** + * Get the Report's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'report_downloads', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'ID.', 'wc-admin' ), + ), + 'product_id' => array( + 'type' => 'integer', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'Product ID.', 'wc-admin' ), + ), + 'date' => array( + 'description' => __( "The date of the download, in the site's timezone.", 'wc-admin' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_gmt' => array( + 'description' => __( 'The date of the download, as GMT.', 'wc-admin' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'download_id' => array( + 'type' => 'string', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'Download ID.', 'wc-admin' ), + ), + 'file_name' => array( + 'type' => 'string', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'File name.', 'wc-admin' ), + ), + 'product_id' => array( + 'type' => 'integer', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'Product ID.', 'wc-admin' ), + ), + 'order_id' => array( + 'type' => 'integer', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'Order ID.', 'wc-admin' ), + ), + 'user_id' => array( + 'type' => 'integer', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'User ID for the downloader.', 'wc-admin' ), + ), + 'ip_address' => array( + 'type' => 'string', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'IP address for the downloader.', 'wc-admin' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = array(); + $params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); + $params['page'] = array( + 'description' => __( 'Current page of the collection.', 'wc-admin' ), + 'type' => 'integer', + 'default' => 1, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + 'minimum' => 1, + ); + $params['per_page'] = array( + 'description' => __( 'Maximum number of items to be returned in result set.', 'wc-admin' ), + 'type' => 'integer', + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['after'] = array( + 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'wc-admin' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['before'] = array( + 'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'wc-admin' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['order'] = array( + 'description' => __( 'Order sort attribute ascending or descending.', 'wc-admin' ), + 'type' => 'string', + 'default' => 'desc', + 'enum' => array( 'asc', 'desc' ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'description' => __( 'Sort collection by object attribute.', 'wc-admin' ), + 'type' => 'string', + 'default' => 'date', + 'enum' => array( + 'date', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['match'] = array( + 'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: products, orders, username, ip_address.', 'wc-admin' ), + 'type' => 'string', + 'default' => 'all', + 'enum' => array( + 'all', + 'any', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['product_includes'] = array( + 'description' => __( 'Limit result set to items that have the specified product(s) assigned.', 'wc-admin' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + + ); + $params['product_excludes'] = array( + 'description' => __( 'Limit result set to items that don\'t have the specified product(s) assigned.', 'wc-admin' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'validate_callback' => 'rest_validate_request_arg', + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['order_includes'] = array( + 'description' => __( 'Limit result set to items that have the specified order ids.', 'wc-admin' ), + 'type' => 'array', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + 'items' => array( + 'type' => 'integer', + ), + ); + $params['order_excludes'] = array( + 'description' => __( 'Limit result set to items that don\'t have the specified order ids.', 'wc-admin' ), + 'type' => 'array', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + 'items' => array( + 'type' => 'integer', + ), + ); + $params['user_includes'] = array( + 'description' => __( 'Limit response to objects that have the specified user ids.', 'wc-admin' ), + 'type' => 'array', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + 'items' => array( + 'type' => 'integer', + ), + ); + $params['user_excludes'] = array( + 'description' => __( 'Limit response to objects that don\'t have the specified user ids.', 'wc-admin' ), + 'type' => 'array', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + 'items' => array( + 'type' => 'integer', + ), + ); + $params['ip_address_includes'] = array( + 'description' => __( 'Limit response to objects that have a specified ip address.', 'wc-admin' ), + 'type' => 'array', + 'validate_callback' => 'rest_validate_request_arg', + 'items' => array( + 'type' => 'string', + ), + ); + + $params['ip_address_excludes'] = array( + 'description' => __( 'Limit response to objects that don\'t have a specified ip address.', 'wc-admin' ), + 'type' => 'array', + 'validate_callback' => 'rest_validate_request_arg', + 'items' => array( + 'type' => 'string', + ), + ); + + return $params; + } } diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php index 85823327952..4838ef7a632 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -57,6 +57,7 @@ class WC_Admin_Api_Init { require_once dirname( __FILE__ ) . '/class-wc-admin-reports-taxes-stats-query.php'; require_once dirname( __FILE__ ) . '/class-wc-admin-reports-coupons-query.php'; require_once dirname( __FILE__ ) . '/class-wc-admin-reports-coupons-stats-query.php'; + require_once dirname( __FILE__ ) . '/class-wc-admin-reports-downloads-query.php'; // Data stores. require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-data-store.php'; @@ -69,6 +70,7 @@ class WC_Admin_Api_Init { require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-taxes-stats-data-store.php'; require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-coupons-data-store.php'; require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-coupons-stats-data-store.php'; + require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-downloads-data-store.php'; // Data triggers. require_once dirname( __FILE__ ) . '/wc-admin-order-functions.php'; @@ -104,6 +106,7 @@ class WC_Admin_Api_Init { require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-stats-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-stock-controller.php'; + require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-downloads-controller.php'; $controllers = array( 'WC_Admin_REST_Admin_Notes_Controller', @@ -123,6 +126,7 @@ class WC_Admin_Api_Init { 'WC_Admin_REST_Reports_Coupons_Controller', 'WC_Admin_REST_Reports_Coupons_Stats_Controller', 'WC_Admin_REST_Reports_Stock_Controller', + 'WC_Admin_REST_Reports_Downloads_Controller', ); foreach ( $controllers as $controller ) { @@ -335,6 +339,7 @@ class WC_Admin_Api_Init { 'report-taxes-stats' => 'WC_Admin_Reports_Taxes_Stats_Data_Store', 'report-coupons' => 'WC_Admin_Reports_Coupons_Data_Store', 'report-coupons-stats' => 'WC_Admin_Reports_Coupons_Stats_Data_Store', + 'report-downloads' => 'WC_Admin_Reports_Downloads_Data_Store', 'admin-note' => 'WC_Admin_Notes_Data_Store', ) ); diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-reports-downloads-query.php b/plugins/woocommerce-admin/includes/class-wc-admin-reports-downloads-query.php new file mode 100644 index 00000000000..aaeaa25c3c0 --- /dev/null +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-downloads-query.php @@ -0,0 +1,47 @@ + '2018-07-19 00:00:00', + * 'after' => '2018-07-05 00:00:00', + * 'page' => 2, + * 'products' => array(1,2,3) + * ); + * $report = new WC_Admin_Reports_Downloads_Query( $args ); + * $mydata = $report->get_data(); + * + * @package WooCommerce Admin/Classes + */ + +defined( 'ABSPATH' ) || exit; + +/** + * WC_Admin_Reports_Downloads_Query + */ +class WC_Admin_Reports_Downloads_Query extends WC_Admin_Reports_Query { + + /** + * Valid fields for downloads report. + * + * @return array + */ + protected function get_default_query_vars() { + return array(); + } + + /** + * Get downloads data based on the current query vars. + * + * @return array + */ + public function get_data() { + $args = apply_filters( 'woocommerce_reports_downloads_query_args', $this->get_query_vars() ); + + $data_store = WC_Data_Store::load( 'report-downloads' ); + $results = $data_store->get_data( $args ); + return apply_filters( 'woocommerce_reports_downloads_select_query', $results, $args ); + } + +} diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php index a4b2e34ab1d..e9964d9fb08 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php @@ -634,6 +634,65 @@ class WC_Admin_Reports_Data_Store { return $excluded_coupons_str; } + /** + * Returns comma separated ids of included orders, based on query arguments from the user. + * + * @param array $query_args Parameters supplied by the user. + * @return string + */ + protected function get_included_orders( $query_args ) { + $included_orders_str = ''; + + if ( isset( $query_args['order_includes'] ) && is_array( $query_args['order_includes'] ) && count( $query_args['order_includes'] ) > 0 ) { + $included_orders_str = implode( ',', $query_args['order_includes'] ); + } + return $included_orders_str; + } + + /** + * Returns comma separated ids of excluded orders, based on query arguments from the user. + * + * @param array $query_args Parameters supplied by the user. + * @return string + */ + protected function get_excluded_orders( $query_args ) { + $excluded_orders_str = ''; + + if ( isset( $query_args['order_excludes'] ) && is_array( $query_args['order_excludes'] ) && count( $query_args['order_excludes'] ) > 0 ) { + $excluded_orders_str = implode( ',', $query_args['order_excludes'] ); + } + return $excluded_orders_str; + } + + /** + * Returns comma separated ids of included users, based on query arguments from the user. + * + * @param array $query_args Parameters supplied by the user. + * @return string + */ + protected function get_included_users( $query_args ) { + $included_users_str = ''; + + if ( isset( $query_args['user_includes'] ) && is_array( $query_args['user_includes'] ) && count( $query_args['user_includes'] ) > 0 ) { + $included_users_str = implode( ',', $query_args['user_includes'] ); + } + return $included_users_str; + } + + /** + * Returns comma separated ids of excluded users, based on query arguments from the user. + * + * @param array $query_args Parameters supplied by the user. + * @return string + */ + protected function get_excluded_users( $query_args ) { + $excluded_users_str = ''; + + if ( isset( $query_args['user_excludes'] ) && is_array( $query_args['user_excludes'] ) && count( $query_args['user_excludes'] ) > 0 ) { + $excluded_users_str = implode( ',', $query_args['user_excludes'] ); + } + return $excluded_users_str; + } /** * Returns order status subquery to be used in WHERE SQL query, based on query arguments from the user. diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-downloads-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-downloads-data-store.php new file mode 100644 index 00000000000..86555a8b1ec --- /dev/null +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-downloads-data-store.php @@ -0,0 +1,383 @@ + 'intval', + 'date' => 'strval', + 'date_gmt' => 'strval', + 'download_id' => 'strval', // String because this can sometimes be a hash. + 'file_name' => 'strval', + 'product_id' => 'intval', + 'order_id' => 'intval', + 'user_id' => 'intval', + 'ip_address' => 'strval', + ); + + /** + * SQL columns to select in the db query and their mapping to SQL code. + * + * @var array + */ + protected $report_columns = array( + 'id' => 'download_log_id as id', + 'date' => 'timestamp as date_gmt', + 'download_id' => 'product_permissions.download_id', + 'product_id' => 'product_permissions.product_id', + 'order_id' => 'product_permissions.order_id', + 'user_id' => 'product_permissions.user_id', + 'ip_address' => 'user_ip_address as ip_address', + ); + + /** + * Constructor + */ + public function __construct() { + global $wpdb; + } + + /** + * Updates the database query with parameters used for downloads report. + * + * @param array $query_args Query arguments supplied by the user. + * @return array Array of parameters used for SQL query. + */ + protected function get_sql_query_params( $query_args ) { + global $wpdb; + + $lookup_table = $wpdb->prefix . self::TABLE_NAME; + $operator = $this->get_match_operator( $query_args ); + $where_filters = array(); + + $sql_query_params = $this->get_time_period_sql_params( $query_args, $lookup_table ); + $sql_query_params = array_merge( $sql_query_params, $this->get_limit_sql_params( $query_args ) ); + $sql_query_params = array_merge( $sql_query_params, $this->get_order_by_sql_params( $query_args ) ); + + $included_products = $this->get_included_products( $query_args ); + $excluded_products = $this->get_excluded_products( $query_args ); + if ( $included_products ) { + $where_filters[] = " {$lookup_table}.permission_id IN ( + SELECT + DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id + FROM + {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE + {$wpdb->prefix}woocommerce_downloadable_product_permissions.product_id IN ({$included_products}) + )"; + } + + if ( $excluded_products ) { + $where_filters[] = " {$lookup_table}.permission_id NOT IN ( + SELECT + DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id + FROM + {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE + {$wpdb->prefix}woocommerce_downloadable_product_permissions.product_id IN ({$excluded_products}) + )"; + } + + $included_orders = $this->get_included_orders( $query_args ); + $excluded_orders = $this->get_excluded_orders( $query_args ); + if ( $included_orders ) { + $where_filters[] = " {$lookup_table}.permission_id IN ( + SELECT + DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id + FROM + {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE + {$wpdb->prefix}woocommerce_downloadable_product_permissions.order_id IN ({$included_orders}) + )"; + } + + if ( $excluded_orders ) { + $where_filters[] = " {$lookup_table}.permission_id NOT IN ( + SELECT + DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id + FROM + {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE + {$wpdb->prefix}woocommerce_downloadable_product_permissions.order_id IN ({$excluded_orders}) + )"; + } + + $included_users = $this->get_included_users( $query_args ); + $excluded_users = $this->get_excluded_users( $query_args ); + if ( $included_users ) { + $where_filters[] = " {$lookup_table}.permission_id IN ( + SELECT + DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id + FROM + {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE + {$wpdb->prefix}woocommerce_downloadable_product_permissions.user_id IN ({$included_users}) + )"; + } + + if ( $excluded_users ) { + $where_filters[] = " {$lookup_table}.permission_id NOT IN ( + SELECT + DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id + FROM + {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE + {$wpdb->prefix}woocommerce_downloadable_product_permissions.user_id IN ({$excluded_users}) + )"; + } + + $included_ip_addresses = $this->get_included_ip_addresses( $query_args ); + $excluded_ip_addresses = $this->get_excluded_ip_addresses( $query_args ); + if ( $included_ip_addresses ) { + $where_filters[] = " {$lookup_table}.user_ip_address IN ('{$included_ip_addresses}')"; + } + + if ( $excluded_ip_addresses ) { + $where_filters[] = " {$lookup_table}.user_ip_address NOT IN ('{$excluded_ip_addresses}')"; + } + + $where_subclause = implode( " $operator ", $where_filters ); + if ( $where_subclause ) { + $sql_query_params['where_clause'] .= " AND ( $where_subclause )"; + } + + $sql_query_params['from_clause'] .= " JOIN {$wpdb->prefix}woocommerce_downloadable_product_permissions as product_permissions ON {$lookup_table}.permission_id = product_permissions.permission_id"; + + return $sql_query_params; + } + + /** + * Returns comma separated ids of included ip address, based on query arguments from the user. + * + * @param array $query_args Parameters supplied by the user. + * @return string + */ + protected function get_included_ip_addresses( $query_args ) { + $included_ips_str = ''; + + if ( isset( $query_args['ip_address_includes'] ) && is_array( $query_args['ip_address_includes'] ) && count( $query_args['ip_address_includes'] ) > 0 ) { + $ip_includes = array(); + foreach ( $query_args['ip_address_includes'] as $ip ) { + $ip_includes[] = esc_sql( $ip ); + } + $included_ips_str = implode( "','", $ip_includes ); + } + return $included_ips_str; + } + + /** + * Returns comma separated ids of excluded ip address, based on query arguments from the user. + * + * @param array $query_args Parameters supplied by the user. + * @return string + */ + protected function get_excluded_ip_addresses( $query_args ) { + $excluded_ips_str = ''; + + if ( isset( $query_args['ip_address_excludes'] ) && is_array( $query_args['ip_address_excludes'] ) && count( $query_args['ip_address_excludes'] ) > 0 ) { + $ip_excludes = array(); + foreach ( $query_args['ip_address_excludes'] as $ip ) { + $ip_excludes[] = esc_sql( $ip ); + } + $excluded_ips_str = implode( ',', $ip_excludes ); + } + return $excluded_ips_str; + } + + + /** + * Fills WHERE clause of SQL request with date-related constraints. + * + * @param array $query_args Parameters supplied by the user. + * @param string $table_name Name of the db table relevant for the date constraint. + * @return array + */ + protected function get_time_period_sql_params( $query_args, $table_name ) { + $sql_query = array( + 'from_clause' => '', + 'where_time_clause' => '', + 'where_clause' => '', + ); + + if ( isset( $query_args['before'] ) && '' !== $query_args['before'] ) { + $datetime = new DateTime( $query_args['before'] ); + $datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format ); + $sql_query['where_time_clause'] .= " AND {$table_name}.timestamp <= '$datetime_str'"; + + } + + if ( isset( $query_args['after'] ) && '' !== $query_args['after'] ) { + $datetime = new DateTime( $query_args['after'] ); + $datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format ); + $sql_query['where_time_clause'] .= " AND {$table_name}.timestamp >= '$datetime_str'"; + } + + return $sql_query; + } + + /** + * Fills ORDER BY clause of SQL request based on user supplied parameters. + * + * @param array $query_args Parameters supplied by the user. + * @return array + */ + protected function get_order_by_sql_params( $query_args ) { + $sql_query['order_by_clause'] = ''; + if ( isset( $query_args['orderby'] ) ) { + $sql_query['order_by_clause'] = $this->normalize_order_by( $query_args['orderby'] ); + } + + if ( isset( $query_args['order'] ) ) { + $sql_query['order_by_clause'] .= ' ' . $query_args['order']; + } else { + $sql_query['order_by_clause'] .= ' DESC'; + } + + return $sql_query; + } + + /** + * Returns the report data based on parameters supplied by the user. + * + * @param array $query_args Query parameters. + * @return stdClass|WP_Error Data. + */ + public function get_data( $query_args ) { + global $wpdb; + + $table_name = $wpdb->prefix . self::TABLE_NAME; + $now = time(); + $week_back = $now - WEEK_IN_SECONDS; + + // These defaults are only partially applied when used via REST API, as that has its own defaults. + $defaults = array( + 'per_page' => get_option( 'posts_per_page' ), + 'page' => 1, + 'order' => 'DESC', + 'orderby' => 'timestamp', + 'before' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $now ), + 'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ), + 'fields' => '*', + ); + $query_args = wp_parse_args( $query_args, $defaults ); + + $cache_key = $this->get_cache_key( $query_args ); + $data = wp_cache_get( $cache_key, $this->cache_group ); + + if ( false === $data ) { + $data = (object) array( + 'data' => array(), + 'total' => 0, + 'pages' => 0, + 'page_no' => 0, + ); + + $selections = $this->selected_columns( $query_args ); + $sql_query_params = $this->get_sql_query_params( $query_args ); + + $db_records_count = (int) $wpdb->get_var( + "SELECT COUNT(*) FROM ( + SELECT + {$table_name}.download_log_id + FROM + {$table_name} + {$sql_query_params['from_clause']} + WHERE + 1=1 + {$sql_query_params['where_time_clause']} + {$sql_query_params['where_clause']} + GROUP BY + {$table_name}.download_log_id + ) AS tt" + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + $total_pages = (int) ceil( $db_records_count / $sql_query_params['per_page'] ); + if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) { + return $data; + } + + $download_data = $wpdb->get_results( + "SELECT + {$selections} + FROM + {$table_name} + {$sql_query_params['from_clause']} + WHERE + 1=1 + {$sql_query_params['where_time_clause']} + {$sql_query_params['where_clause']} + GROUP BY + {$table_name}.download_log_id + ORDER BY + {$sql_query_params['order_by_clause']} + {$sql_query_params['limit']} + ", + ARRAY_A + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + if ( null === $download_data ) { + return $data; + } + + $download_data = array_map( array( $this, 'cast_numbers' ), $download_data ); + $data = (object) array( + 'data' => $download_data, + 'total' => $db_records_count, + 'pages' => $total_pages, + 'page_no' => (int) $query_args['page'], + ); + + wp_cache_set( $cache_key, $data, $this->cache_group ); + } + + return $data; + } + + /** + * Returns string to be used as cache key for the data. + * + * @param array $params Query parameters. + * @return string + */ + protected function get_cache_key( $params ) { + return 'woocommerce_' . self::TABLE_NAME . '_' . md5( wp_json_encode( $params ) ); + } + + /** + * Maps ordering specified by the user to columns in the database/fields in the data. + * + * @param string $order_by Sorting criterion. + * @return string + */ + protected function normalize_order_by( $order_by ) { + global $wpdb; + + if ( 'date' === $order_by ) { + return $wpdb->prefix . 'wc_download_log.timestamp'; + } + + return $order_by; + } + +} diff --git a/plugins/woocommerce-admin/tests/api/reports-downloads.php b/plugins/woocommerce-admin/tests/api/reports-downloads.php new file mode 100644 index 00000000000..f79ad47fb4e --- /dev/null +++ b/plugins/woocommerce-admin/tests/api/reports-downloads.php @@ -0,0 +1,401 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + + $this->assertArrayHasKey( $this->endpoint, $routes ); + } + + /** + * Test getting report. + */ + public function test_get_report() { + global $wpdb; + wp_set_current_user( $this->user ); + WC_Helper_Reports::reset_stats_dbs(); + + // Populate all of the data. + $prod_download = new WC_Product_Download(); + $prod_download->set_file( plugin_dir_url( __FILE__ ) . '/assets/images/help.png' ); + $prod_download->set_id( 1 ); + + $product = new WC_Product_Simple(); + $product->set_name( 'Test Product' ); + $product->set_downloadable( 'yes' ); + $product->set_downloads( array( $prod_download ) ); + $product->set_regular_price( 25 ); + $product->save(); + + $order = WC_Helper_Order::create_order( 1, $product ); + $order->set_status( 'completed' ); + $order->set_total( 100 ); + $order->save(); + + $download = new WC_Customer_Download(); + $download->set_user_id( $this->user ); + $download->set_order_id( $order->get_id() ); + $download->set_product_id( $product->get_id() ); + $download->set_download_id( $prod_download->get_id() ); + $download->save(); + + $object = new WC_Customer_Download_Log(); + $object->set_permission_id( $download->get_id() ); + $object->set_user_id( $this->user ); + $object->set_user_ip_address( '1.2.3.4' ); + $id = $object->save(); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); + $reports = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $reports ) ); + + $download_report = reset( $reports ); + + $this->assertEquals( 1, $download_report['download_id'] ); + $this->assertEquals( $product->get_id(), $download_report['product_id'] ); + $this->assertEquals( $order->get_id(), $download_report['order_id'] ); + $this->assertEquals( $this->user, $download_report['user_id'] ); + $this->assertEquals( '1.2.3.4', $download_report['ip_address'] ); + $this->assertEquals( 'help.png', $download_report['file_name'] ); + } + + /** + * Does some test setup so we can filter with different options in later tests. + */ + public function filter_setup() { + global $wpdb; + wp_set_current_user( $this->user ); + WC_Helper_Reports::reset_stats_dbs(); + $time = time(); + + // First set of data. + $prod_download = new WC_Product_Download(); + $prod_download->set_file( plugin_dir_url( __FILE__ ) . '/assets/images/help.png' ); + $prod_download->set_id( 1 ); + + $product = new WC_Product_Simple(); + $product->set_name( 'Test Product' ); + $product->set_downloadable( 'yes' ); + $product->set_downloads( array( $prod_download ) ); + $product->set_regular_price( 25 ); + $product->save(); + $product_1 = $product->get_id(); + + $order = WC_Helper_Order::create_order( 1, $product ); + $order->set_status( 'completed' ); + $order->set_total( 25 ); + $order->save(); + $order_1 = $order->get_id(); + + $download = new WC_Customer_Download(); + $download->set_user_id( 1 ); + $download->set_order_id( $order->get_id() ); + $download->set_product_id( $product->get_id() ); + $download->set_download_id( $prod_download->get_id() ); + $download->save(); + + $object = new WC_Customer_Download_Log(); + $object->set_permission_id( $download->get_id() ); + $object->set_user_id( 1 ); + $object->set_user_ip_address( '1.2.3.4' ); + $id = $object->save(); + + // Second set of data. + $prod_download = new WC_Product_Download(); + $prod_download->set_file( plugin_dir_url( __FILE__ ) . '/assets/images/test.png' ); + $prod_download->set_id( 2 ); + + $product = new WC_Product_Simple(); + $product->set_name( 'Test Product 2' ); + $product->set_downloadable( 'yes' ); + $product->set_downloads( array( $prod_download ) ); + $product->set_regular_price( 10 ); + $product->save(); + $product_2 = $product->get_id(); + + $order = WC_Helper_Order::create_order( 2, $product ); + $order->set_status( 'completed' ); + $order->set_total( 10 ); + $order->save(); + $order_2 = $order->get_id(); + + $download = new WC_Customer_Download(); + $download->set_user_id( 2 ); + $download->set_order_id( $order->get_id() ); + $download->set_product_id( $product->get_id() ); + $download->set_download_id( $prod_download->get_id() ); + $download->save(); + + $object = new WC_Customer_Download_Log(); + $object->set_permission_id( $download->get_id() ); + $object->set_user_id( 2 ); + $object->set_user_ip_address( '5.4.3.2.1' ); + $object->set_timestamp( date( 'Y-m-d H:00:00', $time - ( 2 * DAY_IN_SECONDS ) ) ); + $id = $object->save(); + + return array( + 'time' => $time, + 'product_1' => $product_1, + 'product_2' => $product_2, + 'order_1' => $order_1, + 'order_2' => $order_2, + ); + } + + /** + * Test getting report with date filter. + */ + public function test_get_report_with_date_filter() { + $test_info = $this->filter_setup(); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); + $reports = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 2, count( $reports ) ); + + // Test date filtering. + $request = new WP_REST_Request( 'GET', $this->endpoint ); + $request->set_query_params( + array( + 'before' => date( 'Y-m-d H:00:00', $test_info['time'] + DAY_IN_SECONDS ), + 'after' => date( 'Y-m-d H:00:00', $test_info['time'] - DAY_IN_SECONDS ), + ) + ); + $response = $this->server->dispatch( $request ); + $reports = $response->get_data(); + $download_report = reset( $reports ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $reports ) ); + $this->assertEquals( 'help.png', $download_report['file_name'] ); + } + + /** + * Test getting report with product filter. + */ + public function test_get_report_with_product_filter() { + $test_info = $this->filter_setup(); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); + $reports = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 2, count( $reports ) ); + + // Test includes filtering. + $request = new WP_REST_Request( 'GET', $this->endpoint ); + $request->set_query_params( + array( + 'product_includes' => $test_info['product_1'], + ) + ); + $response = $this->server->dispatch( $request ); + $reports = $response->get_data(); + $download_report = reset( $reports ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $reports ) ); + $this->assertEquals( 'help.png', $download_report['file_name'] ); + + // Test excludes filtering. + $request = new WP_REST_Request( 'GET', $this->endpoint ); + $request->set_query_params( + array( + 'product_excludes' => $test_info['product_1'], + ) + ); + $response = $this->server->dispatch( $request ); + $reports = $response->get_data(); + $download_report = reset( $reports ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $reports ) ); + $this->assertEquals( 'test.png', $download_report['file_name'] ); + } + + /** + * Test getting report with order filter. + */ + public function test_get_report_with_order_filter() { + $test_info = $this->filter_setup(); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); + $reports = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 2, count( $reports ) ); + + // Test includes filtering. + $request = new WP_REST_Request( 'GET', $this->endpoint ); + $request->set_query_params( + array( + 'order_includes' => $test_info['order_1'], + ) + ); + $response = $this->server->dispatch( $request ); + $reports = $response->get_data(); + $download_report = reset( $reports ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $reports ) ); + $this->assertEquals( 'help.png', $download_report['file_name'] ); + + // Test excludes filtering. + $request = new WP_REST_Request( 'GET', $this->endpoint ); + $request->set_query_params( + array( + 'order_excludes' => $test_info['order_1'], + ) + ); + $response = $this->server->dispatch( $request ); + $reports = $response->get_data(); + $download_report = reset( $reports ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $reports ) ); + $this->assertEquals( 'test.png', $download_report['file_name'] ); + } + + /** + * Test getting report with user filter. + */ + public function test_get_report_with_user_filter() { + $test_info = $this->filter_setup(); + + // Test includes filtering. + $request = new WP_REST_Request( 'GET', $this->endpoint ); + $request->set_query_params( + array( + 'user_includes' => 1, + ) + ); + $response = $this->server->dispatch( $request ); + $reports = $response->get_data(); + $download_report = reset( $reports ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $reports ) ); + $this->assertEquals( 'help.png', $download_report['file_name'] ); + $this->assertEquals( 1, $download_report['user_id'] ); + + // Test excludes filtering. + $request = new WP_REST_Request( 'GET', $this->endpoint ); + $request->set_query_params( + array( + 'user_excludes' => 1, + ) + ); + $response = $this->server->dispatch( $request ); + $reports = $response->get_data(); + $download_report = reset( $reports ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $reports ) ); + $this->assertEquals( 'test.png', $download_report['file_name'] ); + $this->assertEquals( 2, $download_report['user_id'] ); + } + + /** + * Test getting report with ip address filter. + */ + public function test_get_report_with_ip_address_filter() { + $test_info = $this->filter_setup(); + + // Test includes filtering. + $request = new WP_REST_Request( 'GET', $this->endpoint ); + $request->set_query_params( + array( + 'ip_address_includes' => '1.2.3.4', + ) + ); + $response = $this->server->dispatch( $request ); + $reports = $response->get_data(); + $download_report = reset( $reports ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $reports ) ); + $this->assertEquals( 'help.png', $download_report['file_name'] ); + + // Test excludes filtering. + $request = new WP_REST_Request( 'GET', $this->endpoint ); + $request->set_query_params( + array( + 'ip_address_excludes' => '1.2.3.4', + ) + ); + $response = $this->server->dispatch( $request ); + $reports = $response->get_data(); + $download_report = reset( $reports ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $reports ) ); + $this->assertEquals( 'test.png', $download_report['file_name'] ); + } + + /** + * Test getting reports without valid permissions. + */ + public function test_get_reports_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test reports schema. + */ + public function test_reports_schema() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'OPTIONS', $this->endpoint ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 9, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'product_id', $properties ); + $this->assertArrayHasKey( 'date', $properties ); + $this->assertArrayHasKey( 'date_gmt', $properties ); + $this->assertArrayHasKey( 'download_id', $properties ); + $this->assertArrayHasKey( 'file_name', $properties ); + $this->assertArrayHasKey( 'order_id', $properties ); + $this->assertArrayHasKey( 'user_id', $properties ); + $this->assertArrayHasKey( 'ip_address', $properties ); + } +} From ebd857cefab26aea7a6e85d0761f7412431c5bda Mon Sep 17 00:00:00 2001 From: Robert Elliott Date: Sat, 22 Dec 2018 02:24:26 +0200 Subject: [PATCH 12/53] Add/chart dashboard block (https://github.com/woocommerce/woocommerce-admin/pull/1002) * added new block and dashboard charts section title and filters... lots of duplicate keys toggle chart types in block heading * Updates per feedback. * Add some documentaiton about new interactive prop on chart/legend * Revert a few files that only had whitespace changes. --- .../components/report-chart/index.js | 5 +- .../dashboard/dashboard-charts/block.js | 51 +++++++ .../dashboard/dashboard-charts/block.scss | 38 +++++ .../dashboard/dashboard-charts/config.js | 39 ++++++ .../dashboard/dashboard-charts/index.js | 130 ++++++++++++++++++ .../dashboard/dashboard-charts/style.scss | 18 +++ .../client/dashboard/index.js | 2 + .../packages/components/CHANGELOG.md | 4 + .../components/src/chart/d3chart/legend.js | 8 +- .../packages/components/src/chart/index.js | 84 +++++------ 10 files changed, 337 insertions(+), 42 deletions(-) create mode 100644 plugins/woocommerce-admin/client/dashboard/dashboard-charts/block.js create mode 100644 plugins/woocommerce-admin/client/dashboard/dashboard-charts/block.scss create mode 100644 plugins/woocommerce-admin/client/dashboard/dashboard-charts/config.js create mode 100644 plugins/woocommerce-admin/client/dashboard/dashboard-charts/index.js create mode 100644 plugins/woocommerce-admin/client/dashboard/dashboard-charts/style.scss diff --git a/plugins/woocommerce-admin/client/analytics/components/report-chart/index.js b/plugins/woocommerce-admin/client/analytics/components/report-chart/index.js index 85f991720f7..cf2edcf4c34 100644 --- a/plugins/woocommerce-admin/client/analytics/components/report-chart/index.js +++ b/plugins/woocommerce-admin/client/analytics/components/report-chart/index.js @@ -65,7 +65,7 @@ export class ReportChart extends Component { } render() { - const { query, itemsLabel, path, primaryData, secondaryData, selectedChart } = this.props; + const { query, itemsLabel, mode, path, primaryData, secondaryData, selectedChart } = this.props; if ( primaryData.isError || secondaryData.isError ) { return ; @@ -111,7 +111,7 @@ export class ReportChart extends Component { type={ getChartTypeForQuery( query ) } allowedIntervals={ allowedIntervals } itemsLabel={ itemsLabel } - mode={ this.getChartMode() } + mode={ mode || this.getChartMode() } tooltipLabelFormat={ formats.tooltipLabelFormat } tooltipValueFormat={ getTooltipValueFormat( selectedChart.type ) } tooltipTitle={ selectedChart.label } @@ -128,6 +128,7 @@ export class ReportChart extends Component { ReportChart.propTypes = { filters: PropTypes.array, itemsLabel: PropTypes.string, + mode: PropTypes.string, path: PropTypes.string.isRequired, primaryData: PropTypes.object.isRequired, query: PropTypes.object.isRequired, diff --git a/plugins/woocommerce-admin/client/dashboard/dashboard-charts/block.js b/plugins/woocommerce-admin/client/dashboard/dashboard-charts/block.js new file mode 100644 index 00000000000..8c8e56ffabc --- /dev/null +++ b/plugins/woocommerce-admin/client/dashboard/dashboard-charts/block.js @@ -0,0 +1,51 @@ +/** @format */ +/** + * External dependencies + */ +import { Component, Fragment } from '@wordpress/element'; +import PropTypes from 'prop-types'; + +/** + * WooCommerce dependencies + */ +import { Card } from '@woocommerce/components'; + +/** + * Internal dependencies + */ +import ReportChart from 'analytics/components/report-chart'; +import './block.scss'; + +class ChartBlock extends Component { + render() { + const { charts, endpoint, path, query } = this.props; + + if ( ! charts || ! charts.length ) { + return null; + } + + return ( + + + + + + ); + } +} + +ChartBlock.propTypes = { + charts: PropTypes.array, + endpoint: PropTypes.string.isRequired, + path: PropTypes.string.isRequired, + query: PropTypes.object.isRequired, +}; + +export default ChartBlock; diff --git a/plugins/woocommerce-admin/client/dashboard/dashboard-charts/block.scss b/plugins/woocommerce-admin/client/dashboard/dashboard-charts/block.scss new file mode 100644 index 00000000000..e9c14d280ed --- /dev/null +++ b/plugins/woocommerce-admin/client/dashboard/dashboard-charts/block.scss @@ -0,0 +1,38 @@ +/** @format */ + +.woocommerce-dashboard__chart-block { + .woocommerce-card__body { + padding: 0; + position: relative; + + .woocommerce-chart { + border: none; + margin-bottom: 0; + margin-top: 0; + + .woocommerce-legend__item > button { + cursor: default; + &:hover { + background: $core-grey-light-100; + } + .woocommerce-legend__item-container { + cursor: default; + .woocommerce-legend__item-checkmark.woocommerce-legend__item-checkmark-checked::after { + display: none; + } + } + } + + &:hover { + background: $core-grey-light-100; + .woocommerce-legend__item > button { + background: $core-grey-light-100; + } + } + } + } + + &:hover { + background: $core-grey-light-100; + } +} diff --git a/plugins/woocommerce-admin/client/dashboard/dashboard-charts/config.js b/plugins/woocommerce-admin/client/dashboard/dashboard-charts/config.js new file mode 100644 index 00000000000..073d5bd747d --- /dev/null +++ b/plugins/woocommerce-admin/client/dashboard/dashboard-charts/config.js @@ -0,0 +1,39 @@ +/** @format */ +/** + * Internal dependencies + */ + +import { charts as ordersCharts } from 'analytics/report/orders/config'; +import { charts as productsCharts } from 'analytics/report/products/config'; +import { charts as revenueCharts } from 'analytics/report/revenue/config'; +import { charts as categoriesCharts } from 'analytics/report/categories/config'; +import { charts as couponsCharts } from 'analytics/report/coupons/config'; +import { charts as taxesCharts } from 'analytics/report/taxes/config'; + +const allCharts = ordersCharts + .map( d => ( { ...d, endpoint: 'orders' } ) ) + .concat( + productsCharts.map( d => ( { ...d, endpoint: 'products' } ) ), + revenueCharts.map( d => ( { ...d, endpoint: 'revenue' } ) ), + categoriesCharts.map( d => ( { ...d, endpoint: 'categories' } ) ), + couponsCharts.map( d => ( { ...d, endpoint: 'orders' } ) ), + taxesCharts.map( d => ( { ...d, endpoint: 'taxes' } ) ) + ); + +// Need to remove duplicate charts, by key, from the configs +const uniqCharts = allCharts.reduce( ( a, b ) => { + if ( a.findIndex( d => d.key === b.key ) < 0 ) { + a.push( b ); + } + return a; +}, [] ); + +// Default charts. +// TODO: Implement user-based toggling/persistence. +const defaultCharts = [ 'items_sold', 'gross_revenue' ]; + +export const showCharts = uniqCharts.map( d => ( { + ...d, + show: defaultCharts.indexOf( d.key ) >= 0, +} ) ); +export const getChartFromKey = key => allCharts.filter( d => d.key === key ); diff --git a/plugins/woocommerce-admin/client/dashboard/dashboard-charts/index.js b/plugins/woocommerce-admin/client/dashboard/dashboard-charts/index.js new file mode 100644 index 00000000000..da602fe965f --- /dev/null +++ b/plugins/woocommerce-admin/client/dashboard/dashboard-charts/index.js @@ -0,0 +1,130 @@ +/** @format */ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import classNames from 'classnames'; +import Gridicon from 'gridicons'; +import { ToggleControl, IconButton, NavigableMenu } from '@wordpress/components'; +import { Component, Fragment } from '@wordpress/element'; +import PropTypes from 'prop-types'; + +/** + * WooCommerce dependencies + */ +import { EllipsisMenu, MenuItem, SectionHeader } from '@woocommerce/components'; + +/** + * Internal dependencies + */ +import ChartBlock from './block'; +import { getChartFromKey, showCharts } from './config'; +import './style.scss'; + +class DashboardCharts extends Component { + constructor( props ) { + super( ...arguments ); + this.state = { + showCharts, + query: props.query, + }; + + this.toggle = this.toggle.bind( this ); + } + + toggle( key ) { + return () => { + this.setState( state => { + const foundIndex = state.showCharts.findIndex( x => x.key === key ); + state.showCharts[ foundIndex ].show = ! state.showCharts[ foundIndex ].show; + return state; + } ); + }; + } + + handleTypeToggle( type ) { + return () => { + this.setState( state => ( { query: { ...state.query, type } } ) ); + }; + } + + renderMenu() { + return ( + + { this.state.showCharts.map( chart => { + return ( + + + + ); + } ) } + + ); + } + + render() { + const { path } = this.props; + const { query } = this.state; + return ( + +
+ + + } + title={ __( 'Line chart', 'wc-admin' ) } + aria-checked={ query.type === 'line' } + role="menuitemradio" + tabIndex={ query.type === 'line' ? 0 : -1 } + onClick={ this.handleTypeToggle( 'line' ) } + /> + } + title={ __( 'Bar chart', 'wc-admin' ) } + aria-checked={ query.type === 'bar' } + role="menuitemradio" + tabIndex={ query.type === 'bar' ? 0 : -1 } + onClick={ this.handleTypeToggle( 'bar' ) } + /> + + +
+ { this.state.showCharts.map( chart => { + return ! chart.show ? null : ( +
+ +
+ ); + } ) } +
+
+
+ ); + } +} + +DashboardCharts.propTypes = { + path: PropTypes.string.isRequired, + query: PropTypes.object.isRequired, +}; + +export default DashboardCharts; diff --git a/plugins/woocommerce-admin/client/dashboard/dashboard-charts/style.scss b/plugins/woocommerce-admin/client/dashboard/dashboard-charts/style.scss new file mode 100644 index 00000000000..5ef59d5264d --- /dev/null +++ b/plugins/woocommerce-admin/client/dashboard/dashboard-charts/style.scss @@ -0,0 +1,18 @@ +/** @format */ + +.woocommerce-dashboard__dashboard-charts { + border-bottom: 0; + border-right: 0; + + .woocommerce-section-header__actions { + flex-grow: 0; + } + + .woocommerce-card__body { + padding: 0; + } + + .woocommerce-summary { + margin: 0; + } +} diff --git a/plugins/woocommerce-admin/client/dashboard/index.js b/plugins/woocommerce-admin/client/dashboard/index.js index 36089e78361..27c29b85c8f 100644 --- a/plugins/woocommerce-admin/client/dashboard/index.js +++ b/plugins/woocommerce-admin/client/dashboard/index.js @@ -12,6 +12,7 @@ import './style.scss'; import Header from 'header'; import StorePerformance from './store-performance'; import TopSellingProducts from './top-selling-products'; +import DashboardCharts from './dashboard-charts'; import { ReportFilters } from '@woocommerce/components'; export default class Dashboard extends Component { @@ -27,6 +28,7 @@ export default class Dashboard extends Component { + ); } diff --git a/plugins/woocommerce-admin/packages/components/CHANGELOG.md b/plugins/woocommerce-admin/packages/components/CHANGELOG.md index fba739a637b..e1c6429c88a 100644 --- a/plugins/woocommerce-admin/packages/components/CHANGELOG.md +++ b/plugins/woocommerce-admin/packages/components/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.3.1 + +- Added `interactive` prop for `d3chart/legend` to signal if legend items are clickable or not. + # 1.3.0 - Update `` to use header keys to denote which columns are shown diff --git a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/legend.js b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/legend.js index c51111c326e..e539fbcc5c6 100644 --- a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/legend.js +++ b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/legend.js @@ -48,6 +48,7 @@ class D3Legend extends Component { data, handleLegendHover, handleLegendToggle, + interactive, legendDirection, legendValueFormat, totalLabel, @@ -92,7 +93,7 @@ class D3Legend extends Component {