diff --git a/plugins/woocommerce-admin/client/marketplace/assets/images/alert.svg b/plugins/woocommerce-admin/client/marketplace/assets/images/alert.svg new file mode 100644 index 00000000000..a28cc095e32 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/assets/images/alert.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/my-subscriptions.scss b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/my-subscriptions.scss index 32d6b569588..3a6884b453f 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/my-subscriptions.scss +++ b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/my-subscriptions.scss @@ -110,10 +110,13 @@ padding: 0 12px; } -.woocommerce-marketplace__my-subscriptions__table .components-button.is-link { +.woocommerce-marketplace__my-subscriptions .components-button.is-link { text-decoration: none; padding: 6px 12px; } +.woocommerce-marketplace__my-subscriptions .components-button.is-secondary:hover:not(:disabled) { + color: var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)); +} .woocommerce-marketplace__my-subscriptions--connect { display: flex; @@ -157,3 +160,50 @@ margin-right: $grid-unit-10; } } + +.woocommerce-marketplace__my-subscriptions__notices { + .components-notice { + margin-left: 0; + margin-right: 0; + background-color: #fff; + box-shadow: 0 2px 6px 0 rgba($gray-100, 0.05); + border: 1px solid var(--gutenberg-gray-100, #f0f0f0); + padding-right: $grid-unit-15; + + &::before { + content: ''; + display: block; + width: 4px; + height: 100%; + background-color: var(--wp-admin-theme-color, #007cba); + position: absolute; + left: 0; + top: 0; + bottom: 0; + } + + &.is-error::before { + background-color: $alert-red; + } + + .components-notice__content { + display: flex; + align-items: center; + gap: $grid-unit-15; + } + .components-notice__dismiss.has-icon { + width: 24px; + min-width: 24px; + height: 24px; + align-self: center; + padding: $grid-unit-05; + > svg { + fill: $gray-900; + } + } + } + .components-notice__action.components-button.is-link { + margin: 0; + padding: 0; + } +} diff --git a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/my-subscriptions.tsx b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/my-subscriptions.tsx index 75dd4465e91..17ec6c2a756 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/my-subscriptions.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/my-subscriptions.tsx @@ -3,9 +3,9 @@ */ import { getNewPath } from '@woocommerce/navigation'; import { Button, Tooltip } from '@wordpress/components'; +import { help } from '@wordpress/icons'; import { useContext } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; -import { help } from '@wordpress/icons'; /** * Internal dependencies @@ -22,6 +22,7 @@ import { installedSubscriptionRow, } from './table/table-rows'; import { Subscription } from './types'; +import Notices from './notices'; export default function MySubscriptions(): JSX.Element { const { subscriptions, isLoading } = useContext( SubscriptionsContext ); @@ -78,6 +79,9 @@ export default function MySubscriptions(): JSX.Element { return (
+
+ +

{ __( 'Installed on this store', 'woocommerce' ) } diff --git a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/notices.tsx b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/notices.tsx new file mode 100644 index 00000000000..e91443cfb4a --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/notices.tsx @@ -0,0 +1,49 @@ +/** + * External dependencies + */ +import { Notice } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import Alert from '../../assets/images/alert.svg'; +import { Notice as NoticeType } from '../../contexts/types'; +import { noticeStore } from '../../contexts/notice-store'; +import { removeNotice } from '../../utils/functions'; + +export default function Notices() { + const notices: NoticeType[] = useSelect( + ( select ) => select( noticeStore ).notices(), + [] + ); + + const actions = ( notice: NoticeType ) => { + if ( ! notice.options?.actions ) { + return []; + } + return notice.options?.actions.map( ( action ) => { + return { + ...action, + variant: 'link', + className: 'is-link', + }; + } ); + }; + + const errorNotices = []; + for ( const notice of notices ) { + errorNotices.push( + removeNotice( notice.productKey ) } + key={ notice.productKey } + actions={ actions( notice ) } + > + + { notice.message } + + ); + } + return <>{ errorNotices }; +} diff --git a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/connect-button.tsx b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/connect-button.tsx index 233b6755c89..29e6d6cd4a4 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/connect-button.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/connect-button.tsx @@ -2,7 +2,6 @@ * External dependencies */ import { Button, Icon } from '@wordpress/components'; -import { useDispatch } from '@wordpress/data'; import { useContext, useState } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; @@ -10,8 +9,13 @@ import { __, sprintf } from '@wordpress/i18n'; * Internal dependencies */ import { SubscriptionsContext } from '../../../../contexts/subscriptions-context'; -import { connectProduct } from '../../../../utils/functions'; +import { + addNotice, + connectProduct, + removeNotice, +} from '../../../../utils/functions'; import { Subscription } from '../../types'; +import { NoticeStatus } from '../../../../contexts/types'; interface ConnectProps { subscription: Subscription; @@ -21,21 +25,22 @@ interface ConnectProps { export default function ConnectButton( props: ConnectProps ) { const [ isConnecting, setIsConnecting ] = useState( false ); - const { createWarningNotice, createSuccessNotice } = - useDispatch( 'core/notices' ); const { loadSubscriptions } = useContext( SubscriptionsContext ); const connect = () => { setIsConnecting( true ); + removeNotice( props.subscription.product_key ); connectProduct( props.subscription ) .then( () => { loadSubscriptions( false ).then( () => { - createSuccessNotice( + addNotice( + props.subscription.product_key, sprintf( // translators: %s is the product name. __( '%s successfully connected.', 'woocommerce' ), props.subscription.product_name ), + NoticeStatus.Success, { icon: , } @@ -47,12 +52,14 @@ export default function ConnectButton( props: ConnectProps ) { } ); } ) .catch( () => { - createWarningNotice( + addNotice( + props.subscription.product_key, sprintf( // translators: %s is the product name. __( '%s couldn’t be connected.', 'woocommerce' ), props.subscription.product_name ), + NoticeStatus.Error, { actions: [ { diff --git a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/install.tsx b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/install.tsx index 4431d4a2c7b..64cdb62a2fb 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/install.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/install.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { Button, Icon } from '@wordpress/components'; -import { dispatch, useDispatch, useSelect } from '@wordpress/data'; +import { dispatch, useSelect } from '@wordpress/data'; import { useContext } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; @@ -10,17 +10,20 @@ import { __, sprintf } from '@wordpress/i18n'; * Internal dependencies */ import { SubscriptionsContext } from '../../../../contexts/subscriptions-context'; -import { installProduct } from '../../../../utils/functions'; +import { + addNotice, + installProduct, + removeNotice, +} from '../../../../utils/functions'; import { Subscription } from '../../types'; import { installingStore } from '../../../../contexts/install-store'; +import { NoticeStatus } from '../../../../contexts/types'; interface InstallProps { subscription: Subscription; } export default function Install( props: InstallProps ) { - const { createWarningNotice, createSuccessNotice } = - useDispatch( 'core/notices' ); const { loadSubscriptions } = useContext( SubscriptionsContext ); const loading: boolean = useSelect( @@ -45,15 +48,18 @@ export default function Install( props: InstallProps ) { const install = () => { startInstall(); + removeNotice( props.subscription.product_key ); installProduct( props.subscription ) .then( () => { loadSubscriptions( false ).then( () => { - createSuccessNotice( + addNotice( + props.subscription.product_key, sprintf( // translators: %s is the product name. __( '%s successfully installed.', 'woocommerce' ), props.subscription.product_name ), + NoticeStatus.Success, { icon: , } @@ -71,14 +77,19 @@ export default function Install( props: InstallProps ) { if ( error?.success === false && error?.data.message ) { errorMessage += ' ' + error.data.message; } - createWarningNotice( errorMessage, { - actions: [ - { - label: __( 'Try again', 'woocommerce' ), - onClick: install, - }, - ], - } ); + addNotice( + props.subscription.product_key, + errorMessage, + NoticeStatus.Error, + { + actions: [ + { + label: __( 'Try again', 'woocommerce' ), + onClick: install, + }, + ], + } + ); stopInstall(); } ); } ); diff --git a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/update.tsx b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/update.tsx index e5463a316bf..c0dd68e01d5 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/update.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/update.tsx @@ -2,7 +2,6 @@ * External dependencies */ import { Button, Icon } from '@wordpress/components'; -import { useDispatch } from '@wordpress/data'; import { useContext, useState } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; @@ -14,6 +13,12 @@ import { Subscription } from '../../types'; import ConnectModal from './connect-modal'; import RenewModal from './renew-modal'; import SubscribeModal from './subscribe-modal'; +import { + addNotice, + removeNotice, + updateProduct, +} from '../../../../utils/functions'; +import { NoticeStatus } from '../../../../contexts/types'; interface UpdateProps { subscription: Subscription; @@ -22,8 +27,6 @@ interface UpdateProps { export default function Update( props: UpdateProps ) { const [ showModal, setShowModal ] = useState( false ); const [ isUpdating, setIsUpdating ] = useState( false ); - const { createWarningNotice, createSuccessNotice } = - useDispatch( 'core/notices' ); const { loadSubscriptions } = useContext( SubscriptionsContext ); const canUpdate = @@ -37,13 +40,16 @@ export default function Update( props: UpdateProps ) { setShowModal( true ); return; } + removeNotice( props.subscription.product_key ); if ( ! window.wp.updates ) { - createWarningNotice( + addNotice( + props.subscription.product_key, sprintf( // translators: %s is the product name. __( '%s couldn’t be updated.', 'woocommerce' ), props.subscription.product_name ), + NoticeStatus.Error, { actions: [ { @@ -63,36 +69,33 @@ export default function Update( props: UpdateProps ) { setIsUpdating( true ); - const action = - props.subscription.local.type === 'plugin' - ? 'update-plugin' - : 'update-theme'; - window.wp.updates - .ajax( action, { - slug: props.subscription.local.slug, - plugin: props.subscription.local.path, - theme: props.subscription.local.path, - } ) + updateProduct( props.subscription ) .then( () => { - loadSubscriptions( false ); - createSuccessNotice( - sprintf( - // translators: %s is the product name. - __( '%s updated successfully.', 'woocommerce' ), - props.subscription.product_name - ), - { - icon: , - } - ); + loadSubscriptions( false ).then( () => { + addNotice( + props.subscription.product_key, + sprintf( + // translators: %s is the product name. + __( '%s updated successfully.', 'woocommerce' ), + props.subscription.product_name + ), + NoticeStatus.Success, + { + icon: , + } + ); + setIsUpdating( false ); + } ); } ) .catch( () => { - createWarningNotice( + addNotice( + props.subscription.product_key, sprintf( // translators: %s is the product name. __( '%s couldn’t be updated.', 'woocommerce' ), props.subscription.product_name ), + NoticeStatus.Error, { actions: [ { @@ -102,8 +105,6 @@ export default function Update( props: UpdateProps ) { ], } ); - } ) - .always( () => { setIsUpdating( false ); } ); } diff --git a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/rows/actions-dropdown-menu.tsx b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/rows/actions-dropdown-menu.tsx index 2d26a6c39b5..d87e7c6c302 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/rows/actions-dropdown-menu.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/rows/actions-dropdown-menu.tsx @@ -15,7 +15,7 @@ export default function ActionsDropdownMenu() { ) } controls={ [ { - title: __( 'Manage in Woo.com', 'woocommerce' ), + title: __( 'Manage on Woo.com', 'woocommerce' ), icon: external, onClick: () => { window.location.href = diff --git a/plugins/woocommerce-admin/client/marketplace/contexts/install-store.tsx b/plugins/woocommerce-admin/client/marketplace/contexts/install-store.tsx index 48659c4bbb5..46d76eaa739 100644 --- a/plugins/woocommerce-admin/client/marketplace/contexts/install-store.tsx +++ b/plugins/woocommerce-admin/client/marketplace/contexts/install-store.tsx @@ -3,31 +3,22 @@ */ import { createReduxStore, register } from '@wordpress/data'; +/** + * Internal dependencies + */ +import { InstallingState } from './types'; + const INSTALLING_STORE_NAME = 'woocommerce-admin/installing'; -interface InstallingState { - installingProducts: string[]; +export interface InstallingStateErrorAction { + label: string; + onClick: () => void; } const DEFAULT_STATE: InstallingState = { installingProducts: [], }; -const actions = { - startInstalling( productKey: string ) { - return { - type: 'START_INSTALLING', - productKey, - }; - }, - stopInstalling( productKey: string ) { - return { - type: 'STOP_INSTALLING', - productKey, - }; - }, -}; - const store = createReduxStore( INSTALLING_STORE_NAME, { reducer( state: InstallingState | undefined = DEFAULT_STATE, action ) { switch ( action.type ) { @@ -52,9 +43,20 @@ const store = createReduxStore( INSTALLING_STORE_NAME, { return state; }, - - actions, - + actions: { + startInstalling( productKey: string ) { + return { + type: 'START_INSTALLING', + productKey, + }; + }, + stopInstalling( productKey: string ) { + return { + type: 'STOP_INSTALLING', + productKey, + }; + }, + }, selectors: { isInstalling( state: InstallingState | undefined, diff --git a/plugins/woocommerce-admin/client/marketplace/contexts/notice-store.tsx b/plugins/woocommerce-admin/client/marketplace/contexts/notice-store.tsx new file mode 100644 index 00000000000..9616375b2dd --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/contexts/notice-store.tsx @@ -0,0 +1,90 @@ +/** + * External dependencies + */ +import { createReduxStore, register } from '@wordpress/data'; +import { Options } from '@wordpress/notices'; + +/** + * Internal dependencies + */ +import { NoticeState, Notice, NoticeStatus } from './types'; + +const NOTICE_STORE_NAME = 'woocommerce-admin/subscription-notices'; + +const DEFAULT_STATE: NoticeState = { + notices: {}, +}; + +const store = createReduxStore( NOTICE_STORE_NAME, { + reducer( state: NoticeState | undefined = DEFAULT_STATE, action ) { + switch ( action.type ) { + case 'ADD_NOTICE': + return { + ...state, + notices: { + ...state.notices, + [ action.productKey ]: { + productKey: action.productKey, + message: action.message, + status: action.status, + options: action.options, + }, + }, + }; + case 'REMOVE_NOTICE': + const notices = { ...state.notices }; + if ( notices[ action.productKey ] ) { + delete notices[ action.productKey ]; + } + return { + ...state, + notices, + }; + } + + return state; + }, + actions: { + addNotice( + productKey: string, + message: string, + status: NoticeStatus, + options?: Partial< Options > + ) { + return { + type: 'ADD_NOTICE', + productKey, + message, + status, + options, + }; + }, + removeNotice( productKey: string ) { + return { + type: 'REMOVE_NOTICE', + productKey, + }; + }, + }, + selectors: { + notices( state: NoticeState | undefined ): Notice[] { + if ( ! state ) { + return []; + } + return Object.values( state.notices ); + }, + getNotice( + state: NoticeState | undefined, + productKey: string + ): Notice | undefined { + if ( ! state ) { + return; + } + return state.notices[ productKey ]; + }, + }, +} ); + +register( store ); + +export { store as noticeStore, NOTICE_STORE_NAME }; diff --git a/plugins/woocommerce-admin/client/marketplace/contexts/types.ts b/plugins/woocommerce-admin/client/marketplace/contexts/types.ts index eb857e4930c..83e0d1c532d 100644 --- a/plugins/woocommerce-admin/client/marketplace/contexts/types.ts +++ b/plugins/woocommerce-admin/client/marketplace/contexts/types.ts @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { Options } from '@wordpress/notices'; + /** * Internal dependencies */ @@ -17,3 +22,25 @@ export type SubscriptionsContextType = { isLoading: boolean; setIsLoading: ( isLoading: boolean ) => void; }; + +export enum NoticeStatus { + Success = 'success', + Error = 'error', +} + +export interface Notice { + productKey: string; + message: string; + status: NoticeStatus; + options?: Partial< Options > | undefined; +} + +export interface NoticeState { + notices: { + [ key: string ]: Notice; + }; +} + +export interface InstallingState { + installingProducts: string[]; +} diff --git a/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx b/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx index 82a6c8525f1..f0e5488b434 100644 --- a/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx +++ b/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx @@ -3,7 +3,8 @@ */ import apiFetch from '@wordpress/api-fetch'; import { __, sprintf } from '@wordpress/i18n'; - +import { dispatch } from '@wordpress/data'; +import { Options } from '@wordpress/notices'; /** * Internal dependencies */ @@ -21,6 +22,8 @@ import { SearchAPIJSONType, SearchAPIProductType, } from '../components/product-list/types'; +import { NoticeStatus } from '../contexts/types'; +import { noticeStore } from '../contexts/notice-store'; interface ProductGroup { id: string; @@ -314,6 +317,35 @@ function installProduct( subscription: Subscription ): Promise< void > { } ); } +function updateProduct( subscription: Subscription ): Promise< void > { + return wpAjax( 'update-' + subscription.product_type, { + slug: subscription.local.slug, + [ subscription.product_type ]: subscription.local.path, + } ); +} + +function addNotice( + productKey: string, + message: string, + status?: NoticeStatus, + options?: Partial< Options > +) { + if ( status === NoticeStatus.Error ) { + dispatch( noticeStore ).addNotice( + productKey, + message, + status, + options + ); + } else { + dispatch( 'core/notices' ).createSuccessNotice( message, options ); + } +} + +const removeNotice = ( productKey: string ) => { + dispatch( noticeStore ).removeNotice( productKey ); +}; + // Append UTM parameters to a URL, being aware of existing query parameters const appendURLParams = ( url: string, @@ -342,4 +374,7 @@ export { fetchSearchResults, fetchSubscriptions, installProduct, + updateProduct, + addNotice, + removeNotice, }; diff --git a/plugins/woocommerce/changelog/add-my-subscriptions-notices b/plugins/woocommerce/changelog/add-my-subscriptions-notices new file mode 100644 index 00000000000..a6214e73e9e --- /dev/null +++ b/plugins/woocommerce/changelog/add-my-subscriptions-notices @@ -0,0 +1,4 @@ +Significance: patch +Type: update +Comment: Add My subscriptions error notices and move existing notices to helper functions. +