Migrate woo.data export & import store to TS

This commit is contained in:
Chi-Hsuan Huang 2022-05-23 16:51:27 +08:00
parent 19a3e2c892
commit b8d79d86cf
21 changed files with 313 additions and 102 deletions

View File

@ -48,6 +48,7 @@
"@babel/runtime": "^7.17.2",
"@testing-library/react": "^12.1.3",
"@testing-library/react-hooks": "^7.0.2",
"@types/md5": "^2.3.2",
"@types/wordpress__compose": "^4.0.1",
"@types/wordpress__core-data": "^2.4.5",
"@types/wordpress__data": "^6.0.0",

View File

@ -2,20 +2,26 @@
* External dependencies
*/
import { controls as dataControls } from '@wordpress/data-controls';
import { Action } from '@wordpress/data';
import apiFetch, { APIFetchOptions } from '@wordpress/api-fetch';
import apiFetch from '@wordpress/api-fetch';
export const fetchWithHeaders = ( options ) => {
export const fetchWithHeaders = ( options: APIFetchOptions ) => {
return {
type: 'FETCH_WITH_HEADERS',
options,
};
};
export type FetchWithHeadersResponse< Data > = {
headers: Response[ 'headers' ];
status: Response[ 'status' ];
data: Data;
};
const controls = {
...dataControls,
FETCH_WITH_HEADERS( { options } ) {
return apiFetch( { ...options, parse: false } )
FETCH_WITH_HEADERS( action: Action ) {
return apiFetch< Response >( { ...action.options, parse: false } )
.then( ( response ) => {
return Promise.all( [
response.headers,

View File

@ -1,8 +1,8 @@
const TYPES = {
START_EXPORT: 'START_EXPORT',
SET_EXPORT_ID: 'SET_EXPORT_ID',
SET_ERROR: 'SET_ERROR',
SET_IS_REQUESTING: 'SET_IS_REQUESTING',
START_EXPORT: 'START_EXPORT' as const,
SET_EXPORT_ID: 'SET_EXPORT_ID' as const,
SET_ERROR: 'SET_ERROR' as const,
SET_IS_REQUESTING: 'SET_IS_REQUESTING' as const,
};
export default TYPES;

View File

@ -1,11 +1,16 @@
/**
* Internal dependencies
*/
import { fetchWithHeaders } from '../controls';
import { fetchWithHeaders, FetchWithHeadersResponse } from '../controls';
import TYPES from './action-types';
import { NAMESPACE } from '../constants';
import { SelectorArgs, ExportArgs } from './types';
export function setExportId( exportType, exportArgs, exportId ) {
export function setExportId(
exportType: string,
exportArgs: ExportArgs,
exportId: string
) {
return {
type: TYPES.SET_EXPORT_ID,
exportType,
@ -14,7 +19,11 @@ export function setExportId( exportType, exportArgs, exportId ) {
};
}
export function setIsRequesting( selector, selectorArgs, isRequesting ) {
export function setIsRequesting(
selector: string,
selectorArgs: SelectorArgs,
isRequesting: boolean
) {
return {
type: TYPES.SET_IS_REQUESTING,
selector,
@ -23,7 +32,11 @@ export function setIsRequesting( selector, selectorArgs, isRequesting ) {
};
}
export function setError( selector, selectorArgs, error ) {
export function setError(
selector: string,
selectorArgs: SelectorArgs,
error: unknown
) {
return {
type: TYPES.SET_ERROR,
selector,
@ -32,11 +45,14 @@ export function setError( selector, selectorArgs, error ) {
};
}
export function* startExport( type, args ) {
export function* startExport( type: string, args: ExportArgs ) {
yield setIsRequesting( 'startExport', { type, args }, true );
try {
const response = yield fetchWithHeaders( {
const response: FetchWithHeadersResponse< {
export_id: string;
message: string;
} > = yield fetchWithHeaders( {
path: `${ NAMESPACE }/reports/${ type }/export`,
method: 'POST',
data: {
@ -46,7 +62,6 @@ export function* startExport( type, args ) {
} );
yield setIsRequesting( 'startExport', { type, args }, false );
const { export_id: exportId, message } = response.data;
if ( exportId ) {
@ -57,8 +72,18 @@ export function* startExport( type, args ) {
return response.data;
} catch ( error ) {
yield setError( 'startExport', { type, args }, error.message );
if ( error instanceof Error ) {
yield setError( 'startExport', { type, args }, error.message );
} else {
// eslint-disable-next-line no-console
console.error( `Unexpected Error: ${ JSON.stringify( error ) }` );
// eslint-enable-next-line no-console
}
yield setIsRequesting( 'startExport', { type, args }, false );
throw error;
}
}
export type Action = ReturnType<
typeof setExportId | typeof setError | typeof setIsRequesting
>;

View File

@ -1,4 +1,4 @@
/**
* Internal dependencies
*/
export const STORE_NAME = 'wc/admin/export';
export const STORE_NAME = 'wc/admin/export' as const;

View File

@ -1,23 +1,36 @@
/**
* External dependencies
*/
import { registerStore } from '@wordpress/data';
import { controls } from '@wordpress/data-controls';
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 controls from '../controls';
import reducer from './reducer';
import reducer, { State } from './reducer';
import { WPDataSelectors } from '../types';
export * from './types';
export type { State };
registerStore( STORE_NAME, {
reducer,
registerStore< State >( STORE_NAME, {
reducer: reducer as Reducer< State, AnyAction >,
actions,
controls,
selectors,
} );
export const EXPORT_STORE_NAME = STORE_NAME;
declare module '@wordpress/data' {
// TODO: convert action.js to TS
function dispatch(
key: typeof STORE_NAME
): DispatchFromMap< typeof actions >;
function select(
key: typeof STORE_NAME
): SelectFromMap< typeof selectors > & WPDataSelectors;
}

View File

@ -1,40 +1,42 @@
/**
* External dependencies
*/
import type { Reducer } from 'redux';
/**
* Internal dependencies
*/
import TYPES from './action-types';
import { Action } from './actions';
import { hashExportArgs } from './utils';
import { ExportState } from './types';
const exportReducer = (
const reducer: Reducer< ExportState, Action > = (
state = {
errors: {},
requesting: {},
exportMeta: {},
exportIds: {},
},
{
error,
exportArgs,
exportId,
exportType,
isRequesting,
selector,
selectorArgs,
type,
}
action
) => {
switch ( type ) {
switch ( action.type ) {
case TYPES.SET_IS_REQUESTING:
return {
...state,
requesting: {
...state.requesting,
[ selector ]: {
...state.requesting[ selector ],
[ hashExportArgs( selectorArgs ) ]: isRequesting,
[ action.selector ]: {
...state.requesting[ action.selector ],
[ hashExportArgs(
action.selectorArgs
) ]: action.isRequesting,
},
},
};
case TYPES.SET_EXPORT_ID:
const { exportType, exportArgs, exportId } = action;
return {
...state,
exportMeta: {
@ -60,9 +62,9 @@ const exportReducer = (
...state,
errors: {
...state.errors,
[ selector ]: {
...state.errors[ selector ],
[ hashExportArgs( selectorArgs ) ]: error,
[ action.selector ]: {
...state.errors[ action.selector ],
[ hashExportArgs( action.selectorArgs ) ]: action.error,
},
},
};
@ -71,4 +73,5 @@ const exportReducer = (
}
};
export default exportReducer;
export type State = ReturnType< typeof reducer >;
export default reducer;

View File

@ -2,22 +2,35 @@
* Internal dependencies
*/
import { hashExportArgs } from './utils';
import { ExportState, SelectorArgs, ExportArgs } from './types';
export const isExportRequesting = ( state, selector, selectorArgs ) => {
export const isExportRequesting = (
state: ExportState,
selector: string,
selectorArgs: SelectorArgs
) => {
return Boolean(
state.requesting[ selector ] &&
state.requesting[ selector ][ hashExportArgs( selectorArgs ) ]
);
};
export const getExportId = ( state, exportType, exportArgs ) => {
export const getExportId = (
state: ExportState,
exportType: string,
exportArgs: ExportArgs
) => {
return (
state.exportIds[ exportType ] &&
state.exportIds[ exportType ][ hashExportArgs( exportArgs ) ]
);
};
export const getError = ( state, selector, selectorArgs ) => {
export const getError = (
state: ExportState,
selector: string,
selectorArgs: SelectorArgs
) => {
return (
state.errors[ selector ] &&
state.errors[ selector ][ hashExportArgs( selectorArgs ) ]

View File

@ -18,6 +18,7 @@ const defaultState = {
describe( 'export 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

@ -0,0 +1,32 @@
export type ExportArgs = {
[ key: string ]: unknown;
};
export type SelectorArgs = {
type: string;
args: ExportArgs;
};
export type ExportState = {
errors: {
[ selector: string ]: {
[ hashExportArgs: string ]: unknown;
};
};
requesting: {
[ selector: string ]: {
[ hashExportArgs: string ]: boolean;
};
};
exportMeta: {
[ exportId: string ]: {
exportType: string;
exportArgs: ExportArgs;
};
};
exportIds: {
[ exportType: string ]: {
[ hashExportArgs: string ]: string;
};
};
};

View File

@ -7,7 +7,8 @@ import md5 from 'md5';
* Internal dependencies
*/
import { getResourceName } from '../utils';
import { ExportArgs } from './types';
export const hashExportArgs = ( args ) => {
export const hashExportArgs = ( args: ExportArgs ) => {
return md5( getResourceName( 'export', args ) );
};

View File

@ -1,11 +1,11 @@
const TYPES = {
SET_IMPORT_DATE: 'SET_IMPORT_DATE',
SET_IMPORT_ERROR: 'SET_IMPORT_ERROR',
SET_IMPORT_PERIOD: 'SET_IMPORT_PERIOD',
SET_IMPORT_STARTED: 'SET_IMPORT_STARTED',
SET_IMPORT_STATUS: 'SET_IMPORT_STATUS',
SET_IMPORT_TOTALS: 'SET_IMPORT_TOTALS',
SET_SKIP_IMPORTED: 'SET_SKIP_IMPORTED',
SET_IMPORT_DATE: 'SET_IMPORT_DATE' as const,
SET_IMPORT_ERROR: 'SET_IMPORT_ERROR' as const,
SET_IMPORT_PERIOD: 'SET_IMPORT_PERIOD' as const,
SET_IMPORT_STARTED: 'SET_IMPORT_STARTED' as const,
SET_IMPORT_STATUS: 'SET_IMPORT_STATUS' as const,
SET_IMPORT_TOTALS: 'SET_IMPORT_TOTALS' as const,
SET_SKIP_IMPORTED: 'SET_SKIP_IMPORTED' as const,
};
export default TYPES;

View File

@ -7,15 +7,21 @@ import { apiFetch } from '@wordpress/data-controls';
* Internal dependencies
*/
import TYPES from './action-types';
import {
ImportStatusQuery,
ImportStatus,
ImportTotals,
ImportTotalsQuery,
} from './types';
export function setImportStarted( activeImport ) {
export function setImportStarted( activeImport: boolean ) {
return {
type: TYPES.SET_IMPORT_STARTED,
activeImport,
};
}
export function setImportPeriod( date, dateModified ) {
export function setImportPeriod( date: string, dateModified: boolean ) {
if ( ! dateModified ) {
return {
type: TYPES.SET_IMPORT_PERIOD,
@ -28,14 +34,17 @@ export function setImportPeriod( date, dateModified ) {
};
}
export function setSkipPrevious( skipPrevious ) {
export function setSkipPrevious( skipPrevious: boolean ) {
return {
type: TYPES.SET_SKIP_IMPORTED,
skipPrevious,
};
}
export function setImportStatus( query, importStatus ) {
export function setImportStatus(
query: ImportStatusQuery,
importStatus: ImportStatus
) {
return {
type: TYPES.SET_IMPORT_STATUS,
importStatus,
@ -43,7 +52,10 @@ export function setImportStatus( query, importStatus ) {
};
}
export function setImportTotals( query, importTotals ) {
export function setImportTotals(
query: ImportTotalsQuery,
importTotals: ImportTotals
) {
return {
type: TYPES.SET_IMPORT_TOTALS,
importTotals,
@ -51,21 +63,33 @@ export function setImportTotals( query, importTotals ) {
};
}
export function setImportError( query, error ) {
export function setImportError(
queryOrPath: ImportStatusQuery | ImportTotalsQuery | string,
error: unknown
) {
return {
type: TYPES.SET_IMPORT_ERROR,
error,
query,
query: queryOrPath,
};
}
export function* updateImportation( path, importStarted = false ) {
export function* updateImportation( path: string, importStarted = false ) {
yield setImportStarted( importStarted );
try {
const response = yield apiFetch( { path, method: 'POST' } );
const response: unknown = yield apiFetch( { path, method: 'POST' } );
return response;
} catch ( error ) {
yield setImportError( path, error );
throw error;
}
}
export type Action = ReturnType<
| typeof setImportStarted
| typeof setImportPeriod
| typeof setImportStatus
| typeof setImportTotals
| typeof setImportError
| typeof setSkipPrevious
>;

View File

@ -1,4 +1,4 @@
/**
* Internal dependencies
*/
export const STORE_NAME = 'wc/admin/import';
export const STORE_NAME = 'wc/admin/import' as const;

View File

@ -1,25 +1,36 @@
/**
* External dependencies
*/
import { registerStore } from '@wordpress/data';
import { controls } from '@wordpress/data-controls';
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 from './reducer';
import reducer, { State } from './reducer';
import { WPDataSelectors } from '../types';
export * from './types';
export type { State };
registerStore( STORE_NAME, {
reducer,
registerStore< State >( STORE_NAME, {
reducer: reducer as Reducer< State, AnyAction >,
actions,
controls,
selectors,
resolvers,
} );
export const IMPORT_STORE_NAME = STORE_NAME;
declare module '@wordpress/data' {
// TODO: convert action.js to TS
function dispatch(
key: typeof STORE_NAME
): DispatchFromMap< typeof actions >;
function select(
key: typeof STORE_NAME
): SelectFromMap< typeof selectors > & WPDataSelectors;
}

View File

@ -3,13 +3,16 @@
*/
import { __ } from '@wordpress/i18n';
import moment from 'moment';
import type { Reducer } from 'redux';
/**
* Internal dependencies
*/
import TYPES from './action-types';
import { ImportState } from './types';
import { Action } from './actions';
const reducer = (
const reducer: Reducer< ImportState, Action > = (
state = {
activeImport: false,
importStatus: {},
@ -22,19 +25,11 @@ const reducer = (
},
skipPrevious: true,
},
{
type,
query,
importStatus,
importTotals,
activeImport,
date,
error,
skipPrevious,
}
action
) => {
switch ( type ) {
switch ( action.type ) {
case TYPES.SET_IMPORT_STARTED:
const { activeImport } = action;
state = {
...state,
activeImport,
@ -48,7 +43,7 @@ const reducer = (
...state,
period: {
...state.period,
label: date,
label: action.date,
},
activeImport: false,
};
@ -57,7 +52,7 @@ const reducer = (
state = {
...state,
period: {
date,
date: action.date,
label: 'custom',
},
activeImport: false,
@ -66,11 +61,12 @@ const reducer = (
case TYPES.SET_SKIP_IMPORTED:
state = {
...state,
skipPrevious,
skipPrevious: action.skipPrevious,
activeImport: false,
};
break;
case TYPES.SET_IMPORT_STATUS:
const { query, importStatus } = action;
state = {
...state,
importStatus: {
@ -88,7 +84,7 @@ const reducer = (
...state,
importTotals: {
...state.importTotals,
[ JSON.stringify( query ) ]: importTotals,
[ JSON.stringify( action.query ) ]: action.importTotals,
},
};
break;
@ -97,7 +93,7 @@ const reducer = (
...state,
errors: {
...state.errors,
[ JSON.stringify( query ) ]: error,
[ JSON.stringify( action.query ) ]: action.error,
},
};
break;
@ -105,4 +101,5 @@ const reducer = (
return state;
};
export type State = ReturnType< typeof reducer >;
export default reducer;

View File

@ -10,27 +10,33 @@ import { omit } from 'lodash';
*/
import { NAMESPACE } from '../constants';
import { setImportError, setImportStatus, setImportTotals } from './actions';
import {
ImportStatusQuery,
ImportTotalsQuery,
ImportStatus,
ImportTotals,
} from './types';
export function* getImportStatus( query ) {
export function* getImportStatus( query: ImportStatusQuery ) {
try {
const url = addQueryArgs(
`${ NAMESPACE }/reports/import/status`,
omit( query, [ 'timestamp' ] )
typeof query === 'object' ? omit( query, [ 'timestamp' ] ) : {}
);
const response = yield apiFetch( { path: url } );
const response: ImportStatus = yield apiFetch( { path: url } );
yield setImportStatus( query, response );
} catch ( error ) {
yield setImportError( query, error );
}
}
export function* getImportTotals( query ) {
export function* getImportTotals( query: ImportTotalsQuery ) {
try {
const url = addQueryArgs(
`${ NAMESPACE }/reports/import/totals`,
query
);
const response = yield apiFetch( { path: url } );
const response: ImportTotals = yield apiFetch( { path: url } );
yield setImportTotals( query, response );
} catch ( error ) {
yield setImportError( query, error );

View File

@ -1,19 +1,31 @@
export const getImportStarted = ( state ) => {
/**
* Internal dependencies
*/
import { ImportState, ImportStatusQuery, ImportTotalsQuery } from './types';
export const getImportStarted = ( state: ImportState ) => {
const { activeImport, lastImportStartTimestamp } = state;
return { activeImport, lastImportStartTimestamp } || {};
};
export const getFormSettings = ( state ) => {
export const getFormSettings = ( state: ImportState ) => {
const { period, skipPrevious } = state;
return { period, skipPrevious } || {};
};
export const getImportStatus = ( state, query ) => {
export const getImportStatus = (
state: ImportState,
query: ImportStatusQuery
) => {
const stringifiedQuery = JSON.stringify( query );
return state.importStatus[ stringifiedQuery ] || {};
};
export const getImportTotals = ( state, query ) => {
export const getImportTotals = (
state: ImportState,
query: ImportTotalsQuery
) => {
const { importTotals, lastImportStartTimestamp } = state;
const stringifiedQuery = JSON.stringify( query );
return (
@ -24,7 +36,10 @@ export const getImportTotals = ( state, query ) => {
);
};
export const getImportError = ( state, query ) => {
export const getImportError = (
state: ImportState,
query: ImportTotalsQuery | ImportStatusQuery | string
) => {
const stringifiedQuery = JSON.stringify( query );
return state.errors[ stringifiedQuery ] || false;
};

View File

@ -24,6 +24,7 @@ const defaultState = {
describe( 'import 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 );
@ -113,7 +114,10 @@ describe( 'import reducer', () => {
error: { code: 'error' },
} );
const stringifiedQuery = JSON.stringify( query );
expect( state.errors[ stringifiedQuery ].code ).toBe( 'error' );
expect(
( state.errors[ stringifiedQuery ] as {
code: string;
} ).code
).toBe( 'error' );
} );
} );

View File

@ -0,0 +1,52 @@
type SchedulerName = 'orders' | 'customers';
type isImporting = {
is_importing: boolean;
};
type SchedulerImportStatus = {
[ schedulerName in SchedulerName ]: {
imported: number;
totals: number;
};
} & {
imported_from?: string | number;
};
export type ImportStatus =
| isImporting
| ( isImporting & SchedulerImportStatus );
export type ImportStatusQuery = number;
export type ImportTotals = {
[ schedulerName in SchedulerName ]: number;
};
export type ImportTotalsQuery = {
skip_existing: boolean;
days: number;
};
export type ImportState = {
activeImport: boolean;
importStatus:
| Record< string, never >
| {
[ queryString: string ]: ImportStatus;
};
importTotals:
| Record< string, never >
| {
[ queryString: string ]: ImportTotals;
};
errors: {
[ queryString: string ]: unknown;
};
lastImportStartTimestamp: number;
period: {
date: string;
label: string;
};
skipPrevious: boolean;
};

View File

@ -485,6 +485,7 @@ importers:
'@babel/runtime': ^7.17.2
'@testing-library/react': ^12.1.3
'@testing-library/react-hooks': ^7.0.2
'@types/md5': ^2.3.2
'@types/wordpress__compose': ^4.0.1
'@types/wordpress__core-data': ^2.4.5
'@types/wordpress__data': ^6.0.0
@ -536,6 +537,7 @@ importers:
'@babel/runtime': 7.17.7
'@testing-library/react': 12.1.4
'@testing-library/react-hooks': 7.0.2
'@types/md5': 2.3.2
'@types/wordpress__compose': 4.0.1
'@types/wordpress__core-data': 2.4.5
'@types/wordpress__data': 6.0.0
@ -13418,6 +13420,10 @@ packages:
/@types/lodash/4.14.180:
resolution: {integrity: sha512-XOKXa1KIxtNXgASAnwj7cnttJxS4fksBRywK/9LzRV5YxrF80BXZIGeQSuoESQ/VkUj30Ae0+YcuHc15wJCB2g==}
/@types/md5/2.3.2:
resolution: {integrity: sha512-v+JFDu96+UYJ3/UWzB0mEglIS//MZXgRaJ4ubUPwOM0gvLc/kcQ3TWNYwENEK7/EcXGQVrW8h/XqednSjBd/Og==}
dev: true
/@types/mdast/3.0.10:
resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==}
dependencies:
@ -13711,6 +13717,7 @@ packages:
re-resizable: 4.11.0
transitivePeerDependencies:
- react
- react-dom
dev: true
/@types/wordpress__compose/4.0.1:
@ -18577,7 +18584,7 @@ packages:
dev: true
/buffer-crc32/0.2.13:
resolution: {integrity: sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=}
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
/buffer-fill/1.0.0:
resolution: {integrity: sha1-+PeLdniYiO858gXNY39o5wISKyw=}
@ -39221,7 +39228,7 @@ packages:
tapable: 1.1.3
terser-webpack-plugin: 1.4.5_webpack@4.46.0
watchpack: 1.7.5
webpack-cli: 3.3.12_webpack@5.70.0
webpack-cli: 3.3.12_webpack@4.46.0
webpack-sources: 1.4.3
dev: true