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

[WIP] Try fresh-data wp-plugin as external. #301

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions client/fresh-data-plugin/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/** @format */

export function dataRequested( api, clientKey, resourceNames, time = new Date() ) {
return {
type: 'RESOURCES_REQUESTED',
api,
clientKey,
resourceNames,
time,
};
}

export function dataReceived( api, clientKey, resources, time = new Date() ) {
return {
type: 'RESOURCES_RECEIVED',
api,
clientKey,
resources,
time,
};
}
127 changes: 127 additions & 0 deletions client/fresh-data-plugin/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* External dependencies
*
* @format
*/

import { FreshDataApi } from '@fresh-data/framework';

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

export const SECOND = 1000;
export const MINUTE = 60 * SECOND;
export const HOUR = 60 * MINUTE;
export { default as withResources } from './with-resources';

/**
* Fresh data API.
*
* @property {Methods} methods Functions that correspond to API verbs like
* "GET" or "POST" and used by operations.
* @property {Operations} operations Functions to handle operations on the
* api, such as "read", or "update".
* @property {Selectors} selectors Selector functions which can require
* resources and return their current values.
* @property {Mutations} mutations Mutation functions that map to operations and
* designed to be used by the application.
*
* @typedef {WPFreshDataApiSpec}
*/

/**
* Fresh data store options.
*
* @property {WPFreshDataAPI} apiSpec A fresh-data API Specification.
*
* @typedef {WPFreshDataStoreOptions}
*/

/**
* Creates a fresh-data api client for use in @wordpress/data.
*
* @param {Object} apiSpec The api specification for the client.
* @param {Object} dispatchActions `dataRequested` and `dataReceived` action creators
* wrapped in a dispatch function.
*
* @return {Object} An ApiClient ready to be used.
*/
export function createApiClient( apiSpec, dispatchActions ) {
if ( ! apiSpec.methods ) {
throw new TypeError( 'apiSpec must specify methods' );
}
if ( ! apiSpec.operations ) {
throw new TypeError( 'apiSpec must specify operations' );
}

// TODO: Remove ApiClass after fresh-data 0.3.0 is released.
class ApiClass extends FreshDataApi {
constructor() {
super();
this.methods = apiSpec.methods;
this.operations = apiSpec.operations;
this.mutations = apiSpec.mutations;
this.selectors = apiSpec.selectors || {};
}
}

const api = new ApiClass();
api.setDataHandlers( dispatchActions );
return api.createClient( 'client' );
}

/**
* Registers a fresh-data api.
*
* @param {WPDataRegistry} registry Data registry.
* @param {string} reducerKey Name of reducerKey and api instance.
* @param {WPFreshDataStoreOptions} options Options given to registerStore.
*
* @return {Object} The api client created.
*/
export function registerApiClient( registry, reducerKey, options ) {
const { apiSpec } = options;

const store = registry.registerReducer( reducerKey, reducer, options.persist );
registry.registerActions( reducerKey, actions );

const dispatchActions = registry.dispatch( reducerKey );
const apiClient = createApiClient( apiSpec, dispatchActions );

// Subscribe to the store directly so we don't get all the global events
store.subscribe( () => {
const state = store.getState() || {};
const clientState = state.client || {}; // TODO: Remove this after fresh-data 0.3.0
apiClient.setState( clientState );
} );

return { store, apiClient };
}

/**
* Data plugin to map api data to component requirements.
*
* @param {WPDataRegistry} registry Data registry.
*
* @return {WPDataPlugin} Data plugin.
*/
export default function( registry ) {
const apisByReducerKey = {};

return {
registerStore( reducerKey, options ) {
if ( options.apiSpec ) {
const { store, apiClient } = registerApiClient( registry, reducerKey, options );
apisByReducerKey[ reducerKey ] = apiClient;
return store;
}
return registry.registerStore( reducerKey, options );
},
getApiClient( reducerKey ) {
return apisByReducerKey[ reducerKey ] || null;
},
};
}
51 changes: 51 additions & 0 deletions client/fresh-data-plugin/reducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/** @format */

export function reduceRequested( state, action ) {
const { resourceNames, time } = action;
const apiState = state || {};
const clientState = apiState.client || {};
const existingResources = clientState.resources || {};

const resources = resourceNames.reduce( ( newResources, resourceName ) => {
const existingResource = existingResources[ resourceName ];
newResources[ resourceName ] = { ...existingResource, lastRequested: time };
return newResources;
}, existingResources );

return { ...state, client: { resources: resources } };
}

export function reduceReceived( state = {}, action ) {
const { resources, time } = action;
const apiState = state || {};
const clientState = apiState.client || {};
const existingResources = clientState.resources || {};

const updatedResources = Object.keys( resources ).reduce( ( newResources, resourceName ) => {
const existingResource = existingResources[ resourceName ];
const resource = action.resources[ resourceName ];
if ( resource.data ) {
resource.lastReceived = time;
}
if ( resource.error ) {
resource.lastError = time;
}
newResources[ resourceName ] = { ...existingResource, ...resource };
return newResources;
}, existingResources );

return { ...state, client: { resources: updatedResources } };
}

const reducer = ( state = {}, action ) => {
switch ( action.type ) {
case 'RESOURCES_REQUESTED':
return reduceRequested( state, action );
case 'RESOURCES_RECEIVED':
return reduceReceived( state, action );
default:
return state;
}
};

export default reducer;
144 changes: 144 additions & 0 deletions client/fresh-data-plugin/with-resources.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/** @format */
/**
* External dependencies
*/
import { Component } from '@wordpress/element';
import isShallowEqual from '@wordpress/is-shallow-equal';
import { remountOnPropChange, createHigherOrderComponent } from '@wordpress/compose';
import { RegistryConsumer } from '@wordpress/data';

/**
* Higher-order component used to inject state-derived props using registered
* selectors.
*
* @param {string} apiName Name of api from which resources are needed.
* @param {Function} mapSelectorsToProps Function called on every state change,
* expected to return object of props to
* merge with the component's own props.
* @param {Function} [mapMutationsToProps] Function called which maps resource
* mutations to event handle callbacks.
* @return {Component} Enhanced component with merged state data props.
*/
const withResources = ( apiName, mapSelectorsToProps, mapMutationsToProps ) =>
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 = {};

const ComponentWithSelectors = remountOnPropChange( 'registry' )(
class extends Component {
constructor( props ) {
super( props );

this.subscribe();

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

/**
* Given a props object, returns the next merge props by mapStateToProps.
*
* @param {Object} props Props to pass as argument to mapStateToProps.
*
* @return {Object} Props to merge into rendered wrapped element.
*/
getNextMergeProps( props ) {
const apiClient = props.registry.getApiClient( apiName );
let selectorProps = DEFAULT_MERGE_PROPS;
let mutationProps = {};

apiClient.setComponentData( this, selectors => {
selectorProps = mapSelectorsToProps( selectors, props.ownProps );
} );

if ( mapMutationsToProps ) {
const mutations = apiClient.getMutations();
mutationProps = mapMutationsToProps( mutations, this.props );
}

return {
...selectorProps,
...mutationProps,
};
}

componentDidMount() {
this.canRunSelection = true;
}

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

shouldComponentUpdate( nextProps, nextState ) {
const hasPropsChanged = ! 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 merge props change as a result of the incoming props, they
// should be reflected as such in the upcoming render.
if ( hasPropsChanged ) {
const nextMergeProps = this.getNextMergeProps( nextProps );
if ( ! isShallowEqual( this.mergeProps, nextMergeProps ) ) {
// Side effects are typically discouraged in lifecycle methods, but
// this component is heavily used and this is the most performant
// code we've found thus far.
// Prior efforts to use `getDerivedStateFromProps` have demonstrated
// miserable performance.
this.mergeProps = nextMergeProps;
}
}

return true;
}

subscribe() {
const apiClient = this.props.registry.getApiClient( apiName );
this.unsubscribe = apiClient.subscribe( () => {
if ( ! this.canRunSelection ) {
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
// because 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( {} );
} );
}

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

return ownProps => (
<RegistryConsumer>
{ registry => <ComponentWithSelectors ownProps={ ownProps } registry={ registry } /> }
</RegistryConsumer>
);
}, 'withResources' );

export default withResources;
5 changes: 3 additions & 2 deletions client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@ import { APIProvider } from '@wordpress/components';
import { pick } from 'lodash';
import { render } from '@wordpress/element';
import { Provider as SlotFillProvider } from 'react-slot-fill';
import { plugins, use, RegistryProvider } from '@wordpress/data';
import { use, RegistryProvider } from '@wordpress/data';
import 'react-dates/initialize';

/**
* Internal dependencies
*/
import './stylesheets/_index.scss';
import { PageLayout } from './layout';
import freshDataPlugin from './fresh-data-plugin';
import wcApiSpec from 'wc-api-spec';

const registry = use( plugins.freshData );
const registry = use( freshDataPlugin );

registry.registerStore( 'wc-api', { apiSpec: wcApiSpec } );

Expand Down
4 changes: 1 addition & 3 deletions client/layout/activity-panel/panels/orders.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import { __, _n, sprintf } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { compose } from '@wordpress/compose';
import { withResources, plugins } from '@wordpress/data';
import { Component, Fragment } from '@wordpress/element';
import PropTypes from 'prop-types';
import { noop } from 'lodash';
Expand All @@ -23,8 +22,7 @@ import Gravatar from 'components/gravatar';
import Flag from 'components/flag';
import OrderStatus from 'components/order-status';
import { Section } from 'layout/section';

const { MINUTE } = plugins.freshData;
import { MINUTE, withResources } from 'fresh-data-plugin';

class OrdersPanel extends Component {
constructor() {
Expand Down
Loading