fix/cys intro refactor banner (#40561)

* tests

lint

* refactor cys intro banner

* refactor cys intro modal

* changelog

* fix banner classname
This commit is contained in:
RJ 2023-10-04 20:24:51 +08:00 committed by GitHub
parent 7918e1ee5c
commit 246b9a5c76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 423 additions and 145 deletions

View File

@ -1,19 +1,27 @@
/**
* External dependencies
*/
import { Modal } from '@wordpress/components';
import { Button, Modal } from '@wordpress/components';
import { Sender } from 'xstate';
import { __ } from '@wordpress/i18n';
import { Link } from '@woocommerce/components';
import { createInterpolateElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { customizeStoreStateMachineEvents } from '..';
import { ADMIN_URL } from '~/utils/admin-settings';
export const DesignChangeWarningModal = ( {
title,
children,
isOpen = false,
onRequestClose = () => {},
setOpenDesignChangeWarningModal,
sendEvent,
classname = 'woocommerce-customize-store__design-change-warning-modal',
}: {
title: string;
children?: JSX.Element[];
isOpen?: boolean;
onRequestClose?: () => void;
setOpenDesignChangeWarningModal: ( arg0: boolean ) => void;
sendEvent: Sender< customizeStoreStateMachineEvents >;
classname?: string;
} ) => {
if ( ! isOpen ) {
@ -23,11 +31,49 @@ export const DesignChangeWarningModal = ( {
return (
<Modal
className={ classname }
title={ title }
onRequestClose={ onRequestClose }
title={ __(
'Are you sure you want to start a new design?',
'woocommerce'
) }
onRequestClose={ () => setOpenDesignChangeWarningModal( false ) }
shouldCloseOnClickOutside={ false }
>
{ children }
<p>
{ createInterpolateElement(
__(
"The [AI designer*] will create a new store design for you, and you'll lose any changes you've made to your active theme. If you'd prefer to continue editing your theme, you can do so via the <EditorLink>Editor</EditorLink>.",
'woocommerce'
),
{
EditorLink: (
<Link
onClick={ () => {
window.open(
`${ ADMIN_URL }site-editor.php`,
'_blank'
);
return false;
} }
href=""
/>
),
}
) }
</p>
<div className="woocommerce-customize-store__design-change-warning-modal-footer">
<Button
onClick={ () => setOpenDesignChangeWarningModal( false ) }
variant="link"
>
{ __( 'Cancel', 'woocommerce' ) }
</Button>
<Button
onClick={ () => sendEvent( { type: 'DESIGN_WITH_AI' } ) }
variant="primary"
>
{ __( 'Design with AI', 'woocommerce' ) }
</Button>
</div>
</Modal>
);
};

View File

@ -1,16 +1,13 @@
/**
* External dependencies
*/
import { useState, createInterpolateElement } from '@wordpress/element';
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { chevronLeft } from '@wordpress/icons';
import classNames from 'classnames';
import { Link } from '@woocommerce/components';
import {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
__unstableMotion as motion,
Button,
} from '@wordpress/components';
/**
@ -20,9 +17,14 @@ import { CustomizeStoreComponent } from '../types';
import { SiteHub } from '../assembler-hub/site-hub';
import { ThemeCard } from './theme-card';
import { DesignChangeWarningModal } from './design-change-warning-modal';
import './intro.scss';
import { useNetworkStatus } from '~/utils/react-hooks/use-network-status';
import { ADMIN_URL } from '~/utils/admin-settings';
import './intro.scss';
import {
NetworkOfflineBanner,
ThemeHasModsBanner,
JetpackOfflineBanner,
DefaultBanner,
} from './intro-banners';
export type events =
| { type: 'DESIGN_WITH_AI' }
@ -35,6 +37,25 @@ export type events =
export * as actions from './actions';
export * as services from './services';
type BannerStatus = keyof typeof BANNER_COMPONENTS;
const BANNER_COMPONENTS = {
'network-offline': NetworkOfflineBanner,
'task-incomplete-active-theme-has-mods': ThemeHasModsBanner,
'jetpack-offline': JetpackOfflineBanner,
'existing-ai-theme': DefaultBanner,
default: DefaultBanner,
};
const MODAL_COMPONENTS = {
'no-modal': null,
'task-incomplete-override-design-changes': DesignChangeWarningModal,
'task-complete-with-ai-theme': null,
'task-complete-without-ai-theme': null,
};
type ModalStatus = keyof typeof MODAL_COMPONENTS;
export const Intro: CustomizeStoreComponent = ( { sendEvent, context } ) => {
const {
intro: { themeCards, activeThemeHasMods, customizeStoreTaskCompleted },
@ -44,97 +65,50 @@ export const Intro: CustomizeStoreComponent = ( { sendEvent, context } ) => {
const isNetworkOffline = useNetworkStatus();
let bannerText;
let bannerTitle;
let bannerButtonText;
if ( isNetworkOffline ) {
bannerText = __(
"Unfortunately, the [AI Store designer] isn't available right now as we can't detect your network. Please check your internet connection and try again.",
'woocommerce'
);
bannerTitle = __(
'Looking to design your store using AI?',
'woocommerce'
);
bannerButtonText = __( 'Retry', 'woocommerce' );
} else if ( isJetpackOffline ) {
bannerTitle = __(
'Looking to design your store using AI?',
'woocommerce'
);
bannerText = __(
"It looks like you're using Jetpack's offline mode — switch to online mode to start designing with AI.",
'woocommerce'
);
bannerButtonText = __( 'Find out how', 'woocommerce' );
} else {
bannerTitle = __(
'Use the power of AI to design your store',
'woocommerce'
);
bannerText = __(
'Design the look of your store, create pages, and generate copy using our built-in AI tools.',
'woocommerce'
);
bannerButtonText = __( 'Design with AI', 'woocommerce' );
}
const [ openDesignChangeWarningModal, setOpenDesignChangeWarningModal ] =
useState( false );
let modalStatus: ModalStatus = 'no-modal';
let bannerStatus: BannerStatus = 'default';
switch ( true ) {
case isNetworkOffline:
bannerStatus = 'network-offline';
break;
case isJetpackOffline as boolean:
bannerStatus = 'jetpack-offline';
break;
case activeThemeHasMods && ! customizeStoreTaskCompleted:
bannerStatus = 'task-incomplete-active-theme-has-mods';
break;
case context.intro.currentThemeIsAiGenerated:
bannerStatus = 'existing-ai-theme';
break;
}
switch ( true ) {
case openDesignChangeWarningModal === false:
modalStatus = 'no-modal';
break;
case bannerStatus === 'task-incomplete-active-theme-has-mods':
modalStatus = 'task-incomplete-override-design-changes';
break;
}
const ModalComponent = MODAL_COMPONENTS[ modalStatus ];
const BannerComponent = BANNER_COMPONENTS[ bannerStatus ];
return (
<>
{ openDesignChangeWarningModal && (
<DesignChangeWarningModal
title={ __(
'Are you sure you want to start a new design?',
'woocommerce'
) }
isOpen={ true }
onRequestClose={ () => {
setOpenDesignChangeWarningModal( false );
} }
>
<p>
{ createInterpolateElement(
__(
"The [AI designer*] will create a new store design for you, and you'll lose any changes you've made to your active theme. If you'd prefer to continue editing your theme, you can do so via the <EditorLink>Editor</EditorLink>.",
'woocommerce'
),
{
EditorLink: (
<Link
onClick={ () => {
window.open(
`${ ADMIN_URL }site-editor.php`,
'_blank'
);
return false;
} }
href=""
/>
),
}
) }
</p>
<div className="woocommerce-customize-store__design-change-warning-modal-footer">
<Button
onClick={ () =>
setOpenDesignChangeWarningModal( false )
}
variant="link"
>
{ __( 'Cancel', 'woocommerce' ) }
</Button>
<Button
onClick={ () =>
sendEvent( { type: 'DESIGN_WITH_AI' } )
}
variant="primary"
>
{ __( 'Design with AI', 'woocommerce' ) }
</Button>
</div>
</DesignChangeWarningModal>
{ ModalComponent && (
<ModalComponent
isOpen={ openDesignChangeWarningModal }
sendEvent={ sendEvent }
setOpenDesignChangeWarningModal={
setOpenDesignChangeWarningModal
}
/>
) }
<div className="woocommerce-customize-store-header">
<SiteHub
@ -168,49 +142,12 @@ export const Intro: CustomizeStoreComponent = ( { sendEvent, context } ) => {
</div>
<div className="woocommerce-customize-store-main">
<div
className={ classNames(
'woocommerce-customize-store-banner',
{
'offline-banner':
isNetworkOffline || isJetpackOffline,
}
) }
>
<div
className={ `woocommerce-customize-store-banner-content` }
>
<h1>{ bannerTitle }</h1>
<p>{ bannerText }</p>
<Button
onClick={ () => {
if ( isJetpackOffline ) {
sendEvent( {
type: 'JETPACK_OFFLINE_HOWTO',
} );
} else if ( isNetworkOffline === false ) {
if (
activeThemeHasMods &&
! customizeStoreTaskCompleted
) {
setOpenDesignChangeWarningModal(
true
);
} else {
sendEvent( {
type: 'DESIGN_WITH_AI',
} );
}
}
} }
variant={
isJetpackOffline ? 'link' : 'primary'
}
>
{ bannerButtonText }
</Button>
</div>
</div>
<BannerComponent
setOpenDesignChangeWarningModal={
setOpenDesignChangeWarningModal
}
sendEvent={ sendEvent }
/>
<p className="select-theme-text">
{ __(

View File

@ -0,0 +1,145 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import classNames from 'classnames';
import { Button } from '@wordpress/components';
/**
* Internal dependencies
*/
import { Intro } from '.';
export const BaseIntroBanner = ( {
bannerTitle,
bannerText,
bannerClass,
buttonIsLink,
bannerButtonOnClick,
bannerButtonText,
}: {
bannerTitle: string;
bannerText: string;
bannerClass: string;
buttonIsLink: boolean;
bannerButtonOnClick: () => void;
bannerButtonText: string;
} ) => {
return (
<div
className={ classNames(
'woocommerce-customize-store-banner',
bannerClass
) }
>
<div className={ `woocommerce-customize-store-banner-content` }>
<h1>{ bannerTitle }</h1>
<p>{ bannerText }</p>
<Button
onClick={ () => bannerButtonOnClick() }
variant={ buttonIsLink ? 'link' : 'primary' }
>
{ bannerButtonText }
</Button>
</div>
</div>
);
};
export const NetworkOfflineBanner = () => {
return (
<BaseIntroBanner
bannerTitle={ __(
'Looking to design your store using AI?',
'woocommerce'
) }
bannerText={ __(
"Unfortunately, the [AI Store designer] isn't available right now as we can't detect your network. Please check your internet connection and try again.",
'woocommerce'
) }
bannerClass="offline-banner"
buttonIsLink={ false }
bannerButtonOnClick={ () => {} }
bannerButtonText={ __( 'Retry', 'woocommerce' ) }
/>
);
};
export const JetpackOfflineBanner = ( {
sendEvent,
}: {
sendEvent: React.ComponentProps< typeof Intro >[ 'sendEvent' ];
} ) => {
return (
<BaseIntroBanner
bannerTitle={ __(
'Looking to design your store using AI?',
'woocommerce'
) }
bannerText={ __(
"It looks like you're using Jetpack's offline mode — switch to online mode to start designing with AI.",
'woocommerce'
) }
bannerClass="offline-banner"
buttonIsLink={ false }
bannerButtonOnClick={ () => {
sendEvent( {
type: 'JETPACK_OFFLINE_HOWTO',
} );
} }
bannerButtonText={ __( 'Find out how', 'woocommerce' ) }
/>
);
};
export const DefaultBanner = ( {
sendEvent,
}: {
sendEvent: React.ComponentProps< typeof Intro >[ 'sendEvent' ];
} ) => {
return (
<BaseIntroBanner
bannerTitle={ __(
'Use the power of AI to design your store',
'woocommerce'
) }
bannerText={ __(
'Design the look of your store, create pages, and generate copy using our built-in AI tools.',
'woocommerce'
) }
bannerClass=""
buttonIsLink={ false }
bannerButtonOnClick={ () => {
sendEvent( {
type: 'DESIGN_WITH_AI',
} );
} }
bannerButtonText={ __( 'Design with AI', 'woocommerce' ) }
/>
);
};
export const ThemeHasModsBanner = ( {
setOpenDesignChangeWarningModal,
}: {
setOpenDesignChangeWarningModal: ( arg0: boolean ) => void;
} ) => {
return (
<BaseIntroBanner
bannerTitle={ __(
'Use the power of AI to design your store',
'woocommerce'
) }
bannerText={ __(
'Design the look of your store, create pages, and generate copy using our built-in AI tools.',
'woocommerce'
) }
bannerClass=""
buttonIsLink={ false }
bannerButtonOnClick={ () => {
setOpenDesignChangeWarningModal( true );
} }
bannerButtonText={ __( 'Design with AI', 'woocommerce' ) }
/>
);
};

View File

@ -0,0 +1,80 @@
/**
* External dependencies
*/
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { AnyInterpreter } from 'xstate';
/**
* Internal dependencies
*/
import { Intro } from '../';
import { useNetworkStatus } from '~/utils/react-hooks/use-network-status';
jest.mock( '../../assembler-hub/site-hub', () => ( {
SiteHub: jest.fn( () => null ),
} ) );
jest.mock( '~/utils/react-hooks/use-network-status', () => ( {
useNetworkStatus: jest.fn(),
} ) );
describe( 'Intro Banners', () => {
it( 'should display NetworkOfflineBanner when network is offline', () => {
( useNetworkStatus as jest.Mock ).mockImplementation( () => true );
render(
<Intro
sendEvent={ jest.fn() }
context={ {
intro: {
hasErrors: false,
activeTheme: '',
themeCards: [],
activeThemeHasMods: false,
customizeStoreTaskCompleted: false,
currentThemeIsAiGenerated: false,
},
themeConfiguration: {},
} }
parentMachine={ null as unknown as AnyInterpreter }
/>
);
expect(
screen.getByText(
/Please check your internet connection and try again./i
)
).toBeInTheDocument();
} );
it( 'should display the default banner by default', () => {
const sendEventMock = jest.fn();
( useNetworkStatus as jest.Mock ).mockImplementation( () => false );
render(
<Intro
sendEvent={ sendEventMock }
context={ {
intro: {
hasErrors: false,
activeTheme: '',
themeCards: [],
activeThemeHasMods: false,
customizeStoreTaskCompleted: false,
currentThemeIsAiGenerated: false,
},
themeConfiguration: {},
} }
parentMachine={ null as unknown as AnyInterpreter }
/>
);
expect(
screen.getByText( /Use the power of AI to design your store/i )
).toBeInTheDocument();
const button = screen.getByRole( 'button', {
name: /Design with AI/i,
} );
fireEvent.click( button );
expect( sendEventMock ).toHaveBeenCalledWith( {
type: 'DESIGN_WITH_AI',
} );
} );
} );

View File

@ -0,0 +1,62 @@
/**
* External dependencies
*/
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { AnyInterpreter } from 'xstate';
/**
* Internal dependencies
*/
import { Intro } from '../';
jest.mock( '../../assembler-hub/site-hub', () => ( {
SiteHub: jest.fn( () => null ),
} ) );
jest.mock( '~/utils/react-hooks/use-network-status', () => ( {
useNetworkStatus: jest.fn(),
} ) );
describe( 'Intro Modals', () => {
it( 'should display DesignChangeWarningModal when activeThemeHasMods and button is clicked', async () => {
const sendEventMock = jest.fn();
render(
<Intro
sendEvent={ sendEventMock }
context={ {
intro: {
hasErrors: false,
activeTheme: '',
themeCards: [],
activeThemeHasMods: true,
customizeStoreTaskCompleted: false,
currentThemeIsAiGenerated: false,
},
themeConfiguration: {},
} }
parentMachine={ null as unknown as AnyInterpreter }
/>
);
const bannerButton = screen.getByRole( 'button', {
name: /Design with AI/i,
} );
fireEvent.click( bannerButton );
await waitFor( () => {
expect(
screen.getByText(
/Are you sure you want to start a new design?/i
)
).toBeInTheDocument();
} );
const modalButton = screen.getByRole( 'button', {
name: /Design with AI/i,
} );
fireEvent.click( modalButton );
expect( sendEventMock ).toHaveBeenCalledWith( {
type: 'DESIGN_WITH_AI',
} );
} );
} );

View File

@ -27,5 +27,6 @@ export type customizeStoreStateMachineContext = {
activeTheme: string;
activeThemeHasMods: boolean;
customizeStoreTaskCompleted: boolean;
currentThemeIsAiGenerated: boolean;
};
};

View File

@ -0,0 +1,7 @@
Significance: patch
Type: fix
Comment: Refactored CYS Intro banner and added tests