diff --git a/packages/data/src/options/actions.js b/packages/data/src/options/actions.js index b6b6b269b9b..62f5fc012bb 100644 --- a/packages/data/src/options/actions.js +++ b/packages/data/src/options/actions.js @@ -17,17 +17,11 @@ export function receiveOptions( options ) { }; } -export function setIsRequesting( isRequesting ) { - return { - type: TYPES.SET_IS_REQUESTING, - isRequesting, - }; -} - -export function setRequestingError( error ) { +export function setRequestingError( error, name ) { return { type: TYPES.SET_REQUESTING_ERROR, error, + name, }; } diff --git a/packages/data/src/options/controls.js b/packages/data/src/options/controls.js new file mode 100644 index 00000000000..957150d28d2 --- /dev/null +++ b/packages/data/src/options/controls.js @@ -0,0 +1,50 @@ +/** + * External dependencies + */ +import { controls as dataControls } from '@wordpress/data-controls'; +import apiFetch from '@wordpress/api-fetch'; + +/** + * Internal dependencies + */ +import { WC_ADMIN_NAMESPACE } from '../constants'; + +let optionNames = []; +const fetches = {}; + +export const batchFetch = ( optionName ) => { + return { + type: 'BATCH_FETCH', + optionName, + }; +}; + +export const controls = { + ...dataControls, + BATCH_FETCH( { optionName } ) { + optionNames.push( optionName ); + + return new Promise( resolve => { + setTimeout( function() { + const names = optionNames.join(','); + if ( fetches[ names ] ) { + return fetches[ names ].then( ( result ) => { + resolve( result[ optionName ] ); + } ); + } + + const url = WC_ADMIN_NAMESPACE + '/options?options=' + names; + fetches[ names ] = apiFetch( { path: url } ); + fetches[names].then( ( result ) => resolve( result ) ) + + // Clear option names after all resolved; + setTimeout( () => { + optionNames = []; + // Delete the fetch after to allow wp data to handle cache invalidation. + delete fetches[ names ]; + }, 1 ) + + }, 1 ); + } ); + }, +}; diff --git a/packages/data/src/options/index.js b/packages/data/src/options/index.js index 5e03c0bb99a..8e017be45b3 100644 --- a/packages/data/src/options/index.js +++ b/packages/data/src/options/index.js @@ -3,7 +3,6 @@ */ import { registerStore } from '@wordpress/data'; -import { controls } from '@wordpress/data-controls'; /** * Internal dependencies @@ -12,6 +11,7 @@ import { STORE_NAME } from './constants'; import * as selectors from './selectors'; import * as actions from './actions'; import * as resolvers from './resolvers'; +import { controls } from './controls'; import reducer from './reducer'; registerStore( STORE_NAME, { diff --git a/packages/data/src/options/reducer.js b/packages/data/src/options/reducer.js index 71f707c5f4e..759f3ab32ec 100644 --- a/packages/data/src/options/reducer.js +++ b/packages/data/src/options/reducer.js @@ -4,21 +4,14 @@ import TYPES from './action-types'; const optionsReducer = ( - state = { isRequesting: false, isUpdating: false }, - { type, options, isRequesting, error, isUpdating } + state = { isUpdating: false, requestingErrors: {} }, + { type, options, error, isUpdating, name } ) => { switch ( type ) { case TYPES.RECEIVE_OPTIONS: state = { ...state, ...options, - isRequesting: false, - }; - break; - case TYPES.SET_IS_REQUESTING: - state = { - ...state, - isRequesting, }; break; case TYPES.SET_IS_UPDATING: @@ -30,8 +23,9 @@ const optionsReducer = ( case TYPES.SET_REQUESTING_ERROR: state = { ...state, - requestingError: error, - isRequesting: false, + requestingErrors: { + [ name ]: error, + }, }; break; case TYPES.SET_UPDATING_ERROR: diff --git a/packages/data/src/options/resolvers.js b/packages/data/src/options/resolvers.js index 04735747f4c..87aa9860896 100644 --- a/packages/data/src/options/resolvers.js +++ b/packages/data/src/options/resolvers.js @@ -1,17 +1,16 @@ /** * External Dependencies */ - import { apiFetch } from '@wordpress/data-controls'; +import { batchFetch } from './controls'; /** * Internal dependencies */ import { WC_ADMIN_NAMESPACE } from '../constants'; -import { receiveOptions, setIsRequesting, setRequestingError } from './actions'; +import { receiveOptions, setRequestingError } from './actions'; export function* getOptionsWithRequest( names ) { - yield setIsRequesting( true ); const url = WC_ADMIN_NAMESPACE + '/options?options=' + names.join( ',' ); try { @@ -19,7 +18,21 @@ export function* getOptionsWithRequest( names ) { yield receiveOptions( results ); return results; } catch ( error ) { - yield setRequestingError( error ); + yield setRequestingError( error, names ); return error; } } + +/** + * Request an option value. + * + * @param {string} name - Option name + */ +export function* getOption( name ) { + try { + const result = yield batchFetch( name ); + yield receiveOptions( result ); + } catch ( error ) { + yield setRequestingError( error, name ); + } +} diff --git a/packages/data/src/options/selectors.js b/packages/data/src/options/selectors.js index 1cd138eff6c..ab4ab814041 100644 --- a/packages/data/src/options/selectors.js +++ b/packages/data/src/options/selectors.js @@ -27,6 +27,16 @@ export const getOptions = ( state, names ) => { }, {} ); }; +/** + * Get option from state tree. + * + * @param {Object} state - Reducer state + * @param {Array} name - Option name + */ +export const getOption = ( state, name ) => { + return state[ name ]; +}; + /** * Get options from state tree or make a request if unresolved. * @@ -40,22 +50,14 @@ export const getOptionsWithRequest = ( state, names ) => { }, {} ); }; -/** - * Determine if options are being requested. - * - * @param {Object} state - Reducer state - */ -export const isGetOptionsRequesting = ( state ) => { - return state.isRequesting || false; -}; - /** * Determine if an options request resulted in an error. * * @param {Object} state - Reducer state + * @param {string} name - Option name */ -export const getOptionsRequestingError = ( state ) => { - return state.requestingError || false; +export const getOptionsRequestingError = ( state, name ) => { + return state.requestingErrors[ name ] || false; }; /** diff --git a/packages/data/src/options/test/reducer.js b/packages/data/src/options/test/reducer.js new file mode 100644 index 00000000000..27b38089d41 --- /dev/null +++ b/packages/data/src/options/test/reducer.js @@ -0,0 +1,60 @@ +/** + * Internal dependencies + */ +import reducer from '../reducer'; +import TYPES from '../action-types'; + +const defaultState = { isUpdating: false, requestingErrors: {} }; + +describe( 'options reducer', () => { + it( 'should return a default state', () => { + const state = reducer( undefined, {} ); + expect( state ).toEqual( defaultState ); + expect( state ).not.toBe( defaultState ); + } ); + + it( 'should handle RECEIVE_OPTIONS', () => { + const state = reducer( defaultState, { + type: TYPES.RECEIVE_OPTIONS, + options: { test_option: 'abc' }, + } ); + + /* eslint-disable dot-notation */ + expect( state.requestingErrors[ 'test_option' ] ).toBeUndefined(); + expect( state[ 'test_option' ] ).toBe( 'abc' ); + /* eslint-enable dot-notation */ + } ); + + it( 'should handle SET_REQUESTING_ERROR', () => { + const state = reducer( defaultState, { + type: TYPES.SET_REQUESTING_ERROR, + error: 'My bad', + name: 'test_option' + } ); + + /* eslint-disable dot-notation */ + expect( state.requestingErrors[ 'test_option' ] ).toBe( 'My bad' ); + expect( state[ 'test_option' ] ).toBeUndefined(); + /* eslint-enable dot-notation */ + } ); + + it( 'should handle SET_UPDATING_ERROR', () => { + const state = reducer( defaultState, { + type: TYPES.SET_UPDATING_ERROR, + error: 'My bad', + } ); + + expect( state.updatingError ).toBe( 'My bad' ); + expect( state.isUpdating ).toBe( false ); + } ); + + it( 'should handle SET_IS_UPDATING', () => { + const state = reducer( defaultState, { + type: TYPES.SET_IS_UPDATING, + isUpdating: true, + } ); + + expect( state.isUpdating ).toBe( true ); + } ); + +} ); \ No newline at end of file diff --git a/packages/data/src/options/with-options-hydration.js b/packages/data/src/options/with-options-hydration.js index 7d465baa693..6c27bddd3f6 100644 --- a/packages/data/src/options/with-options-hydration.js +++ b/packages/data/src/options/with-options-hydration.js @@ -26,14 +26,16 @@ export const withOptionsHydration = ( data ) => ( OriginalComponent ) => { } = registry.dispatch( STORE_NAME ); const names = Object.keys( dataRef.current ); - if ( - ! isResolving( 'getOptionsWithRequest', names ) && - ! hasFinishedResolution( 'getOptionsWithRequest', names ) - ) { - startResolution( 'getOptionsWithRequest', names ); - receiveOptions( dataRef.current ); - finishResolution( 'getOptionsWithRequest', names ); - } + names.forEach( ( name ) => { + if ( + ! isResolving( 'getOption', [ name ] ) && + ! hasFinishedResolution( 'getOption', [ name ] ) + ) { + startResolution( 'getOption', [ name ] ); + receiveOptions( { [ name ]: dataRef.current[ name ] } ); + finishResolution( 'getOption', [ name ] ); + } + } ); }, [] ); return ;