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:
parent
7918e1ee5c
commit
246b9a5c76
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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">
|
||||
{ __(
|
||||
|
|
|
@ -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' ) }
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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',
|
||||
} );
|
||||
} );
|
||||
} );
|
|
@ -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',
|
||||
} );
|
||||
} );
|
||||
} );
|
|
@ -27,5 +27,6 @@ export type customizeStoreStateMachineContext = {
|
|||
activeTheme: string;
|
||||
activeThemeHasMods: boolean;
|
||||
customizeStoreTaskCompleted: boolean;
|
||||
currentThemeIsAiGenerated: boolean;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Comment: Refactored CYS Intro banner and added tests
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue