* Add initial plugin list components and marketing task

* Add marketing task styles

* Fix action button alignment

* Only allow specific plugin lists

* Add button to activate already installed plugins

* Record event when marketing plugin is installed

* Update plugin list when plugins are installed or activated

* Disable and set buttons as busy when installing/activating

* Update data source to use v2 controller

* Add changelog entry
This commit is contained in:
Joshua T Flowers 2021-07-29 12:10:53 -04:00 committed by GitHub
parent f5de7ef892
commit 7167242dfb
10 changed files with 429 additions and 1 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: Add
Add marketing extensions task to task list

View File

@ -17,6 +17,7 @@ import { recordEvent } from '@woocommerce/tracks';
*/ */
import Appearance from './tasks/appearance'; import Appearance from './tasks/appearance';
import { getCategorizedOnboardingProducts } from '../dashboard/utils'; import { getCategorizedOnboardingProducts } from '../dashboard/utils';
import { Marketing } from './tasks/Marketing';
import { Products } from './tasks/products'; import { Products } from './tasks/products';
import Shipping from './tasks/shipping'; import Shipping from './tasks/shipping';
import Tax from './tasks/tax'; import Tax from './tasks/tax';
@ -328,6 +329,28 @@ export function getAllTasks( {
time: __( '1 minute', 'woocommerce-admin' ), time: __( '1 minute', 'woocommerce-admin' ),
type: 'setup', type: 'setup',
}, },
{
key: 'marketing',
title: __( 'Set up marketing tools', 'woocommerce-admin' ),
content: __(
'Add recommended marketing tools to reach new customers and grow your business',
'woocommerce-admin'
),
container: <Marketing />,
onClick: () => {
onTaskSelect( 'marketing' );
updateQueryString( { task: 'marketing' } );
},
// @todo This should use the free extensions data store.
completed:
[].filter( ( plugin ) => installedPlugins.includes( plugin ) )
.length > 0,
visible:
window.wcAdminFeatures &&
window.wcAdminFeatures[ 'remote-extensions-list' ],
time: __( '1 minute', 'woocommerce-admin' ),
type: 'setup',
},
{ {
key: 'appearance', key: 'appearance',
title: __( 'Personalize my store', 'woocommerce-admin' ), title: __( 'Personalize my store', 'woocommerce-admin' ),

View File

@ -0,0 +1,15 @@
.woocommerce-task-marketing {
.components-card__header {
flex-direction: column;
align-items: flex-start;
h2 {
color: $gray-900;
margin-bottom: $gap-smaller;
}
p {
color: $gray-700;
}
}
}

View File

@ -0,0 +1,42 @@
.woocommerce-plugin-list__plugin {
display: flex;
padding: $gap-large;
border-top: 1px solid #e5e5e5;
&:last-child {
border-bottom: 1px solid #e5e5e5;
}
h4 {
margin-bottom: $gap-small;
font-weight: 600;
color: $gray-900;
}
p {
color: $gray-700;
font-weight: 400;
color: #3c434a;
}
}
.woocommerce-plugin-list__plugin-logo {
margin-right: 45px;
display: flex;
align-items: center;
img {
width: 50px;
}
}
.woocommerce-plugin-list__plugin-text {
max-width: 370px;
margin-right: $gap;
}
.woocommerce-plugin-list__plugin-action {
display: flex;
align-items: center;
margin-left: auto;
}

View File

@ -0,0 +1,93 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { getAdminLink } from '@woocommerce/wc-admin-settings';
import { Text } from '@woocommerce/experimental';
/**
* Internal dependencies
*/
import './Plugin.scss';
export type PluginProps = {
isActive: boolean;
isBusy?: boolean;
isDisabled?: boolean;
isInstalled: boolean;
description?: string;
installAndActivate?: ( slug: string ) => void;
imageUrl?: string;
manageUrl?: string;
name: string;
slug: string;
};
export const Plugin: React.FC< PluginProps > = ( {
description,
imageUrl,
installAndActivate = () => {},
isActive,
isBusy,
isDisabled,
isInstalled,
manageUrl,
name,
slug,
} ) => {
return (
<div className="woocommerce-plugin-list__plugin">
{ imageUrl && (
<div className="woocommerce-plugin-list__plugin-logo">
<img
src={ imageUrl }
alt={ sprintf(
/* translators: %s = name of the plugin */
__( '%s logo', 'woocommerce-admin' ),
name
) }
/>
</div>
) }
<div className="woocommerce-plugin-list__plugin-text">
<Text variant="subtitle.small" as="h4">
{ name }
</Text>
<Text variant="subtitle.small">{ description }</Text>
</div>
<div className="woocommerce-plugin-list__plugin-action">
{ isActive && manageUrl && (
<Button
disabled={ isDisabled }
isBusy={ isBusy }
isSecondary
href={ getAdminLink( manageUrl ) }
>
{ __( 'Manage', 'woocommmerce-admin' ) }
</Button>
) }
{ isInstalled && ! isActive && (
<Button
disabled={ isDisabled }
isBusy={ isBusy }
isSecondary
onClick={ () => installAndActivate( slug ) }
>
{ __( 'Activate', 'woocommmerce-admin' ) }
</Button>
) }
{ ! isInstalled && (
<Button
disabled={ isDisabled }
isBusy={ isBusy }
isSecondary
onClick={ () => installAndActivate( slug ) }
>
{ __( 'Install', 'woocommmerce-admin' ) }
</Button>
) }
</div>
</div>
);
};

View File

@ -0,0 +1,12 @@
.woocommerce-plugin-list__title {
padding: $gap-small 30px;
background: $gray-200;
margin-bottom: 1px;
margin-top: 1px;
position: relative;
h3 {
font-weight: 500;
color: $black;
}
}

View File

@ -0,0 +1,63 @@
/**
* External dependencies
*/
import { Text } from '@woocommerce/experimental';
/**
* Internal dependencies
*/
import { Plugin, PluginProps } from './Plugin';
import './PluginList.scss';
export type PluginListProps = {
currentPlugin?: string | null;
key: string;
installAndActivate?: ( slug: string ) => void;
plugins?: PluginProps[];
title?: string;
};
export const PluginList: React.FC< PluginListProps > = ( {
currentPlugin,
installAndActivate = () => {},
plugins = [],
title,
} ) => {
return (
<div className="woocommerce-plugin-list">
{ title && (
<div className="woocommerce-plugin-list__title">
<Text variant="sectionheading" as="h3">
{ title }
</Text>
</div>
) }
{ plugins.map( ( plugin ) => {
const {
description,
imageUrl,
isActive,
isInstalled,
manageUrl,
slug,
name,
} = plugin;
return (
<Plugin
key={ slug }
description={ description }
manageUrl={ manageUrl }
name={ name }
imageUrl={ imageUrl }
installAndActivate={ installAndActivate }
isActive={ isActive }
isBusy={ currentPlugin === slug }
isDisabled={ !! currentPlugin }
isInstalled={ isInstalled }
slug={ slug }
/>
);
} ) }
</div>
);
};

View File

@ -0,0 +1,175 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';
import { Card, CardHeader, Spinner } from '@wordpress/components';
import { PLUGINS_STORE_NAME, WCDataSelector } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
import { Text } from '@woocommerce/experimental';
import { useEffect, useMemo, useState } from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import './Marketing.scss';
import { createNoticesFromResponse } from '~/lib/notices';
import { PluginList, PluginListProps } from './PluginList';
import { PluginProps } from './Plugin';
type ExtensionList = {
key: string;
title: string;
plugins: Extension[];
};
type Extension = {
description: string;
key: string;
image_url: string;
manage_url: string;
name: string;
slug: string;
};
const ALLOWED_PLUGIN_LISTS = [ 'reach', 'grow' ];
export const Marketing: React.FC = () => {
const [ fetchedExtensions, setFetchedExtensions ] = useState<
ExtensionList[]
>( [] );
const [ currentPlugin, setCurrentPlugin ] = useState< string | null >(
null
);
const [ isFetching, setIsFetching ] = useState( true );
const { installAndActivatePlugins } = useDispatch( PLUGINS_STORE_NAME );
const { activePlugins, installedPlugins } = useSelect(
( select: WCDataSelector ) => {
const { getActivePlugins, getInstalledPlugins } = select(
PLUGINS_STORE_NAME
);
return {
activePlugins: getActivePlugins(),
installedPlugins: getInstalledPlugins(),
};
}
);
const transformExtensionToPlugin = (
extension: Extension
): PluginProps => {
const { description, image_url, key, manage_url, name } = extension;
const slug = key.split( ':' )[ 0 ];
return {
description,
slug,
imageUrl: image_url,
isActive: activePlugins.includes( slug ),
isInstalled: installedPlugins.includes( slug ),
manageUrl: manage_url,
name,
};
};
useEffect( () => {
apiFetch( {
path: '/wc-admin/onboarding/free-extensions',
} )
.then( ( results: ExtensionList[] ) => {
if ( results?.length ) {
setFetchedExtensions( results );
}
setIsFetching( false );
} )
.catch( () => {
// @todo Handle error checking.
setIsFetching( false );
} );
}, [] );
const pluginLists: PluginListProps[] = useMemo( () => {
return fetchedExtensions
.map( ( list ) => {
return {
...list,
plugins: list.plugins.map( ( extension ) =>
transformExtensionToPlugin( extension )
),
};
} )
.filter( ( list ) => ALLOWED_PLUGIN_LISTS.includes( list.key ) );
}, [ installedPlugins, activePlugins, fetchedExtensions ] );
const getInstalledMarketingPlugins = () => {
const installed: string[] = [];
pluginLists.forEach( ( list: PluginListProps ) => {
return list.plugins?.forEach( ( plugin ) => {
if ( plugin.isInstalled ) {
installed.push( plugin.slug );
}
} );
} );
return installed;
};
const installAndActivate = ( slug: string ) => {
setCurrentPlugin( slug );
installAndActivatePlugins( [ slug ] )
.then( ( response: { errors: Record< string, string > } ) => {
recordEvent( 'tasklist_marketing_install', {
selected_extension: slug,
installed_extensions: getInstalledMarketingPlugins(),
} );
createNoticesFromResponse( response );
setCurrentPlugin( null );
} )
.catch( ( response: { errors: Record< string, string > } ) => {
createNoticesFromResponse( response );
setCurrentPlugin( null );
} );
};
if ( isFetching ) {
return <Spinner />;
}
return (
<div className="woocommerce-task-marketing">
<Card className="woocommerce-task-card">
<CardHeader>
<Text
variant="title.small"
as="h2"
className="woocommerce-task-card__title"
>
{ __(
'Recommended marketing extensions',
'woocommerce-admin'
) }
</Text>
<Text>
{ __(
'We recommend adding one of the following marketing tools for your store. The extension will be installed and activated for you when you click "Get started".',
'woocommerce-admin'
) }
</Text>
</CardHeader>
{ pluginLists.map( ( list ) => {
const { key, title, plugins } = list;
return (
<PluginList
currentPlugin={ currentPlugin }
installAndActivate={ installAndActivate }
key={ key }
plugins={ plugins }
title={ title }
/>
);
} ) }
</Card>
</div>
);
};

View File

@ -69,4 +69,5 @@ export type PluginSelectors = {
getInstalledPlugins: WPDataSelector< typeof getInstalledPlugins >; getInstalledPlugins: WPDataSelector< typeof getInstalledPlugins >;
getRecommendedPlugins: WPDataSelector< typeof getRecommendedPlugins >; getRecommendedPlugins: WPDataSelector< typeof getRecommendedPlugins >;
isJetpackConnected: WPDataSelector< typeof isJetpackConnected >; isJetpackConnected: WPDataSelector< typeof isJetpackConnected >;
isPluginsRequesting: WPDataSelector< typeof isPluginsRequesting >;
} & WPDataSelectors; } & WPDataSelectors;

View File

@ -13,7 +13,7 @@ defined( 'ABSPATH' ) || exit;
*/ */
class DataSourcePoller { class DataSourcePoller {
const DATA_SOURCES = array( const DATA_SOURCES = array(
'https://woocommerce.com/wp-json/wccom/obw-free-extensions/1.0/extensions.json', 'https://woocommerce.com/wp-json/wccom/obw-free-extensions/2.0/extensions.json',
); );
/** /**