Add Channels card into Multichannel Marketing page (#36541)

This commit is contained in:
Gan Eng Chin 2023-02-04 03:20:34 +08:00 committed by GitHub
commit 1a06d5bfeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 994 additions and 2 deletions

View File

@ -0,0 +1,4 @@
.woocommerce-marketing-card-header-description {
@include font-size( 14 );
color: $gray-700;
}

View File

@ -0,0 +1,12 @@
/**
* Internal dependencies
*/
import './CardHeaderDescription.scss';
export const CardHeaderDescription: React.FC = ( { children } ) => {
return (
<div className="woocommerce-marketing-card-header-description">
{ children }
</div>
);
};

View File

@ -0,0 +1 @@
export { CardHeaderDescription } from './CardHeaderDescription';

View File

@ -0,0 +1,130 @@
/**
* External dependencies
*/
import { useState } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { Button } from '@wordpress/components';
import { Pill } from '@woocommerce/components';
import { __ } from '@wordpress/i18n';
import { recordEvent } from '@woocommerce/tracks';
import { PLUGINS_STORE_NAME } from '@woocommerce/data';
/**
* Internal dependencies
*/
import { PluginCardBody } from '~/marketing/components';
import { RecommendedPlugin } from '~/marketing/types';
import { getInAppPurchaseUrl } from '~/lib/in-app-purchase';
import { createNoticesFromResponse } from '~/lib/notices';
import { useIsPluginInstalledNotActivated } from './useIsPluginInstalledNotActivated';
import './PluginCardBody.scss';
type SmartPluginCardBodyProps = {
plugin: RecommendedPlugin;
onInstalledAndActivated?: () => void;
};
/**
* A smart wrapper around PluginCardBody that accepts a `plugin` prop.
*
* It knows how to render the action button for the plugin,
* and has the logic for installing and activating plugin.
* This allows users to install and activate multiple plugins at the same time.
*/
export const SmartPluginCardBody = ( {
plugin,
onInstalledAndActivated = () => {},
}: SmartPluginCardBodyProps ) => {
const [ currentPlugin, setCurrentPlugin ] = useState< string | null >(
null
);
const { installAndActivatePlugins } = useDispatch( PLUGINS_STORE_NAME );
const { isPluginInstalledNotActivated } =
useIsPluginInstalledNotActivated();
/**
* Install and activate a plugin.
*
* When the process is successful, `onInstalledAndActivated` will be called.
* A success notice will be displayed.
*
* When the process is not successful, an error notice will be displayed.
*/
const installAndActivate = async () => {
setCurrentPlugin( plugin.product );
try {
recordEvent( 'marketing_recommended_extension', {
name: plugin.title,
} );
const response = await installAndActivatePlugins( [
plugin.product,
] );
onInstalledAndActivated();
createNoticesFromResponse( response );
} catch ( error ) {
createNoticesFromResponse( error );
}
setCurrentPlugin( null );
};
const renderButton = () => {
const buttonDisabled = !! currentPlugin;
if ( isPluginInstalledNotActivated( plugin.product ) ) {
return (
<Button
variant="secondary"
isBusy={ currentPlugin === plugin.product }
disabled={ buttonDisabled }
onClick={ installAndActivate }
>
{ __( 'Activate', 'woocommerce' ) }
</Button>
);
}
if ( plugin.direct_install ) {
return (
<Button
variant="secondary"
isBusy={ currentPlugin === plugin.product }
disabled={ buttonDisabled }
onClick={ installAndActivate }
>
{ __( 'Install plugin', 'woocommerce' ) }
</Button>
);
}
return (
<Button
variant="secondary"
href={ getInAppPurchaseUrl( plugin.url ) }
disabled={ buttonDisabled }
onClick={ () => {
recordEvent( 'marketing_recommended_extension', {
name: plugin.title,
} );
} }
>
{ __( 'View details', 'woocommerce' ) }
</Button>
);
};
return (
<PluginCardBody
icon={ <img src={ plugin.icon } alt={ plugin.title } /> }
name={ plugin.title }
pills={ plugin.tags.map( ( tag ) => (
<Pill key={ tag.slug }>{ tag.name }</Pill>
) ) }
description={ plugin.description }
button={ renderButton() }
/>
);
};

View File

@ -1 +1,2 @@
export { PluginCardBody } from './PluginCardBody';
export { SmartPluginCardBody } from './SmartPluginCardBody';

View File

@ -0,0 +1,24 @@
/**
* External dependencies
*/
import { useCallback } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { PLUGINS_STORE_NAME } from '@woocommerce/data';
export const useIsPluginInstalledNotActivated = () => {
const { getPluginInstallState } = useSelect( ( select ) => {
return {
getPluginInstallState:
select( PLUGINS_STORE_NAME ).getPluginInstallState,
};
} );
const isPluginInstalledNotActivated = useCallback(
( slug: string ) => {
return getPluginInstallState( slug ) === 'installed';
},
[ getPluginInstallState ]
);
return { isPluginInstalledNotActivated };
};

View File

@ -4,6 +4,7 @@ export { default as ProductIcon } from './product-icon';
export { default as Slider } from './slider';
export { default as ReadBlogMessage } from './ReadBlogMessage';
export { CollapsibleCard, CardBody, CardDivider } from './CollapsibleCard';
export { PluginCardBody } from './PluginCardBody';
export { PluginCardBody, SmartPluginCardBody } from './PluginCardBody';
export { CardHeaderTitle } from './CardHeaderTitle';
export { CardHeaderDescription } from './CardHeaderDescription';
export { CenteredSpinner } from './CenteredSpinner';

View File

@ -0,0 +1,10 @@
export const TYPES = {
RECEIVE_REGISTERED_CHANNELS_SUCCESS:
'RECEIVE_REGISTERED_CHANNELS_SUCCESS' as const,
RECEIVE_REGISTERED_CHANNELS_ERROR:
'RECEIVE_REGISTERED_CHANNELS_ERROR' as const,
RECEIVE_RECOMMENDED_CHANNELS_SUCCESS:
'RECEIVE_RECOMMENDED_CHANNELS_SUCCESS' as const,
RECEIVE_RECOMMENDED_CHANNELS_ERROR:
'RECEIVE_RECOMMENDED_CHANNELS_ERROR' as const,
};

View File

@ -0,0 +1,46 @@
/**
* Internal dependencies
*/
import { TYPES } from './action-types';
import { ApiFetchError, RegisteredChannel, RecommendedChannel } from './types';
export const receiveRegisteredChannelsSuccess = (
channels: Array< RegisteredChannel >
) => {
return {
type: TYPES.RECEIVE_REGISTERED_CHANNELS_SUCCESS,
payload: channels,
};
};
export const receiveRegisteredChannelsError = ( error: ApiFetchError ) => {
return {
type: TYPES.RECEIVE_REGISTERED_CHANNELS_ERROR,
payload: error,
error: true,
};
};
export const receiveRecommendedChannelsSuccess = (
channels: Array< RecommendedChannel >
) => {
return {
type: TYPES.RECEIVE_RECOMMENDED_CHANNELS_SUCCESS,
payload: channels,
};
};
export const receiveRecommendedChannelsError = ( error: ApiFetchError ) => {
return {
type: TYPES.RECEIVE_RECOMMENDED_CHANNELS_ERROR,
payload: error,
error: true,
};
};
export type Action = ReturnType<
| typeof receiveRegisteredChannelsSuccess
| typeof receiveRegisteredChannelsError
| typeof receiveRecommendedChannelsSuccess
| typeof receiveRecommendedChannelsError
>;

View File

@ -0,0 +1,2 @@
export const STORE_KEY = 'wc/marketing-multichannel';
export const API_NAMESPACE = '/wc-admin/marketing';

View File

@ -0,0 +1,14 @@
/**
* Internal dependencies
*/
import { ApiFetchError } from './types';
export const isObject = ( obj: unknown ): obj is Record< string, unknown > => {
return !! obj && typeof obj === 'object';
};
export const isApiFetchError = ( obj: unknown ): obj is ApiFetchError => {
return (
isObject( obj ) && 'code' in obj && 'data' in obj && 'message' in obj
);
};

View File

@ -0,0 +1,26 @@
/**
* External dependencies
*/
import { createReduxStore, register } from '@wordpress/data';
import { Reducer, AnyAction } from 'redux';
import { controls } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import { State } from './types';
import { STORE_KEY } from './constants';
import { reducer } from './reducer';
import * as actions from './actions';
import * as selectors from './selectors';
import * as resolvers from './resolvers';
const store = createReduxStore( STORE_KEY, {
reducer: reducer as Reducer< State, AnyAction >,
actions,
selectors,
resolvers,
controls,
} );
register( store );

View File

@ -0,0 +1,61 @@
/**
* External dependencies
*/
import type { Reducer } from 'redux';
/**
* Internal dependencies
*/
import { State } from './types';
import { Action } from './actions';
import { TYPES } from './action-types';
const initialState = {
registeredChannels: {
data: undefined,
error: undefined,
},
recommendedChannels: {
data: undefined,
error: undefined,
},
};
export const reducer: Reducer< State, Action > = (
state = initialState,
action
) => {
switch ( action.type ) {
case TYPES.RECEIVE_REGISTERED_CHANNELS_SUCCESS:
return {
...state,
registeredChannels: {
data: action.payload,
},
};
case TYPES.RECEIVE_REGISTERED_CHANNELS_ERROR:
return {
...state,
registeredChannels: {
error: action.payload,
},
};
case TYPES.RECEIVE_RECOMMENDED_CHANNELS_SUCCESS:
return {
...state,
recommendedChannels: {
data: action.payload,
},
};
case TYPES.RECEIVE_RECOMMENDED_CHANNELS_ERROR:
return {
...state,
recommendedChannels: {
error: action.payload,
},
};
default:
return state;
}
};

View File

@ -0,0 +1,49 @@
/**
* External dependencies
*/
import { apiFetch } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import {
receiveRegisteredChannelsSuccess,
receiveRegisteredChannelsError,
receiveRecommendedChannelsSuccess,
receiveRecommendedChannelsError,
} from './actions';
import { RegisteredChannel, RecommendedChannel } from './types';
import { API_NAMESPACE } from './constants';
import { isApiFetchError } from './guards';
export function* getRegisteredChannels() {
try {
const data: RegisteredChannel[] = yield apiFetch( {
path: `${ API_NAMESPACE }/channels`,
} );
yield receiveRegisteredChannelsSuccess( data );
} catch ( error ) {
if ( isApiFetchError( error ) ) {
yield receiveRegisteredChannelsError( error );
}
throw error;
}
}
export function* getRecommendedChannels() {
try {
const data: RecommendedChannel[] = yield apiFetch( {
path: `${ API_NAMESPACE }/recommendations?category=channels`,
} );
yield receiveRecommendedChannelsSuccess( data );
} catch ( error ) {
if ( isApiFetchError( error ) ) {
yield receiveRecommendedChannelsError( error );
}
throw error;
}
}

View File

@ -0,0 +1,12 @@
/**
* Internal dependencies
*/
import { State } from './types';
export const getRegisteredChannels = ( state: State ) => {
return state.registeredChannels;
};
export const getRecommendedChannels = ( state: State ) => {
return state.recommendedChannels;
};

View File

@ -0,0 +1,56 @@
export type ApiFetchError = {
code: string;
data: {
status: number;
};
message: string;
};
export type RegisteredChannel = {
slug: string;
is_setup_completed: boolean;
settings_url: string;
name: string;
description: string;
product_listings_status: string;
errors_count: number;
icon: string;
};
export type RegisteredChannelsState = {
data?: Array< RegisteredChannel >;
error?: ApiFetchError;
};
type Subcategory = {
slug: string;
name: string;
};
type Tag = {
slug: string;
name: string;
};
export type RecommendedChannel = {
title: string;
description: string;
url: string;
direct_install: boolean;
icon: string;
product: string;
plugin: string;
categories: Array< string >;
subcategories: Array< Subcategory >;
tags: Array< Tag >;
};
export type RecommendedChannelsState = {
data?: Array< RecommendedChannel >;
error?: ApiFetchError;
};
export type State = {
registeredChannels: RegisteredChannelsState;
recommendedChannels: RecommendedChannelsState;
};

View File

@ -1 +1,3 @@
export { useInstalledPlugins } from './useInstalledPlugins';
export { useRegisteredChannels } from './useRegisteredChannels';
export { useRecommendedChannels } from './useRecommendedChannels';

View File

@ -0,0 +1,48 @@
/**
* External dependencies
*/
import { useSelect } from '@wordpress/data';
import { PLUGINS_STORE_NAME } from '@woocommerce/data';
import { differenceWith } from 'lodash';
/**
* Internal dependencies
*/
import { STORE_KEY } from '~/marketing/data-multichannel/constants';
import {
RecommendedChannelsState,
RecommendedChannel,
} from '~/marketing/data-multichannel/types';
type UseRecommendedChannels = {
loading: boolean;
data?: Array< RecommendedChannel >;
};
export const useRecommendedChannels = (): UseRecommendedChannels => {
return useSelect( ( select ) => {
const { hasFinishedResolution, getRecommendedChannels } =
select( STORE_KEY );
const { data, error } =
getRecommendedChannels< RecommendedChannelsState >();
const { getActivePlugins } = select( PLUGINS_STORE_NAME );
const activePlugins = getActivePlugins();
/**
* Recommended channels that are not in "active" state,
* i.e. channels that are not installed or not activated yet.
*/
const nonActiveRecommendedChannels =
data &&
differenceWith( data, activePlugins, ( a, b ) => {
return a.product === b;
} );
return {
loading: ! hasFinishedResolution( 'getRecommendedChannels' ),
data: nonActiveRecommendedChannels,
error,
};
} );
};

View File

@ -0,0 +1,81 @@
/**
* External dependencies
*/
import { useDispatch, useSelect } from '@wordpress/data';
import { __, sprintf } from '@wordpress/i18n';
import { useCallback } from '@wordpress/element';
/**
* Internal dependencies
*/
import { RegisteredChannel, SyncStatusType } from '~/marketing/types';
import { STORE_KEY } from '~/marketing/data-multichannel/constants';
import {
ApiFetchError,
RegisteredChannel as APIRegisteredChannel,
RegisteredChannelsState,
} from '~/marketing/data-multichannel/types';
type UseRegisteredChannels = {
loading: boolean;
data?: Array< RegisteredChannel >;
error?: ApiFetchError;
refetch: () => void;
};
/**
* A object that maps the product listings status in
* plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php backend
* to SyncStatusType frontend.
*/
const statusMap: Record< string, SyncStatusType > = {
'sync-in-progress': 'syncing',
'sync-failed': 'failed',
synced: 'synced',
};
const convert = ( data: APIRegisteredChannel ): RegisteredChannel => {
const issueType = data.errors_count >= 1 ? 'error' : 'none';
const issueText =
data.errors_count >= 1
? sprintf(
// translators: %d: The number of issues to resolve.
__( '%d issues to resolve', 'woocommerce' ),
data.errors_count
)
: __( 'No issues to resolve', 'woocommerce' );
return {
slug: data.slug,
title: data.name,
description: data.description,
icon: data.icon,
isSetupCompleted: data.is_setup_completed,
setupUrl: data.settings_url,
manageUrl: data.settings_url,
syncStatus: statusMap[ data.product_listings_status ],
issueType,
issueText,
};
};
export const useRegisteredChannels = (): UseRegisteredChannels => {
const { invalidateResolution } = useDispatch( STORE_KEY );
const refetch = useCallback( () => {
invalidateResolution( 'getRegisteredChannels' );
}, [ invalidateResolution ] );
return useSelect( ( select ) => {
const { hasFinishedResolution, getRegisteredChannels } =
select( STORE_KEY );
const state = getRegisteredChannels< RegisteredChannelsState >();
return {
loading: ! hasFinishedResolution( 'getRegisteredChannels' ),
data: state.data?.map( convert ),
error: state.error,
refetch,
};
} );
};

View File

@ -0,0 +1,13 @@
.woocommerce-marketing-channels-card {
.components-card-header {
flex-direction: column;
align-items: flex-start;
gap: $gap-smallest;
}
.components-button.is-link {
@include font-size( 14 );
font-weight: 600;
text-decoration: none;
}
}

View File

@ -0,0 +1,119 @@
/**
* External dependencies
*/
import { Fragment, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import {
Card,
CardHeader,
CardBody,
CardDivider,
Button,
Icon,
} from '@wordpress/components';
import { chevronUp, chevronDown } from '@wordpress/icons';
/**
* Internal dependencies
*/
import { RecommendedChannel } from '~/marketing/data-multichannel/types';
import {
CardHeaderTitle,
CardHeaderDescription,
SmartPluginCardBody,
} from '~/marketing/components';
import { RegisteredChannel } from '~/marketing/types';
import { RegisteredChannelCardBody } from './RegisteredChannelCardBody';
import './Channels.scss';
type ChannelsProps = {
registeredChannels: Array< RegisteredChannel >;
recommendedChannels: Array< RecommendedChannel >;
onInstalledAndActivated?: () => void;
};
export const Channels: React.FC< ChannelsProps > = ( {
registeredChannels,
recommendedChannels,
onInstalledAndActivated,
} ) => {
const hasRegisteredChannels = registeredChannels.length >= 1;
/**
* State to collapse / expand the recommended channels.
* Initial state is expanded if there are no registered channels in first page load.
*/
const [ expanded, setExpanded ] = useState( ! hasRegisteredChannels );
return (
<Card className="woocommerce-marketing-channels-card">
<CardHeader>
<CardHeaderTitle>
{ __( 'Channels', 'woocommerce' ) }
</CardHeaderTitle>
{ ! hasRegisteredChannels && (
<CardHeaderDescription>
{ __(
'Start by adding a channel to your store',
'woocommerce'
) }
</CardHeaderDescription>
) }
</CardHeader>
{ /* Registered channels section. */ }
{ registeredChannels.map( ( el, idx ) => {
return (
<Fragment key={ el.slug }>
<RegisteredChannelCardBody registeredChannel={ el } />
{ idx !== registeredChannels.length - 1 && (
<CardDivider />
) }
</Fragment>
);
} ) }
{ /* Recommended channels section. */ }
{ recommendedChannels.length >= 1 && (
<div>
{ hasRegisteredChannels && (
<>
<CardDivider />
<CardBody>
<Button
variant="link"
onClick={ () => setExpanded( ! expanded ) }
>
{ __( 'Add channels', 'woocommerce' ) }
<Icon
icon={
expanded ? chevronUp : chevronDown
}
size={ 24 }
/>
</Button>
</CardBody>
</>
) }
{ expanded &&
recommendedChannels.map( ( el, idx ) => {
return (
<Fragment key={ el.plugin }>
<SmartPluginCardBody
plugin={ el }
onInstalledAndActivated={
onInstalledAndActivated
}
/>
{ idx !==
recommendedChannels.length - 1 && (
<CardDivider />
) }
</Fragment>
);
} ) }
</div>
) }
</Card>
);
};

View File

@ -0,0 +1,15 @@
.woocommerce-marketing-issue-status {
display: flex;
align-items: center;
gap: $gap-smallest;
&__error {
color: $alert-red;
fill: $alert-red;
}
&__warning {
color: $alert-yellow;
fill: $alert-yellow;
}
}

View File

@ -0,0 +1,56 @@
/**
* External dependencies
*/
import GridiconNotice from 'gridicons/dist/notice';
import classnames from 'classnames';
/**
* Internal dependencies
*/
import { RegisteredChannel } from '~/marketing/types';
import { iconSize } from './iconSize';
import './IssueStatus.scss';
type IssueStatusPropsType = {
registeredChannel: RegisteredChannel;
};
const issueStatusClassName = 'woocommerce-marketing-issue-status';
export const IssueStatus: React.FC< IssueStatusPropsType > = ( {
registeredChannel,
} ) => {
if ( registeredChannel.issueType === 'error' ) {
return (
<div
className={ classnames(
issueStatusClassName,
`${ issueStatusClassName }__error`
) }
>
<GridiconNotice size={ iconSize } />
{ registeredChannel.issueText }
</div>
);
}
if ( registeredChannel.issueType === 'warning' ) {
return (
<div
className={ classnames(
issueStatusClassName,
`${ issueStatusClassName }__warning`
) }
>
<GridiconNotice size={ iconSize } />
{ registeredChannel.issueText }
</div>
);
}
return (
<div className={ issueStatusClassName }>
{ registeredChannel.issueText }
</div>
);
};

View File

@ -0,0 +1,10 @@
.woocommerce-marketing-registered-channel-card-body {
.woocommerce-marketing-registered-channel-description {
display: flex;
gap: $gap-smaller;
&__separator::before {
content: '';
}
}
}

View File

@ -0,0 +1,75 @@
/**
* External dependencies
*/
import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { PluginCardBody } from '~/marketing/components';
import { RegisteredChannel } from '~/marketing/types';
import { SyncStatus } from './SyncStatus';
import { IssueStatus } from './IssueStatus';
import './RegisteredChannelCardBody.scss';
type RegisteredChannelCardBodyProps = {
registeredChannel: RegisteredChannel;
};
export const RegisteredChannelCardBody: React.FC<
RegisteredChannelCardBodyProps
> = ( { registeredChannel } ) => {
/**
* The description section in the channel card.
*
* If setup is not completed, this would be the channel description.
*
* If setup is completed, this would be an element with sync status and issue status.
*/
const description = ! registeredChannel.isSetupCompleted ? (
registeredChannel.description
) : (
<div className="woocommerce-marketing-registered-channel-description">
{ registeredChannel.syncStatus && (
<>
<SyncStatus status={ registeredChannel.syncStatus } />
<div className="woocommerce-marketing-registered-channel-description__separator" />
</>
) }
<IssueStatus registeredChannel={ registeredChannel } />
</div>
);
/**
* The action button in the channel card.
*
* If setup is not completed, this would be a "Finish setup" primary button.
*
* If setup is completed, this would be a "Manage" secondary button.
*/
const button = ! registeredChannel.isSetupCompleted ? (
<Button variant="primary" href={ registeredChannel.setupUrl }>
{ __( 'Finish setup', 'woocommerce' ) }
</Button>
) : (
<Button variant="secondary" href={ registeredChannel.manageUrl }>
{ __( 'Manage', 'woocommerce' ) }
</Button>
);
return (
<PluginCardBody
className="woocommerce-marketing-registered-channel-card-body"
icon={
<img
src={ registeredChannel.icon }
alt={ registeredChannel.title }
/>
}
name={ registeredChannel.title }
description={ description }
button={ button }
/>
);
};

View File

@ -0,0 +1,16 @@
.woocommerce-marketing-sync-status {
display: flex;
align-items: center;
gap: $gap-smallest;
&__failed {
color: $alert-red;
fill: $alert-red;
}
&__syncing,
&__synced {
color: $studio-green-50;
fill: $studio-green-50;
}
}

View File

@ -0,0 +1,52 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import GridiconCheckmarkCircle from 'gridicons/dist/checkmark-circle';
import GridiconSync from 'gridicons/dist/sync';
import GridiconNotice from 'gridicons/dist/notice';
import classnames from 'classnames';
/**
* Internal dependencies
*/
import { SyncStatusType } from '~/marketing/types';
import { iconSize } from './iconSize';
import './SyncStatus.scss';
type SyncStatusPropsType = {
status: SyncStatusType;
};
const className = 'woocommerce-marketing-sync-status';
export const SyncStatus: React.FC< SyncStatusPropsType > = ( { status } ) => {
if ( status === 'failed' ) {
return (
<div
className={ classnames( className, `${ className }__failed` ) }
>
<GridiconNotice size={ iconSize } />
{ __( 'Sync failed', 'woocommerce' ) }
</div>
);
}
if ( status === 'syncing' ) {
return (
<div
className={ classnames( className, `${ className }__syncing` ) }
>
<GridiconSync size={ iconSize } />
{ __( 'Syncing', 'woocommerce' ) }
</div>
);
}
return (
<div className={ classnames( className, `${ className }__synced` ) }>
<GridiconCheckmarkCircle size={ iconSize } />
{ __( 'Synced', 'woocommerce' ) }
</div>
);
};

View File

@ -0,0 +1 @@
export const iconSize = 18;

View File

@ -0,0 +1 @@
export { Channels } from './Channels';

View File

@ -6,22 +6,52 @@ import { useUser } from '@woocommerce/data';
/**
* Internal dependencies
*/
import '~/marketing/data';
import '~/marketing/data-multichannel';
import { CenteredSpinner } from '~/marketing/components';
import {
useRegisteredChannels,
useRecommendedChannels,
} from '~/marketing/hooks';
import { getAdminSetting } from '~/utils/admin-settings';
import { Channels } from './Channels';
import { InstalledExtensions } from './InstalledExtensions';
import { DiscoverTools } from './DiscoverTools';
import { LearnMarketing } from './LearnMarketing';
import '~/marketing/data';
import './MarketingOverviewMultichannel.scss';
export const MarketingOverviewMultichannel: React.FC = () => {
const {
loading: loadingRegistered,
data: dataRegistered,
refetch,
} = useRegisteredChannels();
const { loading: loadingRecommended, data: dataRecommended } =
useRecommendedChannels();
const { currentUserCan } = useUser();
const shouldShowExtensions =
getAdminSetting( 'allowMarketplaceSuggestions', false ) &&
currentUserCan( 'install_plugins' );
if (
( loadingRegistered && ! dataRegistered ) ||
( loadingRecommended && ! dataRecommended )
) {
return <CenteredSpinner />;
}
return (
<div className="woocommerce-marketing-overview-multichannel">
{ dataRegistered &&
dataRecommended &&
( dataRegistered.length || dataRecommended.length ) && (
<Channels
registeredChannels={ dataRegistered }
recommendedChannels={ dataRecommended }
onInstalledAndActivated={ refetch }
/>
) }
<InstalledExtensions />
{ shouldShowExtensions && <DiscoverTools /> }
<LearnMarketing />

View File

@ -0,0 +1,15 @@
export type SyncStatusType = 'synced' | 'syncing' | 'failed';
export type IssueTypeType = 'error' | 'warning' | 'none';
export type RegisteredChannel = {
slug: string;
title: string;
description: string;
icon: string;
isSetupCompleted: boolean;
setupUrl: string;
manageUrl: string;
syncStatus: SyncStatusType;
issueType: IssueTypeType;
issueText: string;
};

View File

@ -1,2 +1,7 @@
export { InstalledPlugin } from './InstalledPlugin';
export { RecommendedPlugin } from './RecommendedPlugin';
export {
SyncStatusType,
IssueTypeType,
RegisteredChannel,
} from './RegisteredChannel';

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add a new Channels card in multichannel marketing page.