Create action for installing and activate plugins (https://github.com/woocommerce/woocommerce-admin/pull/4473)

* Add installAndActivatePlugins method to plugin store

* Use new install/actiate method in Plugins component

* Refactor benefits page to use await

* Refactor business details page to use new install method

* Replace Plugins component in Jetpack CTA

* Format and throw errors in plugin data store

* Add generic response handling function

* Add default error messages to plugin API
This commit is contained in:
Joshua T Flowers 2020-06-12 09:38:33 +03:00 committed by GitHub
parent 2aa59bc8e6
commit 046f6c8bd4
9 changed files with 358 additions and 324 deletions

View File

@ -6,36 +6,45 @@ import { compose } from '@wordpress/compose';
import { Button } from '@wordpress/components'; import { Button } from '@wordpress/components';
import { useState } from 'react'; import { useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withDispatch, withSelect } from '@wordpress/data';
/** /**
* WooCommerce dependencies * WooCommerce dependencies
*/ */
import { H, Plugins } from '@woocommerce/components'; import { H } from '@woocommerce/components';
import { getAdminLink } from '@woocommerce/wc-admin-settings'; import { getAdminLink } from '@woocommerce/wc-admin-settings';
import { PLUGINS_STORE_NAME, useUserPreferences } from '@woocommerce/data'; import { PLUGINS_STORE_NAME, useUserPreferences } from '@woocommerce/data';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import withSelect from 'wc-api/with-select';
import Connect from 'dashboard/components/connect'; import Connect from 'dashboard/components/connect';
import { recordEvent } from 'lib/tracks'; import { recordEvent } from 'lib/tracks';
import { createNoticesFromResponse } from 'lib/notices';
function InstallJetpackCta( { function InstallJetpackCta( {
isJetpackInstalled, isJetpackInstalled,
isJetpackActivated, isJetpackActivated,
isJetpackConnected, isJetpackConnected,
installAndActivatePlugins,
isInstalling,
} ) { } ) {
const { updateUserPreferences, ...userPrefs } = useUserPreferences(); const { updateUserPreferences, ...userPrefs } = useUserPreferences();
const [ isInstalling, setIsInstalling ] = useState( false );
const [ isConnecting, setIsConnecting ] = useState( false ); const [ isConnecting, setIsConnecting ] = useState( false );
const [ isDismissed, setIsDismissed ] = useState( const [ isDismissed, setIsDismissed ] = useState(
( userPrefs.homepage_stats || {} ).installJetpackDismissed ( userPrefs.homepage_stats || {} ).installJetpackDismissed
); );
function install() { async function install() {
setIsInstalling( true );
recordEvent( 'statsoverview_install_jetpack' ); recordEvent( 'statsoverview_install_jetpack' );
installAndActivatePlugins( [ 'jetpack' ] )
.then( () => {
setIsConnecting( ! isJetpackConnected );
} )
.catch( ( error ) => {
createNoticesFromResponse( error );
} );
} }
function dismiss() { function dismiss() {
@ -53,22 +62,6 @@ function InstallJetpackCta( {
recordEvent( 'statsoverview_dismiss_install_jetpack' ); recordEvent( 'statsoverview_dismiss_install_jetpack' );
} }
function getPluginInstaller() {
return (
<Plugins
autoInstall
onComplete={ () => {
setIsInstalling( false );
setIsConnecting( ! isJetpackConnected );
} }
onError={ () => {
setIsInstalling( false );
} }
pluginSlugs={ [ 'jetpack' ] }
/>
);
}
function getConnector() { function getConnector() {
return ( return (
<Connect <Connect
@ -124,7 +117,6 @@ function InstallJetpackCta( {
</Button> </Button>
</footer> </footer>
{ isInstalling && getPluginInstaller() }
{ isConnecting && getConnector() } { isConnecting && getConnector() }
</article> </article>
); );
@ -141,14 +133,25 @@ export default compose(
withSelect( ( select ) => { withSelect( ( select ) => {
const { const {
isJetpackConnected, isJetpackConnected,
isPluginsRequesting,
getInstalledPlugins, getInstalledPlugins,
getActivePlugins, getActivePlugins,
} = select( PLUGINS_STORE_NAME ); } = select( PLUGINS_STORE_NAME );
return { return {
isInstalling:
isPluginsRequesting( 'installPlugins' ) ||
isPluginsRequesting( 'activatePlugins' ),
isJetpackInstalled: getInstalledPlugins().includes( 'jetpack' ), isJetpackInstalled: getInstalledPlugins().includes( 'jetpack' ),
isJetpackActivated: getActivePlugins().includes( 'jetpack' ), isJetpackActivated: getActivePlugins().includes( 'jetpack' ),
isJetpackConnected: isJetpackConnected(), isJetpackConnected: isJetpackConnected(),
}; };
} ),
withDispatch( ( dispatch ) => {
const { installAndActivatePlugins } = dispatch( PLUGINS_STORE_NAME );
return {
installAndActivatePlugins,
};
} ) } )
)( InstallJetpackCta ); )( InstallJetpackCta );

View File

@ -0,0 +1,18 @@
/**
* External dependencies
*/
import { dispatch } from '@wordpress/data';
export function createNoticesFromResponse( response ) {
const { createNotice } = dispatch( 'core/notices' );
if ( response.error_data && response.errors && Object.keys( response.errors ).length ) {
// Loop over multi-error responses.
Object.keys( response.errors ).forEach( errorKey => {
createNotice( 'error', response.errors[ errorKey ].join( ' ' ) );
} );
} else if ( response.message ) {
// Handle generic messages.
createNotice( response.code ? 'error' : 'success', response.message );
}
}

View File

@ -0,0 +1,69 @@
/**
* External dependencies
*/
import { dispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import { createNoticesFromResponse } from '../../notices';
jest.mock( '@wordpress/data', () => ( {
dispatch: jest.fn().mockReturnValue( {
createNotice: jest.fn(),
} ),
} ) );
describe( 'createNoticesFromResponse', () => {
beforeEach( () => {
jest.clearAllMocks();
} );
const { createNotice } = dispatch( 'core/notices' );
test( 'should create notice based on message when no errors exist', () => {
const response = { message: 'Generic response message' };
createNoticesFromResponse( response );
expect( createNotice ).toHaveBeenCalledWith(
'success',
response.message,
);
} );
test( 'should create an error notice when an error code and message exists', () => {
const response = { code: 'invalid_code', message: 'Error message' };
createNoticesFromResponse( response );
expect( createNotice ).toHaveBeenCalledWith(
'error',
response.message,
);
} );
test( 'should create error messages for each item', () => {
const response = { errors: {
item1: [
'Item1 - Error 1.',
'Item1 - Error 2.',
],
item2: [
'Item2 - Error 1.',
]
}, error_data: [] };
createNoticesFromResponse( response );
expect( createNotice ).toHaveBeenCalledTimes(2);
const call1 = createNotice.mock.calls[0];
const call2 = createNotice.mock.calls[1];
expect( call1 ).toEqual( [ 'error', response.errors.item1.join( ' ' ) ] );
expect( call2 ).toEqual( [ 'error', response.errors.item2[0] ] );
} );
test( 'should not call createNotice when no message or errors exist', () => {
const response = { data: {} };
createNoticesFromResponse( response );
expect( createNotice ).not.toHaveBeenCalled();
} );
} );

View File

@ -11,7 +11,7 @@ import { filter } from 'lodash';
/** /**
* WooCommerce dependencies * WooCommerce dependencies
*/ */
import { Card, H, Plugins } from '@woocommerce/components'; import { Card, H } from '@woocommerce/components';
import { getAdminLink } from '@woocommerce/wc-admin-settings'; import { getAdminLink } from '@woocommerce/wc-admin-settings';
import { import {
pluginNames, pluginNames,
@ -24,6 +24,7 @@ import {
* Internal dependencies * Internal dependencies
*/ */
import Connect from 'dashboard/components/connect'; import Connect from 'dashboard/components/connect';
import { createNoticesFromResponse } from 'lib/notices';
import Logo from './logo'; import Logo from './logo';
import ManagementIcon from './images/management'; import ManagementIcon from './images/management';
import SalesTaxIcon from './images/sales_tax'; import SalesTaxIcon from './images/sales_tax';
@ -34,10 +35,6 @@ import { recordEvent } from 'lib/tracks';
class Benefits extends Component { class Benefits extends Component {
constructor( props ) { constructor( props ) {
super( props ); super( props );
this.state = {
isConnecting: false,
isInstalling: false,
};
this.isJetpackActive = props.activePlugins.includes( 'jetpack' ); this.isJetpackActive = props.activePlugins.includes( 'jetpack' );
this.isWcsActive = props.activePlugins.includes( this.isWcsActive = props.activePlugins.includes(
@ -59,26 +56,6 @@ class Benefits extends Component {
this.skipPluginInstall = this.skipPluginInstall.bind( this ); this.skipPluginInstall = this.skipPluginInstall.bind( this );
} }
componentDidUpdate( prevProps, prevState ) {
const { goToNextStep } = this.props;
// No longer pending or updating profile items, go to next step.
if (
! this.isPending() &&
( prevProps.isRequesting ||
prevState.isConnecting ||
prevState.isInstalling )
) {
goToNextStep();
}
}
isPending() {
const { isConnecting, isInstalling } = this.state;
const { isRequesting } = this.props;
return isConnecting || isInstalling || isRequesting;
}
async skipPluginInstall() { async skipPluginInstall() {
const { const {
createNotice, createNotice,
@ -108,21 +85,46 @@ class Benefits extends Component {
goToNextStep(); goToNextStep();
} }
startPluginInstall() { async startPluginInstall() {
const { updateProfileItems, updateOptions } = this.props; const {
createNotice,
this.setState( { isInstalling: true } ); goToNextStep,
installAndActivatePlugins,
updateOptions( { isJetpackConnected,
woocommerce_setup_jetpack_opted_in: true, updateProfileItems,
} ); updateOptions,
} = this.props;
const plugins = this.isJetpackActive ? 'installed-wcs' : 'installed'; const plugins = this.isJetpackActive ? 'installed-wcs' : 'installed';
recordEvent( 'storeprofiler_install_plugins', { recordEvent( 'storeprofiler_install_plugins', {
install: true, install: true,
plugins, plugins,
} ); } );
updateProfileItems( { plugins } );
await Promise.all( [
installAndActivatePlugins( this.pluginsToInstall ),
updateProfileItems( { plugins } ),
updateOptions( {
woocommerce_setup_jetpack_opted_in: true,
} ),
] ).catch( ( pluginError, profileError ) => {
if ( pluginError ) {
createNoticesFromResponse( pluginError );
}
if ( profileError ) {
createNotice(
'error',
__(
'There was a problem updating your preferences.',
'woocommerce-admin'
)
);
}
} );
if ( isJetpackConnected ) {
goToNextStep();
}
} }
renderBenefit( benefit ) { renderBenefit( benefit ) {
@ -200,12 +202,22 @@ class Benefits extends Component {
} }
render() { render() {
const { isConnecting, isInstalling } = this.state; const {
const { isJetpackConnected, isRequesting } = this.props; activePlugins,
goToNextStep,
isJetpackConnected,
isInstallingActivating,
isRequesting,
} = this.props;
const pluginNamesString = this.pluginsToInstall const pluginNamesString = this.pluginsToInstall
.map( ( pluginSlug ) => pluginNames[ pluginSlug ] ) .map( ( pluginSlug ) => pluginNames[ pluginSlug ] )
.join( ' ' + __( 'and', 'woocommerce-admin' ) + ' ' ); .join( ' ' + __( 'and', 'woocommerce-admin' ) + ' ' );
const pluginsRemaining = this.pluginsToInstall.filter(
( plugin ) => ! activePlugins.includes( plugin )
);
const isInstallAction =
isInstallingActivating || ! pluginsRemaining.length;
return ( return (
<Card className="woocommerce-profile-wizard__benefits-card"> <Card className="woocommerce-profile-wizard__benefits-card">
@ -222,10 +234,8 @@ class Benefits extends Component {
<div className="woocommerce-profile-wizard__card-actions"> <div className="woocommerce-profile-wizard__card-actions">
<Button <Button
isPrimary isPrimary
isBusy={ isBusy={ isInstallAction }
this.isPending() && ( isInstalling || isConnecting ) disabled={ isRequesting || isInstallAction }
}
disabled={ this.isPending() }
onClick={ this.startPluginInstall } onClick={ this.startPluginInstall }
className="woocommerce-profile-wizard__continue" className="woocommerce-profile-wizard__continue"
> >
@ -233,36 +243,18 @@ class Benefits extends Component {
</Button> </Button>
<Button <Button
isSecondary isSecondary
isBusy={ isBusy={ isRequesting && ! isInstallAction }
this.isPending() && ! isInstalling && ! isConnecting disabled={ isRequesting || isInstallAction }
}
disabled={ this.isPending() }
className="woocommerce-profile-wizard__skip" className="woocommerce-profile-wizard__skip"
onClick={ this.skipPluginInstall } onClick={ this.skipPluginInstall }
> >
{ __( 'No thanks', 'woocommerce-admin' ) } { __( 'No thanks', 'woocommerce-admin' ) }
</Button> </Button>
{ isInstalling && (
<Plugins
autoInstall
onComplete={ () =>
this.setState( {
isInstalling: false,
isConnecting: ! isJetpackConnected,
} )
}
onError={ () =>
this.setState( {
isInstalling: false,
} )
}
pluginSlugs={ this.pluginsToInstall }
/>
) }
{ /* Make sure we're finished requesting since this will auto redirect us. */ } { /* Make sure we're finished requesting since this will auto redirect us. */ }
{ isConnecting && ! isJetpackConnected && ! isRequesting && ( { ! isJetpackConnected &&
! isRequesting &&
! pluginsRemaining.length && (
<Connect <Connect
autoConnect autoConnect
onConnect={ () => { onConnect={ () => {
@ -270,9 +262,7 @@ class Benefits extends Component {
'storeprofiler_jetpack_connect_redirect' 'storeprofiler_jetpack_connect_redirect'
); );
} } } }
onError={ () => onError={ () => goToNextStep() }
this.setState( { isConnecting: false } )
}
redirectUrl={ getAdminLink( redirectUrl={ getAdminLink(
'admin.php?page=wc-admin&reset_profiler=0' 'admin.php?page=wc-admin&reset_profiler=0'
) } ) }
@ -308,31 +298,35 @@ export default compose(
isOnboardingRequesting, isOnboardingRequesting,
} = select( ONBOARDING_STORE_NAME ); } = select( ONBOARDING_STORE_NAME );
const { getActivePlugins, isJetpackConnected } = select( const {
PLUGINS_STORE_NAME getActivePlugins,
); isJetpackConnected,
isPluginsRequesting,
const isProfileItemsError = Boolean( } = select( PLUGINS_STORE_NAME );
getOnboardingError( 'updateProfileItems' )
);
const activePlugins = getActivePlugins();
const profileItems = getProfileItems();
return { return {
activePlugins, activePlugins: getActivePlugins(),
isProfileItemsError, isProfileItemsError: Boolean(
profileItems, getOnboardingError( 'updateProfileItems' )
),
profileItems: getProfileItems(),
isJetpackConnected: isJetpackConnected(), isJetpackConnected: isJetpackConnected(),
isRequesting: isOnboardingRequesting( 'updateProfileItems' ), isRequesting: isOnboardingRequesting( 'updateProfileItems' ),
isInstallingActivating:
isPluginsRequesting( 'installPlugins' ) ||
isPluginsRequesting( 'activatePlugins' ) ||
isPluginsRequesting( 'getJetpackConnectUrl' ),
}; };
} ), } ),
withDispatch( ( dispatch ) => { withDispatch( ( dispatch ) => {
const { installAndActivatePlugins } = dispatch( PLUGINS_STORE_NAME );
const { updateProfileItems } = dispatch( ONBOARDING_STORE_NAME ); const { updateProfileItems } = dispatch( ONBOARDING_STORE_NAME );
const { updateOptions } = dispatch( OPTIONS_STORE_NAME ); const { updateOptions } = dispatch( OPTIONS_STORE_NAME );
const { createNotice } = dispatch( 'core/notices' ); const { createNotice } = dispatch( 'core/notices' );
return { return {
createNotice, createNotice,
installAndActivatePlugins,
updateProfileItems, updateProfileItems,
updateOptions, updateOptions,
}; };

View File

@ -13,7 +13,12 @@ import { keys, get, pickBy } from 'lodash';
*/ */
import { formatValue } from '@woocommerce/number'; import { formatValue } from '@woocommerce/number';
import { getSetting } from '@woocommerce/wc-admin-settings'; import { getSetting } from '@woocommerce/wc-admin-settings';
import { ONBOARDING_STORE_NAME, pluginNames, SETTINGS_STORE_NAME } from '@woocommerce/data'; import {
ONBOARDING_STORE_NAME,
PLUGINS_STORE_NAME,
pluginNames,
SETTINGS_STORE_NAME,
} from '@woocommerce/data';
/** /**
* Internal dependencies * Internal dependencies
@ -24,12 +29,11 @@ import {
SelectControl, SelectControl,
Form, Form,
TextControl, TextControl,
Plugins,
} from '@woocommerce/components'; } from '@woocommerce/components';
import withWCApiSelect from 'wc-api/with-select';
import { recordEvent } from 'lib/tracks'; import { recordEvent } from 'lib/tracks';
import { getCurrencyRegion } from 'dashboard/utils'; import { getCurrencyRegion } from 'dashboard/utils';
import { CurrencyContext } from 'lib/currency-context'; import { CurrencyContext } from 'lib/currency-context';
import { createNoticesFromResponse } from 'lib/notices';
const wcAdminAssetUrl = getSetting( 'wcAdminAssetUrl', '' ); const wcAdminAssetUrl = getSetting( 'wcAdminAssetUrl', '' );
@ -60,12 +64,6 @@ class BusinessDetails extends Component {
: true, : true,
}; };
this.state = {
installExtensions: false,
isInstallingExtensions: false,
extensionInstallError: false,
};
this.extensions = [ this.extensions = [
'facebook-for-woocommerce', 'facebook-for-woocommerce',
'mailchimp-for-woocommerce', 'mailchimp-for-woocommerce',
@ -82,7 +80,7 @@ class BusinessDetails extends Component {
const { const {
createNotice, createNotice,
goToNextStep, goToNextStep,
isError, installAndActivatePlugins,
updateProfileItems, updateProfileItems,
} = this.props; } = this.props;
const { const {
@ -125,19 +123,8 @@ class BusinessDetails extends Component {
} }
} ); } );
await updateProfileItems( updates ); const promises = [
updateProfileItems( updates ).catch( () => {
if ( ! isError ) {
if ( businessExtensions.length === 0 ) {
goToNextStep();
return;
}
this.setState( {
installExtensions: true,
isInstallingExtensions: true,
} );
} else {
createNotice( createNotice(
'error', 'error',
__( __(
@ -145,7 +132,29 @@ class BusinessDetails extends Component {
'woocommerce-admin' 'woocommerce-admin'
) )
); );
throw new Error();
} ),
];
if ( businessExtensions.length ) {
promises.push(
installAndActivatePlugins( businessExtensions )
.then( ( response ) => {
createNoticesFromResponse( response );
} )
.catch( ( error ) => {
this.setState( {
hasInstallActivateError: true,
} );
createNoticesFromResponse( error );
throw new Error();
} )
);
} }
Promise.all( promises ).then( () => {
goToNextStep();
} );
} }
validate( values ) { validate( values ) {
@ -274,7 +283,7 @@ class BusinessDetails extends Component {
} }
renderBusinessExtensionHelpText( values ) { renderBusinessExtensionHelpText( values ) {
const { isInstallingExtensions } = this.state; const { isInstallingActivating } = this.props;
const extensions = this.getBusinessExtensions( values ); const extensions = this.getBusinessExtensions( values );
if ( extensions.length === 0 ) { if ( extensions.length === 0 ) {
@ -287,7 +296,7 @@ class BusinessDetails extends Component {
} ) } )
.join( ', ' ); .join( ', ' );
if ( isInstallingExtensions ) { if ( isInstallingActivating ) {
return ( return (
<p> <p>
{ sprintf( { sprintf(
@ -319,9 +328,6 @@ class BusinessDetails extends Component {
} }
renderBusinessExtensions( values, getInputProps ) { renderBusinessExtensions( values, getInputProps ) {
const { installExtensions, extensionInstallError } = this.state;
const { goToNextStep } = this.props;
const extensionsToInstall = this.getBusinessExtensions( values );
const extensionBenefits = [ const extensionBenefits = [
{ {
slug: 'facebook-for-woocommerce', slug: 'facebook-for-woocommerce',
@ -382,33 +388,16 @@ class BusinessDetails extends Component {
</div> </div>
</div> </div>
) ) } ) ) }
{ installExtensions && (
<div className="woocommerce-profile-wizard__card-actions">
<Plugins
onComplete={ () => {
goToNextStep();
} }
onSkip={ () => {
goToNextStep();
} }
onError={ () => {
this.setState( {
extensionInstallError: true,
isInstallingExtensions: false,
} );
} }
autoInstall={ ! extensionInstallError }
pluginSlugs={ extensionsToInstall }
/>
</div>
) }
</Fragment> </Fragment>
); );
} }
render() { render() {
const { isInstallingExtensions, extensionInstallError } = this.state; const {
goToNextStep,
isInstallingActivating,
hasInstallActivateError,
} = this.props;
const { formatCurrency } = this.context; const { formatCurrency } = this.context;
const productCountOptions = [ const productCountOptions = [
{ {
@ -662,20 +651,35 @@ class BusinessDetails extends Component {
getInputProps getInputProps
) } ) }
{ ! extensionInstallError && ( <div className="woocommerce-profile-wizard__card-actions">
<Button <Button
isPrimary isPrimary
className="woocommerce-profile-wizard__continue" className="woocommerce-profile-wizard__continue"
onClick={ handleSubmit } onClick={ handleSubmit }
disabled={ ! isValidForm } disabled={ ! isValidForm }
isBusy={ isInstallingExtensions } isBusy={ isInstallingActivating }
>
{ ! hasInstallActivateError
? __(
'Continue',
'woocommerce-admin'
)
: __(
'Retry',
'woocommerce-admin'
) }
</Button>
{ hasInstallActivateError && (
<Button
onClick={ () => goToNextStep() }
> >
{ __( { __(
'Continue', 'Continue without installing',
'woocommerce-admin' 'woocommerce-admin'
) } ) }
</Button> </Button>
) } ) }
</div>
</Fragment> </Fragment>
</Card> </Card>
@ -692,37 +696,43 @@ class BusinessDetails extends Component {
BusinessDetails.contextType = CurrencyContext; BusinessDetails.contextType = CurrencyContext;
export default compose( export default compose(
withWCApiSelect( ( select ) => {
const { getProfileItems, getOnboardingError } = select( ONBOARDING_STORE_NAME );
return {
isError: Boolean( getOnboardingError( 'updateProfileItems' ) ),
profileItems: getProfileItems(),
};
} ),
withSelect( ( select ) => { withSelect( ( select ) => {
const { const {
getSettings, getSettings,
getSettingsError, getSettingsError,
isGetSettingsRequesting, isGetSettingsRequesting,
} = select( SETTINGS_STORE_NAME ); } = select( SETTINGS_STORE_NAME );
const { getProfileItems, getOnboardingError } = select(
ONBOARDING_STORE_NAME
);
const { getPluginsError, isPluginsRequesting } = select(
PLUGINS_STORE_NAME
);
const { general: settings = {} } = getSettings( 'general' ); const { general: settings = {} } = getSettings( 'general' );
const isSettingsError = Boolean( getSettingsError( 'general' ) );
const isSettingsRequesting = isGetSettingsRequesting( 'general' );
return { return {
isSettingsError, hasInstallActivateError:
isSettingsRequesting, getPluginsError( 'installPlugins' ) ||
getPluginsError( 'activatePlugins' ),
isError: Boolean( getOnboardingError( 'updateProfileItems' ) ),
profileItems: getProfileItems(),
isSettingsError: Boolean( getSettingsError( 'general' ) ),
isSettingsRequesting: isGetSettingsRequesting( 'general' ),
settings, settings,
isInstallingActivating:
isPluginsRequesting( 'installPlugins' ) ||
isPluginsRequesting( 'activatePlugins' ) ||
isPluginsRequesting( 'getJetpackConnectUrl' ),
}; };
} ), } ),
withDispatch( ( dispatch ) => { withDispatch( ( dispatch ) => {
const { updateProfileItems } = dispatch( ONBOARDING_STORE_NAME ); const { updateProfileItems } = dispatch( ONBOARDING_STORE_NAME );
const { installAndActivatePlugins } = dispatch( PLUGINS_STORE_NAME );
const { createNotice } = dispatch( 'core/notices' ); const { createNotice } = dispatch( 'core/notices' );
return { return {
createNotice, createNotice,
installAndActivatePlugins,
updateProfileItems, updateProfileItems,
}; };
} ) } )

View File

@ -11,7 +11,8 @@ import { withSelect, withDispatch } from '@wordpress/data';
/** /**
* WooCommerce dependencies * WooCommerce dependencies
*/ */
import { pluginNames, PLUGINS_STORE_NAME } from '@woocommerce/data'; import { createNoticesFromResponse } from 'lib/notices';
import { PLUGINS_STORE_NAME } from '@woocommerce/data';
export class Plugins extends Component { export class Plugins extends Component {
constructor() { constructor() {
@ -41,9 +42,8 @@ export class Plugins extends Component {
} }
const { const {
installAndActivatePlugins,
isRequesting, isRequesting,
installPlugins,
activatePlugins,
pluginSlugs, pluginSlugs,
} = this.props; } = this.props;
@ -52,60 +52,26 @@ export class Plugins extends Component {
return false; return false;
} }
const installs = await installPlugins( pluginSlugs ); installAndActivatePlugins( pluginSlugs )
.then( ( response ) => {
if ( installs.errors && Object.keys( installs.errors.errors ).length ) { createNoticesFromResponse( response );
this.handleErrors( installs.errors ); this.handleSuccess( response.data.activated );
return; } )
} .catch( ( error ) => {
createNoticesFromResponse( error );
const activations = await activatePlugins( pluginSlugs ); this.handleErrors( error.errors );
} );
if ( activations.success && activations.data.activated ) {
this.handleSuccess( activations.data.activated );
return;
}
if ( activations.errors ) {
this.handleErrors( activations.errors );
}
} }
handleErrors( errors ) { handleErrors( errors ) {
const { onError, createNotice } = this.props; const { onError } = this.props;
const { errors: pluginErrors } = errors;
if ( pluginErrors ) {
Object.keys( pluginErrors ).forEach( ( plugin ) => {
createNotice(
'error',
// Replace the slug with a plugin name if a constant exists.
pluginNames[ plugin ]
? pluginErrors[ plugin ][ 0 ].replace(
`\`${ plugin }\``,
pluginNames[ plugin ]
)
: pluginErrors[ plugin ][ 0 ]
);
} );
} else if ( errors.message ) {
createNotice( 'error', errors.message );
}
this.setState( { hasErrors: true } ); this.setState( { hasErrors: true } );
onError( errors ); onError( errors );
} }
handleSuccess( activePlugins ) { handleSuccess( activePlugins ) {
const { createNotice, onComplete } = this.props; const { onComplete } = this.props;
createNotice(
'success',
__(
'Plugins were successfully installed and activated.',
'woocommerce-admin'
)
);
onComplete( activePlugins ); onComplete( activePlugins );
} }
@ -221,15 +187,10 @@ export default compose(
}; };
} ), } ),
withDispatch( ( dispatch ) => { withDispatch( ( dispatch ) => {
const { createNotice } = dispatch( 'core/notices' ); const { installAndActivatePlugins } = dispatch( PLUGINS_STORE_NAME );
const { activatePlugins, installPlugins } = dispatch(
PLUGINS_STORE_NAME
);
return { return {
activatePlugins, installAndActivatePlugins,
createNotice,
installPlugins,
}; };
} ) } )
)( Plugins ); )( Plugins );

View File

@ -46,16 +46,12 @@ describe( 'Rendering', () => {
describe( 'Installing and activating', () => { describe( 'Installing and activating', () => {
let pluginsWrapper; let pluginsWrapper;
const installPlugins = jest.fn().mockReturnValue( { const installAndActivatePlugins = jest.fn().mockResolvedValue( {
success: true,
} );
const activatePlugins = jest.fn().mockReturnValue( {
success: true, success: true,
data: { data: {
activated: [ 'jetpack' ], activated: [ 'jetpack' ],
}, },
} ); } );
const createNotice = jest.fn();
const onComplete = jest.fn(); const onComplete = jest.fn();
beforeEach( () => { beforeEach( () => {
@ -63,40 +59,23 @@ describe( 'Installing and activating', () => {
<Plugins <Plugins
pluginSlugs={ [ 'jetpack' ] } pluginSlugs={ [ 'jetpack' ] }
onComplete={ onComplete } onComplete={ onComplete }
installPlugins={ installPlugins } installAndActivatePlugins={ installAndActivatePlugins }
activatePlugins={ activatePlugins }
createNotice={ createNotice }
/> />
); );
} ); } );
it( 'should call installPlugins', async () => { it( 'should call installAndActivatePlugins', async () => {
const installButton = pluginsWrapper.find( Button ).at( 0 ); const installButton = pluginsWrapper.find( Button ).at( 0 );
installButton.simulate( 'click' ); installButton.simulate( 'click' );
expect( installPlugins ).toHaveBeenCalledWith( [ 'jetpack' ] ); expect( installAndActivatePlugins ).toHaveBeenCalledWith( [ 'jetpack' ] );
} ); } );
it( 'should call activatePlugin', async () => {
const installButton = pluginsWrapper.find( Button ).at( 0 );
installButton.simulate( 'click' );
expect( activatePlugins ).toHaveBeenCalledWith( [ 'jetpack' ] );
} );
it( 'should create a success notice', async () => {
const installButton = pluginsWrapper.find( Button ).at( 0 );
installButton.simulate( 'click' );
expect( createNotice ).toHaveBeenCalledWith(
'success',
'Plugins were successfully installed and activated.'
);
} );
it( 'should call the onComplete callback', async () => { it( 'should call the onComplete callback', async () => {
const installButton = pluginsWrapper.find( Button ).at( 0 ); const installButton = pluginsWrapper.find( Button ).at( 0 );
installButton.simulate( 'click' ); installButton.simulate( 'click' );
expect( onComplete ).toHaveBeenCalledWith( [ 'jetpack' ] ); await expect( onComplete ).toHaveBeenCalledWith( [ 'jetpack' ] );
} ); } );
} ); } );
@ -107,43 +86,28 @@ describe( 'Installing and activating errors', () => {
'failed-plugin': [ 'error message' ], 'failed-plugin': [ 'error message' ],
}, },
}; };
const installPlugins = jest.fn().mockReturnValue( { const installAndActivatePlugins = jest.fn().mockRejectedValue( {
errors, errors,
} ); } );
const activatePlugins = jest.fn().mockReturnValue( { const onComplete = jest.fn();
success: false,
} );
const createNotice = jest.fn();
const onError = jest.fn(); const onError = jest.fn();
beforeEach( () => { beforeEach( () => {
pluginsWrapper = shallow( pluginsWrapper = shallow(
<Plugins <Plugins
pluginSlugs={ [ 'jetpack' ] } pluginSlugs={ [ 'jetpack' ] }
onComplete={ () => {} } onComplete={ onComplete }
installPlugins={ installPlugins } installAndActivatePlugins={ installAndActivatePlugins }
activatePlugins={ activatePlugins }
createNotice={ createNotice }
onError={ onError } onError={ onError }
/> />
); );
} ); } );
it( 'should not call activatePlugin on install error', async () => { it( 'should not call onComplete on install error', async () => {
const installButton = pluginsWrapper.find( Button ).at( 0 ); const installButton = pluginsWrapper.find( Button ).at( 0 );
installButton.simulate( 'click' ); installButton.simulate( 'click' );
expect( activatePlugins ).not.toHaveBeenCalled(); expect( onComplete ).not.toHaveBeenCalled();
} );
it( 'should create an error notice', async () => {
const installButton = pluginsWrapper.find( Button ).at( 0 );
installButton.simulate( 'click' );
expect( createNotice ).toHaveBeenCalledWith(
'error',
errors.errors[ 'failed-plugin' ][ 0 ]
);
} ); } );
it( 'should call the onError callback', async () => { it( 'should call the onError callback', async () => {

View File

@ -1,13 +1,12 @@
/** /**
* External Dependencies * External Dependencies
*/ */
import { apiFetch, dispatch } from '@wordpress/data-controls';
import { apiFetch } from '@wordpress/data-controls';
import { __ } from '@wordpress/i18n';
/** /**
* Internal Dependencies * Internal Dependencies
*/ */
import { pluginNames, STORE_NAME } from './constants';
import TYPES from './action-types'; import TYPES from './action-types';
import { WC_ADMIN_NAMESPACE } from '../constants'; import { WC_ADMIN_NAMESPACE } from '../constants';
@ -68,16 +67,12 @@ export function* installPlugins( plugins ) {
data: { plugins: plugins.join( ',' ) }, data: { plugins: plugins.join( ',' ) },
} ); } );
if ( ! results ) { if ( results.data.installed.length ) {
throw new Error();
}
if ( results.success && results.data.installed ) {
yield updateInstalledPlugins( results.data.installed ); yield updateInstalledPlugins( results.data.installed );
} }
if ( Object.keys( results.errors.errors ).length ) { if ( Object.keys( results.errors.errors ).length ) {
yield setError( 'installPlugins', results.errors ); throw results.errors;
} }
yield setIsRequesting( 'installPlugins', false ); yield setIsRequesting( 'installPlugins', false );
@ -85,16 +80,7 @@ export function* installPlugins( plugins ) {
return results; return results;
} catch ( error ) { } catch ( error ) {
yield setError( 'installPlugins', error ); yield setError( 'installPlugins', error );
yield setIsRequesting( 'installPlugins', false ); throw formatErrors( error );
return {
errors: {
message: __(
'Something went wrong while trying to install your plugins.',
'woocommerce-admin'
),
},
};
} }
} }
@ -108,24 +94,47 @@ export function* activatePlugins( plugins ) {
data: { plugins: plugins.join( ',' ) }, data: { plugins: plugins.join( ',' ) },
} ); } );
if ( results.success && results.data.activated ) { if ( results.data.activated.length ) {
yield updateActivePlugins( results.data.activated ); yield updateActivePlugins( results.data.activated );
yield setIsRequesting( 'activatePlugins', false );
return results;
} }
throw new Error(); if ( Object.keys( results.errors.errors ).length ) {
throw results.errors;
}
yield setIsRequesting( 'activatePlugins', false );
return results;
} catch ( error ) { } catch ( error ) {
yield setError( 'activatePlugins', error ); yield setError( 'activatePlugins', error );
yield setIsRequesting( 'activatePlugins', false ); throw formatErrors( error );
}
}
return { export function* installAndActivatePlugins( plugins ) {
errors: { try {
message: __( yield dispatch( STORE_NAME, 'installPlugins', plugins );
'Something went wrong while trying to activate your plugins.', const activations = yield dispatch( STORE_NAME, 'activatePlugins', plugins );
'woocommerce-admin' return activations;
), } catch ( error ) {
} throw error;
};
} }
} }
export function formatErrors( response ) {
if ( response.errors ) {
// Replace the slug with a plugin name if a constant exists.
Object.keys( response.errors ).forEach( plugin => {
response.errors[ plugin ] = response.errors[ plugin ].map( pluginError => {
return pluginNames[ plugin ]
? pluginError.replace(
`\`${ plugin }\``,
pluginNames[ plugin ]
)
: pluginError;
} );
} );
}
return response;
}

View File

@ -234,7 +234,7 @@ class Plugins extends \WC_REST_Data_Controller {
$errors->add( $errors->add(
$plugin, $plugin,
/* translators: %s: plugin slug (example: woocommerce-services) */ /* translators: %s: plugin slug (example: woocommerce-services) */
sprintf( __( 'The requested plugin `%s`. is not in the list of allowed plugins.', 'woocommerce-admin' ), $slug ) sprintf( __( 'The requested plugin `%s` is not in the list of allowed plugins.', 'woocommerce-admin' ), $slug )
); );
continue; continue;
} }
@ -315,6 +315,9 @@ class Plugins extends \WC_REST_Data_Controller {
), ),
'errors' => $errors, 'errors' => $errors,
'success' => count( $errors->errors ) === 0, 'success' => count( $errors->errors ) === 0,
'message' => count( $errors->errors ) === 0
? __( 'Plugins were successfully installed.', 'woocommerce-admin' )
: __( 'There was a problem installing some of the requested plugins.', 'woocommerce-admin' ),
); );
} }
@ -422,6 +425,9 @@ class Plugins extends \WC_REST_Data_Controller {
), ),
'errors' => $errors, 'errors' => $errors,
'success' => count( $errors->errors ) === 0, 'success' => count( $errors->errors ) === 0,
'message' => count( $errors->errors ) === 0
? __( 'Plugins were successfully activated.', 'woocommerce-admin' )
: __( 'There was a problem activating some of the requested plugins.', 'woocommerce-admin' ),
) ); ) );
} }