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

Commit

Permalink
data: Add fresh-data and replace orders in table
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
coderkevin committed Nov 14, 2018
1 parent cf0491e commit d6aefc5
Show file tree
Hide file tree
Showing 13 changed files with 347 additions and 2 deletions.
3 changes: 2 additions & 1 deletion client/analytics/report/orders/table.js
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,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();
52 changes: 52 additions & 0 deletions client/wc-api/wp-data-store/create-api-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/** @format */
/**
* External dependencies
*/
import { ApiClient } from '@fresh-data/framework';
import { createStore as createReduxStore } from 'redux';

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

function createStore( name ) {
const devTools = window.__REDUX_DEVTOOLS_EXTENSION__;

return createReduxStore( reducer, devTools && devTools( { name: name, instanceId: name } ) );
}

function createDataHandlers( store ) {
return {
dataRequested: resourceNames => {
store.dispatch( {
type: 'FRESH_DATA_REQUESTED',
resourceNames,
time: new Date(),
} );
},
dataReceived: resources => {
store.dispatch( {
type: 'FRESH_DATA_RECEIVED',
resources,
time: new Date(),
} );
},
};
}

function createApiClient( name, apiSpec ) {
const store = createStore( name );
const dataHandlers = createDataHandlers( store );
const apiClient = new ApiClient( apiSpec );
apiClient.setDataHandlers( dataHandlers );

const storeChanged = () => {
apiClient.setState( store.getState() );
};
store.subscribe( storeChanged );

return apiClient;
}

export default createApiClient;
16 changes: 16 additions & 0 deletions client/wc-api/wp-data-store/create-store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/** @format */
/**
* External dependencies
*/
import { createStore } from 'redux';

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

export default name => {
const devTools = window.__REDUX_DEVTOOLS_EXTENSION__;

return createStore( reducer, devTools && devTools( { name: name, instanceId: name } ) );
};
45 changes: 45 additions & 0 deletions client/wc-api/wp-data-store/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/** @format */
/**
* External dependencies
*/
import { registerGenericStore } from '@wordpress/data';

/**
* Internal dependencies
*/
import createApiClient from './create-api-client';
import wcApiSpec from '../wc-api-spec';

function createWcApiStore() {
const apiClient = createApiClient( 'wc-api', wcApiSpec );

function getComponentSelectors( component ) {
const componentRequirements = [];
const apiSelectors = apiClient.getSelectors( componentRequirements );

apiClient.clearComponentRequirements( component );

return Object.keys( apiSelectors ).reduce( ( componentSelectors, selectorName ) => {
componentSelectors[ selectorName ] = ( ...args ) => {
const result = apiSelectors[ selectorName ]( ...args );
apiClient.setComponentRequirements( component, componentRequirements );
return result;
};
return componentSelectors;
}, {} );
}

return {
getSelectors( context ) {
const component = context && context.component ? context.component : context;
return getComponentSelectors( component );
},
getActions() {
// TODO: Add mutations here.
return {};
},
subscribe: apiClient.subscribe,
};
}

registerGenericStore( 'wc-api', createWcApiStore() );
59 changes: 59 additions & 0 deletions client/wc-api/wp-data-store/reducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/** @format */

const defaultState = {
resources: {},
};

/**
* Primary reducer for fresh-data apiclient data.
* @param {Object} state The base state for fresh-data.
* @param {Object} action Action object to be processed.
* @return {Object} The new state, or the previous state if this action doesn't belong to fresh-data.
*/
export default function reducer( state = defaultState, action ) {
switch ( action.type ) {
case 'FRESH_DATA_REQUESTED':
return reduceRequested( state, action );
case 'FRESH_DATA_RECEIVED':
return reduceReceived( state, action );
default:
return state;
}
}

export function reduceRequested( state, action ) {
const newResources = action.resourceNames.reduce( ( resources, name ) => {
resources[ name ] = { lastRequested: action.time };
return resources;
}, {} );
return reduceResources( state, newResources );
}

export function reduceReceived( state, action ) {
const newResources = Object.keys( action.resources ).reduce( ( resources, name ) => {
const resource = { ...action.resources[ name ] };
if ( resource.data ) {
resource.lastReceived = action.time;
}
if ( resource.error ) {
resource.lastError = action.time;
}
resources[ name ] = resource;
return resources;
}, {} );
return reduceResources( state, newResources );
}

export function reduceResources( state, newResources ) {
const updatedResources = Object.keys( newResources ).reduce(
( resources, resourceName ) => {
const resource = resources[ resourceName ];
const newResource = newResources[ resourceName ];
resources[ resourceName ] = { ...resource, ...newResource };
return resources;
},
{ ...state.resources }
);

return { ...state, resources: updatedResources };
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@
"react-router-dom": "4.3.1",
"react-slot-fill": "^2.0.1",
"react-transition-group": "^2.4.0",
"react-world-flags": "1.2.4"
"react-world-flags": "1.2.4",
"redux": "^4.0.0"
},
"husky": {
"hooks": {
Expand Down

0 comments on commit d6aefc5

Please sign in to comment.