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

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
*/
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 );
} );

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
*/
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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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