Migrate woo data options to TS

This commit is contained in:
Chi-Hsuan Huang 2022-05-24 09:44:29 +08:00
parent 78f96109da
commit 5f7d0cd0e1
14 changed files with 137 additions and 66 deletions

View File

@ -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;

View File

@ -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;

View File

@ -8,15 +8,16 @@ import { apiFetch } from '@wordpress/data-controls';
*/ */
import TYPES from './action-types'; import TYPES from './action-types';
import { WC_ADMIN_NAMESPACE } from '../constants'; import { WC_ADMIN_NAMESPACE } from '../constants';
import { Options } from './types';
export function receiveOptions( options ) { export function receiveOptions( options: Options ) {
return { return {
type: TYPES.RECEIVE_OPTIONS, type: TYPES.RECEIVE_OPTIONS,
options, options,
}; };
} }
export function setRequestingError( error, name ) { export function setRequestingError( error: unknown, name: string ) {
return { return {
type: TYPES.SET_REQUESTING_ERROR, type: TYPES.SET_REQUESTING_ERROR,
error, error,
@ -24,35 +25,51 @@ export function setRequestingError( error, name ) {
}; };
} }
export function setUpdatingError( error ) { export function setUpdatingError( error: unknown ) {
return { return {
type: TYPES.SET_UPDATING_ERROR, type: TYPES.SET_UPDATING_ERROR,
error, error,
}; };
} }
export function setIsUpdating( isUpdating ) { export function setIsUpdating( isUpdating: boolean ) {
return { return {
type: TYPES.SET_IS_UPDATING, type: TYPES.SET_IS_UPDATING,
isUpdating, isUpdating,
}; };
} }
export function* updateOptions( data ) { export function* updateOptions( data: Options ) {
yield setIsUpdating( true ); yield setIsUpdating( true );
yield receiveOptions( data ); yield receiveOptions( data );
try { try {
const results = yield apiFetch( { const results: unknown = yield apiFetch( {
path: WC_ADMIN_NAMESPACE + '/options', path: WC_ADMIN_NAMESPACE + '/options',
method: 'POST', method: 'POST',
data, data,
} ); } );
yield setIsUpdating( false ); yield setIsUpdating( false );
if ( typeof results !== 'object' ) {
throw new Error(
`Invalid update options response from server: ${ results }`
);
}
return { success: true, ...results }; return { success: true, ...results };
} catch ( error ) { } catch ( error ) {
yield setUpdatingError( error ); yield setUpdatingError( error );
if ( typeof error !== 'object' ) {
throw new Error( `Unexpected error: ${ error }` );
}
return { success: false, ...error }; return { success: false, ...error };
} }
} }
export type Action = ReturnType<
| typeof receiveOptions
| typeof setRequestingError
| typeof setUpdatingError
| typeof setIsUpdating
>;

View File

@ -1 +1 @@
export const STORE_NAME = 'wc/admin/options'; export const STORE_NAME = 'wc/admin/options' as const;

View File

@ -2,6 +2,7 @@
* External dependencies * External dependencies
*/ */
import { controls as dataControls } from '@wordpress/data-controls'; import { controls as dataControls } from '@wordpress/data-controls';
import { Action } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch'; import apiFetch from '@wordpress/api-fetch';
/** /**
@ -9,10 +10,12 @@ import apiFetch from '@wordpress/api-fetch';
*/ */
import { WC_ADMIN_NAMESPACE } from '../constants'; import { WC_ADMIN_NAMESPACE } from '../constants';
let optionNames = []; let optionNames: string[] = [];
const fetches = {}; const fetches: {
[ key: string ]: Promise< unknown >;
} = {};
export const batchFetch = ( optionName ) => { export const batchFetch = ( optionName: string ) => {
return { return {
type: 'BATCH_FETCH', type: 'BATCH_FETCH',
optionName, optionName,
@ -21,12 +24,15 @@ export const batchFetch = ( optionName ) => {
export const controls = { export const controls = {
...dataControls, ...dataControls,
BATCH_FETCH( { optionName } ) { BATCH_FETCH( { optionName }: Action ) {
optionNames.push( optionName ); optionNames.push( optionName );
return new Promise( ( resolve ) => { return new Promise( ( resolve ) => {
setTimeout( function () { setTimeout( function () {
if ( fetches[ optionName ] ) { if (
fetches.hasOwnProperty( optionName ) &&
fetches[ optionName ]
) {
return fetches[ optionName ].then( ( result ) => { return fetches[ optionName ].then( ( result ) => {
resolve( result ); resolve( result );
} ); } );

View File

@ -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;

View File

@ -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;
}

View File

@ -1,38 +1,46 @@
/**
* External dependencies
*/
import type { Reducer } from 'redux';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import TYPES from './action-types'; import TYPES from './action-types';
import { Action } from './actions';
import { OptionsState } from './types';
const optionsReducer = ( const optionsReducer: Reducer< OptionsState, Action > = (
state = { isUpdating: false, requestingErrors: {} }, state = { isUpdating: false, requestingErrors: {} },
{ type, options, error, isUpdating, name } action
) => { ) => {
switch ( type ) { switch ( action.type ) {
case TYPES.RECEIVE_OPTIONS: case TYPES.RECEIVE_OPTIONS:
state = { state = {
...state, ...state,
...options, ...action.options,
}; };
break; break;
case TYPES.SET_IS_UPDATING: case TYPES.SET_IS_UPDATING:
state = { state = {
...state, ...state,
isUpdating, isUpdating: action.isUpdating,
}; };
break; break;
case TYPES.SET_REQUESTING_ERROR: case TYPES.SET_REQUESTING_ERROR:
state = { state = {
...state, ...state,
requestingErrors: { requestingErrors: {
[ name ]: error, [ action.name ]: action.error,
}, },
}; };
break; break;
case TYPES.SET_UPDATING_ERROR: case TYPES.SET_UPDATING_ERROR:
state = { state = {
...state, ...state,
error, error: action.error,
updatingError: error, updatingError: action.error,
isUpdating: false, isUpdating: false,
}; };
break; break;
@ -40,4 +48,5 @@ const optionsReducer = (
return state; return state;
}; };
export type State = ReturnType< typeof optionsReducer >;
export default optionsReducer; export default optionsReducer;

View File

@ -3,15 +3,16 @@
*/ */
import { receiveOptions, setRequestingError } from './actions'; import { receiveOptions, setRequestingError } from './actions';
import { batchFetch } from './controls'; import { batchFetch } from './controls';
import { Options } from './types';
/** /**
* Request an option value. * Request an option value.
* *
* @param {string} name - Option name * @param {string} name - Option name
*/ */
export function* getOption( name ) { export function* getOption( name: string ) {
try { try {
const result = yield batchFetch( name ); const result: Options = yield batchFetch( name );
yield receiveOptions( result ); yield receiveOptions( result );
} catch ( error ) { } catch ( error ) {
yield setRequestingError( error, name ); yield setRequestingError( error, name );

View File

@ -1,10 +1,15 @@
/**
* Internal dependencies
*/
import { OptionsState } from './types';
/** /**
* Get option from state tree. * Get option from state tree.
* *
* @param {Object} state - Reducer state * @param {Object} state - Reducer state
* @param {Array} name - Option name * @param {Array} name - Option name
*/ */
export const getOption = ( state, name ) => { export const getOption = ( state: OptionsState, name: string ) => {
return state[ name ]; return state[ name ];
}; };
@ -14,7 +19,10 @@ export const getOption = ( state, name ) => {
* @param {Object} state - Reducer state * @param {Object} state - Reducer state
* @param {string} name - Option name * @param {string} name - Option name
*/ */
export const getOptionsRequestingError = ( state, name ) => { export const getOptionsRequestingError = (
state: OptionsState,
name: string
) => {
return state.requestingErrors[ name ] || false; return state.requestingErrors[ name ] || false;
}; };
@ -23,7 +31,7 @@ export const getOptionsRequestingError = ( state, name ) => {
* *
* @param {Object} state - Reducer state * @param {Object} state - Reducer state
*/ */
export const isOptionsUpdating = ( state ) => { export const isOptionsUpdating = ( state: OptionsState ) => {
return state.isUpdating || false; return state.isUpdating || false;
}; };
@ -32,6 +40,6 @@ export const isOptionsUpdating = ( state ) => {
* *
* @param {Object} state - Reducer state * @param {Object} state - Reducer state
*/ */
export const getOptionsUpdatingError = ( state ) => { export const getOptionsUpdatingError = ( state: OptionsState ) => {
return state.updatingError || false; return state.updatingError || false;
}; };

View File

@ -12,6 +12,7 @@ const defaultState = { isUpdating: false, requestingErrors: {} };
describe( 'options reducer', () => { describe( 'options reducer', () => {
it( 'should return a default state', () => { it( 'should return a default state', () => {
// @ts-expect-error reducer action should not be empty but it is
const state = reducer( undefined, {} ); const state = reducer( undefined, {} );
expect( state ).toEqual( defaultState ); expect( state ).toEqual( defaultState );
expect( state ).not.toBe( defaultState ); expect( state ).not.toBe( defaultState );

View File

@ -39,7 +39,7 @@ describe( 'withOptionsHydration', () => {
const startResolutionMock = jest.fn(); const startResolutionMock = jest.fn();
const receiveOptionsMock = jest.fn(); const receiveOptionsMock = jest.fn();
beforeEach( () => { beforeEach( () => {
useSelect.mockImplementation( ( callback ) => { ( useSelect as jest.Mock ).mockImplementation( ( callback ) => {
callback( callback(
() => ( { () => ( {
isResolving: isResolvingMock, isResolving: isResolvingMock,

View File

@ -17,3 +17,18 @@ export type OptionsSelectors = {
isOptionsUpdating: WPDataSelector< typeof isOptionsUpdating >; isOptionsUpdating: WPDataSelector< typeof isOptionsUpdating >;
getOptionsUpdatingError: WPDataSelector< typeof getOptionsUpdatingError >; getOptionsUpdatingError: WPDataSelector< typeof getOptionsUpdatingError >;
} & WPDataSelectors; } & WPDataSelectors;
export type Options = {
[ key: string ]: unknown;
};
export type OptionsState = {
isUpdating: boolean;
requestingErrors:
| {
[ name: string ]: unknown;
}
| Record< string, never >;
error?: unknown;
updatingError?: unknown;
} & Options;

View File

@ -2,18 +2,20 @@
* External dependencies * External dependencies
*/ */
import { createHigherOrderComponent } from '@wordpress/compose'; import { createHigherOrderComponent } from '@wordpress/compose';
import { useSelect } from '@wordpress/data'; import { useSelect, select as WPSelect } from '@wordpress/data';
import { createElement, useRef } from '@wordpress/element'; import { createElement, useRef } from '@wordpress/element';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { STORE_NAME } from './constants'; import { STORE_NAME } from './constants';
import { Options } from './types';
export const useOptionsHydration = ( data ) => { export const useOptionsHydration = ( data: Options ) => {
const dataRef = useRef( data ); 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 ) { if ( ! dataRef.current ) {
return; return;
} }
@ -39,8 +41,8 @@ export const useOptionsHydration = ( data ) => {
}, [] ); }, [] );
}; };
export const withOptionsHydration = ( data ) => export const withOptionsHydration = ( data: Options ) =>
createHigherOrderComponent( createHigherOrderComponent< Record< string, unknown > >(
( OriginalComponent ) => ( props ) => { ( OriginalComponent ) => ( props ) => {
useOptionsHydration( data ); useOptionsHydration( data );