diff --git a/packages/js/data/src/options/action-types.js b/packages/js/data/src/options/action-types.js deleted file mode 100644 index e7765ef2e86..00000000000 --- a/packages/js/data/src/options/action-types.js +++ /dev/null @@ -1,9 +0,0 @@ -const TYPES = { - RECEIVE_OPTIONS: 'RECEIVE_OPTIONS', - SET_IS_REQUESTING: 'SET_IS_REQUESTING', - SET_IS_UPDATING: 'SET_IS_UPDATING', - SET_REQUESTING_ERROR: 'SET_REQUESTING_ERROR', - SET_UPDATING_ERROR: 'SET_UPDATING_ERROR', -}; - -export default TYPES; diff --git a/packages/js/data/src/options/action-types.ts b/packages/js/data/src/options/action-types.ts new file mode 100644 index 00000000000..a9f668f3c60 --- /dev/null +++ b/packages/js/data/src/options/action-types.ts @@ -0,0 +1,9 @@ +const TYPES = { + RECEIVE_OPTIONS: 'RECEIVE_OPTIONS' as const, + SET_IS_REQUESTING: 'SET_IS_REQUESTING' as const, + SET_IS_UPDATING: 'SET_IS_UPDATING' as const, + SET_REQUESTING_ERROR: 'SET_REQUESTING_ERROR' as const, + SET_UPDATING_ERROR: 'SET_UPDATING_ERROR' as const, +}; + +export default TYPES; diff --git a/packages/js/data/src/options/actions.js b/packages/js/data/src/options/actions.ts similarity index 51% rename from packages/js/data/src/options/actions.js rename to packages/js/data/src/options/actions.ts index 70b9f6931d9..8381afa1038 100644 --- a/packages/js/data/src/options/actions.js +++ b/packages/js/data/src/options/actions.ts @@ -8,15 +8,16 @@ import { apiFetch } from '@wordpress/data-controls'; */ import TYPES from './action-types'; import { WC_ADMIN_NAMESPACE } from '../constants'; +import { Options } from './types'; -export function receiveOptions( options ) { +export function receiveOptions( options: Options ) { return { type: TYPES.RECEIVE_OPTIONS, options, }; } -export function setRequestingError( error, name ) { +export function setRequestingError( error: unknown, name: string ) { return { type: TYPES.SET_REQUESTING_ERROR, error, @@ -24,35 +25,51 @@ export function setRequestingError( error, name ) { }; } -export function setUpdatingError( error ) { +export function setUpdatingError( error: unknown ) { return { type: TYPES.SET_UPDATING_ERROR, error, }; } -export function setIsUpdating( isUpdating ) { +export function setIsUpdating( isUpdating: boolean ) { return { type: TYPES.SET_IS_UPDATING, isUpdating, }; } -export function* updateOptions( data ) { +export function* updateOptions( data: Options ) { yield setIsUpdating( true ); yield receiveOptions( data ); try { - const results = yield apiFetch( { + const results: unknown = yield apiFetch( { path: WC_ADMIN_NAMESPACE + '/options', method: 'POST', data, } ); yield setIsUpdating( false ); + + if ( typeof results !== 'object' ) { + throw new Error( + `Invalid update options response from server: ${ results }` + ); + } return { success: true, ...results }; } catch ( error ) { yield setUpdatingError( error ); + if ( typeof error !== 'object' ) { + throw new Error( `Unexpected error: ${ error }` ); + } return { success: false, ...error }; } } + +export type Action = ReturnType< + | typeof receiveOptions + | typeof setRequestingError + | typeof setUpdatingError + | typeof setIsUpdating +>; diff --git a/packages/js/data/src/options/constants.ts b/packages/js/data/src/options/constants.ts index b6d1b4cbe5f..bc2fa731c3e 100644 --- a/packages/js/data/src/options/constants.ts +++ b/packages/js/data/src/options/constants.ts @@ -1 +1 @@ -export const STORE_NAME = 'wc/admin/options'; +export const STORE_NAME = 'wc/admin/options' as const; diff --git a/packages/js/data/src/options/controls.js b/packages/js/data/src/options/controls.ts similarity index 78% rename from packages/js/data/src/options/controls.js rename to packages/js/data/src/options/controls.ts index f849a01ca47..eb3964b8641 100644 --- a/packages/js/data/src/options/controls.js +++ b/packages/js/data/src/options/controls.ts @@ -2,6 +2,7 @@ * External dependencies */ import { controls as dataControls } from '@wordpress/data-controls'; +import { Action } from '@wordpress/data'; import apiFetch from '@wordpress/api-fetch'; /** @@ -9,10 +10,12 @@ import apiFetch from '@wordpress/api-fetch'; */ import { WC_ADMIN_NAMESPACE } from '../constants'; -let optionNames = []; -const fetches = {}; +let optionNames: string[] = []; +const fetches: { + [ key: string ]: Promise< unknown >; +} = {}; -export const batchFetch = ( optionName ) => { +export const batchFetch = ( optionName: string ) => { return { type: 'BATCH_FETCH', optionName, @@ -21,12 +24,15 @@ export const batchFetch = ( optionName ) => { export const controls = { ...dataControls, - BATCH_FETCH( { optionName } ) { + BATCH_FETCH( { optionName }: Action ) { optionNames.push( optionName ); return new Promise( ( resolve ) => { setTimeout( function () { - if ( fetches[ optionName ] ) { + if ( + fetches.hasOwnProperty( optionName ) && + fetches[ optionName ] + ) { return fetches[ optionName ].then( ( result ) => { resolve( result ); } ); diff --git a/packages/js/data/src/options/index.js b/packages/js/data/src/options/index.js deleted file mode 100644 index 8e017be45b3..00000000000 --- a/packages/js/data/src/options/index.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * External dependencies - */ - -import { registerStore } from '@wordpress/data'; - -/** - * Internal dependencies - */ -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, { - reducer, - actions, - controls, - selectors, - resolvers, -} ); - -export const OPTIONS_STORE_NAME = STORE_NAME; diff --git a/packages/js/data/src/options/index.ts b/packages/js/data/src/options/index.ts new file mode 100644 index 00000000000..7e1a79166e7 --- /dev/null +++ b/packages/js/data/src/options/index.ts @@ -0,0 +1,37 @@ +/** + * External dependencies + */ +import { registerStore } from '@wordpress/data'; +import { SelectFromMap, DispatchFromMap } from '@automattic/data-stores'; +import { Reducer, AnyAction } from 'redux'; +/** + * Internal dependencies + */ +import { STORE_NAME } from './constants'; +import * as selectors from './selectors'; +import * as actions from './actions'; +import * as resolvers from './resolvers'; +import reducer, { State } from './reducer'; +import { controls } from './controls'; +import { WPDataSelectors } from '../types'; +export * from './types'; +export type { State }; + +registerStore< State >( STORE_NAME, { + reducer: reducer as Reducer< State, AnyAction >, + actions, + controls, + selectors, + resolvers, +} ); + +export const OPTIONS_STORE_NAME = STORE_NAME; + +declare module '@wordpress/data' { + function dispatch( + key: typeof STORE_NAME + ): DispatchFromMap< typeof actions >; + function select( + key: typeof STORE_NAME + ): SelectFromMap< typeof selectors > & WPDataSelectors; +} diff --git a/packages/js/data/src/options/reducer.js b/packages/js/data/src/options/reducer.ts similarity index 54% rename from packages/js/data/src/options/reducer.js rename to packages/js/data/src/options/reducer.ts index 759f3ab32ec..734b1aad76d 100644 --- a/packages/js/data/src/options/reducer.js +++ b/packages/js/data/src/options/reducer.ts @@ -1,38 +1,46 @@ +/** + * External dependencies + */ + +import type { Reducer } from 'redux'; + /** * Internal dependencies */ import TYPES from './action-types'; +import { Action } from './actions'; +import { OptionsState } from './types'; -const optionsReducer = ( +const optionsReducer: Reducer< OptionsState, Action > = ( state = { isUpdating: false, requestingErrors: {} }, - { type, options, error, isUpdating, name } + action ) => { - switch ( type ) { + switch ( action.type ) { case TYPES.RECEIVE_OPTIONS: state = { ...state, - ...options, + ...action.options, }; break; case TYPES.SET_IS_UPDATING: state = { ...state, - isUpdating, + isUpdating: action.isUpdating, }; break; case TYPES.SET_REQUESTING_ERROR: state = { ...state, requestingErrors: { - [ name ]: error, + [ action.name ]: action.error, }, }; break; case TYPES.SET_UPDATING_ERROR: state = { ...state, - error, - updatingError: error, + error: action.error, + updatingError: action.error, isUpdating: false, }; break; @@ -40,4 +48,5 @@ const optionsReducer = ( return state; }; +export type State = ReturnType< typeof optionsReducer >; export default optionsReducer; diff --git a/packages/js/data/src/options/resolvers.js b/packages/js/data/src/options/resolvers.ts similarity index 71% rename from packages/js/data/src/options/resolvers.js rename to packages/js/data/src/options/resolvers.ts index ec1103ea16a..4edf2146586 100644 --- a/packages/js/data/src/options/resolvers.js +++ b/packages/js/data/src/options/resolvers.ts @@ -3,15 +3,16 @@ */ import { receiveOptions, setRequestingError } from './actions'; import { batchFetch } from './controls'; +import { Options } from './types'; /** * Request an option value. * * @param {string} name - Option name */ -export function* getOption( name ) { +export function* getOption( name: string ) { try { - const result = yield batchFetch( name ); + const result: Options = yield batchFetch( name ); yield receiveOptions( result ); } catch ( error ) { yield setRequestingError( error, name ); diff --git a/packages/js/data/src/options/selectors.js b/packages/js/data/src/options/selectors.ts similarity index 63% rename from packages/js/data/src/options/selectors.js rename to packages/js/data/src/options/selectors.ts index 1883494c8b0..393f86b75c9 100644 --- a/packages/js/data/src/options/selectors.js +++ b/packages/js/data/src/options/selectors.ts @@ -1,10 +1,15 @@ +/** + * Internal dependencies + */ +import { OptionsState } from './types'; + /** * Get option from state tree. * * @param {Object} state - Reducer state * @param {Array} name - Option name */ -export const getOption = ( state, name ) => { +export const getOption = ( state: OptionsState, name: string ) => { return state[ name ]; }; @@ -14,7 +19,10 @@ export const getOption = ( state, name ) => { * @param {Object} state - Reducer state * @param {string} name - Option name */ -export const getOptionsRequestingError = ( state, name ) => { +export const getOptionsRequestingError = ( + state: OptionsState, + name: string +) => { return state.requestingErrors[ name ] || false; }; @@ -23,7 +31,7 @@ export const getOptionsRequestingError = ( state, name ) => { * * @param {Object} state - Reducer state */ -export const isOptionsUpdating = ( state ) => { +export const isOptionsUpdating = ( state: OptionsState ) => { return state.isUpdating || false; }; @@ -32,6 +40,6 @@ export const isOptionsUpdating = ( state ) => { * * @param {Object} state - Reducer state */ -export const getOptionsUpdatingError = ( state ) => { +export const getOptionsUpdatingError = ( state: OptionsState ) => { return state.updatingError || false; }; diff --git a/packages/js/data/src/options/test/reducer.js b/packages/js/data/src/options/test/reducer.ts similarity index 96% rename from packages/js/data/src/options/test/reducer.js rename to packages/js/data/src/options/test/reducer.ts index 950e118a897..512ab7c9133 100644 --- a/packages/js/data/src/options/test/reducer.js +++ b/packages/js/data/src/options/test/reducer.ts @@ -12,6 +12,7 @@ const defaultState = { isUpdating: false, requestingErrors: {} }; describe( 'options reducer', () => { it( 'should return a default state', () => { + // @ts-expect-error reducer action should not be empty but it is const state = reducer( undefined, {} ); expect( state ).toEqual( defaultState ); expect( state ).not.toBe( defaultState ); diff --git a/packages/js/data/src/options/test/with-options-hydration.js b/packages/js/data/src/options/test/with-options-hydration.tsx similarity index 97% rename from packages/js/data/src/options/test/with-options-hydration.js rename to packages/js/data/src/options/test/with-options-hydration.tsx index 750cee08e2f..d0c8737a757 100644 --- a/packages/js/data/src/options/test/with-options-hydration.js +++ b/packages/js/data/src/options/test/with-options-hydration.tsx @@ -39,7 +39,7 @@ describe( 'withOptionsHydration', () => { const startResolutionMock = jest.fn(); const receiveOptionsMock = jest.fn(); beforeEach( () => { - useSelect.mockImplementation( ( callback ) => { + ( useSelect as jest.Mock ).mockImplementation( ( callback ) => { callback( () => ( { isResolving: isResolvingMock, diff --git a/packages/js/data/src/options/types.ts b/packages/js/data/src/options/types.ts index abfadb138c9..0bba452d526 100644 --- a/packages/js/data/src/options/types.ts +++ b/packages/js/data/src/options/types.ts @@ -17,3 +17,18 @@ export type OptionsSelectors = { isOptionsUpdating: WPDataSelector< typeof isOptionsUpdating >; getOptionsUpdatingError: WPDataSelector< typeof getOptionsUpdatingError >; } & WPDataSelectors; + +export type Options = { + [ key: string ]: unknown; +}; + +export type OptionsState = { + isUpdating: boolean; + requestingErrors: + | { + [ name: string ]: unknown; + } + | Record< string, never >; + error?: unknown; + updatingError?: unknown; +} & Options; diff --git a/packages/js/data/src/options/with-options-hydration.js b/packages/js/data/src/options/with-options-hydration.tsx similarity index 72% rename from packages/js/data/src/options/with-options-hydration.js rename to packages/js/data/src/options/with-options-hydration.tsx index 2a63512e96d..8a4d5bc7ef3 100644 --- a/packages/js/data/src/options/with-options-hydration.js +++ b/packages/js/data/src/options/with-options-hydration.tsx @@ -2,18 +2,20 @@ * External dependencies */ import { createHigherOrderComponent } from '@wordpress/compose'; -import { useSelect } from '@wordpress/data'; +import { useSelect, select as WPSelect } from '@wordpress/data'; import { createElement, useRef } from '@wordpress/element'; /** * Internal dependencies */ import { STORE_NAME } from './constants'; +import { Options } from './types'; -export const useOptionsHydration = ( data ) => { +export const useOptionsHydration = ( data: Options ) => { const dataRef = useRef( data ); - useSelect( ( select, registry ) => { + // @ts-expect-error registry is not defined in the wp.data typings + useSelect( ( select: typeof WPSelect, registry ) => { if ( ! dataRef.current ) { return; } @@ -39,8 +41,8 @@ export const useOptionsHydration = ( data ) => { }, [] ); }; -export const withOptionsHydration = ( data ) => - createHigherOrderComponent( +export const withOptionsHydration = ( data: Options ) => + createHigherOrderComponent< Record< string, unknown > >( ( OriginalComponent ) => ( props ) => { useOptionsHydration( data );