* Add SlotFill area to header

* Add activity panel fill

* Move activity panel to root client folder

* Move activity panel registration to its own folder

* Add navigation fill

* Add page title slotfill

* Slot fill the back button

* Move mobile banner to slot fill

* Fix navigation fill on embed pages

* Add changelog entry

* Allow order prop to header item fill

* Split header into before and after

* Fix header title gaps

* Fix nav and mobile app banner placement

* Fix display options import

* Only use last item for page header title fill

* Use function to pass fill props instead of bind

* Rename header slots

* Fix mobile banner dismissal check

* Fix up inbox panel rename

* Update task title in tests

* Fix up task status retrieval
This commit is contained in:
Joshua T Flowers 2021-12-14 11:56:42 -05:00 committed by GitHub
parent e31086327b
commit 7aeb0a19d2
65 changed files with 472 additions and 309 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: Enhancement
Add SlotFill areas to header #7805

View File

@ -12,19 +12,21 @@ import {
ONBOARDING_STORE_NAME,
OPTIONS_STORE_NAME,
useUser,
useUserPreferences,
} from '@woocommerce/data';
import { getHistory, getNewPath } from '@woocommerce/navigation';
import { recordEvent } from '@woocommerce/tracks';
import { useSlot } from '@woocommerce/experimental';
/**
* Internal dependencies
*/
import './style.scss';
import { IconFlag } from './icon-flag';
import { isNotesPanelVisible } from './unread-indicators';
import { isWCAdmin } from '../../dashboard/utils';
import { isWCAdmin } from '~/dashboard/utils';
import { Tabs } from './tabs';
import { SetupProgress } from './setup-progress';
import { IconFlag } from './icon-flag';
import { DisplayOptions } from './display-options';
import { HighlightTooltip } from './highlight-tooltip';
import { Panel } from './panel';
@ -32,8 +34,8 @@ import {
getLowStockCount as getLowStockProducts,
getOrderStatuses,
getUnreadOrders,
} from '../../homescreen/activity-panel/orders/utils';
import { getUnapprovedReviews } from '../../homescreen/activity-panel/reviews/utils';
} from '../homescreen/activity-panel/orders/utils';
import { getUnapprovedReviews } from '../homescreen/activity-panel/reviews/utils';
import { ABBREVIATED_NOTIFICATION_SLOT_NAME } from './panels/inbox/abbreviated-notifications-panel';
const HelpPanel = lazy( () =>
@ -46,13 +48,14 @@ const InboxPanel = lazy( () =>
)
);
export const ActivityPanel = ( { isEmbedded, query, userPreferencesData } ) => {
export const ActivityPanel = ( { isEmbedded, query } ) => {
const [ currentTab, setCurrentTab ] = useState( '' );
const [ isPanelClosing, setIsPanelClosing ] = useState( false );
const [ isPanelOpen, setIsPanelOpen ] = useState( false );
const [ isPanelSwitching, setIsPanelSwitching ] = useState( false );
const { fills } = useSlot( ABBREVIATED_NOTIFICATION_SLOT_NAME );
const hasExtendedNotifications = Boolean( fills?.length );
const { updateUserPreferences, ...userData } = useUserPreferences();
const getPreviewSiteBtnTrackData = ( select, getOption ) => {
let trackData = {};
@ -322,11 +325,8 @@ export const ActivityPanel = ( { isEmbedded, query, userPreferencesData } ) => {
const closedHelpPanelHighlight = () => {
recordEvent( 'help_tooltip_click' );
if (
userPreferencesData &&
userPreferencesData.updateUserPreferences
) {
userPreferencesData.updateUserPreferences( {
if ( userData && updateUserPreferences ) {
updateUserPreferences( {
help_panel_highlight_shown: 'yes',
} );
}
@ -335,11 +335,8 @@ export const ActivityPanel = ( { isEmbedded, query, userPreferencesData } ) => {
const shouldShowHelpTooltip = () => {
const { task } = query;
const startedTasks =
userPreferencesData &&
userPreferencesData.task_list_tracked_started_tasks;
const highlightShown =
userPreferencesData &&
userPreferencesData.help_panel_highlight_shown;
userData && userData.task_list_tracked_started_tasks;
const highlightShown = userData && userData.help_panel_highlight_shown;
if (
task &&
highlightShown !== 'yes' &&

View File

@ -0,0 +1,27 @@
/**
* External dependencies
*/
import { registerPlugin } from '@wordpress/plugins';
/**
* Internal dependencies
*/
import ActivityPanel from './activity-panel';
import { WooHeaderItem } from '~/header/utils';
const ActivityPanelHeaderItem = () => (
<WooHeaderItem order={ 20 }>
{ ( { isEmbedded, query } ) => {
if ( ! window.wcAdminFeatures[ 'activity-panels' ] ) {
return null;
}
return <ActivityPanel isEmbedded={ isEmbedded } query={ query } />;
} }
</WooHeaderItem>
);
registerPlugin( 'activity-panel-header-item', {
render: ActivityPanelHeaderItem,
scope: 'woocommerce-admin',
} );

View File

@ -8,8 +8,8 @@ import { Spinner } from '@woocommerce/components';
/**
* Internal dependencies
*/
import useFocusOnMount from '../../hooks/useFocusOnMount';
import useFocusOutside from '../../hooks/useFocusOutside';
import useFocusOnMount from '~/hooks/useFocusOnMount';
import useFocusOutside from '~/hooks/useFocusOutside';
export const Panel = ( {
content,

View File

@ -21,7 +21,7 @@ import { recordEvent } from '@woocommerce/tracks';
* Internal dependencies
*/
import ActivityHeader from '../activity-header';
import { getCountryCode } from '../../../dashboard/utils';
import { getCountryCode } from '~/dashboard/utils';
export const SETUP_TASK_HELP_ITEMS_FILTER =
'woocommerce_admin_setup_task_help_items';

View File

@ -17,9 +17,9 @@ import {
getLowStockCount,
getOrderStatuses,
getUnreadOrders,
} from '../../../../homescreen/activity-panel/orders/utils';
import { getUnapprovedReviews } from '../../../../homescreen/activity-panel/reviews/utils';
import { isWCAdmin } from '../../../../dashboard/utils';
} from '~/homescreen/activity-panel/orders/utils';
import { getUnapprovedReviews } from '~/homescreen/activity-panel/reviews/utils';
import { isWCAdmin } from '~/dashboard/utils';
import { Bell } from './icons/bell';
const EXTENDED_TASK_LIST_ID = 'extended_task_list';

View File

@ -2,7 +2,7 @@
* Internal dependencies
*/
import './inbox.scss';
import NotesPanel from '../../../../inbox-panel';
import NotesPanel from '~/inbox-panel';
import { AbbreviatedNotificationsPanel } from './abbreviated-notifications-panel';
export const InboxPanel = ( {

View File

@ -9,18 +9,21 @@ import {
createEvent,
} from '@testing-library/react';
import { useSelect } from '@wordpress/data';
import { useUser } from '@woocommerce/data';
import { useUser, useUserPreferences } from '@woocommerce/data';
import { useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import { ActivityPanel } from '../';
import { ActivityPanel } from '../activity-panel';
import { Panel } from '../panel';
jest.mock( '@woocommerce/data', () => ( {
...jest.requireActual( '@woocommerce/data' ),
useUser: jest.fn().mockReturnValue( { currentUserCan: () => true } ),
useUserPreferences: jest.fn().mockReturnValue( {
updateUserPreferences: () => {},
} ),
} ) );
// We aren't testing the <DisplayOptions /> component here.
@ -214,14 +217,12 @@ describe( 'Activity Panel', () => {
describe( 'help panel tooltip', () => {
it( 'should render highlight tooltip when task count is at-least 2, task is not completed, and tooltip not shown yet', () => {
useUserPreferences.mockReturnValue( {
updateUserPreferences: () => {},
task_list_tracked_started_tasks: { payment: 2 },
} );
const { getByText } = render(
<ActivityPanel
userPreferencesData={ {
task_list_tracked_started_tasks: { payment: 2 },
} }
isEmbedded
query={ { task: 'payment' } }
/>
<ActivityPanel isEmbedded query={ { task: 'payment' } } />
);
expect( getByText( '[HighlightTooltip]' ) ).toBeInTheDocument();
@ -234,26 +235,23 @@ describe( 'Activity Panel', () => {
setupTaskListHidden: false,
trackedCompletedTasks: [],
} ) );
useUserPreferences.mockReturnValue( {
updateUserPreferences: () => {},
task_list_tracked_started_tasks: { payment: 1 },
} );
render(
<ActivityPanel
userPreferencesData={ {
task_list_tracked_started_tasks: { payment: 1 },
} }
isEmbedded
query={ { task: 'payment' } }
/>
<ActivityPanel isEmbedded query={ { task: 'payment' } } />
);
expect( screen.queryByText( '[HighlightTooltip]' ) ).toBeNull();
useUserPreferences.mockReturnValue( {
updateUserPreferences: () => {},
task_list_tracked_started_tasks: {},
} );
render(
<ActivityPanel
userPreferencesData={ {
task_list_tracked_started_tasks: {},
} }
isEmbedded
query={ { task: 'payment' } }
/>
<ActivityPanel isEmbedded query={ { task: 'payment' } } />
);
expect( screen.queryByText( '[HighlightTooltip]' ) ).toBeNull();
@ -267,29 +265,26 @@ describe( 'Activity Panel', () => {
isCompletedTask: true,
} ) );
useUserPreferences.mockReturnValue( {
updateUserPreferences: () => {},
task_list_tracked_started_tasks: { payment: 2 },
} );
const { queryByText } = render(
<ActivityPanel
userPreferencesData={ {
task_list_tracked_started_tasks: { payment: 2 },
} }
isEmbedded
query={ { task: 'payment' } }
/>
<ActivityPanel isEmbedded query={ { task: 'payment' } } />
);
expect( queryByText( '[HighlightTooltip]' ) ).toBeNull();
} );
it( 'should not render highlight tooltip when task is visited twice, not completed, but already shown', () => {
useUserPreferences.mockReturnValue( {
task_list_tracked_started_tasks: { payment: 2 },
help_panel_highlight_shown: 'yes',
} );
const { queryByText } = render(
<ActivityPanel
userPreferencesData={ {
task_list_tracked_started_tasks: { payment: 2 },
help_panel_highlight_shown: 'yes',
} }
isEmbedded
query={ { task: 'payment' } }
/>
<ActivityPanel isEmbedded query={ { task: 'payment' } } />
);
expect( queryByText( '[HighlightTooltip]' ) ).toBeNull();

View File

@ -11,7 +11,7 @@ import { getSetting } from '@woocommerce/wc-admin-settings';
/**
* Internal dependencies
*/
import { getUnreadNotesCount } from '../../inbox-panel/utils';
import { getUnreadNotesCount } from '~/inbox-panel/utils';
const UNREAD_NOTES_QUERY = {
page: 1,

View File

@ -3,93 +3,40 @@
*/
import { __, sprintf } from '@wordpress/i18n';
import { useEffect, useLayoutEffect, useRef } from '@wordpress/element';
import { Tooltip } from '@wordpress/components';
import classnames from 'classnames';
import { decodeEntities } from '@wordpress/html-entities';
import { useUserPreferences } from '@woocommerce/data';
import { getSetting } from '@woocommerce/wc-admin-settings';
import { Text } from '@woocommerce/experimental';
import { Icon, chevronLeft } from '@wordpress/icons';
import { getHistory, updateQueryString } from '@woocommerce/navigation';
import { ENTER, SPACE } from '@wordpress/keycodes';
import { recordEvent } from '@woocommerce/tracks';
import { Text, useSlot } from '@woocommerce/experimental';
/**
* Internal dependencies
*/
import './style.scss';
import ActivityPanel from './activity-panel';
import { MobileAppBanner } from '../mobile-banner';
import useIsScrolled from '../hooks/useIsScrolled';
import Navigation from '../navigation';
import {
WooHeaderNavigationItem,
WooHeaderItem,
WooHeaderPageTitle,
} from './utils';
const renderTaskListBackButton = () => {
const currentUrl = new URL( window.location.href );
const task = currentUrl.searchParams.get( 'task' );
if ( task ) {
const homeText = __( 'WooCommerce Home', 'woocommerce-admin' );
const navigateHome = () => {
recordEvent( 'topbar_back_button', {
page_name: getPageTitle( window.title ),
} );
updateQueryString( {}, getHistory().location.pathname, {} );
};
// if it's a task list page, render a back button to the homescreen
return (
<Tooltip text={ homeText }>
<div
tabIndex="0"
role="button"
data-testid="header-back-button"
className="woocommerce-layout__header-back-button"
onKeyDown={ ( { keyCode } ) => {
if ( keyCode === ENTER || keyCode === SPACE ) {
navigateHome();
}
} }
>
<Icon icon={ chevronLeft } onClick={ navigateHome } />
</div>
</Tooltip>
);
}
return null;
};
const getPageTitle = ( defaultTitle ) => {
const currentUrl = new URL( window.location.href );
const task = currentUrl.searchParams.get( 'task' );
// If it's the task list then render a title based on which task the user is on.
return (
{
payments: __( 'Set up payments', 'woocommerce-admin' ),
tax: __( 'Add tax rates', 'woocommerce-admin' ),
appearance: __( 'Personalize your store', 'woocommerce-admin' ),
marketing: __( 'Set up marketing tools', 'woocommerce-admin' ),
products: __( 'Add products', 'woocommerce-admin' ),
shipping: __( 'Set up shipping costs', 'woocommerce-admin' ),
}[ task ] || defaultTitle
);
};
export const PAGE_TITLE_FILTER = 'woocommerce_admin_header_page_title';
export const Header = ( { sections, isEmbedded = false, query } ) => {
const headerElement = useRef( null );
const siteTitle = getSetting( 'siteTitle', '' );
const pageTitle = sections.slice( -1 )[ 0 ];
const isScrolled = useIsScrolled();
const { updateUserPreferences, ...userData } = useUserPreferences();
const isModalDismissed = userData.android_app_banner_dismissed === 'yes';
let debounceTimer = null;
const className = classnames( 'woocommerce-layout__header', {
'is-scrolled': isScrolled,
} );
const pageTitleSlot = useSlot( 'woocommerce_header_page_title' );
const hasPageTitleFills = Boolean( pageTitleSlot?.fills?.length );
const headerItemSlot = useSlot( 'woocommerce_header_item' );
const headerItemSlotFills = headerItemSlot?.fills;
useLayoutEffect( () => {
updateBodyMargin();
window.addEventListener( 'resize', updateBodyMargin );
@ -103,7 +50,7 @@ export const Header = ( { sections, isEmbedded = false, query } ) => {
wpBody.style.marginTop = null;
};
}, [ isModalDismissed ] );
}, [ headerItemSlotFills ] );
const updateBodyMargin = () => {
clearTimeout( debounceTimer );
@ -145,44 +92,29 @@ export const Header = ( { sections, isEmbedded = false, query } ) => {
}
}, [ isEmbedded, sections, siteTitle ] );
const dismissHandler = () => {
updateUserPreferences( {
android_app_banner_dismissed: 'yes',
} );
};
const backButton = renderTaskListBackButton();
const backButtonClass = backButton ? 'with-back-button' : '';
return (
<div className={ className } ref={ headerElement }>
{ ! isModalDismissed && (
<MobileAppBanner
onDismiss={ dismissHandler }
onInstall={ dismissHandler }
/>
) }
<div className="woocommerce-layout__header-wrapper">
{ window.wcAdminFeatures.navigation && <Navigation /> }
{ renderTaskListBackButton() }
<WooHeaderNavigationItem.Slot
fillProps={ { isEmbedded, query } }
/>
<Text
className={ `woocommerce-layout__header-heading ${ backButtonClass }` }
className={ `woocommerce-layout__header-heading` }
as="h1"
>
{ getPageTitle( decodeEntities( pageTitle ) ) }
{ decodeEntities(
hasPageTitleFills ? (
<WooHeaderPageTitle.Slot
fillProps={ { isEmbedded, query } }
/>
) : (
pageTitle
)
) }
</Text>
{ window.wcAdminFeatures[ 'activity-panels' ] && (
<ActivityPanel
isEmbedded={ isEmbedded }
query={ query }
userPreferencesData={ {
...userData,
updateUserPreferences,
} }
/>
) }
<WooHeaderItem.Slot fillProps={ { isEmbedded, query } } />
</div>
</div>
);

View File

@ -17,16 +17,6 @@
min-height: $header-height;
}
.woocommerce-layout__header-back-button {
cursor: pointer;
margin-left: $gutter-large;
display: flex;
&:focus {
box-shadow: inset -1px -1px 0 #757575, inset 1px 1px 0 #757575;
}
}
@include breakpoint( '<782px' ) {
flex-flow: row wrap;
top: $adminbar-height-mobile;
@ -53,10 +43,6 @@
background: $studio-white;
font-weight: 600;
font-size: 14px;
&.with-back-button {
padding-left: $fallback-gutter;
}
}
}

View File

@ -49,13 +49,6 @@ const encodedBreadcrumb = [
'Accounts &amp; Privacy',
];
const stubLocation = ( location ) => {
jest.spyOn( window, 'location', 'get' ).mockReturnValue( {
...window.location,
...location,
} );
};
describe( 'Header', () => {
beforeEach( () => {
// Mock RAF to be synchronous for testing
@ -75,20 +68,6 @@ describe( 'Header', () => {
window.requestAnimationFrame.mockRestore();
} );
it( 'should render a back button and a custom title on task list pages', () => {
stubLocation( { href: 'http://localhost?task=payments' } );
const { queryByTestId, queryByText } = render(
<Header sections={ encodedBreadcrumb } isEmbedded={ false } />
);
expect(
queryByTestId( 'header-back-button' )
).not.toBeEmptyDOMElement();
expect( queryByText( 'Set up payments' ) ).not.toBeEmptyDOMElement();
} );
it( 'should render decoded breadcrumb name', () => {
const { queryByText } = render(
<Header sections={ encodedBreadcrumb } isEmbedded={ true } />

View File

@ -0,0 +1,123 @@
/**
* External dependencies
*/
import { Slot, Fill } from '@wordpress/components';
import { cloneElement } from '@wordpress/element';
/**
* Ordered header item.
*
* @param {Node} children - Node children.
* @param {number} order - Node order.
* @param {Array} props - Fill props.
* @return {Node} Node.
*/
const createOrderedChildren = ( children, order, props ) => {
return typeof children === 'function'
? cloneElement( children( props ), { order } )
: cloneElement( children, { ...props, order } );
};
/**
* Create a Fill for extensions to add items to the WooCommerce Admin header.
*
* @slotFill WooHeaderItem
* @example
* const MyHeaderItem = () => (
* <WooHeaderItem>My header item</WooHeaderItem>
* );
*
* registerPlugin( 'my-extension', {
* render: MyHeaderItem,
* scope: 'woocommerce-admin',
* } );
* @param {Object} param0
* @param {Array} param0.children - Node children.
* @param {Array} param0.order - Node order.
*/
export const WooHeaderItem = ( { children, order = 1 } ) => {
return (
<Fill name={ 'woocommerce_header_item' }>
{ ( fillProps ) => {
return createOrderedChildren( children, order, fillProps );
} }
</Fill>
);
};
WooHeaderItem.Slot = ( { fillProps } ) => (
<Slot name={ 'woocommerce_header_item' } fillProps={ fillProps }>
{ ( fills ) => {
return fills.sort( ( a, b ) => {
return a[ 0 ].props.order - b[ 0 ].props.order;
} );
} }
</Slot>
);
/**
* Create a Fill for extensions to add items to the WooCommerce Admin
* navigation area left of the page title.
*
* @slotFill WooHeaderNavigationItem
* @example
* const MyNavigationItem = () => (
* <WooHeaderNavigationItem>My nav item</WooHeaderNavigationItem>
* );
*
* registerPlugin( 'my-extension', {
* render: MyNavigationItem,
* scope: 'woocommerce-admin',
* } );
* @param {Object} param0
* @param {Array} param0.children - Node children.
* @param {Array} param0.order - Node order.
*/
export const WooHeaderNavigationItem = ( { children, order = 1 } ) => {
return (
<Fill name={ 'woocommerce_header_navigation_item' }>
{ ( fillProps ) => {
return createOrderedChildren( children, order, fillProps );
} }
</Fill>
);
};
WooHeaderNavigationItem.Slot = ( { fillProps } ) => (
<Slot name={ 'woocommerce_header_navigation_item' } fillProps={ fillProps }>
{ ( fills ) => {
return fills.sort( ( a, b ) => {
return a[ 0 ].props.order - b[ 0 ].props.order;
} );
} }
</Slot>
);
/**
* Create a Fill for extensions to add custom page titles.
*
* @slotFill WooHeaderPageTitle
* @example
* const MyPageTitle = () => (
* <WooHeaderPageTitle>My page title</WooHeaderPageTitle>
* );
*
* registerPlugin( 'my-page-title', {
* render: MyPageTitle,
* scope: 'woocommerce-admin',
* } );
* @param {Object} param0
* @param {Array} param0.children - Node children.
*/
export const WooHeaderPageTitle = ( { children } ) => {
return <Fill name={ 'woocommerce_header_page_title' }>{ children }</Fill>;
};
WooHeaderPageTitle.Slot = ( { fillProps } ) => (
<Slot name={ 'woocommerce_header_page_title' } fillProps={ fillProps }>
{ ( fills ) => {
const last = fills.pop();
return [ last ];
} }
</Slot>
);

View File

@ -25,7 +25,7 @@ import { recordEvent } from '@woocommerce/tracks';
import {
ActivityCard,
ActivityCardPlaceholder,
} from '../../../header/activity-panel/activity-card';
} from '~/activity-panel/activity-card';
import './style.scss';
function recordOrderEvent( eventName ) {

View File

@ -30,7 +30,7 @@ import './style.scss';
import {
ActivityCard,
ActivityCardPlaceholder,
} from '../../../header/activity-panel/activity-card';
} from '~/activity-panel/activity-card';
import CheckmarkCircleIcon from './checkmark-circle-icon';
import { CurrencyContext } from '../../../lib/currency-context';
import sanitizeHTML from '../../../lib/sanitize-html';

View File

@ -15,7 +15,7 @@ import moment from 'moment';
/**
* Internal dependencies
*/
import { ActivityCard } from '../../../header/activity-panel/activity-card';
import { ActivityCard } from '~/activity-panel/activity-card';
export class ProductStockCard extends Component {
constructor( props ) {

View File

@ -12,7 +12,7 @@ import { ITEMS_STORE_NAME } from '@woocommerce/data';
/**
* Internal dependencies
*/
import { ActivityCardPlaceholder } from '../../../header/activity-panel/activity-card';
import { ActivityCardPlaceholder } from '~/activity-panel/activity-card';
import { ProductStockCard } from './card';
import { getLowStockCountQuery } from '../orders/utils';

View File

@ -26,7 +26,7 @@ import { useExperiment } from '@woocommerce/explat';
/**
* Internal dependencies
*/
import ActivityHeader from '../header/activity-panel/activity-header';
import ActivityHeader from '~/activity-panel/activity-header';
import { ActivityPanel } from './activity-panel';
import { Column } from './column';
import InboxPanel from '../inbox-panel';

View File

@ -23,7 +23,7 @@ import moment from 'moment';
/**
* Internal dependencies
*/
import { ActivityCard } from '../header/activity-panel/activity-card';
import { ActivityCard } from '~/activity-panel/activity-card';
import { hasValidNotes, truncateRenderableHTML } from './utils';
import { getScreenName } from '../utils';
import DismissAllModal from './dissmiss-all-modal';

View File

@ -29,6 +29,8 @@ import { Controller, getPages } from './controller';
import { Header } from '../header';
import Notices from './notices';
import TransientNotices from './transient-notices';
import '~/activity-panel';
import '~/mobile-banner';
import './navigation';
const StoreAlerts = lazy( () =>

View File

@ -18,6 +18,8 @@ import { NAVIGATION_STORE_NAME } from '@woocommerce/data';
import getReports from '../analytics/report/get-reports';
import { getPages } from './controller';
import { isWCAdmin } from '../dashboard/utils';
import Navigation from '~/navigation';
import { WooHeaderNavigationItem } from '~/header/utils';
const NavigationPlugin = () => {
const { persistedQuery } = useSelect( ( select ) => {
@ -26,13 +28,21 @@ const NavigationPlugin = () => {
};
} );
if ( ! window.wcAdminFeatures.navigation ) {
return null;
}
/**
* If the current page is embedded, stay with the default urls
* provided by Navigation because the router isn't present to
* respond to <Link /> component's manipulation of the url.
*/
if ( ! isWCAdmin( window.location.href ) ) {
return null;
return (
<WooHeaderNavigationItem order={ -100 }>
<Navigation />
</WooHeaderNavigationItem>
);
}
const reports = getReports().filter( ( item ) => item.navArgs );
@ -51,6 +61,9 @@ const NavigationPlugin = () => {
return (
<>
<WooHeaderNavigationItem order={ -100 }>
<Navigation />
</WooHeaderNavigationItem>
{ pages.map( ( page ) => (
<WooNavigationItem
item={ page.navArgs.id }

View File

@ -0,0 +1,91 @@
/**
* External dependencies
*/
import React, { useEffect, useState } from '@wordpress/element';
import { Button } from '@wordpress/components';
import { Icon } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import { recordEvent } from '@woocommerce/tracks';
import GridiconCrossIcon from 'gridicons/dist/cross-small';
/**
* Internal dependencies
*/
import { platform, ANDROID_PLATFORM } from '../lib/platform';
import { AppIcon } from './app-icon';
import { PLAY_STORE_LINK, TRACKING_EVENT_NAME } from './constants';
import './banner.scss';
const SHOW_APP_BANNER_MODIFIER_CLASS = 'woocommerce-layout__show-app-banner';
export const Banner = ( { onInstall, onDismiss } ) => {
const [ isActioned, setIsActioned ] = useState( false );
const isVisible = platform() === ANDROID_PLATFORM && ! isActioned;
useEffect( () => {
const layout = document.getElementsByClassName(
'woocommerce-layout'
)[ 0 ];
if ( isVisible && layout ) {
// This is a hack to allow the mobile banner to work in the context of the header which is
// position fixed. This can be refactored away when we move away from the activity panel
// in future.
layout.classList.add( SHOW_APP_BANNER_MODIFIER_CLASS );
}
return () => {
if ( layout ) {
layout.classList.remove( SHOW_APP_BANNER_MODIFIER_CLASS );
}
};
}, [ isVisible ] );
if ( ! isVisible ) {
return null;
}
return (
<div className="woocommerce-mobile-app-banner">
<Icon
icon={ <GridiconCrossIcon data-testid="dismiss-btn" /> }
onClick={ () => {
onDismiss();
setIsActioned( true );
recordEvent( TRACKING_EVENT_NAME, {
action: 'dismiss',
} );
} }
/>
<AppIcon />
<div className="woocommerce-mobile-app-banner__description">
<p className="woocommerce-mobile-app-banner__description__text">
{ __(
'Run your store from anywhere',
'woocommerce-admin'
) }
</p>
<p className="woocommerce-mobile-app-banner__description__text">
{ __(
'Download the WooCommerce app',
'woocommerce-admin'
) }
</p>
</div>
<Button
href={ PLAY_STORE_LINK }
isSecondary
onClick={ () => {
onInstall();
setIsActioned( true );
recordEvent( TRACKING_EVENT_NAME, {
action: 'install',
} );
} }
>
{ __( 'Install', 'woocommerce-admin' ) }
</Button>
</div>
);
};

View File

@ -4,15 +4,18 @@ $banner-height: 56px;
// This class is a hack, added conditionally when displaying the app banner, it
// can be refactored away when the activity panel goes away.
.woocommerce-layout__show-app-banner {
padding-top: $banner-height;
@include breakpoint( '>782px' ) {
padding-top: 0;
@include breakpoint( '<782px' ) {
.woocommerce-layout__header-wrapper {
padding-top: $banner-height;
}
}
}
.woocommerce-mobile-app-banner {
background-color: $woocommerce-brand-purple;
position: absolute;
top: 0;
left: 0;
width: 100%;
display: flex;
height: $banner-height;

View File

@ -1,93 +1,37 @@
/**
* External dependencies
*/
import React, { useEffect, useState } from '@wordpress/element';
import { Button } from '@wordpress/components';
import { Icon } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import { recordEvent } from '@woocommerce/tracks';
import GridiconCrossIcon from 'gridicons/dist/cross-small';
import { registerPlugin } from '@wordpress/plugins';
import { useUserPreferences } from '@woocommerce/data';
/**
* Internal dependencies
*/
import { platform, ANDROID_PLATFORM } from '../lib/platform';
import { AppIcon } from './app-icon';
import './style.scss';
import { PLAY_STORE_LINK, TRACKING_EVENT_NAME } from './constants';
import { Banner } from './banner';
import { WooHeaderItem } from '~/header/utils';
const SHOW_APP_BANNER_MODIFIER_CLASS = 'woocommerce-layout__show-app-banner';
export const MobileAppBanner = () => {
const { updateUserPreferences, ...userData } = useUserPreferences();
const isDismissed = userData.android_app_banner_dismissed === 'yes';
export const MobileAppBanner = ( { onInstall, onDismiss } ) => {
useEffect( () => {
const layout = document.getElementsByClassName(
'woocommerce-layout'
)[ 0 ];
const onClick = () => {
updateUserPreferences( {
android_app_banner_dismissed: 'yes',
} );
};
if ( platform() === ANDROID_PLATFORM ) {
if ( layout ) {
// This is a hack to allow the mobile banner to work in the context of the header which is
// position fixed. This can be refactored away when we move away from the activity panel
// in future.
layout.classList.add( SHOW_APP_BANNER_MODIFIER_CLASS );
}
}
return () => {
if ( layout ) {
layout.classList.remove( SHOW_APP_BANNER_MODIFIER_CLASS );
}
};
}, [] );
const [ isDismissed, setDismissed ] = useState( false );
// On iOS the "Smart App Banner" meta tag is used so only display this on Android.
if ( platform() === ANDROID_PLATFORM && ! isDismissed ) {
return (
<div className="woocommerce-mobile-app-banner">
<Icon
icon={ <GridiconCrossIcon data-testid="dismiss-btn" /> }
onClick={ () => {
onDismiss();
setDismissed( true );
recordEvent( TRACKING_EVENT_NAME, {
action: 'dismiss',
} );
} }
/>
<AppIcon />
<div className="woocommerce-mobile-app-banner__description">
<p className="woocommerce-mobile-app-banner__description__text">
{ __(
'Run your store from anywhere',
'woocommerce-admin'
) }
</p>
<p className="woocommerce-mobile-app-banner__description__text">
{ __(
'Download the WooCommerce app',
'woocommerce-admin'
) }
</p>
</div>
<Button
href={ PLAY_STORE_LINK }
isSecondary
onClick={ () => {
onInstall();
setDismissed( true );
recordEvent( TRACKING_EVENT_NAME, {
action: 'install',
} );
} }
>
{ __( 'Install', 'woocommerce-admin' ) }
</Button>
</div>
);
if ( isDismissed ) {
return null;
}
return null;
return (
<WooHeaderItem>
<Banner onDismiss={ onClick } onInstall={ onClick } />
</WooHeaderItem>
);
};
registerPlugin( 'mobile-banner-header-item', {
render: MobileAppBanner,
scope: 'woocommerce-admin',
} );

View File

@ -22,18 +22,18 @@ import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import { MobileAppBanner } from '../index';
import { Banner } from '../banner';
import { platform } from '../../lib/platform';
import { TRACKING_EVENT_NAME } from '../constants';
describe( 'MobileAppBanner', () => {
describe( 'Banner', () => {
beforeEach( () => {
platform.mockReturnValue( 'android' );
} );
it( 'closes if the user dismisses it', () => {
const { container, getByTestId } = render(
<MobileAppBanner onInstall={ () => {} } onDismiss={ () => {} } />
<Banner onInstall={ () => {} } onDismiss={ () => {} } />
);
fireEvent.click( getByTestId( 'dismiss-btn' ) );
@ -42,7 +42,7 @@ describe( 'MobileAppBanner', () => {
it( 'closes if the user clicks install', () => {
const { queryByRole, container } = render(
<MobileAppBanner onInstall={ () => {} } onDismiss={ () => {} } />
<Banner onInstall={ () => {} } onDismiss={ () => {} } />
);
fireEvent.click( queryByRole( 'link' ) );
@ -51,7 +51,7 @@ describe( 'MobileAppBanner', () => {
it( 'records a tracking event for install', () => {
const { queryByRole } = render(
<MobileAppBanner onInstall={ () => {} } onDismiss={ () => {} } />
<Banner onInstall={ () => {} } onDismiss={ () => {} } />
);
fireEvent.click( queryByRole( 'link' ) );
@ -62,7 +62,7 @@ describe( 'MobileAppBanner', () => {
it( 'records a dismiss event for dismiss', () => {
const { container, getByTestId } = render(
<MobileAppBanner onInstall={ () => {} } onDismiss={ () => {} } />
<Banner onInstall={ () => {} } onDismiss={ () => {} } />
);
fireEvent.click( getByTestId( 'dismiss-btn' ) );
@ -76,10 +76,7 @@ describe( 'MobileAppBanner', () => {
it( 'calls the onDismiss handler when dismiss is clicked', () => {
const dismissHandler = jest.fn();
const { getByTestId } = render(
<MobileAppBanner
onInstall={ () => {} }
onDismiss={ dismissHandler }
/>
<Banner onInstall={ () => {} } onDismiss={ dismissHandler } />
);
fireEvent.click( getByTestId( 'dismiss-btn' ) );
@ -89,10 +86,7 @@ describe( 'MobileAppBanner', () => {
it( 'calls the onInstall handler when install is clicked', () => {
const installHandler = jest.fn();
const { queryByRole } = render(
<MobileAppBanner
onInstall={ installHandler }
onDismiss={ () => {} }
/>
<Banner onInstall={ installHandler } onDismiss={ () => {} } />
);
fireEvent.click( queryByRole( 'link' ) );
@ -103,7 +97,7 @@ describe( 'MobileAppBanner', () => {
platform.mockReturnValue( 'ios' );
const { container } = render(
<MobileAppBanner onInstall={ () => {} } onDismiss={ () => {} } />
<Banner onInstall={ () => {} } onDismiss={ () => {} } />
);
expect( container ).toBeEmptyDOMElement();

View File

@ -8,7 +8,7 @@ import { useDispatch, useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import { HighlightTooltip } from '../../../header/activity-panel/highlight-tooltip';
import { HighlightTooltip } from '~/activity-panel/highlight-tooltip';
const tooltipHiddenOption = 'woocommerce_navigation_favorites_tooltip_hidden';

View File

@ -0,0 +1,12 @@
.woocommerce-layout__header-back-button {
cursor: pointer;
margin-left: $fallback-gutter-large;
margin-left: $gutter-large;
margin-right: -$gap;
display: flex;
z-index: 2;
&:focus {
box-shadow: inset -1px -1px 0 #757575, inset 1px 1px 0 #757575;
}
}

View File

@ -0,0 +1,48 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Tooltip } from '@wordpress/components';
import { Icon, chevronLeft } from '@wordpress/icons';
import { getHistory, updateQueryString } from '@woocommerce/navigation';
import { ENTER, SPACE } from '@wordpress/keycodes';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import './back-button.scss';
export type BackButtonProps = {
title: string;
};
export const BackButton: React.FC< BackButtonProps > = ( { title } ) => {
const homeText = __( 'WooCommerce Home', 'woocommerce-admin' );
const navigateHome = () => {
recordEvent( 'topbar_back_button', {
page_name: title,
} );
updateQueryString( {}, getHistory().location.pathname, {} );
};
// if it's a task list page, render a back button to the homescreen
return (
<Tooltip text={ homeText }>
<div
tabIndex={ 0 }
role="button"
data-testid="header-back-button"
className="woocommerce-layout__header-back-button"
onKeyDown={ ( { keyCode } ) => {
if ( keyCode === ENTER || keyCode === SPACE ) {
navigateHome();
}
} }
>
<Icon icon={ chevronLeft } onClick={ navigateHome } />
</div>
</Tooltip>
);
};

View File

@ -1,12 +1,19 @@
/**
* External dependencies
*/
import { addFilter } from '@wordpress/hooks';
import { WooOnboardingTask } from '@woocommerce/onboarding';
import { getHistory, getNewPath } from '@woocommerce/navigation';
import { ONBOARDING_STORE_NAME, TaskType } from '@woocommerce/data';
import { useCallback } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import { WooHeaderNavigationItem, WooHeaderPageTitle } from '~/header/utils';
import { BackButton } from './back-button';
export type TaskProps = {
query: { task: string };
task?: TaskType;
@ -26,9 +33,15 @@ export const Task: React.FC< TaskProps > = ( { query, task } ) => {
}, [ id ] );
return (
<WooOnboardingTask.Slot
id={ id }
fillProps={ { onComplete, query, task } }
/>
<>
<WooHeaderNavigationItem>
<BackButton title={ task.title } />
</WooHeaderNavigationItem>
<WooHeaderPageTitle>{ task.title }</WooHeaderPageTitle>
<WooOnboardingTask.Slot
id={ id }
fillProps={ { onComplete, query, task } }
/>
</>
);
};

View File

@ -13,7 +13,7 @@ import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import { DisplayOption } from '../header/activity-panel/display-options';
import { DisplayOption } from '~/activity-panel/display-options';
import { Task } from './task';
import { TaskList } from './task-list';
import { TasksPlaceholder } from './placeholder';

View File

@ -48,7 +48,7 @@ describe( 'Task', () => {
it( 'should pass the task name as id to the OnboardingTask.Slot', () => {
const { queryByText } = render(
<div>
<Task query={ { task: 'test' } } />
<Task query={ { task: 'test' } } task={ { title: 'Test' } } />
</div>
);
expect( queryByText( 'test' ) ).toBeInTheDocument();
@ -63,7 +63,7 @@ describe( 'Task', () => {
} );
const { getByRole } = render(
<div>
<Task query={ { task: 'test' } } />
<Task query={ { task: 'test' } } task={ { title: 'Test' } } />
</div>
);
act( () => {

View File

@ -28,7 +28,7 @@ jest.mock( '../placeholder', () => ( {
TasksPlaceholder: () => <div>task-placeholder</div>,
} ) );
jest.mock( '../../header/activity-panel/display-options', () => ( {
jest.mock( '~/activity-panel/display-options', () => ( {
DisplayOption: ( { children } ) => <div>{ children } </div>,
} ) );

View File

@ -13,7 +13,7 @@ import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import { DisplayOption } from '../header/activity-panel/display-options';
import { DisplayOption } from '~/activity-panel/display-options';
import { Task } from '../tasks/task';
import { TaskList } from '../tasks/task-list';
import { TasksPlaceholder } from '../tasks/placeholder';

View File

@ -8,7 +8,7 @@ export class ProductsSetup extends BasePage {
url = 'wp-admin/admin.php?page=wc-admin&task=products';
async isDisplayed() {
await waitForElementByText( 'h1', 'Add products' );
await waitForElementByText( 'h1', 'Add my products' );
}
async isStartWithATemplateDisplayed( templatesCount: number ) {