Merge branch 'master' into fix/749

This commit is contained in:
Peter Fabian 2018-11-16 12:16:50 +01:00
commit 16d5265aed
286 changed files with 5250 additions and 3100 deletions

View File

@ -6,5 +6,5 @@ _Replace this with a good description of your changes & reasoning._
### Detailed test instructions:
- Ex: Open page `url`
- Click XYZ…
- Ex: Open page `url`
- Click XYZ…

View File

@ -5,6 +5,7 @@
"at-rule-no-unknown": null,
"comment-empty-line-before": null,
"declaration-block-no-duplicate-properties": null,
"declaration-colon-newline-after": null,
"declaration-property-unit-whitelist": null,
"font-weight-notation": null,
"max-line-length": null,
@ -12,6 +13,8 @@
"no-duplicate-selectors": null,
"rule-empty-line-before": null,
"selector-class-pattern": null,
"value-keyword-case": null
"string-quotes": "single",
"value-keyword-case": null,
"value-list-comma-newline-after": null
}
}
}

View File

@ -1,33 +1,36 @@
sudo: required
language: php
php:
- 7.1
env:
- WP_VERSION=latest WP_MULTISITE=0 RUN_PHPCS=1 WP_CORE_DIR=/tmp/wordpress NODE_RELEASE=8.x
matrix:
include:
- name: "PHP 7.2 unit tests, PHP Coding standards check and JS tests"
php: 7.2
env: WP_VERSION=latest WP_MULTISITE=0 WP_CORE_DIR=/tmp/wordpress NODE_RELEASE=8.x RUN_PHPCS=1 RUN_JS=1
- name: "PHP 7.1 unit tests"
php: 7.1
env: WP_VERSION=latest WP_MULTISITE=0 WP_CORE_DIR=/tmp/wordpress NODE_RELEASE=8.x
- name: "PHP 7.0 unit tests"
php: 7.0
env: WP_VERSION=latest WP_MULTISITE=0 WP_CORE_DIR=/tmp/wordpress NODE_RELEASE=8.x
- name: "PHP 5.6 unit tests"
php: 5.6
env: WP_VERSION=latest WP_MULTISITE=0 WP_CORE_DIR=/tmp/wordpress NODE_RELEASE=8.x
before_script:
- phpenv config-rm xdebug.ini
- export PATH="$HOME/.composer/vendor/bin:$PATH"
- bash bin/install-wp-tests.sh wc_admin_test root '' localhost $WP_VERSION
- bash bin/travis.sh before
- sudo rm -rf ~/.nvm
- curl -sL "https://deb.nodesource.com/setup_${NODE_RELEASE}" | sudo -E bash -
- sudo apt-get install -y nodejs
- node --version
- npm --version
install:
- npm install
node_js:
- "8"
before_script:
- phpenv config-rm xdebug.ini
- export PATH="$WP_CORE_DIR/wp-content/plugins/wc-admin/vendor/bin:$PATH"
- bash bin/install-wp-tests.sh wc_admin_test root '' localhost $WP_VERSION
- bash bin/travis.sh before
- node --version
- npm --version
- timedatectl
script:
- npm run lint
- npm run build
- npm test
- bash bin/js_lint_test.sh
- bash bin/phpunit.sh
- bash bin/phpcs.sh

View File

@ -0,0 +1,24 @@
# Contributing to WooCommerce Admin
Hi! Thank you for your interest in contributing to WooCommerce Admin. We appreciate it.
There are many ways to contribute reporting bugs, feature suggestions, and fixing bugs.
## Reporting Bugs, Asking Questions, Sending Suggestions
Open [a GitHub issue](https://github.com/woocommerce/wc-admin/issues/new/choose), that's all. If you have write access, add any appropriate labels.
If you're filing a bug, specific steps to reproduce are always helpful. Please include what you expected to see and what happened instead.
## We're Here To Help
We encourage you to ask for help. We want your first experience with WooCommerce Admin to be a good one, so don't be shy. If you're wondering why something is the way it is, or how a decision was made, you can tag issues with [Type] Question or prefix them with “Question:”
## License
WooCommerce Admin is licensed under [GNU General Public License v2 (or later)](/LICENSE.md).
All materials contributed should be compatible with the GPLv2. This means that if you own the material, you agree to license it under the GPLv2 license. If you are contributing code that is not your own, such as adding a component from another Open Source project, or adding an `npm` package, you need to make sure you follow these steps:
1. Check that the code has a license. If you can't find one, you can try to contact the original author and get permission to use, or ask them to release under a compatible Open Source license.
2. Check the license is compatible with [GPLv2](http://www.gnu.org/licenses/license-list.en.html#GPLCompatibleLicenses), note that the Apache 2.0 license is *not* compatible.

View File

@ -20,6 +20,7 @@ After cloning the repo, install dependencies with `npm install`. Now you can bui
- `npm run build` : Build a production version
- `npm start` : Build a development version, watch files for changes
- `npm run build:release` : Build a WordPress plugin ZIP file (`wc-admin.zip` will be created in the repository root)
There are also some helper scripts:
@ -29,3 +30,7 @@ There are also some helper scripts:
## Privacy
If you have enabled WooCommerce usage tracking ( option `woocommerce_allow_tracking` ) then, in addition to the tracking described in https://woocommerce.com/usage-tracking/, this plugin also sends information about the actions that site administrators perform to Automattic - see https://automattic.com/privacy/#information-we-collect-automatically for more information.
## Contributing
There are many ways to contribute reporting bugs, feature suggestions and fixing bugs. For full details, please see [CONTRIBUTING.md](./CONTRIBUTING.md)

View File

@ -0,0 +1,84 @@
#!/bin/bash
# Exit if any command fails.
set -e
# Change to the expected directory.
cd "$(dirname "$0")"
cd ..
# Enable nicer messaging for build status.
BLUE_BOLD='\033[1;34m';
GREEN_BOLD='\033[1;32m';
RED_BOLD='\033[1;31m';
YELLOW_BOLD='\033[1;33m';
COLOR_RESET='\033[0m';
error () {
echo -e "\n${RED_BOLD}$1${COLOR_RESET}\n"
}
status () {
echo -e "\n${BLUE_BOLD}$1${COLOR_RESET}\n"
}
success () {
echo -e "\n${GREEN_BOLD}$1${COLOR_RESET}\n"
}
warning () {
echo -e "\n${YELLOW_BOLD}$1${COLOR_RESET}\n"
}
status "💃 Time to release WooCommerce Admin 🕺"
# Make sure there are no changes in the working tree. Release builds should be
# traceable to a particular commit and reliably reproducible. (This is not
# totally true at the moment because we download nightly vendor scripts).
changed=
if ! git diff --exit-code > /dev/null; then
changed="file(s) modified"
elif ! git diff --cached --exit-code > /dev/null; then
changed="file(s) staged"
fi
if [ ! -z "$changed" ]; then
git status
error "ERROR: Cannot build plugin zip with dirty working tree. ☝️
Commit your changes and try again."
exit 1
fi
# Do a dry run of the repository reset. Prompting the user for a list of all
# files that will be removed should prevent them from losing important files!
status "Resetting the repository to pristine condition. ✨"
git clean -xdf --dry-run
warning "🚨 About to delete everything above! Is this okay? 🚨"
echo -n "[y]es/[N]o: "
read answer
if [ "$answer" != "${answer#[Yy]}" ]; then
# Remove ignored files to reset repository to pristine condition. Previous
# test ensures that changed files abort the plugin build.
status "Cleaning working directory... 🛀"
git clean -xdf
else
error "Fair enough; aborting. Tidy up your repo and try again. 🙂"
exit 1
fi
# Run the build.
status "Generating build... 👷‍♀️"
npm run build
npm run docs
build_files=$(ls dist/*/*.{js,css})
# Generate the plugin zip file.
status "Creating archive... 🎁"
zip -r wc-admin.zip \
wc-admin.php \
lib/*.php \
includes/*.php \
includes/**/*.php \
images/* \
$build_files \
languages/wc-admin.pot \
languages/wc-admin.php \
README.md
success "Done. You've built WooCommerce Admin! 🎉 "

View File

@ -13,6 +13,7 @@ const { parse, resolver } = require( 'react-docgen' );
const { getDescription, getProps, getTitle } = require( './lib/formatting' );
const {
COMPONENTS_FOLDER,
PACKAGES_FOLDER,
deleteExistingDocs,
getExportedFileList,
getMdFileName,
@ -20,13 +21,14 @@ const {
writeTableOfContents,
} = require( './lib/file-system' );
const filePath = path.resolve( COMPONENTS_FOLDER, 'index.js' );
// Start by wiping the existing docs. **Change this if we end up manually editing docs**
deleteExistingDocs();
// Read components file to get a list of exported files, convert that to a list of absolute paths to public components.
const files = getRealFilePaths( getExportedFileList( filePath ) );
const files = [
...getRealFilePaths( getExportedFileList( path.resolve( COMPONENTS_FOLDER, 'index.js' ) ) ),
...getRealFilePaths( getExportedFileList( path.resolve( PACKAGES_FOLDER, 'index.js' ) ), PACKAGES_FOLDER ),
];
// Build the documentation by reading each file.
files.forEach( file => {

View File

@ -15,6 +15,7 @@ const { namedTypes } = types;
const { camelCaseDash } = require( './formatting' );
const COMPONENTS_FOLDER = path.resolve( __dirname, '../../../client/components/' );
const PACKAGES_FOLDER = path.resolve( __dirname, '../../../packages/components/src/' );
const DOCS_FOLDER = path.resolve( __dirname, '../../../docs/components/' );
/**
@ -78,7 +79,7 @@ function getMdFileName( filepath, absolute = true ) {
if ( ! fileParts || ! fileParts[ 1 ] ) {
return;
}
const name = fileParts[ 1 ].split( '/' )[ 0 ];
const name = fileParts[ 1 ].replace( 'src/', '' ).split( '/' )[ 0 ];
if ( ! absolute ) {
return name + '.md';
}
@ -148,7 +149,7 @@ function isFile( file ) {
* @param { array } files A list of readme files.
*/
function writeTableOfContents( files ) {
const mdFiles = files.map( f => getMdFileName( f, false ) );
const mdFiles = files.map( f => getMdFileName( f, false ) ).sort();
const TOC = uniq( mdFiles ).map( doc => {
const name = camelCaseDash( doc.replace( '.md', '' ) );
return ` * [${ name }](components/${ doc })`;
@ -161,6 +162,7 @@ function writeTableOfContents( files ) {
module.exports = {
COMPONENTS_FOLDER,
DOCS_FOLDER,
PACKAGES_FOLDER,
deleteExistingDocs,
getExportedFileList,
getMdFileName,

View File

@ -174,12 +174,14 @@ install_deps() {
# Install WooCommerce
cd "wp-content/plugins/"
# As zip file does not include tests, we have to get it from git repo.
git clone https://github.com/woocommerce/woocommerce.git
git clone --depth 1 https://github.com/woocommerce/woocommerce.git
cd "$WP_CORE_DIR"
php wp-cli.phar plugin activate woocommerce
# Install wc-admin, the correct branch
php wp-cli.phar plugin install https://github.com/$REPO/archive/$BRANCH.zip --activate
if [ "$TRAVIS_PULL_REQUEST_BRANCH" != "" ]; then
# Install wc-admin, the correct branch, if running from Travis CI.
php wp-cli.phar plugin install https://github.com/$REPO/archive/$BRANCH.zip --activate
fi
# Back to original dir
cd "$WORKING_DIR"

View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
if [[ ${RUN_JS} == 1 ]]; then
npm run lint
npm run build
npm test
fi

View File

@ -121,16 +121,14 @@ function buildScssFile( styleFile ) {
mkdirp.sync( path.dirname( outputFile ) );
const builtSass = sass.renderSync( {
file: styleFile,
includePaths: [ path.resolve( __dirname, '../../assets/stylesheets' ) ],
includePaths: [ path.resolve( __dirname, '../../client/stylesheets/abstracts' ) ],
data: (
[
'colors',
'breakpoints',
'variables',
'breakpoints',
'mixins',
'animations',
'z-index',
].map( ( imported ) => `@import "${ imported }";` ).join( ' ' ) +
].map( ( imported ) => `@import "_${ imported }";` ).join( ' ' ) +
fs.readFileSync( styleFile, 'utf8' )
),
} );

View File

@ -1,5 +1,5 @@
module.exports = [
require( './node_modules/@wordpress/postcss-themes' )( {
require( '@wordpress/postcss-themes' )( {
defaults: {
primary: '#0085ba',
secondary: '#11a0d2',

View File

@ -5,6 +5,7 @@ if [[ ${RUN_PHPCS} == 1 ]]; then
if [ "$CHANGED_FILES" != "" ]; then
echo "Running Code Sniffer."
cd "$WP_CORE_DIR/wp-content/plugins/wc-admin/"
./vendor/bin/phpcs --encoding=utf-8 -n -p $CHANGED_FILES
fi
fi

View File

@ -1,5 +1,8 @@
#!/usr/bin/env bash
WORKING_DIR="$PWD"
cd "/tmp/wordpress/wp-content/plugins/wc-admin/"
cd "$WP_CORE_DIR/wp-content/plugins/wc-admin/"
phpunit --version
phpunit -c phpunit.xml.dist
TEST_RESULT=$?
cd "$WORKING_DIR"
exit $TEST_RESULT

View File

@ -3,9 +3,9 @@
if [ $1 == 'before' ]; then
composer global require "phpunit/phpunit=6.*"
if [[ ${RUN_PHPCS} == 1 ]]; then
cd "$WP_CORE_DIR/wp-content/plugins/wc-admin/"
# This can (currently) only run for PHP 7.1+
composer install
fi

View File

@ -12,7 +12,6 @@ import PropTypes from 'prop-types';
/**
* WooCommerce dependencies
*/
import { Chart, ChartPlaceholder } from '@woocommerce/components';
import {
getAllowedIntervalsForQuery,
getCurrentDates,
@ -24,12 +23,21 @@ import {
/**
* Internal dependencies
*/
import { Chart, ChartPlaceholder } from 'components';
import { getReportChartData } from 'store/reports/utils';
import ReportError from 'analytics/components/report-error';
class ReportChart extends Component {
render() {
const { path, primaryData, secondaryData, selectedChart, query } = this.props;
const {
comparisonChart,
query,
itemsLabel,
path,
primaryData,
secondaryData,
selectedChart,
} = this.props;
if ( primaryData.isError || secondaryData.isError ) {
return <ReportError isError />;
@ -84,7 +92,9 @@ class ReportChart extends Component {
title={ selectedChart.label }
interval={ currentInterval }
allowedIntervals={ allowedIntervals }
mode="time-comparison"
itemsLabel={ itemsLabel }
layout={ comparisonChart ? 'comparison' : 'standard' }
mode={ comparisonChart ? 'item-comparison' : 'time-comparison' }
pointLabelFormat={ formats.pointLabelFormat }
tooltipTitle={ selectedChart.label }
xFormat={ formats.xFormat }
@ -97,6 +107,8 @@ class ReportChart extends Component {
}
ReportChart.propTypes = {
comparisonChart: PropTypes.bool,
itemsLabel: PropTypes.string,
path: PropTypes.string.isRequired,
primaryData: PropTypes.object.isRequired,
query: PropTypes.object.isRequired,

View File

@ -7,14 +7,14 @@ import { Component } from '@wordpress/element';
import PropTypes from 'prop-types';
/**
* Internal dependencies
* WooCommerce dependencies
*/
import { EmptyContent } from '@woocommerce/components';
import { getAdminLink } from 'lib/nav-utils';
import { getAdminLink } from '@woocommerce/navigation';
class ReportError extends Component {
render() {
const { isError, isEmpty } = this.props;
const { className, isError, isEmpty } = this.props;
let title, actionLabel, actionURL, actionCallback;
if ( isError ) {
@ -31,6 +31,7 @@ class ReportError extends Component {
}
return (
<EmptyContent
className={ className }
title={ title }
actionLabel={ actionLabel }
actionURL={ actionURL }
@ -41,8 +42,13 @@ class ReportError extends Component {
}
ReportError.propTypes = {
className: PropTypes.string,
isError: PropTypes.bool,
isEmpty: PropTypes.bool,
};
ReportError.defaultProps = {
className: '',
};
export default ReportError;

View File

@ -14,12 +14,12 @@ import PropTypes from 'prop-types';
*/
import { formatCurrency } from '@woocommerce/currency';
import { getDateParamsFromQuery } from '@woocommerce/date';
import { getNewPath } from '@woocommerce/navigation';
import { SummaryList, SummaryListPlaceholder, SummaryNumber } from '@woocommerce/components';
/**
* Internal dependencies
*/
import { getNewPath } from 'lib/nav-utils';
import { getSummaryNumbers } from 'store/reports/utils';
import { numberFormat } from 'lib/number';
import ReportError from 'analytics/components/report-error';

View File

@ -8,6 +8,7 @@ import { Component, Fragment } from '@wordpress/element';
* Internal dependencies
*/
import { filters } from './config';
import CouponsReportTable from './table';
import { ReportFilters } from '@woocommerce/components';
export default class extends Component {
@ -17,6 +18,7 @@ export default class extends Component {
return (
<Fragment>
<ReportFilters query={ query } path={ path } filters={ filters } />
<CouponsReportTable query={ query } />
</Fragment>
);
}

View File

@ -0,0 +1,223 @@
/** @format */
/**
* External dependencies
*/
import { __, _n } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { format as formatDate } from '@wordpress/date';
import { compose } from '@wordpress/compose';
import { withSelect } from '@wordpress/data';
import { get, map, orderBy } from 'lodash';
/**
* WooCommerce dependencies
*/
import {
appendTimestamp,
getCurrentDates,
getIntervalForQuery,
getDateFormatsForInterval,
} from '@woocommerce/date';
import { Link, TableCard } from '@woocommerce/components';
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import { onQueryChange } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import ReportError from 'analytics/components/report-error';
import { QUERY_DEFAULTS } from 'store/constants';
import { getReportChartData, getFilterQuery } from 'store/reports/utils';
class CouponsReportTable extends Component {
getHeadersContent() {
return [
{
label: __( 'Coupon Code', 'wc-admin' ),
// @TODO it should be the coupon code, not the coupon ID
key: 'coupon_id',
required: true,
isLeftAligned: true,
isSortable: true,
},
{
label: __( 'Orders', 'wc-admin' ),
key: 'orders_count',
required: true,
defaultSort: true,
isSortable: true,
isNumeric: true,
},
{
label: __( 'G. Discounted', 'wc-admin' ),
key: 'gross_discount',
isSortable: true,
isNumeric: true,
},
{
label: __( 'Created', 'wc-admin' ),
key: 'created',
isSortable: true,
},
{
label: __( 'Expires', 'wc-admin' ),
key: 'expires',
isSortable: true,
},
{
label: __( 'Type', 'wc-admin' ),
key: 'type',
isSortable: false,
},
];
}
getRowsContent( coupons ) {
const { query } = this.props;
const currentInterval = getIntervalForQuery( query );
const { tableFormat } = getDateFormatsForInterval( currentInterval );
return map( coupons, coupon => {
const { coupon_id, gross_discount, orders_count } = coupon;
// @TODO must link to the coupon detail report
const couponLink = (
<Link href="" type="wc-admin">
{ coupon_id }
</Link>
);
const ordersLink = (
<Link
href={ '/analytics/orders?filter=advanced&code_includes=' + coupon_id }
type="wc-admin"
>
{ orders_count }
</Link>
);
return [
// @TODO it should be the coupon code, not the coupon ID
{
display: couponLink,
value: coupon_id,
},
{
display: ordersLink,
value: orders_count,
},
{
display: formatCurrency( gross_discount ),
value: getCurrencyFormatDecimal( gross_discount ),
},
{
// @TODO
display: formatDate( tableFormat, '' ),
value: '',
},
{
// @TODO
display: formatDate( tableFormat, '' ),
value: '',
},
{
// @TODO
display: '',
value: '',
},
];
} );
}
getSummary( totals ) {
if ( ! totals ) {
return [];
}
return [
{
label: _n( 'coupon', 'coupons', totals.coupons_count, 'wc-admin' ),
value: totals.coupons_count,
},
{
label: _n( 'order', 'orders', totals.orders_count, 'wc-admin' ),
value: totals.orders_count,
},
{
label: __( 'gross discounted', 'wc-admin' ),
value: formatCurrency( totals.gross_discount ),
},
];
}
render() {
const { coupons, isTableDataError, isTableDataRequesting, primaryData, query } = this.props;
const isError = isTableDataError || primaryData.isError;
if ( isError ) {
return <ReportError isError />;
}
const isRequesting = isTableDataRequesting || primaryData.isRequesting;
const tableQuery = {
...query,
orderby: query.orderby || 'date',
order: query.order || 'asc',
};
const headers = this.getHeadersContent();
const orderedCoupons = orderBy( coupons, tableQuery.orderby, tableQuery.order );
const rows = this.getRowsContent( orderedCoupons );
const rowsPerPage = parseInt( tableQuery.per_page ) || QUERY_DEFAULTS.pageSize;
const totalRows = get( primaryData, [ 'data', 'totals', 'coupons_count' ], coupons.length );
const summary = primaryData.data.totals ? this.getSummary( primaryData.data.totals ) : null;
return (
<TableCard
title={ __( 'Coupons', 'wc-admin' ) }
compareBy={ 'coupons' }
ids={ orderedCoupons.map( coupon => coupon.coupon_id ) }
rows={ rows }
totalRows={ totalRows }
rowsPerPage={ rowsPerPage }
headers={ headers }
isLoading={ isRequesting }
onQueryChange={ onQueryChange }
query={ tableQuery }
summary={ summary }
downloadable
/>
);
}
}
export default compose(
withSelect( ( select, props ) => {
const { query } = props;
const datesFromQuery = getCurrentDates( query );
const primaryData = getReportChartData( 'coupons', 'primary', query, select );
const filterQuery = getFilterQuery( 'coupons', query );
const { getCoupons, isGetCouponsError, isGetCouponsRequesting } = select( 'wc-admin' );
const tableQuery = {
orderby: query.orderby || 'date',
order: query.order || 'asc',
page: query.page || 1,
per_page: query.per_page || QUERY_DEFAULTS.pageSize,
after: appendTimestamp( datesFromQuery.primary.after, 'start' ),
before: appendTimestamp( datesFromQuery.primary.before, 'end' ),
...filterQuery,
};
const coupons = getCoupons( tableQuery );
const isTableDataError = isGetCouponsError( tableQuery );
const isTableDataRequesting = isGetCouponsRequesting( tableQuery );
return {
isTableDataError,
isTableDataRequesting,
coupons,
primaryData,
};
} )
)( CouponsReportTable );

View File

@ -8,6 +8,11 @@ import { Component, Fragment } from '@wordpress/element';
import PropTypes from 'prop-types';
import { find } from 'lodash';
/**
* WooCommerce dependencies
*/
import { useFilters } from '@woocommerce/components';
/**
* Internal dependencies
*/
@ -17,7 +22,7 @@ import OrdersReport from './orders';
import ProductsReport from './products';
import RevenueReport from './revenue';
import CouponsReport from './coupons';
import useFilters from 'components/higher-order/use-filters';
import TaxesReport from './taxes';
const REPORTS_FILTER = 'woocommerce-reports-list';
@ -43,6 +48,11 @@ const getReports = () => {
title: __( 'Coupons', 'wc-admin' ),
component: CouponsReport,
},
{
report: 'taxes',
title: __( 'Taxes', 'wc-admin' ),
component: TaxesReport,
},
] );
return reports;

View File

@ -1,80 +0,0 @@
/** @format */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component, Fragment } from '@wordpress/element';
import { find } from 'lodash';
import PropTypes from 'prop-types';
/**
* Internal dependencies
*/
import ReportChart from 'analytics/components/report-chart';
import ReportSummary from 'analytics/components/report-summary';
class OrdersReportChart extends Component {
getCharts() {
return [
{
key: 'orders_count',
label: __( 'Orders Count', 'wc-admin' ),
type: 'number',
},
{
key: 'net_revenue',
label: __( 'Net Revenue', 'wc-admin' ),
type: 'currency',
},
{
key: 'avg_order_value',
label: __( 'Average Order Value', 'wc-admin' ),
type: 'currency',
},
{
key: 'avg_items_per_order',
label: __( 'Average Items Per Order', 'wc-admin' ),
type: 'average',
},
];
}
getSelectedChart() {
const { query } = this.props;
const charts = this.getCharts();
const chart = find( charts, { key: query.chart } );
if ( chart ) {
return chart;
}
return charts[ 0 ];
}
render() {
const { path, query } = this.props;
return (
<Fragment>
<ReportSummary
charts={ this.getCharts() }
endpoint="orders"
query={ query }
selectedChart={ this.getSelectedChart() }
/>
<ReportChart
charts={ this.getCharts() }
endpoint="orders"
path={ path }
query={ query }
selectedChart={ this.getSelectedChart() }
/>
</Fragment>
);
}
}
OrdersReportChart.propTypes = {
path: PropTypes.string.isRequired,
query: PropTypes.object.isRequired,
};
export default OrdersReportChart;

View File

@ -12,6 +12,29 @@ import { NAMESPACE } from 'store/constants';
const { orderStatuses } = wcSettings;
export const charts = [
{
key: 'orders_count',
label: __( 'Orders Count', 'wc-admin' ),
type: 'number',
},
{
key: 'net_revenue',
label: __( 'Net Revenue', 'wc-admin' ),
type: 'currency',
},
{
key: 'avg_order_value',
label: __( 'Average Order Value', 'wc-admin' ),
type: 'currency',
},
{
key: 'avg_items_per_order',
label: __( 'Average Items Per Order', 'wc-admin' ),
type: 'average',
},
];
export const filters = [
{
label: __( 'Show', 'wc-admin' ),

View File

@ -13,9 +13,11 @@ import { ReportFilters } from '@woocommerce/components';
/**
* Internal dependencies
*/
import { advancedFilters, filters } from './config';
import OrdersReportChart from './chart';
import { advancedFilters, charts, filters } from './config';
import getSelectedChart from 'lib/get-selected-chart';
import OrdersReportTable from './table';
import ReportChart from 'analytics/components/report-chart';
import ReportSummary from 'analytics/components/report-summary';
export default class OrdersReport extends Component {
render() {
@ -29,7 +31,19 @@ export default class OrdersReport extends Component {
filters={ filters }
advancedFilters={ advancedFilters }
/>
<OrdersReportChart query={ query } path={ path } />
<ReportSummary
charts={ charts }
endpoint="orders"
query={ query }
selectedChart={ getSelectedChart( query.chart, charts ) }
/>
<ReportChart
charts={ charts }
endpoint="orders"
path={ path }
query={ query }
selectedChart={ getSelectedChart( query.chart, charts ) }
/>
<OrdersReportTable query={ query } />
</Fragment>
);

View File

@ -12,29 +12,25 @@ import { get, map, orderBy } from 'lodash';
/**
* WooCommerce dependencies
*/
import { Link, OrderStatus, TableCard, ViewMoreList } from '@woocommerce/components';
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import {
appendTimestamp,
getCurrentDates,
getIntervalForQuery,
getDateFormatsForInterval,
} from '@woocommerce/date';
import { Link, OrderStatus, TableCard, ViewMoreList } from '@woocommerce/components';
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import { getAdminLink, onQueryChange } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { getAdminLink, onQueryChange } from 'lib/nav-utils';
import ReportError from 'analytics/components/report-error';
import { QUERY_DEFAULTS } from 'store/constants';
import { getReportChartData, getFilterQuery } from 'store/reports/utils';
import './style.scss';
class OrdersReportTable extends Component {
constructor( props ) {
super( props );
}
getHeadersContent() {
return [
{

View File

@ -1,75 +0,0 @@
/** @format */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component, Fragment } from '@wordpress/element';
import { find } from 'lodash';
import PropTypes from 'prop-types';
/**
* Internal dependencies
*/
import ReportChart from 'analytics/components/report-chart';
import ReportSummary from 'analytics/components/report-summary';
class ProductsReportChart extends Component {
getCharts() {
return [
{
key: 'items_sold',
label: __( 'Items Sold', 'wc-admin' ),
type: 'number',
},
{
key: 'gross_revenue',
label: __( 'Gross Revenue', 'wc-admin' ),
type: 'currency',
},
{
key: 'orders_count',
label: __( 'Orders Count', 'wc-admin' ),
type: 'number',
},
];
}
getSelectedChart() {
const { query } = this.props;
const charts = this.getCharts();
const chart = find( charts, { key: query.chart } );
if ( chart ) {
return chart;
}
return charts[ 0 ];
}
render() {
const { path, query } = this.props;
return (
<Fragment>
<ReportSummary
charts={ this.getCharts() }
endpoint="products"
query={ query }
selectedChart={ this.getSelectedChart() }
/>
<ReportChart
charts={ this.getCharts() }
endpoint="products"
path={ path }
query={ query }
selectedChart={ this.getSelectedChart() }
/>
</Fragment>
);
}
}
ProductsReportChart.propTypes = {
path: PropTypes.string.isRequired,
query: PropTypes.object.isRequired,
};
export default ProductsReportChart;

View File

@ -10,6 +10,24 @@ import { __ } from '@wordpress/i18n';
import { getRequestByIdString } from 'lib/async-requests';
import { NAMESPACE } from 'store/constants';
export const charts = [
{
key: 'items_sold',
label: __( 'Items Sold', 'wc-admin' ),
type: 'number',
},
{
key: 'gross_revenue',
label: __( 'Gross Revenue', 'wc-admin' ),
type: 'currency',
},
{
key: 'orders_count',
label: __( 'Orders Count', 'wc-admin' ),
type: 'number',
},
];
const filterConfig = {
label: __( 'Show', 'wc-admin' ),
staticParams: [ 'chart' ],
@ -42,7 +60,7 @@ const filterConfig = {
},
{
label: __( 'Product Comparison', 'wc-admin' ),
value: 'compare-product',
value: 'compare-products',
settings: {
type: 'products',
param: 'products',
@ -60,7 +78,7 @@ const filterConfig = {
},
{
label: __( 'Product Category Comparison', 'wc-admin' ),
value: 'compare-product_cat',
value: 'compare-product_cats',
settings: {
type: 'product_cats',
param: 'categories',
@ -99,13 +117,21 @@ const variationsConfig = {
label: __( 'Comparison', 'wc-admin' ),
value: 'compare',
settings: {
// @TODO create a variations autocompleter
type: 'products',
type: 'variations',
param: 'variations',
getLabels: getRequestByIdString( NAMESPACE + 'products', product => ( {
id: product.id,
label: product.name,
} ) ),
getLabels: getRequestByIdString(
query => NAMESPACE + `products/${ query.products }/variations`,
variation => {
return {
id: variation.id,
label: variation.attributes.reduce(
( desc, attribute, index, arr ) =>
desc + `${ attribute.option }${ arr.length === index + 1 ? '' : ', ' }`,
''
),
};
}
),
labels: {
helpText: __( 'Select at least two variations to compare', 'wc-admin' ),
placeholder: __( 'Search for variations to compare', 'wc-admin' ),

View File

@ -2,6 +2,7 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component, Fragment } from '@wordpress/element';
import PropTypes from 'prop-types';
@ -13,18 +14,39 @@ import { ReportFilters } from '@woocommerce/components';
/**
* Internal dependencies
*/
import { filters } from './config';
import ProductsReportChart from './chart';
import { charts, filters } from './config';
import getSelectedChart from 'lib/get-selected-chart';
import ProductsReportTable from './table';
import ReportChart from 'analytics/components/report-chart';
import ReportSummary from 'analytics/components/report-summary';
export default class ProductsReport extends Component {
render() {
const { path, query } = this.props;
const itemsLabel =
'single_product' === query.filter && !! query.products
? __( '%s variations', 'wc-admin' )
: __( '%s products', 'wc-admin' );
return (
<Fragment>
<ReportFilters query={ query } path={ path } filters={ filters } />
<ProductsReportChart query={ query } path={ path } />
<ReportSummary
charts={ charts }
endpoint="products"
query={ query }
selectedChart={ getSelectedChart( query.chart, charts ) }
/>
<ReportChart
comparisonChart
charts={ charts }
endpoint="products"
itemsLabel={ itemsLabel }
path={ path }
query={ query }
selectedChart={ getSelectedChart( query.chart, charts ) }
/>
<ProductsReportTable query={ query } />
</Fragment>
);

View File

@ -11,14 +11,14 @@ import { get, map, orderBy } from 'lodash';
/**
* WooCommerce dependencies
*/
import { appendTimestamp, getCurrentDates } from '@woocommerce/date';
import { Link, TableCard } from '@woocommerce/components';
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import { appendTimestamp, getCurrentDates } from '@woocommerce/date';
import { getNewPath, getTimeRelatedQuery, onQueryChange } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { getNewPath, getTimeRelatedQuery, onQueryChange } from 'lib/nav-utils';
import ReportError from 'analytics/components/report-error';
import { getFilterQuery, getReportChartData } from 'store/reports/utils';
import { QUERY_DEFAULTS } from 'store/constants';
@ -183,7 +183,7 @@ class ProductsReportTable extends Component {
labels={ labels }
ids={ orderedProducts.map( p => p.product_id ) }
isLoading={ isRequesting }
compareBy={ 'product' }
compareBy={ 'products' }
onQueryChange={ onQueryChange }
query={ tableQuery }
summary={ null } // @TODO

View File

@ -1,90 +0,0 @@
/** @format */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component, Fragment } from '@wordpress/element';
import { find } from 'lodash';
import PropTypes from 'prop-types';
/**
* Internal dependencies
*/
import ReportChart from 'analytics/components/report-chart';
import ReportSummary from 'analytics/components/report-summary';
class RevenueReportChart extends Component {
getCharts() {
return [
{
key: 'gross_revenue',
label: __( 'Gross Revenue', 'wc-admin' ),
type: 'currency',
},
{
key: 'refunds',
label: __( 'Refunds', 'wc-admin' ),
type: 'currency',
},
{
key: 'coupons',
label: __( 'Coupons', 'wc-admin' ),
type: 'currency',
},
{
key: 'taxes',
label: __( 'Taxes', 'wc-admin' ),
type: 'currency',
},
{
key: 'shipping',
label: __( 'Shipping', 'wc-admin' ),
type: 'currency',
},
{
key: 'net_revenue',
label: __( 'Net Revenue', 'wc-admin' ),
type: 'currency',
},
];
}
getSelectedChart() {
const { query } = this.props;
const charts = this.getCharts();
const chart = find( charts, { key: query.chart } );
if ( chart ) {
return chart;
}
return charts[ 0 ];
}
render() {
const { path, query } = this.props;
return (
<Fragment>
<ReportSummary
charts={ this.getCharts() }
endpoint="revenue"
query={ query }
selectedChart={ this.getSelectedChart() }
/>
<ReportChart
charts={ this.getCharts() }
endpoint="revenue"
path={ path }
query={ query }
selectedChart={ this.getSelectedChart() }
/>
</Fragment>
);
}
}
RevenueReportChart.propTypes = {
path: PropTypes.string.isRequired,
query: PropTypes.object.isRequired,
};
export default RevenueReportChart;

View File

@ -0,0 +1,38 @@
/** @format */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
export const charts = [
{
key: 'gross_revenue',
label: __( 'Gross Revenue', 'wc-admin' ),
type: 'currency',
},
{
key: 'refunds',
label: __( 'Refunds', 'wc-admin' ),
type: 'currency',
},
{
key: 'coupons',
label: __( 'Coupons', 'wc-admin' ),
type: 'currency',
},
{
key: 'taxes',
label: __( 'Taxes', 'wc-admin' ),
type: 'currency',
},
{
key: 'shipping',
label: __( 'Shipping', 'wc-admin' ),
type: 'currency',
},
{
key: 'net_revenue',
label: __( 'Net Revenue', 'wc-admin' ),
type: 'currency',
},
];

View File

@ -13,7 +13,10 @@ import { ReportFilters } from '@woocommerce/components';
/**
* Internal dependencies
*/
import RevenueReportChart from './chart';
import { charts } from './config';
import getSelectedChart from 'lib/get-selected-chart';
import ReportChart from 'analytics/components/report-chart';
import ReportSummary from 'analytics/components/report-summary';
import RevenueReportTable from './table';
export default class RevenueReport extends Component {
@ -23,7 +26,19 @@ export default class RevenueReport extends Component {
return (
<Fragment>
<ReportFilters query={ query } path={ path } />
<RevenueReportChart query={ query } path={ path } />
<ReportSummary
charts={ charts }
endpoint="revenue"
query={ query }
selectedChart={ getSelectedChart( query.chart, charts ) }
/>
<ReportChart
charts={ charts }
endpoint="revenue"
path={ path }
query={ query }
selectedChart={ getSelectedChart( query.chart, charts ) }
/>
<RevenueReportTable query={ query } />
</Fragment>
);

View File

@ -12,19 +12,19 @@ import { get, map } from 'lodash';
/**
* WooCommerce dependencies
*/
import { Link, TableCard } from '@woocommerce/components';
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import {
appendTimestamp,
getCurrentDates,
getDateFormatsForInterval,
getIntervalForQuery,
} from '@woocommerce/date';
import { Link, TableCard } from '@woocommerce/components';
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import { onQueryChange } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { onQueryChange } from 'lib/nav-utils';
import ReportError from 'analytics/components/report-error';
import { QUERY_DEFAULTS } from 'store/constants';

View File

@ -0,0 +1,41 @@
/** @format */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
export const charts = [
{
key: 'order_tax',
label: __( 'Order Tax', 'wc-admin' ),
type: 'currency',
},
{
key: 'total_tax',
label: __( 'Total Tax', 'wc-admin' ),
type: 'currency',
},
{
key: 'shipping_tax',
label: __( 'Shipping Tax', 'wc-admin' ),
type: 'currency',
},
{
key: 'orders_count',
label: __( 'Orders Count', 'wc-admin' ),
type: 'number',
},
];
export const filters = [
{
label: __( 'Show', 'wc-admin' ),
staticParams: [ 'chart' ],
param: 'filter',
showFilters: () => true,
filters: [ { label: __( 'All Taxes', 'wc-admin' ), value: 'all' } ],
},
];

View File

@ -0,0 +1,45 @@
/** @format */
/**
* External dependencies
*/
import { Component, Fragment } from '@wordpress/element';
import PropTypes from 'prop-types';
/**
* WooCommerce dependencies
*/
import { ReportFilters } from '@woocommerce/components';
/**
* Internal dependencies
*/
import { charts, filters } from './config';
import getSelectedChart from 'lib/get-selected-chart';
import ReportChart from 'analytics/components/report-chart';
import ReportSummary from 'analytics/components/report-summary';
export default class TaxesReport extends Component {
render() {
const { query, path } = this.props;
return (
<Fragment>
<ReportFilters query={ query } path={ path } filters={ filters } />
<ReportSummary
charts={ charts }
endpoint="taxes"
query={ query }
selectedChart={ getSelectedChart( query.chart, charts ) }
/>
<ReportChart
charts={ charts }
endpoint="taxes"
path={ path }
query={ query }
selectedChart={ getSelectedChart( query.chart, charts ) }
/>
</Fragment>
);
}
}
TaxesReport.propTypes = {
query: PropTypes.object.isRequired,
};

View File

@ -105,6 +105,7 @@ class D3Chart extends Component {
mode,
orderedKeys,
pointLabelFormat,
tooltipPosition,
tooltipFormat,
tooltipTitle,
type,
@ -116,7 +117,6 @@ class D3Chart extends Component {
const { width } = this.state;
const calculatedWidth = width || node.offsetWidth;
const calculatedHeight = height || node.offsetHeight;
const scale = width / node.offsetWidth;
const adjHeight = calculatedHeight - margin.top - margin.bottom;
const adjWidth = calculatedWidth - margin.left - margin.right;
const uniqueKeys = getUniqueKeys( data );
@ -140,7 +140,7 @@ class D3Chart extends Component {
orderedKeys: newOrderedKeys,
pointLabelFormat,
parseDate,
scale,
tooltipPosition,
tooltipFormat: d3TimeFormat( tooltipFormat ),
tooltipTitle,
type,
@ -155,7 +155,7 @@ class D3Chart extends Component {
xScale,
yMax,
yScale,
yTickOffset: getYTickOffset( adjHeight, scale, yMax ),
yTickOffset: getYTickOffset( adjHeight, yMax ),
yFormat,
valueType,
};
@ -241,6 +241,10 @@ D3Chart.propTypes = {
* if `tooltipTitle` is missing, passed to d3TimeFormat.
*/
tooltipFormat: PropTypes.string,
/**
* The position where to render the tooltip can be `over` the chart or `below` the chart.
*/
tooltipPosition: PropTypes.oneOf( [ 'below', 'over' ] ),
/**
* A string to use as a title for the tooltip. Takes preference over `tooltipFormat`.
*/
@ -280,6 +284,7 @@ D3Chart.defaultProps = {
layout: 'standard',
mode: 'item-comparison',
tooltipFormat: '%B %d, %Y',
tooltipPosition: 'over',
type: 'line',
width: 600,
xFormat: '%Y-%m-%d',

View File

@ -32,7 +32,6 @@ export default class D3Base extends Component {
drawChart: PropTypes.func.isRequired,
getParams: PropTypes.func.isRequired,
type: PropTypes.string,
width: PropTypes.number,
};
state = {
@ -41,7 +40,6 @@ export default class D3Base extends Component {
drawChart: null,
getParams: null,
type: null,
width: null,
};
chartRef = createRef();
@ -61,10 +59,6 @@ export default class D3Base extends Component {
state = { ...state, getParams: nextProps.getParams };
}
if ( nextProps.width !== prevState.width ) {
state = { ...state, width: nextProps.width };
}
if ( nextProps.type !== prevState.type ) {
state = { ...state, type: nextProps.type };
}
@ -86,7 +80,6 @@ export default class D3Base extends Component {
return (
( nextState.params !== null && ! isEqual( this.state.params, nextState.params ) ) ||
! isEqual( this.state.data, nextState.data ) ||
this.state.width !== nextState.width ||
this.state.type !== nextState.type
);
}
@ -129,6 +122,8 @@ export default class D3Base extends Component {
const svg = d3Select( this.chartRef.current )
.append( 'svg' )
.attr( 'viewBox', `0 0 ${ width } ${ height }` )
.attr( 'height', height )
.attr( 'width', width )
.attr( 'preserveAspectRatio', 'xMidYMid meet' );
if ( className ) {

View File

@ -5,10 +5,14 @@
import { __ } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
/**
* WooCommerce dependencies
*/
import { Card } from '@woocommerce/components';
/**
* Internal dependencies
*/
import Card from 'components/card';
import Chart from './index';
import dummyOrders from './test/fixtures/dummy-hour';

View File

@ -5,10 +5,14 @@
import { __ } from '@wordpress/i18n';
import { Component, Fragment } from '@wordpress/element';
/**
* WooCommerce dependencies
*/
import { Card } from '@woocommerce/components';
/**
* Internal dependencies
*/
import Card from 'components/card';
import Chart from './index';
import dummyOrders from './test/fixtures/dummy';

View File

@ -1,5 +1,5 @@
```jsx
import { D3Chart, Legend } from '@woocommerce/components';
import { D3Chart, Legend } from 'components';
const noop = () => {};

View File

@ -2,27 +2,29 @@
/**
* External dependencies
*/
import { decodeEntities } from '@wordpress/html-entities';
import { __ } from '@wordpress/i18n';
import classNames from 'classnames';
import { get, isEqual, partial } from 'lodash';
import { Component, createRef } from '@wordpress/element';
import { IconButton, NavigableMenu, SelectControl } from '@wordpress/components';
import PropTypes from 'prop-types';
import { interpolateViridis as d3InterpolateViridis } from 'd3-scale-chromatic';
import { decodeEntities } from '@wordpress/html-entities';
import { formatDefaultLocale as d3FormatDefaultLocale } from 'd3-format';
import { get, isEqual, partial } from 'lodash';
import Gridicon from 'gridicons';
import { IconButton, NavigableMenu, SelectControl } from '@wordpress/components';
import { interpolateViridis as d3InterpolateViridis } from 'd3-scale-chromatic';
import PropTypes from 'prop-types';
import { withViewportMatch } from '@wordpress/viewport';
/**
* WooCommerce dependencies
*/
import { updateQueryString } from '@woocommerce/navigation';
import { H, Section } from '@woocommerce/components';
/**
* Internal dependencies
*/
import D3Chart from './charts';
import Legend from './legend';
import { WIDE_BREAKPOINT } from './utils';
import { H, Section } from 'components/section';
import { gap, gaplarge } from 'stylesheets/abstracts/_variables.scss';
import { updateQueryString } from 'lib/nav-utils';
d3FormatDefaultLocale( {
decimal: '.',
@ -57,16 +59,13 @@ function getOrderedKeys( props ) {
class Chart extends Component {
constructor( props ) {
super( props );
this.chartRef = createRef();
const wpBody = document.getElementById( 'wpbody' ).getBoundingClientRect().width;
const wpWrap = document.getElementById( 'wpwrap' ).getBoundingClientRect().width;
const calcGap = wpWrap > 782 ? gaplarge.match( /\d+/ )[ 0 ] : gap.match( /\d+/ )[ 0 ];
this.chartBodyRef = createRef();
this.state = {
data: props.data,
orderedKeys: getOrderedKeys( props ),
type: props.type,
visibleData: [ ...props.data ],
width: wpBody - 2 * calcGap,
width: 0,
};
this.handleTypeToggle = this.handleTypeToggle.bind( this );
this.handleLegendToggle = this.handleLegendToggle.bind( this );
@ -90,6 +89,7 @@ class Chart extends Component {
}
componentDidMount() {
this.updateDimensions();
window.addEventListener( 'resize', this.updateDimensions );
}
@ -137,7 +137,7 @@ class Chart extends Component {
updateDimensions() {
this.setState( {
width: this.chartRef.current.offsetWidth,
width: this.chartBodyRef.current.offsetWidth,
} );
}
@ -185,13 +185,30 @@ class Chart extends Component {
);
}
getChartHeight() {
const { isViewportLarge, isViewportMobile } = this.props;
if ( isViewportMobile ) {
return 180;
}
if ( isViewportLarge ) {
return 300;
}
return 220;
}
render() {
const { orderedKeys, type, visibleData, width } = this.state;
const {
dateParser,
itemsLabel,
layout,
mode,
pointLabelFormat,
isViewportLarge,
isViewportWide,
title,
tooltipFormat,
tooltipTitle,
@ -201,19 +218,17 @@ class Chart extends Component {
valueType,
} = this.props;
let { yFormat } = this.props;
const legendDirection = layout === 'standard' && width >= WIDE_BREAKPOINT ? 'row' : 'column';
const chartDirection = layout === 'comparison' && width >= WIDE_BREAKPOINT ? 'row' : 'column';
let chartHeight = width > 1329 ? 300 : 220;
chartHeight = width <= 1329 && width > 783 ? 220 : chartHeight;
chartHeight = width <= 783 ? 180 : chartHeight;
const legendDirection = layout === 'standard' && isViewportWide ? 'row' : 'column';
const chartDirection = layout === 'comparison' && isViewportWide ? 'row' : 'column';
const chartHeight = this.getChartHeight();
const legend = (
<Legend
className={ 'woocommerce-chart__legend' }
colorScheme={ d3InterpolateViridis }
data={ orderedKeys }
handleLegendHover={ this.handleLegendHover }
handleLegendToggle={ this.handleLegendToggle }
legendDirection={ legendDirection }
itemsLabel={ itemsLabel }
valueType={ valueType }
/>
);
@ -236,10 +251,10 @@ class Chart extends Component {
break;
}
return (
<div className="woocommerce-chart" ref={ this.chartRef }>
<div className="woocommerce-chart">
<div className="woocommerce-chart__header">
<H className="woocommerce-chart__title">{ title }</H>
{ width >= WIDE_BREAKPOINT && legendDirection === 'row' && legend }
{ isViewportWide && legendDirection === 'row' && legend }
{ this.renderIntervalSelector() }
<NavigableMenu
className="woocommerce-chart__types"
@ -276,29 +291,33 @@ class Chart extends Component {
'woocommerce-chart__body',
`woocommerce-chart__body-${ chartDirection }`
) }
ref={ this.chartBodyRef }
>
{ width >= WIDE_BREAKPOINT && legendDirection === 'column' && legend }
<D3Chart
colorScheme={ d3InterpolateViridis }
data={ visibleData }
dateParser={ dateParser }
height={ chartHeight }
margin={ margin }
mode={ mode }
orderedKeys={ orderedKeys }
pointLabelFormat={ pointLabelFormat }
tooltipFormat={ tooltipFormat }
tooltipTitle={ tooltipTitle }
type={ type }
interval={ interval }
width={ chartDirection === 'row' ? width - 320 : width }
xFormat={ xFormat }
x2Format={ x2Format }
yFormat={ yFormat }
valueType={ valueType }
/>
{ isViewportWide && legendDirection === 'column' && legend }
{ width > 0 && (
<D3Chart
colorScheme={ d3InterpolateViridis }
data={ visibleData }
dateParser={ dateParser }
height={ chartHeight }
margin={ margin }
mode={ mode }
orderedKeys={ orderedKeys }
pointLabelFormat={ pointLabelFormat }
tooltipFormat={ tooltipFormat }
tooltipPosition={ isViewportLarge ? 'over' : 'below' }
tooltipTitle={ tooltipTitle }
type={ type }
interval={ interval }
width={ chartDirection === 'row' ? width - 320 : width }
xFormat={ xFormat }
x2Format={ x2Format }
yFormat={ yFormat }
valueType={ valueType }
/>
) }
</div>
{ width < WIDE_BREAKPOINT && <div className="woocommerce-chart__footer">{ legend }</div> }
{ ! isViewportWide && <div className="woocommerce-chart__footer">{ legend }</div> }
</Section>
</div>
);
@ -396,4 +415,8 @@ Chart.defaultProps = {
interval: 'day',
};
export default Chart;
export default withViewportMatch( {
isViewportMobile: '< medium',
isViewportLarge: '>= large',
isViewportWide: '>= wide',
} )( Chart );

View File

@ -3,8 +3,9 @@
* External dependencies
*/
import classNames from 'classnames';
import { Component } from '@wordpress/element';
import { Component, createRef } from '@wordpress/element';
import PropTypes from 'prop-types';
import { sprintf } from '@wordpress/i18n';
/**
* WooCommerce dependencies
@ -14,7 +15,7 @@ import { formatCurrency } from '@woocommerce/currency';
/**
* Internal dependencies
*/
import './style.scss';
import './legend.scss';
import { getColor } from './utils';
function getFormatedTotal( total, valueType ) {
@ -36,65 +37,108 @@ function getFormatedTotal( total, valueType ) {
* A legend specifically designed for the WooCommerce admin charts.
*/
class Legend extends Component {
constructor() {
super();
this.listRef = createRef();
this.state = {
isScrollable: false,
};
}
componentDidMount() {
this.updateListScroll();
window.addEventListener( 'resize', this.updateListScroll );
}
componentWillUnmount() {
window.removeEventListener( 'resize', this.updateListScroll );
}
updateListScroll = () => {
const list = this.listRef.current;
const scrolledToEnd = list.scrollHeight - list.scrollTop <= list.offsetHeight;
this.setState( {
isScrollable: ! scrolledToEnd,
} );
};
render() {
const {
colorScheme,
data,
handleLegendHover,
handleLegendToggle,
itemsLabel,
legendDirection,
valueType,
} = this.props;
const { isScrollable } = this.state;
const colorParams = {
orderedKeys: data,
colorScheme,
};
const numberOfRowsVisible = data.filter( row => row.visible ).length;
const showTotalLabel = legendDirection === 'column' && data.length > 5 && itemsLabel;
return (
<ul
<div
className={ classNames(
'woocommerce-legend',
`woocommerce-legend__direction-${ legendDirection }`,
{
'has-total': showTotalLabel,
'is-scrollable': isScrollable,
},
this.props.className
) }
>
{ data.map( row => (
<li
className={ classNames( 'woocommerce-legend__item', {
'woocommerce-legend__item-checked': row.visible,
} ) }
key={ row.key }
id={ row.key }
onMouseEnter={ handleLegendHover }
onMouseLeave={ handleLegendHover }
onBlur={ handleLegendHover }
onFocus={ handleLegendHover }
>
<button
onClick={ handleLegendToggle }
<ul
className="woocommerce-legend__list"
ref={ this.listRef }
onScroll={ showTotalLabel ? this.updateListScroll : null }
>
{ data.map( row => (
<li
className={ classNames( 'woocommerce-legend__item', {
'woocommerce-legend__item-checked': row.visible,
} ) }
key={ row.key }
id={ row.key }
disabled={ row.visible && numberOfRowsVisible <= 1 }
onMouseEnter={ handleLegendHover }
onMouseLeave={ handleLegendHover }
onBlur={ handleLegendHover }
onFocus={ handleLegendHover }
>
<div className="woocommerce-legend__item-container" id={ row.key }>
<span
className={ classNames( 'woocommerce-legend__item-checkmark', {
'woocommerce-legend__item-checkmark-checked': row.visible,
} ) }
id={ row.key }
style={ { color: getColor( row.key, colorParams ) } }
/>
<span className="woocommerce-legend__item-title" id={ row.key }>
{ row.key }
</span>
<span className="woocommerce-legend__item-total" id={ row.key }>
{ getFormatedTotal( row.total, valueType ) }
</span>
</div>
</button>
</li>
) ) }
</ul>
<button
onClick={ handleLegendToggle }
id={ row.key }
disabled={ row.visible && numberOfRowsVisible <= 1 }
>
<div className="woocommerce-legend__item-container" id={ row.key }>
<span
className={ classNames( 'woocommerce-legend__item-checkmark', {
'woocommerce-legend__item-checkmark-checked': row.visible,
} ) }
id={ row.key }
style={ { color: getColor( row.key, colorParams ) } }
/>
<span className="woocommerce-legend__item-title" id={ row.key }>
{ row.key }
</span>
<span className="woocommerce-legend__item-total" id={ row.key }>
{ getFormatedTotal( row.total, valueType ) }
</span>
</div>
</button>
</li>
) ) }
</ul>
{ showTotalLabel && (
<div className="woocommerce-legend__total">{ sprintf( itemsLabel, data.length ) }</div>
) }
</div>
);
}
}
@ -124,6 +168,11 @@ Legend.propTypes = {
* Display legend items as a `row` or `column` inside a flex-box.
*/
legendDirection: PropTypes.oneOf( [ 'row', 'column' ] ),
/**
* Label to describe the legend items. It will be displayed in the legend of
* comparison charts when there are many.
*/
itemsLabel: PropTypes.string,
/**
* What type of data is to be displayed? Number, Average, String?
*/

View File

@ -0,0 +1,214 @@
/** @format */
.woocommerce-legend {
&.has-total {
padding-bottom: 50px;
position: relative;
}
&.woocommerce-legend__direction-column {
border-right: 1px solid $core-grey-light-700;
min-width: 320px;
.woocommerce-chart__footer & {
border-right: none;
}
}
}
.woocommerce-legend__list {
color: $black;
display: flex;
height: 100%;
margin: 0;
.woocommerce-legend__direction-column & {
flex-direction: column;
height: 300px;
overflow: auto;
.woocommerce-chart__footer & {
border-top: 1px solid $core-grey-light-700;
height: 100%;
max-height: none;
min-height: none;
}
}
.has-total.woocommerce-legend__direction-column & {
height: 250px;
.woocommerce-chart__footer & {
height: auto;
max-height: 220px;
min-height: none;
}
}
.woocommerce-legend__direction-row & {
flex-direction: row;
}
}
.woocommerce-legend__item {
& > button {
display: flex;
justify-content: center;
align-items: center;
background-color: $white;
color: $core-grey-dark-500;
display: inline-flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
width: 100%;
border: none;
padding: 0;
.woocommerce-legend__item-container {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
position: relative;
padding: 3px 0 3px 24px;
cursor: pointer;
font-size: 13px;
user-select: none;
width: 100%;
&:hover {
input {
~ .woocommerce-legend__item-checkmark {
background-color: $core-grey-light-200;
}
}
}
.woocommerce-legend__item-checkmark {
border: 1px solid $core-grey-light-900;
position: absolute;
top: 4px;
left: 0;
height: 16px;
width: 16px;
background-color: $white;
&::after {
content: '';
position: absolute;
display: none;
}
&.woocommerce-legend__item-checkmark-checked {
background-color: currentColor;
border-color: currentColor;
&::after {
display: block;
left: 5px;
top: 2px;
width: 3px;
height: 6px;
border: solid $white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
}
}
.woocommerce-legend__item-total {
font-weight: bold;
}
}
&:focus {
outline: none;
.woocommerce-legend__item-container {
.woocommerce-legend__item-checkmark {
outline: 2px solid $core-grey-light-900;
}
}
}
&:hover {
background-color: $core-grey-light-100;
}
}
.woocommerce-legend__direction-column & {
margin: 2px 0;
padding: 0;
& > button {
height: 32px;
padding: 0 17px;
}
&:first-child {
margin-top: $gap-small;
}
&:last-child::after {
content: '';
display: block;
height: $gap-small;
width: 100%;
}
}
.woocommerce-legend__direction-row & {
padding: 0;
margin: 0;
& > button {
padding: 0 17px;
.woocommerce-legend__item-container {
height: 50px;
align-items: center;
.woocommerce-legend__item-checkmark {
top: 17px;
}
.woocommerce-legend__item-title {
margin-right: 17px;
}
}
}
}
}
.woocommerce-legend__total {
align-items: center;
background: $white;
border-top: 1px solid $core-grey-light-700;
bottom: 0;
color: $core-grey-dark-500;
display: flex;
height: 50px;
justify-content: center;
left: 0;
position: absolute;
right: 0;
text-transform: uppercase;
&::before {
background: linear-gradient(180deg, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.2));
bottom: 100%;
content: '';
height: 20px;
left: 0;
opacity: 0;
pointer-events: none;
position: absolute;
right: 0;
transition: opacity 0.3s;
}
.is-scrollable &::before {
opacity: 1;
}
}

View File

@ -2,7 +2,7 @@
.woocommerce-chart {
margin-top: -$gap;
margin-bottom: $gap-large;
background: white;
background: $white;
border: 1px solid $core-grey-light-700;
border-top: 0;
@ -61,15 +61,6 @@
.woocommerce-chart__footer {
width: 100%;
.woocommerce-legend {
&.woocommerce-legend__direction-column {
height: 100%;
min-height: none;
border-right: none;
margin-bottom: $gap;
}
}
}
svg {
@ -185,6 +176,12 @@
}
}
}
.y-axis,
.axis-month {
.tick text {
font-size: 10px;
}
}
.focus-grid {
line {
@ -252,146 +249,3 @@
position: absolute;
}
}
.woocommerce-legend {
color: $black;
display: flex;
height: 100%;
margin: 0;
&.woocommerce-legend__direction-column {
flex-direction: column;
border-right: 1px solid $core-grey-light-700;
height: 300px;
min-width: 320px;
li {
margin: 0;
padding: 0;
button {
height: 32px;
padding: 0 17px;
}
&:first-child {
margin-top: 17px;
}
}
}
&.woocommerce-legend__direction-row {
flex-direction: row;
li {
padding: 0;
margin: 0;
button {
padding: 0 17px;
.woocommerce-legend__item-container {
height: 50px;
align-items: center;
.woocommerce-legend__item-checkmark {
top: 17px;
}
.woocommerce-legend__item-title {
margin-right: 17px;
}
}
}
}
}
li {
&.woocommerce-legend__item {
button {
&:hover {
background-color: $core-grey-light-100;
}
}
}
button {
background-color: $white;
color: $core-grey-dark-500;
display: inline-flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
width: 100%;
border: none;
padding: 0;
.woocommerce-legend__item-container {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
position: relative;
padding: 3px 0 3px 24px;
cursor: pointer;
font-size: 13px;
user-select: none;
width: 100%;
&:hover {
input {
~ .woocommerce-legend__item-checkmark {
background-color: $core-grey-light-200;
}
}
}
.woocommerce-legend__item-checkmark {
border: 1px solid $core-grey-light-900;
position: absolute;
top: 2px;
left: 0;
height: 16px;
width: 16px;
background-color: $white;
&::after {
content: '';
position: absolute;
display: none;
}
&.woocommerce-legend__item-checkmark-checked {
background-color: currentColor;
border-color: currentColor;
&::after {
display: block;
left: 5px;
top: 2px;
width: 3px;
height: 6px;
border: solid $white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
}
}
.woocommerce-legend__item-total {
font-weight: bold;
}
}
&:focus {
outline: none;
.woocommerce-legend__item-container {
.woocommerce-legend__item-checkmark {
outline: 2px solid $core-grey-light-900;
}
}
}
}
}
}

View File

@ -3,7 +3,7 @@
*
* @format
*/
import { shallow } from 'enzyme';
import { mount } from 'enzyme';
/**
* Internal dependencies
@ -26,7 +26,7 @@ const data = [
describe( 'Legend', () => {
test( 'should not disable any button if more than one is active', () => {
const topSellingProducts = shallow( <Legend colorScheme={ colorScheme } data={ data } /> );
const topSellingProducts = mount( <Legend colorScheme={ colorScheme } data={ data } /> );
expect( topSellingProducts.find( 'button' ).get( 0 ).props.disabled ).toBeFalsy();
expect( topSellingProducts.find( 'button' ).get( 1 ).props.disabled ).toBeFalsy();
@ -35,7 +35,7 @@ describe( 'Legend', () => {
test( 'should disable the last active button', () => {
data[ 1 ].visible = false;
const topSellingProducts = shallow( <Legend colorScheme={ colorScheme } data={ data } /> );
const topSellingProducts = mount( <Legend colorScheme={ colorScheme } data={ data } /> );
expect( topSellingProducts.find( 'button' ).get( 0 ).props.disabled ).toBeTruthy();
expect( topSellingProducts.find( 'button' ).get( 1 ).props.disabled ).toBeFalsy();

View File

@ -172,12 +172,9 @@ describe( 'getYScale', () => {
describe( 'getYTickOffset', () => {
it( 'properly scale the y values for the y-axis ticks given the height and maximum y value', () => {
const testYTickOffset1 = getYTickOffset( 100, 1, testYMax );
const testYTickOffset1 = getYTickOffset( 100, testYMax );
expect( testYTickOffset1( 0 ) ).toEqual( 112 );
expect( testYTickOffset1( testYMax ) ).toEqual( 12 );
const testYTickOffset2 = getYTickOffset( 100, 2, testYMax );
expect( testYTickOffset2( 0 ) ).toEqual( 124 );
expect( testYTickOffset2( testYMax ) ).toEqual( 24 );
} );
} );

View File

@ -19,7 +19,7 @@ import { format as formatDate } from '@wordpress/date';
/**
* WooCommerce dependencies
*/
import { dayTicksThreshold } from '@woocommerce/date';
import { dayTicksThreshold, weekTicksThreshold } from '@woocommerce/date';
import { formatCurrency } from '@woocommerce/currency';
/**
@ -188,14 +188,13 @@ export const getYScale = ( height, yMax ) =>
/**
* Describes getyTickOffset
* @param {number} height - calculated height of the charting space
* @param {number} scale - ratio of the expected width to calculated width (given the viewbox)
* @param {number} yMax - from `getYMax`
* @returns {function} the D3 linear scale from 0 to the value from `getYMax`, offset by 12 pixels down
*/
export const getYTickOffset = ( height, scale, yMax ) =>
export const getYTickOffset = ( height, yMax ) =>
d3ScaleLinear()
.domain( [ 0, yMax ] )
.rangeRound( [ height + scale * 12, scale * 12 ] );
.rangeRound( [ height + 12, 12 ] );
/**
* Describes getyTickOffset
@ -303,7 +302,10 @@ const calculateXTicksIncrementFactor = ( uniqueDates, maxTicks ) => {
export const getXTicks = ( uniqueDates, width, layout, interval ) => {
const maxTicks = calculateMaxXTicks( width, layout );
if ( uniqueDates.length >= dayTicksThreshold && interval === 'day' ) {
if (
( uniqueDates.length >= dayTicksThreshold && interval === 'day' ) ||
( uniqueDates.length >= weekTicksThreshold && interval === 'week' )
) {
uniqueDates = getFirstDatePerMonth( uniqueDates );
}
if ( uniqueDates.length <= maxTicks ) {
@ -374,6 +376,14 @@ export const compareStrings = ( s1, s2, splitChar = ' ' ) => {
export const drawAxis = ( node, params ) => {
const xScale = params.type === 'line' ? params.xLineScale : params.xScale;
const removeDuplicateDates = ( d, i, ticks, formatter ) => {
const monthDate = d instanceof Date ? d : new Date( d );
let prevMonth = i !== 0 ? ticks[ i - 1 ] : ticks[ i ];
prevMonth = prevMonth instanceof Date ? prevMonth : new Date( prevMonth );
return i === 0
? formatter( monthDate )
: compareStrings( formatter( prevMonth ), formatter( monthDate ) ).join( ' ' );
};
const yGrids = [];
for ( let i = 0; i < 4; i++ ) {
@ -390,7 +400,7 @@ export const drawAxis = ( node, params ) => {
.call(
d3AxisBottom( xScale )
.tickValues( ticks )
.tickFormat( d => params.xFormat( d instanceof Date ? d : new Date( d ) ) )
.tickFormat( ( d, i ) => removeDuplicateDates( d, i, ticks, params.xFormat ) )
);
node
@ -401,23 +411,10 @@ export const drawAxis = ( node, params ) => {
.call(
d3AxisBottom( xScale )
.tickValues( ticks )
.tickFormat( ( d, i ) => {
const monthDate = d instanceof Date ? d : new Date( d );
let prevMonth = i !== 0 ? ticks[ i - 1 ] : ticks[ i ];
prevMonth = prevMonth instanceof Date ? prevMonth : new Date( prevMonth );
return i === 0
? params.x2Format( monthDate )
: compareStrings( params.x2Format( prevMonth ), params.x2Format( monthDate ) ).join(
' '
);
} )
.tickFormat( ( d, i ) => removeDuplicateDates( d, i, ticks, params.x2Format ) )
)
.call( g => g.select( '.domain' ).remove() );
node
.selectAll( '.axis-month .tick text' )
.style( 'font-size', `${ Math.round( params.scale * 10 ) }px` );
node
.append( 'g' )
.attr( 'class', 'pipes' )
@ -453,10 +450,6 @@ export const drawAxis = ( node, params ) => {
.tickFormat( d => d3Format( params.yFormat )( d !== 0 ? d : 0 ) )
);
node
.selectAll( '.y-axis .tick text' )
.style( 'font-size', `${ Math.round( params.scale * 10 ) }px` );
node.selectAll( '.domain' ).remove();
node
.selectAll( '.axis' )
@ -541,19 +534,18 @@ const handleMouseOutLineChart = ( parentNode, params ) => {
params.tooltip.style( 'visibility', 'hidden' );
};
export const WIDE_BREAKPOINT = 1100;
const calculateTooltipXPosition = (
elementCoords,
chartCoords,
tooltipSize,
tooltipMargin,
elementWidthRatio
elementWidthRatio,
tooltipPosition
) => {
const xPosition =
elementCoords.left + elementCoords.width * elementWidthRatio + tooltipMargin - chartCoords.left;
if ( chartCoords.width < WIDE_BREAKPOINT ) {
if ( tooltipPosition === 'below' ) {
return Math.max(
tooltipMargin,
Math.min(
@ -577,8 +569,14 @@ const calculateTooltipXPosition = (
return xPosition;
};
const calculateTooltipYPosition = ( elementCoords, chartCoords, tooltipSize, tooltipMargin ) => {
if ( chartCoords.width < WIDE_BREAKPOINT ) {
const calculateTooltipYPosition = (
elementCoords,
chartCoords,
tooltipSize,
tooltipMargin,
tooltipPosition
) => {
if ( tooltipPosition === 'below' ) {
return chartCoords.height;
}
@ -590,7 +588,7 @@ const calculateTooltipYPosition = ( elementCoords, chartCoords, tooltipSize, too
return yPosition;
};
const calculateTooltipPosition = ( element, chart, elementWidthRatio = 1 ) => {
const calculateTooltipPosition = ( element, chart, tooltipPosition, elementWidthRatio = 1 ) => {
const elementCoords = element.getBoundingClientRect();
const chartCoords = chart.getBoundingClientRect();
const tooltipSize = d3Select( '.tooltip' )
@ -598,7 +596,7 @@ const calculateTooltipPosition = ( element, chart, elementWidthRatio = 1 ) => {
.getBoundingClientRect();
const tooltipMargin = 24;
if ( chartCoords.width < WIDE_BREAKPOINT ) {
if ( tooltipPosition === 'below' ) {
elementWidthRatio = 0;
}
@ -608,9 +606,16 @@ const calculateTooltipPosition = ( element, chart, elementWidthRatio = 1 ) => {
chartCoords,
tooltipSize,
tooltipMargin,
elementWidthRatio
elementWidthRatio,
tooltipPosition
),
y: calculateTooltipYPosition(
elementCoords,
chartCoords,
tooltipSize,
tooltipMargin,
tooltipPosition
),
y: calculateTooltipYPosition( elementCoords, chartCoords, tooltipSize, tooltipMargin ),
};
};
@ -667,7 +672,11 @@ export const drawLines = ( node, data, params ) => {
return `${ label } ${ formatCurrency( d.value ) }`;
} )
.on( 'focus', ( d, i, nodes ) => {
const position = calculateTooltipPosition( d3Event.target, node.node() );
const position = calculateTooltipPosition(
d3Event.target,
node.node(),
params.tooltipPosition
);
handleMouseOverLineChart( d.date, nodes[ i ].parentNode, node, data, params, position );
} )
.on( 'blur', ( d, i, nodes ) => handleMouseOutLineChart( nodes[ i ].parentNode, params ) );
@ -715,7 +724,12 @@ export const drawLines = ( node, data, params ) => {
.attr( 'opacity', 0 )
.on( 'mouseover', ( d, i, nodes ) => {
const elementWidthRatio = i === 0 || i === params.dateSpaces.length - 1 ? 0 : 0.5;
const position = calculateTooltipPosition( d3Event.target, node.node(), elementWidthRatio );
const position = calculateTooltipPosition(
d3Event.target,
node.node(),
params.tooltipPosition,
elementWidthRatio
);
handleMouseOverLineChart( d.date, nodes[ i ].parentNode, node, data, params, position );
} )
.on( 'mouseout', ( d, i, nodes ) => handleMouseOutLineChart( nodes[ i ].parentNode, params ) );
@ -780,7 +794,7 @@ export const drawBars = ( node, data, params ) => {
} )
.on( 'focus', ( d, i, nodes ) => {
const targetNode = d.value > 0 ? d3Event.target : d3Event.target.parentNode;
const position = calculateTooltipPosition( targetNode, node.node() );
const position = calculateTooltipPosition( targetNode, node.node(), params.tooltipPosition );
handleMouseOverBarChart( d.date, nodes[ i ].parentNode, node, data, params, position );
} )
.on( 'blur', ( d, i, nodes ) => handleMouseOutBarChart( nodes[ i ].parentNode, params ) );
@ -794,7 +808,11 @@ export const drawBars = ( node, data, params ) => {
.attr( 'height', params.height )
.attr( 'opacity', '0' )
.on( 'mouseover', ( d, i, nodes ) => {
const position = calculateTooltipPosition( d3Event.target, node.node() );
const position = calculateTooltipPosition(
d3Event.target,
node.node(),
params.tooltipPosition
);
handleMouseOverBarChart( d.date, nodes[ i ].parentNode, node, data, params, position );
} )
.on( 'mouseout', ( d, i, nodes ) => handleMouseOutBarChart( nodes[ i ].parentNode, params ) );

View File

@ -1,19 +0,0 @@
/** @format */
/**
* External dependencies
*/
import { omit } from 'lodash';
export function flatenFilters( filters ) {
const allFilters = [];
filters.forEach( f => {
if ( ! f.subFilters ) {
allFilters.push( f );
} else {
allFilters.push( omit( f, 'subFilters' ) );
const subFilters = flatenFilters( f.subFilters );
allFilters.push( ...subFilters );
}
} );
return allFilters;
}

View File

@ -1,52 +1,6 @@
/** @format */
/**
* External Dependencies
*/
// Turn on react-dates classes/styles, see https://github.com/airbnb/react-dates#initialize
import 'react-dates/initialize';
export { default as AdvancedFilters } from './filters/advanced';
export { default as AnimationSlider } from './animation-slider';
export { default as Card } from './card';
export { default as Chart } from './chart';
export { default as ChartLegend } from './chart/legend';
export { default as ChartPlaceholder } from './chart/placeholder';
export { default as Count } from './count';
export { default as CompareFilter } from './filters/compare';
export { default as D3Chart } from './chart/charts';
export { default as DatePicker } from './filters/date';
export { default as DateRange } from './calendar';
export { default as DropdownButton } from './dropdown-button';
export { default as EllipsisMenu } from './ellipsis-menu';
export { default as EmptyContent } from './empty-content';
export { default as Flag } from './flag';
export { default as FilterPicker } from './filters/filter';
export { default as Gravatar } from './gravatar';
export { H, Section } from './section';
export { default as ImageAsset } from './image-asset';
export { default as Link } from './link';
export { default as MenuItem } from './ellipsis-menu/menu-item';
export { default as MenuTitle } from './ellipsis-menu/menu-title';
export { default as OrderStatus } from './order-status';
export { default as Pagination } from './pagination';
export { default as ProductImage } from './product-image';
export { default as ProductRating } from './rating/product';
export { default as Rating } from './rating';
export { default as ReportFilters } from './filters';
export { default as ReviewRating } from './rating/review';
export { default as Search } from './search';
export { default as SectionHeader } from './section-header';
export { default as SegmentedSelection } from './segmented-selection';
export { default as SplitButton } from './split-button';
export { default as SummaryList } from './summary';
export { default as SummaryListPlaceholder } from './summary/placeholder';
export { default as SummaryNumber } from './summary/item';
export { default as Table } from './table/table';
export { default as TableCard } from './table';
export { default as EmptyTable } from './table/empty';
export { default as TablePlaceholder } from './table/placeholder';
export { default as TableSummary } from './table/summary';
export { default as Tag } from './tag';
export { default as useFilters } from './higher-order/use-filters';
export { default as ViewMoreList } from './view-more-list';

View File

@ -5,7 +5,7 @@
grid-template-columns: 1fr 1fr;
grid-column-gap: $gap-large;
@include breakpoint( '<1100px' ) {
@include breakpoint( '<960px' ) {
grid-template-columns: 1fr;
}
}

View File

@ -13,12 +13,13 @@ import { withSelect } from '@wordpress/data';
*/
import { Card, EmptyTable, TableCard } from '@woocommerce/components';
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import { getAdminLink } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { getAdminLink } from 'lib/nav-utils';
import { numberFormat } from 'lib/number';
import ReportError from 'analytics/components/report-error';
import { NAMESPACE } from 'store/constants';
import './style.scss';
@ -91,7 +92,7 @@ export class TopSellingProducts extends Component {
const title = __( 'Top Selling Products', 'wc-admin' );
if ( isError ) {
// @TODO An error notice should be displayed when there is an error
return <ReportError className="woocommerce-top-selling-products" isError />;
}
if ( ! isRequesting && rows.length === 0 ) {

View File

@ -1,5 +1,9 @@
/** @format */
.woocommerce-top-selling-products {
&.woocommerce-empty-content {
margin-bottom: $gap-large;
}
.woocommerce-card__body {
padding: 0;
}

View File

@ -16,7 +16,8 @@ import PropTypes from 'prop-types';
/**
* Internal dependencies
*/
import * as components from '@woocommerce/components';
import * as components from 'components';
import * as pkgComponents from '@woocommerce/components';
class Example extends Component {
state = {
@ -28,7 +29,15 @@ class Example extends Component {
}
async getCode() {
const readme = require( `components/${ this.props.filePath }/example.md` );
let readme;
try {
readme = require( `components/src/${ this.props.filePath }/example.md` );
} catch ( e ) {
readme = require( `components/${ this.props.filePath }/example.md` );
}
if ( ! readme ) {
return;
}
// Example to render is the first jsx code block that appears in the readme
let code = codeBlocks( readme ).find( block => 'jsx' === block.lang ).code;
@ -47,6 +56,7 @@ class Example extends Component {
const scope = {
...wpComponents,
...components,
...pkgComponents,
Component,
withState,
getSettings,

View File

@ -1,7 +1,7 @@
/** @format */
.woocommerce_devdocs {
@include breakpoint( '>1100px' ) {
@include breakpoint( '>1280px' ) {
&.is-list {
column-gap: $gap-large;
columns: 2;

View File

@ -10,7 +10,7 @@
@include breakpoint( '<782px' ) {
position: relative;
background: #fff;
background: $white;
margin: 0;
padding: 0;
top: -3px;
@ -20,12 +20,12 @@
flex: 1 100%;
}
@include breakpoint( '782px-1100px' ) {
@include breakpoint( '782px-960px' ) {
max-width: 280px;
height: 60px;
}
@include breakpoint( '>1100px' ) {
@include breakpoint( '>960px' ) {
max-width: 400px;
}
@ -40,7 +40,7 @@
height: 60px;
justify-content: flex-end;
@include breakpoint( '>1100px' ) {
@include breakpoint( '>960px' ) {
height: 80px;
}
@ -53,7 +53,7 @@
width: 18px;
height: 18px;
@include breakpoint( '>1100px' ) {
@include breakpoint( '>960px' ) {
width: 24px;
height: 24px;
}
@ -80,7 +80,7 @@
height: 60px;
border-bottom: 3px solid $white;
@include breakpoint( '>1100px' ) {
@include breakpoint( '>960px' ) {
height: 80px;
}
@ -91,7 +91,7 @@
}
font-size: 11px;
@include breakpoint( '>1100px' ) {
@include breakpoint( '>960px' ) {
font-size: 13px;
}
@ -109,14 +109,14 @@
top: 10px;
left: 50%;
@include breakpoint( '782px-1100px' ) {
@include breakpoint( '782px-960px' ) {
top: 8px;
right: 18px;
left: initial;
margin-left: 0;
}
@include breakpoint( '>1100px' ) {
@include breakpoint( '>960px' ) {
top: 16px;
right: 28px;
left: initial;
@ -167,7 +167,7 @@
position: absolute;
padding: 1px;
background: $core-orange;
border: 2px solid white;
border: 2px solid $white;
width: 4px;
height: 4px;
display: inline-block;
@ -209,10 +209,10 @@
// Extra padding is needed at the bottom of the wrapper because of our positioning, height, and overflow rules. Otherwise, some content can get cut off.
padding-bottom: $gap-small * 6;
@include breakpoint( '782px-1100px' ) {
@include breakpoint( '782px-960px' ) {
padding-bottom: $gap-small;
}
@include breakpoint( '>1100px' ) {
@include breakpoint( '>960px' ) {
top: 112px;
padding-bottom: $gap * 2;
}

View File

@ -3,19 +3,23 @@
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { Component, findDOMNode } from '@wordpress/element';
import classnames from 'classnames';
import { decodeEntities } from '@wordpress/html-entities';
import { Fill } from 'react-slot-fill';
import PropTypes from 'prop-types';
import ReactDom from 'react-dom';
/**
* WooCommerce dependencies
*/
import { getNewPath, getTimeRelatedQuery } from '@woocommerce/navigation';
import { Link } from '@woocommerce/components';
/**
* Internal dependencies
*/
import './style.scss';
import ActivityPanel from './activity-panel';
import { Link } from '@woocommerce/components';
class Header extends Component {
constructor() {
@ -29,7 +33,7 @@ class Header extends Component {
}
componentDidMount() {
this.threshold = ReactDom.findDOMNode( this ).offsetTop;
this.threshold = findDOMNode( this ).offsetTop;
window.addEventListener( 'scroll', this.onWindowScroll );
this.updateIsScrolled();
}
@ -84,7 +88,10 @@ class Header extends Component {
</span>
{ _sections.map( ( section, i ) => {
const sectionPiece = Array.isArray( section ) ? (
<Link href={ section[ 0 ] } type={ isEmbedded ? 'wp-admin' : 'wc-admin' }>
<Link
href={ getNewPath( getTimeRelatedQuery(), section[ 0 ], {} ) }
type={ isEmbedded ? 'wp-admin' : 'wc-admin' }
>
{ section[ 1 ] }
</Link>
) : (

View File

@ -24,7 +24,7 @@
flex-flow: row wrap;
}
@include breakpoint( '782px-1100px' ) {
@include breakpoint( '782px-960px' ) {
height: 60px;
}
@ -38,12 +38,12 @@
line-height: 50px;
background: $white;
@include breakpoint( '782px-1100px' ) {
@include breakpoint( '782px-960px' ) {
height: 60px;
line-height: 60px;
}
@include breakpoint( '>1100px' ) {
@include breakpoint( '>960px' ) {
height: 80px;
line-height: 80px;
}

View File

@ -6,6 +6,11 @@ import { Component, createElement } from '@wordpress/element';
import { parse } from 'qs';
import { find, last } from 'lodash';
/**
* WooCommerce dependencies
*/
import { getTimeRelatedQuery, stringifyQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
@ -13,7 +18,6 @@ import Analytics from 'analytics';
import AnalyticsReport from 'analytics/report';
import Dashboard from 'dashboard';
import DevDocs from 'devdocs';
import { getTimeRelatedQuery, stringifyQuery } from 'lib/nav-utils';
const getPages = () => {
const pages = [

View File

@ -8,13 +8,17 @@ import { Slot } from 'react-slot-fill';
import PropTypes from 'prop-types';
import { get } from 'lodash';
/**
* WooCommerce dependencies
*/
import { history } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import './style.scss';
import { Controller, getPages } from './controller';
import Header from 'header';
import history from 'lib/history';
import Notices from './notices';
import { recordPageView } from 'lib/tracks';

View File

@ -8,7 +8,7 @@
margin: 80px 0 0 $fallback-gutter-large;
margin: 80px 0 0 $gutter-large;
@include breakpoint( '>1100px' ) {
@include breakpoint( '>960px' ) {
margin-top: 100px;
}
}

View File

@ -6,20 +6,21 @@ import apiFetch from '@wordpress/api-fetch';
import { identity } from 'lodash';
/**
* Internal dependencies
* WooCommerce dependencies
*/
import { getIdsFromQuery, stringifyQuery } from 'lib/nav-utils';
import { getIdsFromQuery, stringifyQuery } from '@woocommerce/navigation';
/**
* Get a function that accepts ids as they are found in url parameter and
* returns a promise with an optional method applied to results
*
* @param {string} path - api path
* @param {string|function} path - api path string or a function of the query returning api path string
* @param {Function} [handleData] - function applied to each iteration of data
* @returns {Function} - a function of ids returning a promise
*/
export function getRequestByIdString( path, handleData = identity ) {
return function( queryString = '' ) {
return function( queryString = '', query ) {
const pathString = 'function' === typeof path ? path( query ) : path;
const idList = getIdsFromQuery( queryString );
if ( idList.length < 1 ) {
return Promise.resolve( [] );
@ -28,6 +29,6 @@ export function getRequestByIdString( path, handleData = identity ) {
include: idList.join( ',' ),
per_page: idList.length,
} );
return apiFetch( { path: path + payload } ).then( data => data.map( handleData ) );
return apiFetch( { path: pathString + payload } ).then( data => data.map( handleData ) );
};
}

View File

@ -0,0 +1,22 @@
/** @format */
/**
* External dependencies
*/
import { find } from 'lodash';
/**
* Takes a chart name returns the configuration for that chart from and array
* of charts. If the chart is not found it will return the first chart.
*
* @param {string} chartName - the name of the chart to get configuration for
* @param {array} charts - list of charts for a particular report
* @returns {object} - chart configuration object
*/
export default function getSelectedChart( chartName, charts = [] ) {
const chart = find( charts, { key: chartName } );
if ( chart ) {
return chart;
}
return charts[ 0 ];
}

View File

@ -1,24 +0,0 @@
Nav Utils
=========
This is a library of functions used in navigation.
## `getPath()`
Get the current path from history.
## `getQuery()`
Get the current query string, parsed into an object, from history.
## `getAdminLink( string: path )`
JS version of `admin_url`. Returns the full URL for a page in wp-admin.
## `getNewPath( object: query, string: path, object: currentQuery )`
Return a URL with set query parameters. Optional `path`, `currentQuery`, both will default to the current value fetched from `history` if not provided.
## `updateQueryString( object: query )`
Updates the query parameters of the current page.

View File

@ -1,12 +0,0 @@
/** @format */
// 782px is the width designated by Gutenberg's `</ Popover>` component.
// * https://github.com/WordPress/gutenberg/blob/c8f8806d4465a83c1a0bc62d5c61377b56fa7214/components/popover/utils.js#L6
export function isMobileViewport() {
return window.innerWidth <= 782;
}
// Most screens at 1100px or lower are tablets
export function isTabletViewport() {
return window.innerWidth > 782 && window.innerWidth <= 1100;
}

View File

@ -3,6 +3,7 @@
*/
export const NAMESPACE = '/wc/v3/';
export const SWAGGERNAMESPACE = 'https://virtserver.swaggerhub.com/peterfabian/wc-v3-api/1.0.0/';
export const ERROR = 'ERROR';
// WordPress & WooCommerce both set a hard limit of 100 for the per_page parameter

View File

@ -0,0 +1,17 @@
/** @format */
export default {
setCoupons( coupons, query ) {
return {
type: 'SET_COUPONS',
coupons,
query: query || {},
};
},
setCouponsError( query ) {
return {
type: 'SET_COUPONS_ERROR',
query: query || {},
};
},
};

View File

@ -0,0 +1,15 @@
/** @format */
/**
* Internal dependencies
*/
import actions from './actions';
import reducer from './reducer';
import resolvers from './resolvers';
import selectors from './selectors';
export default {
actions,
reducer,
resolvers,
selectors,
};

View File

@ -0,0 +1,32 @@
/** @format */
/**
* External dependencies
*/
import { merge } from 'lodash';
/**
* Internal dependencies
*/
import { ERROR } from 'store/constants';
import { getJsonString } from 'store/utils';
const DEFAULT_STATE = {};
export default function couponsReducer( state = DEFAULT_STATE, action ) {
const queryKey = getJsonString( action.query );
switch ( action.type ) {
case 'SET_COUPONS':
return merge( {}, state, {
[ queryKey ]: action.coupons,
} );
case 'SET_COUPONS_ERROR':
return merge( {}, state, {
[ queryKey ]: ERROR,
} );
}
return state;
}

View File

@ -0,0 +1,35 @@
/** @format */
/**
* External dependencies
*/
import { dispatch } from '@wordpress/data';
// import apiFetch from '@wordpress/api-fetch';
/**
* WooCommerce dependencies
*/
import { stringifyQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
// import { NAMESPACE } from 'store/constants';
import { SWAGGERNAMESPACE } from 'store/constants';
export default {
async getCoupons( ...args ) {
const query = args.length === 1 ? args[ 0 ] : args[ 1 ];
try {
// @TODO update the API endpoint once it's ready
// const coupons = await apiFetch( { path: NAMESPACE + 'reports/coupons' + stringifyQuery( query ) } );
const response = await fetch(
SWAGGERNAMESPACE + 'reports/coupons' + stringifyQuery( query )
);
const coupons = await response.json();
dispatch( 'wc-admin' ).setCoupons( coupons, query );
} catch ( error ) {
dispatch( 'wc-admin' ).setCouponsError( query );
}
},
};

View File

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

View File

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

View File

@ -0,0 +1,53 @@
/*
* @format
*/
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
import { dispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import resolvers from '../resolvers';
const { getCoupons } = resolvers;
jest.mock( '@wordpress/data', () => ( {
dispatch: jest.fn().mockReturnValue( {
setCoupons: jest.fn(),
} ),
} ) );
jest.mock( '@wordpress/api-fetch', () => jest.fn() );
// @TODO reactivate tests when we use the correct API routes instead of swaggerhub
xdescribe( 'getCoupons', () => {
const COUPONS_1 = [ { coupon_id: 1214 }, { coupon_id: 1215 }, { coupon_id: 1216 } ];
const COUPONS_2 = [ { coupon_id: 1 }, { coupon_id: 2 }, { coupon_id: 3 } ];
beforeAll( () => {
apiFetch.mockImplementation( options => {
if ( options.path === '/wc/v3/reports/coupons' ) {
return Promise.resolve( COUPONS_1 );
}
if ( options.path === '/wc/v3/reports/coupons?orderby=coupon_id' ) {
return Promise.resolve( COUPONS_2 );
}
} );
} );
it( 'returns requested report data', async () => {
expect.assertions( 1 );
await getCoupons();
expect( dispatch().setCoupons ).toHaveBeenCalledWith( COUPONS_1, undefined );
} );
it( 'returns requested report data for a specific query', async () => {
expect.assertions( 1 );
await getCoupons( { orderby: 'coupon_id' } );
expect( dispatch().setCoupons ).toHaveBeenCalledWith( COUPONS_2, { orderby: 'coupon_id' } );
} );
} );

View File

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

View File

@ -2,13 +2,13 @@
/**
* External dependencies
*/
import { registerStore } from '@wordpress/data';
import { combineReducers } from 'redux';
import { combineReducers, registerStore } from '@wordpress/data';
/**
* Internal dependencies
*/
import { applyMiddleware, addThunks } from './middleware';
import coupons from 'store/coupons';
import orders from 'store/orders';
import products from 'store/products';
import reports from 'store/reports';
@ -16,6 +16,7 @@ import notes from 'store/notes';
const store = registerStore( 'wc-admin', {
reducer: combineReducers( {
coupons: coupons.reducer,
orders: orders.reducer,
products: products.reducer,
reports: reports.reducer,
@ -23,6 +24,7 @@ const store = registerStore( 'wc-admin', {
} ),
actions: {
...coupons.actions,
...orders.actions,
...products.actions,
...reports.actions,
@ -30,6 +32,7 @@ const store = registerStore( 'wc-admin', {
},
selectors: {
...coupons.selectors,
...orders.selectors,
...products.selectors,
...reports.selectors,
@ -37,6 +40,7 @@ const store = registerStore( 'wc-admin', {
},
resolvers: {
...coupons.resolvers,
...orders.resolvers,
...products.resolvers,
...reports.resolvers,

View File

@ -1,19 +1,27 @@
/** @format */
/**
* External dependencies
*/
import { dispatch } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch';
/**
* WooCommerce dependencies
*/
import { stringifyQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { stringifyQuery } from 'lib/nav-utils';
import { NAMESPACE } from 'store/constants';
export default {
async getNotes( state, query ) {
// TODO: Use controls data plugin or fresh-data instead of async
async getNotes( ...args ) {
// This is interim code to work with either 2.x or 3.x version of @wordpress/data
// TODO: Change to just `getNotes( query )` after Gutenberg plugin uses @wordpress/data 3+
const query = args.length === 1 ? args[ 0 ] : args[ 1 ];
try {
const notes = await apiFetch( { path: NAMESPACE + 'admin/notes' + stringifyQuery( query ) } );
dispatch( 'wc-admin' ).setNotes( notes, query );

View File

@ -40,13 +40,13 @@ describe( 'getNotes', () => {
it( 'returns requested data', async () => {
expect.assertions( 1 );
await getNotes( {} );
await getNotes();
expect( dispatch().setNotes ).toHaveBeenCalledWith( NOTES_1, undefined );
} );
it( 'returns requested data for a specific query', async () => {
expect.assertions( 1 );
await getNotes( {}, { page: 2 } );
await getNotes( { page: 2 } );
expect( dispatch().setNotes ).toHaveBeenCalledWith( NOTES_2, { page: 2 } );
} );
} );

View File

@ -2,15 +2,14 @@
/**
* Internal dependencies
*/
import reducer from './reducer';
import actions from './actions';
import selectors from './selectors';
import reducer from './reducer';
import resolvers from './resolvers';
import selectors from './selectors';
export default {
reducer,
actions,
selectors,
reducer,
resolvers,
selectors,
};

View File

@ -5,14 +5,23 @@
import { dispatch } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch';
/**
* WooCommerce dependencies
*/
import { stringifyQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { stringifyQuery } from 'lib/nav-utils';
import { NAMESPACE } from 'store/constants';
export default {
async getOrders( state, query ) {
// TODO: Use controls data plugin or fresh-data instead of async
async getOrders( ...args ) {
// This is interim code to work with either 2.x or 3.x version of @wordpress/data
// TODO: Change to just `getNotes( query )` after Gutenberg plugin uses @wordpress/data 3+
const query = args.length === 1 ? args[ 0 ] : args[ 1 ];
try {
const orders = await apiFetch( { path: NAMESPACE + 'orders' + stringifyQuery( query ) } );
dispatch( 'wc-admin' ).setOrders( orders, query );

View File

@ -16,7 +16,7 @@ import { ERROR } from 'store/constants';
* Returns orders for a specific query.
*
* @param {Object} state Current state
* @param {Object} query Report query paremters
* @param {Object} query Report query parameters
* @return {Array} Report details
*/
function getOrders( state, query = {} ) {
@ -27,7 +27,7 @@ export default {
getOrders,
/**
* Returns true if a query is pending.
* Returns true if a getOrders request is pending.
*
* @param {Object} state Current state
* @return {Boolean} True if the `getOrders` request is pending, false otherwise
@ -37,7 +37,7 @@ export default {
},
/**
* Returns true if a get orders request has returned an error.
* Returns true if a getOrders request has returned an error.
*
* @param {Object} state Current state
* @param {Object} query Query parameters

View File

@ -40,13 +40,13 @@ describe( 'getOrders', () => {
it( 'returns requested report data', async () => {
expect.assertions( 1 );
await getOrders( {} );
await getOrders();
expect( dispatch().setOrders ).toHaveBeenCalledWith( ORDERS_1, undefined );
} );
it( 'returns requested report data for a specific query', async () => {
expect.assertions( 1 );
await getOrders( {}, { orderby: 'id' } );
await getOrders( { orderby: 'id' } );
expect( dispatch().setOrders ).toHaveBeenCalledWith( ORDERS_2, { orderby: 'id' } );
} );
} );

View File

@ -6,13 +6,13 @@
* External dependencies
*/
import deepFreeze from 'deep-freeze';
import { select } from '@wordpress/data';
/**
* Internal dependencies
*/
import { ERROR } from 'store/constants';
import selectors from '../selectors';
import { select } from '@wordpress/data';
import { getJsonString } from 'store/utils';
const { getOrders, isGetOrdersRequesting, isGetOrdersError } = selectors;

View File

@ -6,11 +6,21 @@ import apiFetch from '@wordpress/api-fetch';
import { dispatch } from '@wordpress/data';
import { stringify } from 'qs';
/**
* Internal dependencies
*/
import { NAMESPACE } from 'store/constants';
export default {
async getProducts( state, query ) {
// TODO: Use controls data plugin or fresh-data instead of async
async getProducts( ...args ) {
// This is interim code to work with either 2.x or 3.x version of @wordpress/data
// TODO: Change to just `getNotes( query )` after Gutenberg plugin uses @wordpress/data 3+
const query = args.length === 1 ? args[ 0 ] : args[ 1 ];
try {
const params = query ? '?' + stringify( query ) : '';
const products = await apiFetch( { path: '/wc/v3/reports/products' + params } );
const products = await apiFetch( { path: NAMESPACE + 'reports/products' + params } );
dispatch( 'wc-admin' ).setProducts( products, query );
} catch ( error ) {
dispatch( 'wc-admin' ).setProductsError( query );

View File

@ -16,7 +16,7 @@ import { getJsonString } from 'store/utils';
* Returns products report details for a specific report query.
*
* @param {Object} state Current state
* @param {Object} query Report query paremters
* @param {Object} query Report query parameters
* @return {Object} Report details
*/
function getProducts( state, query = {} ) {
@ -27,21 +27,21 @@ export default {
getProducts,
/**
* Returns true if a products request is pending.
* Returns true if a getProducts request is pending.
*
* @param {Object} state Current state
* @return {Object} True if the `getProducts` request is pending, false otherwise
* @return {Boolean} True if the `getProducts` request is pending, false otherwise
*/
isGetProductsRequesting( state, ...args ) {
return select( 'core/data' ).isResolving( 'wc-admin', 'getProducts', args );
},
/**
* Returns true if a products request has returned an error.
* Returns true if a getProducts request has returned an error.
*
* @param {Object} state Current state
* @param {Object} query Report query paremters
* @return {Object} True if the `getProducts` request has failed, false otherwise
* @param {Object} query Report query parameters
* @return {Boolean} True if the `getProducts` request has failed, false otherwise
*/
isGetProductsError( state, query ) {
return ERROR === getProducts( state, query );

View File

@ -57,13 +57,13 @@ describe( 'getProducts', () => {
it( 'returns requested products', async () => {
expect.assertions( 1 );
await getProducts( {} );
await getProducts();
expect( dispatch().setProducts ).toHaveBeenCalledWith( PRODUCTS_1, undefined );
} );
it( 'returns requested products for a specific query', async () => {
expect.assertions( 1 );
await getProducts( {}, { orderby: 'date' } );
await getProducts( { orderby: 'date' } );
expect( dispatch().setProducts ).toHaveBeenCalledWith( PRODUCTS_2, { orderby: 'date' } );
} );
} );

View File

@ -6,13 +6,13 @@
* External dependencies
*/
import deepFreeze from 'deep-freeze';
import { select } from '@wordpress/data';
/**
* Internal dependencies
*/
import { ERROR } from 'store/constants';
import selectors from '../selectors';
import { select } from '@wordpress/data';
import { getJsonString } from 'store/utils';
const { getProducts, isGetProductsRequesting, isGetProductsError } = selectors;

View File

@ -3,7 +3,7 @@
/**
* External dependencies
*/
import { combineReducers } from 'redux';
import { combineReducers } from '@wordpress/data';
/**
* Internal dependencies

View File

@ -4,13 +4,13 @@
* Internal dependencies
*/
import actions from './actions';
import selectors from './selectors';
import reducer from './reducer';
import resolvers from './resolvers';
import selectors from './selectors';
export default {
actions,
selectors,
reducer,
resolvers,
selectors,
};

View File

@ -6,17 +6,42 @@
import apiFetch from '@wordpress/api-fetch';
import { dispatch } from '@wordpress/data';
/**
* WooCommerce dependencies
*/
import { stringifyQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { stringifyQuery } from 'lib/nav-utils';
import { NAMESPACE } from 'store/constants';
import { NAMESPACE, SWAGGERNAMESPACE } from 'store/constants';
export default {
async getReportStats( state, endpoint, query ) {
// TODO: Use controls data plugin or fresh-data instead of async
async getReportStats( ...args ) {
// This is interim code to work with either 2.x or 3.x version of @wordpress/data
// TODO: Change to just `getNotes( endpoint, query )`
// after Gutenberg plugin uses @wordpress/data 3+
const [ endpoint, query ] = args.length === 2 ? args : args.slice( 1, 3 );
const statEndpoints = [ 'orders', 'revenue', 'products' ];
let apiPath = endpoint + stringifyQuery( query );
// TODO: Remove once swagger endpoints are phased out.
const swaggerEndpoints = [ 'coupons', 'taxes' ];
if ( swaggerEndpoints.indexOf( endpoint ) >= 0 ) {
apiPath = SWAGGERNAMESPACE + 'reports/' + endpoint + '/stats' + stringifyQuery( query );
try {
const response = await fetch( apiPath );
const report = await response.json();
dispatch( 'wc-admin' ).setReportStats( endpoint, report, query );
} catch ( error ) {
dispatch( 'wc-admin' ).setReportStatsError( endpoint, query );
}
return;
}
if ( statEndpoints.indexOf( endpoint ) >= 0 ) {
apiPath = NAMESPACE + 'reports/' + endpoint + '/stats' + stringifyQuery( query );
}

View File

@ -17,7 +17,7 @@ import { getJsonString } from 'store/utils';
*
* @param {Object} state Current state
* @param {String} endpoint Stats endpoint
* @param {Object} query Report query paremters
* @param {Object} query Report query parameters
* @return {Object} Report details
*/
function getReportStats( state, endpoint, query = {} ) {
@ -29,7 +29,7 @@ export default {
getReportStats,
/**
* Returns true if a stat query is pending.
* Returns true if a stats query is pending.
*
* @param {Object} state Current state
* @return {Boolean} True if the `getReportRevenueStats` request is pending, false otherwise

View File

@ -6,13 +6,13 @@
* External dependencies
*/
import deepFreeze from 'deep-freeze';
import { select } from '@wordpress/data';
/**
* Internal dependencies
*/
import { ERROR } from 'store/constants';
import selectors from '../selectors';
import { select } from '@wordpress/data';
const { getReportStats, isReportStatsRequesting, isReportStatsError } = selectors;
jest.mock( '@wordpress/data', () => ( {

View File

@ -9,21 +9,22 @@ import { find, forEach, isNull } from 'lodash';
* WooCommerce dependencies
*/
import { appendTimestamp, getCurrentDates, getIntervalForQuery } from '@woocommerce/date';
import { flattenFilters, getActiveFiltersFromQuery, getUrlKey } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { MAX_PER_PAGE } from 'store/constants';
import { getActiveFiltersFromQuery, getUrlKey } from 'components/filters/advanced/utils';
import { flatenFilters } from 'components/filters/filter/utils';
import * as couponsConfig from 'analytics/report/coupons/config';
import * as ordersConfig from 'analytics/report/orders/config';
import * as productsConfig from 'analytics/report/products/config';
import * as taxesConfig from 'analytics/report/taxes/config';
const reportConfigs = {
coupons: couponsConfig,
orders: ordersConfig,
products: productsConfig,
taxes: taxesConfig,
};
export function getFilterQuery( endpoint, query ) {
@ -60,7 +61,7 @@ export function getQueryFromConfig( config, advancedFilters, query ) {
);
}
const filter = find( flatenFilters( config.filters ), { value: queryValue } );
const filter = find( flattenFilters( config.filters ), { value: queryValue } );
if ( ! filter ) {
return {};

View File

@ -1,11 +1,13 @@
/** @format */
/* stylelint-disable block-closing-brace-newline-after */
// Breakpoints
// Forked from https://github.com/Automattic/wp-calypso/blob/46ae24d8800fb85da6acf057a640e60dac988a38/assets/stylesheets/shared/mixins/_breakpoints.scss
// Think very carefully before adding a new breakpoint.
// The list below is based on wp-admin's main breakpoints
$breakpoints: 320px, 400px, 600px, 782px, 960px, 1100px, 1365px;
$breakpoints: 320px, 400px, 600px, 782px, 960px, 1280px, 1440px;
@mixin breakpoint( $sizes... ) {
@each $size in $sizes {
@ -44,14 +46,16 @@ $breakpoints: 320px, 400px, 600px, 782px, 960px, 1100px, 1365px;
@each $breakpoint in $breakpoints {
$sizes: $sizes + ' ' + $breakpoint;
}
@warn "ERROR in breakpoint( #{ $size } ) : You can only use these sizes[ #{$sizes} ] using the following syntax [ <#{ nth( $breakpoints, 1 ) } >#{ nth( $breakpoints, 1 ) } #{ nth( $breakpoints, 1 ) }-#{ nth( $breakpoints, 2 ) } ]";
@warn 'ERROR in breakpoint( #{ $size } ) : You can only use these sizes[ #{$sizes} ] using the following syntax [ <#{ nth( $breakpoints, 1 ) } >#{ nth( $breakpoints, 1 ) } #{ nth( $breakpoints, 1 ) }-#{ nth( $breakpoints, 2 ) } ]';
}
} @else {
$sizes: '';
@each $breakpoint in $breakpoints {
$sizes: $sizes + ' ' + $breakpoint;
}
@error "ERROR in breakpoint( #{ $size } ) : Please wrap the breakpoint $size in parenthesis. You can use these sizes[ #{$sizes} ] using the following syntax [ <#{ nth( $breakpoints, 1 ) } >#{ nth( $breakpoints, 1 ) } #{ nth( $breakpoints, 1 ) }-#{ nth( $breakpoints, 2 ) } ]";
@error 'ERROR in breakpoint( #{ $size } ) : Please wrap the breakpoint $size in parenthesis. You can use these sizes[ #{$sizes} ] using the following syntax [ <#{ nth( $breakpoints, 1 ) } >#{ nth( $breakpoints, 1 ) } #{ nth( $breakpoints, 1 ) }-#{ nth( $breakpoints, 2 ) } ]';
}
}
}
/* stylelint-enable */

View File

@ -23,8 +23,3 @@ $light-gray-500: $core-grey-light-500;
$dark-gray-300: $core-grey-dark-300;
$dark-gray-900: $core-grey-dark-900;
$alert-red: $error-red;
:export {
gaplarge: $gap-large;
gap: $gap;
}

View File

@ -48,7 +48,7 @@
margin: 0;
padding-top: 80px;
@include breakpoint( '782px-1100px' ) {
@include breakpoint( '782px-960px' ) {
padding-top: 60px;
}
}

View File

@ -5,7 +5,7 @@
--large-gap: 40px;
--main-gap: 24px;
}
@media (max-width: 1100px) {
@media (max-width: 960px) {
:root {
--large-gap: 24px;
}

View File

@ -11,8 +11,8 @@
},
"require-dev": {
"squizlabs/php_codesniffer": "*",
"wp-coding-standards/wpcs": "1.1.0",
"phpunit/phpunit": "7.4.3",
"wp-coding-standards/wpcs": "1.2.0",
"phpunit/phpunit": "6.5.13",
"woocommerce/woocommerce-sniffs": "*",
"wimg/php-compatibility": "9.0.0",
"dealerdirect/phpcodesniffer-composer-installer": "0.5.0"

View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "97ef081df4492fe71d877ad105e1709b",
"content-hash": "8c25f2e044c24bd6ee6900e7bcbe3c0c",
"packages": [
{
"name": "composer/installers",
@ -298,22 +298,22 @@
},
{
"name": "phar-io/manifest",
"version": "1.0.3",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/phar-io/manifest.git",
"reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4"
"reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
"reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
"url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0",
"reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-phar": "*",
"phar-io/version": "^2.0",
"phar-io/version": "^1.0.1",
"php": "^5.6 || ^7.0"
},
"type": "library",
@ -349,20 +349,20 @@
}
],
"description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
"time": "2018-07-08T19:23:20+00:00"
"time": "2017-03-05T18:14:27+00:00"
},
{
"name": "phar-io/version",
"version": "2.0.1",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/phar-io/version.git",
"reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6"
"reference": "a70c0ced4be299a63d32fa96d9281d03e94041df"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6",
"reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6",
"url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df",
"reference": "a70c0ced4be299a63d32fa96d9281d03e94041df",
"shasum": ""
},
"require": {
@ -396,7 +396,7 @@
}
],
"description": "Library for handling version information and constraints",
"time": "2018-07-08T19:19:57+00:00"
"time": "2017-03-05T17:38:23+00:00"
},
{
"name": "phpdocumentor/reflection-common",
@ -615,40 +615,40 @@
},
{
"name": "phpunit/php-code-coverage",
"version": "6.1.3",
"version": "5.3.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "4d3ae9b21a7d7e440bd0cf65565533117976859f"
"reference": "c89677919c5dd6d3b3852f230a663118762218ac"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4d3ae9b21a7d7e440bd0cf65565533117976859f",
"reference": "4d3ae9b21a7d7e440bd0cf65565533117976859f",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c89677919c5dd6d3b3852f230a663118762218ac",
"reference": "c89677919c5dd6d3b3852f230a663118762218ac",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-xmlwriter": "*",
"php": "^7.1",
"phpunit/php-file-iterator": "^2.0",
"php": "^7.0",
"phpunit/php-file-iterator": "^1.4.2",
"phpunit/php-text-template": "^1.2.1",
"phpunit/php-token-stream": "^3.0",
"phpunit/php-token-stream": "^2.0.1",
"sebastian/code-unit-reverse-lookup": "^1.0.1",
"sebastian/environment": "^3.1 || ^4.0",
"sebastian/environment": "^3.0",
"sebastian/version": "^2.0.1",
"theseer/tokenizer": "^1.1"
},
"require-dev": {
"phpunit/phpunit": "^7.0"
"phpunit/phpunit": "^6.0"
},
"suggest": {
"ext-xdebug": "^2.6.0"
"ext-xdebug": "^2.5.5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "6.1-dev"
"dev-master": "5.3.x-dev"
}
},
"autoload": {
@ -674,32 +674,29 @@
"testing",
"xunit"
],
"time": "2018-10-23T05:59:32+00:00"
"time": "2018-04-06T15:36:58+00:00"
},
{
"name": "phpunit/php-file-iterator",
"version": "2.0.2",
"version": "1.4.5",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
"reference": "050bedf145a257b1ff02746c31894800e5122946"
"reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946",
"reference": "050bedf145a257b1ff02746c31894800e5122946",
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4",
"reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4",
"shasum": ""
},
"require": {
"php": "^7.1"
},
"require-dev": {
"phpunit/phpunit": "^7.1"
"php": ">=5.3.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
"dev-master": "1.4.x-dev"
}
},
"autoload": {
@ -714,7 +711,7 @@
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de",
"email": "sb@sebastian-bergmann.de",
"role": "lead"
}
],
@ -724,7 +721,7 @@
"filesystem",
"iterator"
],
"time": "2018-09-13T20:33:42+00:00"
"time": "2017-11-27T13:52:08+00:00"
},
{
"name": "phpunit/php-text-template",
@ -769,28 +766,28 @@
},
{
"name": "phpunit/php-timer",
"version": "2.0.0",
"version": "1.0.9",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-timer.git",
"reference": "8b8454ea6958c3dee38453d3bd571e023108c91f"
"reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/8b8454ea6958c3dee38453d3bd571e023108c91f",
"reference": "8b8454ea6958c3dee38453d3bd571e023108c91f",
"url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f",
"reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f",
"shasum": ""
},
"require": {
"php": "^7.1"
"php": "^5.3.3 || ^7.0"
},
"require-dev": {
"phpunit/phpunit": "^7.0"
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
"dev-master": "1.0-dev"
}
},
"autoload": {
@ -805,7 +802,7 @@
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de",
"email": "sb@sebastian-bergmann.de",
"role": "lead"
}
],
@ -814,33 +811,33 @@
"keywords": [
"timer"
],
"time": "2018-02-01T13:07:23+00:00"
"time": "2017-02-26T11:10:40+00:00"
},
{
"name": "phpunit/php-token-stream",
"version": "3.0.0",
"version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-token-stream.git",
"reference": "21ad88bbba7c3d93530d93994e0a33cd45f02ace"
"reference": "791198a2c6254db10131eecfe8c06670700904db"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/21ad88bbba7c3d93530d93994e0a33cd45f02ace",
"reference": "21ad88bbba7c3d93530d93994e0a33cd45f02ace",
"url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db",
"reference": "791198a2c6254db10131eecfe8c06670700904db",
"shasum": ""
},
"require": {
"ext-tokenizer": "*",
"php": "^7.1"
"php": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "^7.0"
"phpunit/phpunit": "^6.2.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
"dev-master": "2.0-dev"
}
},
"autoload": {
@ -863,57 +860,57 @@
"keywords": [
"tokenizer"
],
"time": "2018-02-01T13:16:43+00:00"
"time": "2017-11-27T05:48:46+00:00"
},
{
"name": "phpunit/phpunit",
"version": "7.4.3",
"version": "6.5.13",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "c151651fb6ed264038d486ea262e243af72e5e64"
"reference": "0973426fb012359b2f18d3bd1e90ef1172839693"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c151651fb6ed264038d486ea262e243af72e5e64",
"reference": "c151651fb6ed264038d486ea262e243af72e5e64",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0973426fb012359b2f18d3bd1e90ef1172839693",
"reference": "0973426fb012359b2f18d3bd1e90ef1172839693",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.1",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-xml": "*",
"myclabs/deep-copy": "^1.7",
"phar-io/manifest": "^1.0.2",
"phar-io/version": "^2.0",
"php": "^7.1",
"myclabs/deep-copy": "^1.6.1",
"phar-io/manifest": "^1.0.1",
"phar-io/version": "^1.0",
"php": "^7.0",
"phpspec/prophecy": "^1.7",
"phpunit/php-code-coverage": "^6.0.7",
"phpunit/php-file-iterator": "^2.0.1",
"phpunit/php-code-coverage": "^5.3",
"phpunit/php-file-iterator": "^1.4.3",
"phpunit/php-text-template": "^1.2.1",
"phpunit/php-timer": "^2.0",
"sebastian/comparator": "^3.0",
"sebastian/diff": "^3.0",
"sebastian/environment": "^3.1 || ^4.0",
"phpunit/php-timer": "^1.0.9",
"phpunit/phpunit-mock-objects": "^5.0.9",
"sebastian/comparator": "^2.1",
"sebastian/diff": "^2.0",
"sebastian/environment": "^3.1",
"sebastian/exporter": "^3.1",
"sebastian/global-state": "^2.0",
"sebastian/object-enumerator": "^3.0.3",
"sebastian/resource-operations": "^2.0",
"sebastian/resource-operations": "^1.0",
"sebastian/version": "^2.0.1"
},
"conflict": {
"phpunit/phpunit-mock-objects": "*"
"phpdocumentor/reflection-docblock": "3.0.2",
"phpunit/dbunit": "<3.0"
},
"require-dev": {
"ext-pdo": "*"
},
"suggest": {
"ext-soap": "*",
"ext-xdebug": "*",
"phpunit/php-invoker": "^2.0"
"phpunit/php-invoker": "^1.1"
},
"bin": [
"phpunit"
@ -921,7 +918,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "7.4-dev"
"dev-master": "6.5.x-dev"
}
},
"autoload": {
@ -947,7 +944,66 @@
"testing",
"xunit"
],
"time": "2018-10-23T05:57:41+00:00"
"time": "2018-09-08T15:10:43+00:00"
},
{
"name": "phpunit/phpunit-mock-objects",
"version": "5.0.10",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
"reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/cd1cf05c553ecfec36b170070573e540b67d3f1f",
"reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.0.5",
"php": "^7.0",
"phpunit/php-text-template": "^1.2.1",
"sebastian/exporter": "^3.1"
},
"conflict": {
"phpunit/phpunit": "<6.0"
},
"require-dev": {
"phpunit/phpunit": "^6.5.11"
},
"suggest": {
"ext-soap": "*"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.0.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de",
"role": "lead"
}
],
"description": "Mock Object library for PHPUnit",
"homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/",
"keywords": [
"mock",
"xunit"
],
"time": "2018-08-09T05:50:03+00:00"
},
{
"name": "sebastian/code-unit-reverse-lookup",
@ -996,30 +1052,30 @@
},
{
"name": "sebastian/comparator",
"version": "3.0.2",
"version": "2.1.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da"
"reference": "34369daee48eafb2651bea869b4b15d75ccc35f9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
"reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9",
"reference": "34369daee48eafb2651bea869b4b15d75ccc35f9",
"shasum": ""
},
"require": {
"php": "^7.1",
"sebastian/diff": "^3.0",
"php": "^7.0",
"sebastian/diff": "^2.0 || ^3.0",
"sebastian/exporter": "^3.1"
},
"require-dev": {
"phpunit/phpunit": "^7.1"
"phpunit/phpunit": "^6.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
"dev-master": "2.1.x-dev"
}
},
"autoload": {
@ -1056,33 +1112,32 @@
"compare",
"equality"
],
"time": "2018-07-12T15:12:46+00:00"
"time": "2018-02-01T13:46:46+00:00"
},
{
"name": "sebastian/diff",
"version": "3.0.1",
"version": "2.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
"reference": "366541b989927187c4ca70490a35615d3fef2dce"
"reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/366541b989927187c4ca70490a35615d3fef2dce",
"reference": "366541b989927187c4ca70490a35615d3fef2dce",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd",
"reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd",
"shasum": ""
},
"require": {
"php": "^7.1"
"php": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "^7.0",
"symfony/process": "^2 || ^3.3 || ^4"
"phpunit/phpunit": "^6.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
"dev-master": "2.0-dev"
}
},
"autoload": {
@ -1107,12 +1162,9 @@
"description": "Diff implementation",
"homepage": "https://github.com/sebastianbergmann/diff",
"keywords": [
"diff",
"udiff",
"unidiff",
"unified diff"
"diff"
],
"time": "2018-06-10T07:54:39+00:00"
"time": "2017-08-03T08:09:46+00:00"
},
{
"name": "sebastian/environment",
@ -1429,25 +1481,25 @@
},
{
"name": "sebastian/resource-operations",
"version": "2.0.1",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/resource-operations.git",
"reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9"
"reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
"reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
"url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52",
"reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52",
"shasum": ""
},
"require": {
"php": "^7.1"
"php": ">=5.6.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
"dev-master": "1.0.x-dev"
}
},
"autoload": {
@ -1467,7 +1519,7 @@
],
"description": "Provides a list of PHP built-in functions that operate on resources",
"homepage": "https://www.github.com/sebastianbergmann/resource-operations",
"time": "2018-10-04T04:07:39+00:00"
"time": "2015-07-28T20:34:47+00:00"
},
{
"name": "sebastian/version",
@ -1713,24 +1765,23 @@
},
{
"name": "woocommerce/woocommerce-sniffs",
"version": "0.0.2",
"version": "0.0.3",
"source": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce-sniffs.git",
"reference": "2890fd5d98b318f62acb42f2b5cd6d02627cfd82"
"reference": "c7e6e641e47b397ee64eb52a762c265cf476495a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/woocommerce/woocommerce-sniffs/zipball/2890fd5d98b318f62acb42f2b5cd6d02627cfd82",
"reference": "2890fd5d98b318f62acb42f2b5cd6d02627cfd82",
"url": "https://api.github.com/repos/woocommerce/woocommerce-sniffs/zipball/c7e6e641e47b397ee64eb52a762c265cf476495a",
"reference": "c7e6e641e47b397ee64eb52a762c265cf476495a",
"shasum": ""
},
"require": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.5.0",
"php": ">=7.0",
"squizlabs/php_codesniffer": "^3.0.2"
},
"suggest": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.4.3"
"wimg/php-compatibility": "^9.0",
"wp-coding-standards/wpcs": "^1.1"
},
"type": "phpcodesniffer-standard",
"notification-url": "https://packagist.org/downloads/",
@ -1750,20 +1801,20 @@
"woocommerce",
"wordpress"
],
"time": "2018-03-22T18:39:19+00:00"
"time": "2018-11-06T22:51:50+00:00"
},
{
"name": "wp-coding-standards/wpcs",
"version": "1.1.0",
"version": "1.2.0",
"source": {
"type": "git",
"url": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards.git",
"reference": "46d42828ce7355d8b3776e3171f2bda892d179b4"
"reference": "7aa217ab38156c5cb4eae0f04ae376027c407a9b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/WordPress-Coding-Standards/WordPress-Coding-Standards/zipball/46d42828ce7355d8b3776e3171f2bda892d179b4",
"reference": "46d42828ce7355d8b3776e3171f2bda892d179b4",
"url": "https://api.github.com/repos/WordPress-Coding-Standards/WordPress-Coding-Standards/zipball/7aa217ab38156c5cb4eae0f04ae376027c407a9b",
"reference": "7aa217ab38156c5cb4eae0f04ae376027c407a9b",
"shasum": ""
},
"require": {
@ -1771,7 +1822,7 @@
"squizlabs/php_codesniffer": "^2.9.0 || ^3.0.2"
},
"require-dev": {
"phpcompatibility/php-compatibility": "*"
"phpcompatibility/php-compatibility": "^9.0"
},
"suggest": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.4.3 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically."
@ -1793,7 +1844,7 @@
"standards",
"wordpress"
],
"time": "2018-09-10T17:04:05+00:00"
"time": "2018-11-12T10:13:12+00:00"
}
],
"aliases": [],

View File

@ -3,3 +3,4 @@
* [Data](data)
* [Layout](layout)
* [CSS Structure](stylesheets)
* [Examples](examples/)

View File

@ -20,9 +20,11 @@
* [ProductImage](components/product-image.md)
* [Rating](components/rating.md)
* [Search](components/search.md)
* [SectionHeader](components/section-header.md)
* [Section](components/section.md)
* [SegmentedSelection](components/segmented-selection.md)
* [SplitButton](components/split-button.md)
* [Summary](components/summary.md)
* [Table](components/table.md)
* [Tag](components/tag.md)
* [ViewMoreList](components/view-more-list.md)

View File

@ -29,7 +29,6 @@ An `EllipsisMenu`, with filters used to control the content visible in this card
### `title`
- **Required**
- Type: One of type: string, node
- Default: null

Some files were not shown because too many files have changed in this diff Show More