Skip to content
This repository has been archived by the owner on Jul 12, 2024. It is now read-only.

Commit

Permalink
Add wc-api and "Fresh Data" (#833)
Browse files Browse the repository at this point in the history
* data: Add @fresh-data/framework to package.json

* data: Add fresh-data and replace orders in table

This PR adds fresh-data with a WooCommerce API spec to fulfill order
information. It then replaces the existing selectors for the orders
table with the new selectors as a proof-of-concept.

* wc-api: Add temporary code for `withSelect`

This adds temporary code for a `withSelect` function outside of
`@wordpress/data` until the context PR is merged:

WordPress/gutenberg#11460

* wc-api: Update fresh-data to 0.5.0
  • Loading branch information
coderkevin authored Nov 29, 2018
1 parent 3f214cd commit 3dbcbf7
Show file tree
Hide file tree
Showing 15 changed files with 5,193 additions and 4,670 deletions.
5 changes: 3 additions & 2 deletions client/analytics/report/orders/table.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { __, _n, sprintf } from '@wordpress/i18n';
import { Component, Fragment } 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';

/**
Expand All @@ -29,6 +28,7 @@ import ReportError from 'analytics/components/report-error';
import { QUERY_DEFAULTS } from 'store/constants';
import { getReportChartData, getFilterQuery } from 'store/reports/utils';
import { numberFormat } from 'lib/number';
import withSelect from 'wc-api/with-select';
import './style.scss';

class OrdersReportTable extends Component {
Expand Down Expand Up @@ -306,7 +306,8 @@ export default compose(
const primaryData = getReportChartData( 'orders', 'primary', query, select );
const filterQuery = getFilterQuery( 'orders', query );

const { getOrders, isGetOrdersError, isGetOrdersRequesting } = select( 'wc-admin' );
const { getOrders, isGetOrdersError, isGetOrdersRequesting } = select( 'wc-api' );

const tableQuery = {
orderby: query.orderby || 'date',
order: query.order || 'asc',
Expand Down
1 change: 1 addition & 0 deletions client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Provider as SlotFillProvider } from 'react-slot-fill';
import './stylesheets/_index.scss';
import { PageLayout } from './layout';
import 'store';
import 'wc-api/wp-data-store';

render(
<SlotFillProvider>
Expand Down
21 changes: 21 additions & 0 deletions client/wc-api/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/** @format */
/**
* External dependencies
*/
import { SECOND, MINUTE } from '@fresh-data/framework';

export const NAMESPACE = '/wc/v3';

export const DEFAULT_REQUIREMENT = {
timeout: 5 * SECOND,
freshness: 5 * MINUTE,
};

// WordPress & WooCommerce both set a hard limit of 100 for the per_page parameter
export const MAX_PER_PAGE = 100;

export const QUERY_DEFAULTS = {
pageSize: 25,
period: 'month',
compare: 'previous_year',
};
11 changes: 11 additions & 0 deletions client/wc-api/orders/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/** @format */
/**
* Internal dependencies
*/
import operations from './operations';
import selectors from './selectors';

export default {
operations,
selectors,
};
68 changes: 68 additions & 0 deletions client/wc-api/orders/operations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/** @format */
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';

/**
* WooCommerce dependencies
*/
import { stringifyQuery } from '@woocommerce/navigation';

/**
* Internal dependencies
*/
import { isResourcePrefix, getResourceIdentifier, getResourceName } from '../utils';
import { NAMESPACE } from '../constants';

function read( resourceNames, fetch = apiFetch ) {
return [ ...readOrders( resourceNames, fetch ), ...readOrderQueries( resourceNames, fetch ) ];
}

function readOrderQueries( resourceNames, fetch ) {
const filteredNames = resourceNames.filter( name => isResourcePrefix( name, 'order-query' ) );

return filteredNames.map( resourceName => {
const query = getResourceIdentifier( resourceName );
const url = `${ NAMESPACE }/orders${ stringifyQuery( query ) }`;

return fetch( { path: url } )
.then( orders => {
const ids = orders.map( order => order.id );
const orderResources = orders.reduce( ( resources, order ) => {
resources[ getResourceName( 'order', order.id ) ] = { data: order };
return resources;
}, {} );

return {
[ resourceName ]: { data: ids },
...orderResources,
};
} )
.catch( error => {
return { [ resourceName ]: { error } };
} );
} );
}

function readOrders( resourceNames, fetch ) {
const filteredNames = resourceNames.filter( name => isResourcePrefix( name, 'order' ) );
return filteredNames.map( resourceName => readOrder( resourceName, fetch ) );
}

function readOrder( resourceName, fetch ) {
const id = getResourceIdentifier( resourceName );
const url = `${ NAMESPACE }/orders/${ id }`;

return fetch( { path: url } )
.then( order => {
return { [ resourceName ]: { data: order } };
} )
.catch( error => {
return { [ resourceName ]: { error } };
} );
}

export default {
read,
};
33 changes: 33 additions & 0 deletions client/wc-api/orders/selectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/** @format */
/**
* Internal dependencies
*/
import { getResourceName } from '../utils';
import { DEFAULT_REQUIREMENT } from '../constants';

const getOrders = ( getResource, requireResource ) => (
query = {},
requirement = DEFAULT_REQUIREMENT
) => {
const resourceName = getResourceName( 'order-query', query );
const ids = requireResource( requirement, resourceName ).data || [];
const orders = ids.map( id => getResource( getResourceName( 'order', id ) ).data || {} );
return orders;
};

const isGetOrdersRequesting = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'order-query', query );
const { lastRequested, lastReceived } = getResource( resourceName );
return lastRequested && lastRequested > lastReceived;
};

const isGetOrdersError = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'order-query', query );
return getResource( resourceName ).error;
};

export default {
getOrders,
isGetOrdersRequesting,
isGetOrdersError,
};
16 changes: 16 additions & 0 deletions client/wc-api/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/** @format */

export function getResourceName( prefix, identifier ) {
const identifierString = JSON.stringify( identifier, Object.keys( identifier ).sort() );
return `${ prefix }:${ identifierString }`;
}

export function isResourcePrefix( resourceName, prefix ) {
const resourcePrefix = resourceName.substring( 0, resourceName.indexOf( ':' ) );
return resourcePrefix === prefix;
}

export function getResourceIdentifier( resourceName ) {
const identifierString = resourceName.substring( resourceName.indexOf( ':' ) + 1 );
return JSON.parse( identifierString );
}
21 changes: 21 additions & 0 deletions client/wc-api/wc-api-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/** @format */

/**
* Internal dependencies
*/
import orders from './orders';

function createWcApiSpec() {
return {
selectors: {
...orders.selectors,
},
operations: {
read( resourceNames ) {
return [ ...orders.operations.read( resourceNames ) ];
},
},
};
}

export default createWcApiSpec();
167 changes: 167 additions & 0 deletions client/wc-api/with-select.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* NOTE: This is temporary code. It exists only until a version of `@wordpress/data`
* is released which supports this functionality.
*
* TODO: Remove this and use `@wordpress/data` `withSelect` instead after
* this PR is merged: https://github.com/WordPress/gutenberg/pull/11460
*
* @format
*/

/**
* External dependencies
*/
import { isFunction } from 'lodash';
import { Component } from '@wordpress/element';
import isShallowEqual from '@wordpress/is-shallow-equal';
import { createHigherOrderComponent } from '@wordpress/compose';
import { RegistryConsumer } from '@wordpress/data';

/**
* Higher-order component used to inject state-derived props using registered
* selectors.
*
* @param {Function} mapSelectToProps Function called on every state change,
* expected to return object of props to
* merge with the component's own props.
*
* @return {Component} Enhanced component with merged state data props.
*/
const withSelect = mapSelectToProps =>
createHigherOrderComponent( WrappedComponent => {
/**
* Default merge props. A constant value is used as the fallback since it
* can be more efficiently shallow compared in case component is repeatedly
* rendered without its own merge props.
*
* @type {Object}
*/
const DEFAULT_MERGE_PROPS = {};

class ComponentWithSelect extends Component {
constructor( props ) {
super( props );

this.onStoreChange = this.onStoreChange.bind( this );

this.subscribe( props.registry );

this.mergeProps = this.getNextMergeProps( props );
}

/**
* Given a props object, returns the next merge props by mapSelectToProps.
*
* @param {Object} props Props to pass as argument to mapSelectToProps.
*
* @return {Object} Props to merge into rendered wrapped element.
*/
getNextMergeProps( props ) {
const context = { component: this };
const select = reducerKey => {
const selectors = props.registry.select( reducerKey );
if ( isFunction( selectors ) ) {
return selectors( context );
}
return selectors;
};

return mapSelectToProps( select, props.ownProps ) || DEFAULT_MERGE_PROPS;
}

componentDidMount() {
this.canRunSelection = true;

// A state change may have occurred between the constructor and
// mount of the component (e.g. during the wrapped component's own
// constructor), in which case selection should be rerun.
if ( this.hasQueuedSelection ) {
this.hasQueuedSelection = false;
this.onStoreChange();
}
}

componentWillUnmount() {
this.canRunSelection = false;
this.unsubscribe();
}

shouldComponentUpdate( nextProps, nextState ) {
// Cycle subscription if registry changes.
const hasRegistryChanged = nextProps.registry !== this.props.registry;
if ( hasRegistryChanged ) {
this.unsubscribe();
this.subscribe( nextProps.registry );
}

// Treat a registry change as equivalent to `ownProps`, to reflect
// `mergeProps` to rendered component if and only if updated.
const hasPropsChanged =
hasRegistryChanged || ! isShallowEqual( this.props.ownProps, nextProps.ownProps );

// Only render if props have changed or merge props have been updated
// from the store subscriber.
if ( this.state === nextState && ! hasPropsChanged ) {
return false;
}

if ( hasPropsChanged ) {
const nextMergeProps = this.getNextMergeProps( nextProps );
if ( ! isShallowEqual( this.mergeProps, nextMergeProps ) ) {
// If merge props change as a result of the incoming props,
// they should be reflected as such in the upcoming render.
// While side effects are discouraged in lifecycle methods,
// this component is used heavily, and prior efforts to use
// `getDerivedStateFromProps` had demonstrated miserable
// performance.
this.mergeProps = nextMergeProps;
}

// Regardless whether merge props are changing, fall through to
// incur the render since the component will need to receive
// the changed `ownProps`.
}

return true;
}

onStoreChange() {
if ( ! this.canRunSelection ) {
this.hasQueuedSelection = true;
return;
}

const nextMergeProps = this.getNextMergeProps( this.props );
if ( isShallowEqual( this.mergeProps, nextMergeProps ) ) {
return;
}

this.mergeProps = nextMergeProps;

// Schedule an update. Merge props are not assigned to state since
// derivation of merge props from incoming props occurs within
// shouldComponentUpdate, where setState is not allowed. setState
// is used here instead of forceUpdate because forceUpdate bypasses
// shouldComponentUpdate altogether, which isn't desireable if both
// state and props change within the same render. Unfortunately,
// this requires that next merge props are generated twice.
this.setState( {} );
}

subscribe( registry ) {
this.unsubscribe = registry.subscribe( this.onStoreChange );
}

render() {
return <WrappedComponent { ...this.props.ownProps } { ...this.mergeProps } />;
}
}

return ownProps => (
<RegistryConsumer>
{ registry => <ComponentWithSelect ownProps={ ownProps } registry={ registry } /> }
</RegistryConsumer>
);
}, 'withSelect' );

export default withSelect;
Loading

0 comments on commit 3dbcbf7

Please sign in to comment.