Migrate export data store to wp.data (https://github.com/woocommerce/woocommerce-admin/pull/4958)
* Add export ID to success response and fix typos in endpoint schema. * Initial export data store. Allows for starting export, but not checking progress. * Use new export data store in analytics report tables. * Remove defunct report export wc-api files. * Apply review feedback from another wp.data PR. * Use getResourceName() util for more DRYness. * Fix linter error. * Add response status code to fetchWithHeaders control. * Use HTTP status rather than response body to determine success. * Fix tests. * Remove unused import from actions.
This commit is contained in:
parent
df89bb7aac
commit
c16da34c30
|
@ -8,7 +8,7 @@ import { compose } from '@wordpress/compose';
|
|||
import { focus } from '@wordpress/dom';
|
||||
import { withDispatch } from '@wordpress/data';
|
||||
import { get, noop, partial, uniq } from 'lodash';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CompareButton, Search, TableCard } from '@woocommerce/components';
|
||||
|
@ -26,6 +26,7 @@ import {
|
|||
import {
|
||||
getReportChartData,
|
||||
getReportTableData,
|
||||
EXPORT_STORE_NAME,
|
||||
SETTINGS_STORE_NAME,
|
||||
useUserPreferences,
|
||||
} from '@woocommerce/data';
|
||||
|
@ -152,7 +153,7 @@ const ReportTable = ( props ) => {
|
|||
};
|
||||
|
||||
const onClickDownload = () => {
|
||||
const { initiateReportExport, title } = props;
|
||||
const { createNotice, startExport, title } = props;
|
||||
const params = Object.assign( {}, query );
|
||||
const { data, totalResults } = items;
|
||||
let downloadType = 'browser';
|
||||
|
@ -173,7 +174,34 @@ const ReportTable = ( props ) => {
|
|||
);
|
||||
} else {
|
||||
downloadType = 'email';
|
||||
initiateReportExport( endpoint, title, reportQuery );
|
||||
startExport( endpoint, reportQuery )
|
||||
.then( () =>
|
||||
createNotice(
|
||||
'success',
|
||||
sprintf(
|
||||
/* translators: %s = type of report */
|
||||
__(
|
||||
'Your %s Report will be emailed to you.',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
title
|
||||
)
|
||||
)
|
||||
)
|
||||
.catch( ( error ) =>
|
||||
createNotice(
|
||||
'error',
|
||||
error.message ||
|
||||
sprintf(
|
||||
/* translators: %s = type of report */
|
||||
__(
|
||||
'There was a problem exporting your %s Report. Please try again.',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
title
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
recordEvent( 'analytics_table_download', {
|
||||
|
@ -600,10 +628,12 @@ export default compose(
|
|||
};
|
||||
} ),
|
||||
withDispatch( ( dispatch ) => {
|
||||
const { initiateReportExport } = dispatch( 'wc-api' );
|
||||
const { startExport } = dispatch( EXPORT_STORE_NAME );
|
||||
const { createNotice } = dispatch( 'core/notices' );
|
||||
|
||||
return {
|
||||
initiateReportExport,
|
||||
createNotice,
|
||||
startExport,
|
||||
};
|
||||
} )
|
||||
)( ReportTable );
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import mutations from './mutations';
|
||||
import operations from './operations';
|
||||
|
||||
export default {
|
||||
mutations,
|
||||
operations,
|
||||
};
|
|
@ -1,58 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { dispatch } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getResourceName } from '../utils';
|
||||
|
||||
const initiateReportExport = ( operations ) => async (
|
||||
reportType,
|
||||
reportTitle,
|
||||
reportArgs
|
||||
) => {
|
||||
const { createNotice } = dispatch( 'core/notices' );
|
||||
|
||||
const resourceName = getResourceName(
|
||||
`report-export-${ reportType }`,
|
||||
reportArgs
|
||||
);
|
||||
|
||||
const result = await operations.update( [ resourceName ], {
|
||||
[ resourceName ]: reportArgs,
|
||||
} );
|
||||
|
||||
const response = result[ 0 ][ resourceName ];
|
||||
|
||||
if ( response && response.success ) {
|
||||
createNotice(
|
||||
'success',
|
||||
sprintf(
|
||||
__(
|
||||
'Your %s Report will be emailed to you.',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
reportTitle
|
||||
)
|
||||
);
|
||||
}
|
||||
if ( response && response.error ) {
|
||||
createNotice(
|
||||
'error',
|
||||
sprintf(
|
||||
__(
|
||||
'There was a problem exporting your %s Report. Please try again.',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
reportTitle
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
initiateReportExport,
|
||||
};
|
|
@ -1,47 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getResourcePrefix } from '../utils';
|
||||
import { NAMESPACE } from '../constants';
|
||||
|
||||
function update( resourceNames, data, fetch = apiFetch ) {
|
||||
return [ ...initiateExport( resourceNames, data, fetch ) ];
|
||||
}
|
||||
|
||||
function initiateExport( resourceNames, data, fetch ) {
|
||||
const filteredNames = resourceNames.filter( ( name ) => {
|
||||
return name.startsWith( 'report-export-' );
|
||||
} );
|
||||
|
||||
return filteredNames.map( async ( resourceName ) => {
|
||||
const prefix = getResourcePrefix( resourceName );
|
||||
const reportType = prefix.split( '-' ).pop();
|
||||
const url = NAMESPACE + '/reports/' + reportType + '/export';
|
||||
|
||||
try {
|
||||
const result = await fetch( {
|
||||
path: url,
|
||||
method: 'POST',
|
||||
data: {
|
||||
report_args: data[ resourceName ],
|
||||
email: true,
|
||||
},
|
||||
} );
|
||||
|
||||
return {
|
||||
[ resourceName ]: { [ result.status ]: result.message },
|
||||
};
|
||||
} catch ( error ) {
|
||||
return { [ resourceName ]: { error } };
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
export default {
|
||||
update,
|
||||
};
|
|
@ -1,7 +1,6 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import reportExport from './export';
|
||||
import items from './items';
|
||||
import imports from './imports';
|
||||
|
||||
|
@ -9,7 +8,6 @@ function createWcApiSpec() {
|
|||
return {
|
||||
name: 'wcApi',
|
||||
mutations: {
|
||||
...reportExport.mutations,
|
||||
...items.mutations,
|
||||
},
|
||||
selectors: {
|
||||
|
@ -29,10 +27,7 @@ function createWcApiSpec() {
|
|||
];
|
||||
},
|
||||
update( resourceNames, data ) {
|
||||
return [
|
||||
...reportExport.operations.update( resourceNames, data ),
|
||||
...items.operations.update( resourceNames, data ),
|
||||
];
|
||||
return [ ...items.operations.update( resourceNames, data ) ];
|
||||
},
|
||||
updateLocally( resourceNames, data ) {
|
||||
return [
|
||||
|
|
|
@ -17,9 +17,17 @@ const controls = {
|
|||
FETCH_WITH_HEADERS( { options } ) {
|
||||
return apiFetch( { ...options, parse: false } )
|
||||
.then( ( response ) => {
|
||||
return Promise.all( [ response.headers, response.json() ] );
|
||||
return Promise.all( [
|
||||
response.headers,
|
||||
response.status,
|
||||
response.json(),
|
||||
] );
|
||||
} )
|
||||
.then( ( [ headers, data ] ) => ( { headers, data } ) );
|
||||
.then( ( [ headers, status, data ] ) => ( {
|
||||
headers,
|
||||
status,
|
||||
data,
|
||||
} ) );
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
const TYPES = {
|
||||
START_EXPORT: 'START_EXPORT',
|
||||
SET_EXPORT_ID: 'SET_EXPORT_ID',
|
||||
SET_ERROR: 'SET_ERROR',
|
||||
SET_IS_REQUESTING: 'SET_IS_REQUESTING',
|
||||
};
|
||||
|
||||
export default TYPES;
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { fetchWithHeaders } from '../controls';
|
||||
import TYPES from './action-types';
|
||||
import { NAMESPACE } from '../constants';
|
||||
|
||||
export function setExportId( exportType, exportArgs, exportId ) {
|
||||
return {
|
||||
type: TYPES.SET_EXPORT_ID,
|
||||
exportType,
|
||||
exportArgs,
|
||||
exportId,
|
||||
};
|
||||
}
|
||||
|
||||
export function setIsRequesting( selector, selectorArgs, isRequesting ) {
|
||||
return {
|
||||
type: TYPES.SET_IS_REQUESTING,
|
||||
selector,
|
||||
selectorArgs,
|
||||
isRequesting,
|
||||
};
|
||||
}
|
||||
|
||||
export function setError( selector, selectorArgs, error ) {
|
||||
return {
|
||||
type: TYPES.SET_ERROR,
|
||||
selector,
|
||||
selectorArgs,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function* startExport( type, args ) {
|
||||
yield setIsRequesting( 'startExport', { type, args }, true );
|
||||
|
||||
try {
|
||||
const response = yield fetchWithHeaders( {
|
||||
path: `${ NAMESPACE }/reports/${ type }/export`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
report_args: args,
|
||||
email: true,
|
||||
},
|
||||
} );
|
||||
|
||||
yield setIsRequesting( 'startExport', { type, args }, false );
|
||||
|
||||
const { export_id: exportId, message } = response.data;
|
||||
|
||||
if ( exportId ) {
|
||||
yield setExportId( type, args, exportId );
|
||||
} else {
|
||||
throw new Error( message );
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch ( error ) {
|
||||
yield setError( 'startExport', { type, args }, error.message );
|
||||
yield setIsRequesting( 'startExport', { type, args }, false );
|
||||
throw error;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
export const STORE_NAME = 'wc/admin/export';
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
|
||||
import { select, registerStore } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* 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';
|
||||
|
||||
const storeSelectors = select( STORE_NAME );
|
||||
|
||||
// @todo This is used to prevent double registration of the store due to webpack chunks.
|
||||
// The `storeSelectors` condition can be removed once this is fixed.
|
||||
if ( ! storeSelectors ) {
|
||||
registerStore( STORE_NAME, {
|
||||
reducer,
|
||||
actions,
|
||||
controls,
|
||||
selectors,
|
||||
} );
|
||||
}
|
||||
|
||||
export const EXPORT_STORE_NAME = STORE_NAME;
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import TYPES from './action-types';
|
||||
import { hashExportArgs } from './utils';
|
||||
|
||||
const exportReducer = (
|
||||
state = {
|
||||
errors: {},
|
||||
requesting: {},
|
||||
exportMeta: {},
|
||||
exportIds: {},
|
||||
},
|
||||
{
|
||||
error,
|
||||
exportArgs,
|
||||
exportId,
|
||||
exportType,
|
||||
isRequesting,
|
||||
selector,
|
||||
selectorArgs,
|
||||
type,
|
||||
}
|
||||
) => {
|
||||
switch ( type ) {
|
||||
case TYPES.SET_IS_REQUESTING:
|
||||
return {
|
||||
...state,
|
||||
requesting: {
|
||||
...state.requesting,
|
||||
[ selector ]: {
|
||||
...state.requesting[ selector ],
|
||||
[ hashExportArgs( selectorArgs ) ]: isRequesting,
|
||||
},
|
||||
},
|
||||
};
|
||||
case TYPES.SET_EXPORT_ID:
|
||||
return {
|
||||
...state,
|
||||
exportMeta: {
|
||||
...state.exportMeta,
|
||||
[ exportId ]: {
|
||||
exportType,
|
||||
exportArgs,
|
||||
},
|
||||
},
|
||||
exportIds: {
|
||||
...state.exportIds,
|
||||
[ exportType ]: {
|
||||
...state.exportIds[ exportType ],
|
||||
[ hashExportArgs( {
|
||||
type: exportType,
|
||||
args: exportArgs,
|
||||
} ) ]: exportId,
|
||||
},
|
||||
},
|
||||
};
|
||||
case TYPES.SET_ERROR:
|
||||
return {
|
||||
...state,
|
||||
errors: {
|
||||
...state.errors,
|
||||
[ selector ]: {
|
||||
...state.errors[ selector ],
|
||||
[ hashExportArgs( selectorArgs ) ]: error,
|
||||
},
|
||||
},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default exportReducer;
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { hashExportArgs } from './utils';
|
||||
|
||||
export const isExportRequesting = ( state, selector, selectorArgs ) => {
|
||||
return Boolean(
|
||||
state.requesting[ selector ] &&
|
||||
state.requesting[ selector ][ hashExportArgs( selectorArgs ) ]
|
||||
);
|
||||
};
|
||||
|
||||
export const getExportId = ( state, exportType, exportArgs ) => {
|
||||
return (
|
||||
state.exportIds[ exportType ] &&
|
||||
state.exportIds[ exportType ][ hashExportArgs( exportArgs ) ]
|
||||
);
|
||||
};
|
||||
|
||||
export const getError = ( state, selector, selectorArgs ) => {
|
||||
return (
|
||||
state.errors[ selector ] &&
|
||||
state.errors[ selector ][ hashExportArgs( selectorArgs ) ]
|
||||
);
|
||||
};
|
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import reducer from '../reducer';
|
||||
import TYPES from '../action-types';
|
||||
import { hashExportArgs } from '../utils';
|
||||
|
||||
const defaultState = {
|
||||
errors: {},
|
||||
requesting: {},
|
||||
exportMeta: {},
|
||||
exportIds: {},
|
||||
};
|
||||
|
||||
describe( 'export reducer', () => {
|
||||
it( 'should return a default state', () => {
|
||||
const state = reducer( undefined, {} );
|
||||
expect( state ).toEqual( defaultState );
|
||||
expect( state ).not.toBe( defaultState );
|
||||
} );
|
||||
|
||||
it( 'should handle SET_IS_REQUESTING', () => {
|
||||
const selectorArgs = {
|
||||
type: 'orders',
|
||||
args: {
|
||||
after: '2020-01-01T00:00:00',
|
||||
before: '2019-12-31T23:59:59',
|
||||
status_is: 'pending',
|
||||
},
|
||||
};
|
||||
const state = reducer( defaultState, {
|
||||
type: TYPES.SET_IS_REQUESTING,
|
||||
selector: 'startExport',
|
||||
selectorArgs,
|
||||
isRequesting: true,
|
||||
} );
|
||||
|
||||
expect(
|
||||
state.requesting.startExport[ hashExportArgs( selectorArgs ) ]
|
||||
).toBe( true );
|
||||
} );
|
||||
|
||||
it( 'should handle SET_EXPORT_ID', () => {
|
||||
const exportType = 'orders';
|
||||
const exportArgs = {
|
||||
after: '2020-01-01T00:00:00',
|
||||
before: '2019-12-31T23:59:59',
|
||||
status_is: 'pending',
|
||||
};
|
||||
const hashArgs = {
|
||||
type: exportType,
|
||||
args: exportArgs,
|
||||
};
|
||||
const exportId = '15967352870671';
|
||||
const state = reducer( defaultState, {
|
||||
type: TYPES.SET_EXPORT_ID,
|
||||
exportType,
|
||||
exportArgs,
|
||||
exportId,
|
||||
} );
|
||||
|
||||
expect(
|
||||
state.exportIds[ exportType ][ hashExportArgs( hashArgs ) ]
|
||||
).toBe( exportId );
|
||||
expect( state.exportMeta[ exportId ] ).toEqual( {
|
||||
exportType,
|
||||
exportArgs,
|
||||
} );
|
||||
} );
|
||||
|
||||
it( 'should handle SET_ERROR', () => {
|
||||
const selectorArgs = {
|
||||
type: 'orders',
|
||||
args: {
|
||||
after: '2020-01-01T00:00:00',
|
||||
before: '2019-12-31T23:59:59',
|
||||
status_is: 'pending',
|
||||
},
|
||||
};
|
||||
const error = 'There is no data to export for the given request.';
|
||||
const state = reducer( defaultState, {
|
||||
type: TYPES.SET_ERROR,
|
||||
selector: 'startExport',
|
||||
selectorArgs,
|
||||
error,
|
||||
} );
|
||||
|
||||
expect(
|
||||
state.errors.startExport[ hashExportArgs( selectorArgs ) ]
|
||||
).toBe( error );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import crypto from 'crypto';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getResourceName } from '../utils';
|
||||
|
||||
export const hashExportArgs = ( args ) => {
|
||||
return crypto
|
||||
.createHash( 'md5' )
|
||||
.update( getResourceName( 'export', args ) )
|
||||
.digest( 'hex' );
|
||||
};
|
|
@ -32,4 +32,6 @@ export {
|
|||
|
||||
export { MAX_PER_PAGE, QUERY_DEFAULTS } from './constants';
|
||||
|
||||
export { EXPORT_STORE_NAME } from './export';
|
||||
|
||||
export { __experimentalResolveSelect } from './registry';
|
||||
|
|
|
@ -96,14 +96,20 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
|
|||
'title' => 'report_export',
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'status' => array(
|
||||
'description' => __( 'Regeneration status.', 'woocommerce-admin' ),
|
||||
'status' => array(
|
||||
'description' => __( 'Export status.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'message' => array(
|
||||
'description' => __( 'Regenerate data message.', 'woocommerce-admin' ),
|
||||
'message' => array(
|
||||
'description' => __( 'Export status message.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'export_id' => array(
|
||||
'description' => __( 'Export ID.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
|
@ -154,37 +160,34 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
|
|||
$report_args = empty( $request['report_args'] ) ? array() : $request['report_args'];
|
||||
$send_email = isset( $request['email'] ) ? $request['email'] : false;
|
||||
$export_id = str_replace( '.', '', microtime( true ) );
|
||||
|
||||
$total_rows = ReportExporter::queue_report_export( $export_id, $report_type, $report_args, $send_email );
|
||||
$total_rows = ReportExporter::queue_report_export( $export_id, $report_type, $report_args, $send_email );
|
||||
|
||||
if ( 0 === $total_rows ) {
|
||||
$response = rest_ensure_response(
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'status' => 'error',
|
||||
'message' => __( 'There is no data to export for the given request.', 'woocommerce-admin' ),
|
||||
)
|
||||
);
|
||||
|
||||
} else {
|
||||
$response = rest_ensure_response(
|
||||
array(
|
||||
'status' => 'success',
|
||||
'message' => __( 'Your report file is being generated.', 'woocommerce-admin' ),
|
||||
)
|
||||
);
|
||||
|
||||
// Include a link to the export status endpoint.
|
||||
$response->add_links(
|
||||
array(
|
||||
'status' => array(
|
||||
'href' => rest_url( sprintf( '%s/reports/%s/export/%s/status', $this->namespace, $report_type, $export_id ) ),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
ReportExporter::update_export_percentage_complete( $report_type, $export_id, 0 );
|
||||
}
|
||||
|
||||
ReportExporter::update_export_percentage_complete( $report_type, $export_id, 0 );
|
||||
|
||||
$response = rest_ensure_response(
|
||||
array(
|
||||
'message' => __( 'Your report file is being generated.', 'woocommerce-admin' ),
|
||||
'export_id' => $export_id,
|
||||
)
|
||||
);
|
||||
|
||||
// Include a link to the export status endpoint.
|
||||
$response->add_links(
|
||||
array(
|
||||
'status' => array(
|
||||
'href' => rest_url( sprintf( '%s/reports/%s/export/%s/status', $this->namespace, $report_type, $export_id ) ),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$data = $this->prepare_response_for_collection( $response );
|
||||
|
||||
return rest_ensure_response( $data );
|
||||
|
|
|
@ -126,7 +126,6 @@ class WC_Tests_API_Reports_Export extends WC_REST_Unit_Test_Case {
|
|||
$status_url = $export['_links']['status'][0]['href'];
|
||||
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertEquals( 'success', $export['status'] );
|
||||
$this->assertEquals( 'Your report file is being generated.', $export['message'] );
|
||||
$this->assertStringMatchesFormat( '%s/wc-analytics/reports/taxes/export/%d/status', $status_url );
|
||||
|
||||
|
|
Loading…
Reference in New Issue