Update the footer section in "Add products" task (#49782)

* Update printful feature flag

* Remove footer

* Add upload icon

* Add importCSVItem constant

* Add footer stack

* Delete footer test

* Remove redundant printful inclusion

* fixed types

* Update CSS

* Changelog

* Lint

* Fix test

* Fix test again

* Fix SVG to use theme color

---------

Co-authored-by: Ilyas Foo <foo.ilyas@gmail.com>
Co-authored-by: rjchow <me@rjchow.com>
This commit is contained in:
Adrian Duffell 2024-07-23 21:31:46 +08:00 committed by GitHub
parent 726e5fb0d8
commit 7e13bbcbf4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 150 additions and 134 deletions

View File

@ -6,8 +6,8 @@ import ProductIcon from 'gridicons/dist/product';
import CloudOutlineIcon from 'gridicons/dist/cloud-outline'; import CloudOutlineIcon from 'gridicons/dist/cloud-outline';
import TypesIcon from 'gridicons/dist/types'; import TypesIcon from 'gridicons/dist/types';
import { Icon, chevronRight } from '@wordpress/icons'; import { Icon, chevronRight } from '@wordpress/icons';
import { addFilter } from '@wordpress/hooks';
import { recordEvent } from '@woocommerce/tracks'; import { recordEvent } from '@woocommerce/tracks';
import { getAdminLink } from '@woocommerce/settings';
/** /**
* Internal dependencies * Internal dependencies
@ -16,6 +16,7 @@ import Link from './icon/link_24px.js';
import Widget from './icon/widgets_24px.js'; import Widget from './icon/widgets_24px.js';
import LightBulb from './icon/lightbulb_24px.js'; import LightBulb from './icon/lightbulb_24px.js';
import PrintfulIcon from './icon/printful.png'; import PrintfulIcon from './icon/printful.png';
import Upload from './icon/upload_40px.js';
export const productTypes = Object.freeze( [ export const productTypes = Object.freeze( [
{ {
@ -90,7 +91,7 @@ export const PrintfulAdvertProductPlacement = {
'Design and easily sell custom print products online with Printful.', 'Design and easily sell custom print products online with Printful.',
'woocommerce' 'woocommerce'
), ),
className: 'woocommerce-products-list__item-printful-advert', className: 'woocommerce-products-list__item-advert',
before: ( before: (
<img <img
className="printful-sponsored__icon" className="printful-sponsored__icon"
@ -105,12 +106,35 @@ export const PrintfulAdvertProductPlacement = {
}, },
}; };
export const SponsoredProductPlacementType = PrintfulAdvertProductPlacement; export const ImportCSVItem = {
key: 'import-csv' as const,
title: (
<span className="printful-sponsored__text">
{ __( 'Are you already selling somewhere else?', 'woocommerce' ) }
</span>
),
content: __( 'Import your products from a CSV file.', 'woocommerce' ),
className: 'woocommerce-products-list__item-advert',
before: <Upload />,
after: <Icon icon={ chevronRight } />,
onClick: () => {
recordEvent( 'tasklist_add_product', {
method: 'import',
} );
window.location.href = getAdminLink(
'edit.php?post_type=product&page=product_importer&wc_onboarding_active_task=products'
);
},
};
export type SponsoredProductPlacementType =
| typeof PrintfulAdvertProductPlacement
| typeof ImportCSVItem;
export type ProductType = export type ProductType =
| ( typeof productTypes )[ number ] | ( typeof productTypes )[ number ]
| typeof LoadSampleProductType | typeof LoadSampleProductType
| typeof SponsoredProductPlacementType; | SponsoredProductPlacementType;
export type ProductTypeKey = ProductType[ 'key' ]; export type ProductTypeKey = ProductType[ 'key' ];
export const onboardingProductTypesToSurfaced: Readonly< export const onboardingProductTypesToSurfaced: Readonly<
@ -131,13 +155,3 @@ export const SETUP_TASKLIST_PRODUCT_TYPES_FILTER =
export const SETUP_TASKLIST_PRODUCTS_AFTER_FILTER = export const SETUP_TASKLIST_PRODUCTS_AFTER_FILTER =
'woocommerce_admin_task_products_after'; 'woocommerce_admin_task_products_after';
if ( window.wcAdminFeatures && window.wcAdminFeatures.printful === true ) {
addFilter(
SETUP_TASKLIST_PRODUCTS_AFTER_FILTER,
'woocommerce/task-lists/products-sponsored-placement',
( products ) => {
return [ ...products, PrintfulAdvertProductPlacement ];
}
);
}

View File

@ -1,59 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import interpolateComponents from '@automattic/interpolate-components';
import { Text } from '@woocommerce/experimental';
import { Link } from '@woocommerce/components';
import { getAdminLink } from '@woocommerce/settings';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import useRecordCompletionTime from '../use-record-completion-time';
const Footer: React.FC = () => {
const { recordCompletionTime } = useRecordCompletionTime( 'products' );
return (
<div className="woocommerce-products-footer">
<Text className="woocommerce-products-footer__selling-somewhere-else">
{ __(
'Are you already selling somewhere else?',
'woocommerce'
) }
</Text>
<Text className="woocommerce-products-footer__import-options">
{ interpolateComponents( {
mixedString: __(
'{{importCSVLink}}Import your products from a CSV file{{/importCSVLink}}.',
'woocommerce'
),
components: {
importCSVLink: (
<Link
onClick={ () => {
recordEvent( 'tasklist_add_product', {
method: 'import',
} );
recordCompletionTime();
window.location.href = getAdminLink(
'edit.php?post_type=product&page=product_importer&wc_onboarding_active_task=products'
);
return false;
} }
href=""
type="wc-admin"
>
<></>
</Link>
),
},
} ) }
</Text>
</div>
);
};
export default Footer;

View File

@ -0,0 +1,35 @@
const Upload = () => {
return (
<svg
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="20" cy="20" r="20" fill="#007CBA" fillOpacity="0.05" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M26.5 23L26.5 28L28 28L28 23L26.5 23Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 23L12 28L13.5 28L13.5 23L12 23Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 28H28V26.5H12V28Z"
/>
<path
d="M20.25 13L26 18.25M20.25 13L20.25 28M20.25 13L15 18.25"
strokeWidth="1.5"
className="stroke-admin-theme"
/>
</svg>
);
};
export default Upload;

View File

@ -53,7 +53,18 @@
.woocommerce-products-stack { .woocommerce-products-stack {
max-width: 550px; max-width: 550px;
margin-top: 32px; margin-top: 24px;
&:first-child {
margin-top: 32px;
}
}
.woocommerce-list__item-before > svg {
fill: var(--wp-admin-theme-color);
.stroke-admin-theme {
stroke: var(--wp-admin-theme-color);
}
} }
} }

View File

@ -20,12 +20,15 @@ import { getAdminSetting } from '~/utils/admin-settings';
import { getSurfacedProductTypeKeys, getProductTypes } from './utils'; import { getSurfacedProductTypeKeys, getProductTypes } from './utils';
import useProductTypeListItems from './use-product-types-list-items'; import useProductTypeListItems from './use-product-types-list-items';
import Stack from './stack'; import Stack from './stack';
import Footer from './footer';
import LoadSampleProductModal from '../components/load-sample-product-modal'; import LoadSampleProductModal from '../components/load-sample-product-modal';
import useLoadSampleProducts from '../components/use-load-sample-products'; import useLoadSampleProducts from '../components/use-load-sample-products';
import LoadSampleProductConfirmModal from '../components/load-sample-product-confirm-modal'; import LoadSampleProductConfirmModal from '../components/load-sample-product-confirm-modal';
import useRecordCompletionTime from '../use-record-completion-time'; import useRecordCompletionTime from '../use-record-completion-time';
import { SETUP_TASKLIST_PRODUCTS_AFTER_FILTER } from './constants'; import {
SETUP_TASKLIST_PRODUCTS_AFTER_FILTER,
ImportCSVItem,
PrintfulAdvertProductPlacement,
} from './constants';
const getOnboardingProductType = (): string[] => { const getOnboardingProductType = (): string[] => {
const onboardingData = getAdminSetting( 'onboarding' ); const onboardingData = getAdminSetting( 'onboarding' );
@ -72,7 +75,7 @@ export const Products = () => {
() => () =>
productTypes.map( ( productType ) => ( { productTypes.map( ( productType ) => ( {
...productType, ...productType,
onClick: () => { onClick: (): void => {
productType.onClick(); productType.onClick();
recordCompletionTime(); recordCompletionTime();
}, },
@ -113,6 +116,27 @@ export const Products = () => {
return surfacedProductTypesAndAppendedProducts; return surfacedProductTypesAndAppendedProducts;
}, [ surfacedProductTypeKeys, isExpanded, productTypesWithTimeRecord ] ); }, [ surfacedProductTypeKeys, isExpanded, productTypesWithTimeRecord ] );
const footerStack = useMemo( () => {
const options = [];
const importCSVItemWithTimeRecord = {
...ImportCSVItem,
onClick: () => {
ImportCSVItem.onClick();
recordCompletionTime();
},
};
options.push( importCSVItemWithTimeRecord );
if (
window.wcAdminFeatures &&
window.wcAdminFeatures.printful === true
) {
options.push( PrintfulAdvertProductPlacement );
}
return options;
}, [ recordCompletionTime ] );
return ( return (
<div className="woocommerce-task-products"> <div className="woocommerce-task-products">
<Text <Text
@ -143,7 +167,11 @@ export const Products = () => {
setIsExpanded( ! isExpanded ); setIsExpanded( ! isExpanded );
} } } }
/> />
<Footer /> <Stack
items={ footerStack }
showOtherOptions={ false }
isTaskListItemClicked={ isRequesting }
/>
</div> </div>
{ isLoadingSampleProducts ? ( { isLoadingSampleProducts ? (
<LoadSampleProductModal /> <LoadSampleProductModal />

View File

@ -44,7 +44,7 @@
margin-top: 0; margin-top: 0;
} }
&.woocommerce-products-list__item-printful-advert { &.woocommerce-products-list__item-advert {
.woocommerce-list__item-before { .woocommerce-list__item-before {
background: none; background: none;
border-radius: 0; border-radius: 0;

View File

@ -15,7 +15,9 @@ import { ProductType } from './constants';
import './stack.scss'; import './stack.scss';
import useRecordCompletionTime from '../use-record-completion-time'; import useRecordCompletionTime from '../use-record-completion-time';
type StackProps = { type StackProps = StackWithLoadSampleBlurb | StackWithoutText;
type StackWithLoadSampleBlurb = {
items: ( ProductType & { items: ( ProductType & {
onClick: () => void; onClick: () => void;
} )[]; } )[];
@ -24,9 +26,18 @@ type StackProps = {
isTaskListItemClicked?: boolean; isTaskListItemClicked?: boolean;
}; };
type StackWithoutText = {
items: ( ProductType & {
onClick: () => void;
} )[];
showOtherOptions: false;
onClickLoadSampleProduct?: () => void;
isTaskListItemClicked?: boolean;
};
const Stack: React.FC< StackProps > = ( { const Stack: React.FC< StackProps > = ( {
items, items,
onClickLoadSampleProduct, onClickLoadSampleProduct = () => {},
showOtherOptions = true, showOtherOptions = true,
isTaskListItemClicked = false, isTaskListItemClicked = false,
} ) => { } ) => {

View File

@ -1,38 +0,0 @@
/**
* External dependencies
*/
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { recordEvent } from '@woocommerce/tracks';
jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) );
/**
* Internal dependencies
*/
import Footer from '../footer';
describe( 'Footer', () => {
beforeEach( () => {
( recordEvent as jest.Mock ).mockClear();
} );
it( 'should render footer with one links', () => {
const { queryAllByRole } = render( <Footer /> );
expect( queryAllByRole( 'link' ) ).toHaveLength( 1 );
} );
it( 'clicking on import CSV should fire event tasklist_add_product with method:import and task_completion_time', () => {
const { getByText } = render( <Footer /> );
userEvent.click( getByText( 'Import your products from a CSV file' ) );
expect( recordEvent ).toHaveBeenNthCalledWith(
1,
'tasklist_add_product',
{ method: 'import' }
);
expect( recordEvent ).toHaveBeenNthCalledWith(
2,
'task_completion_time',
{ task_name: 'products', time: '0-2s' }
);
} );
} );

View File

@ -72,10 +72,11 @@ describe( 'Products', () => {
product_types: [ 'downloads' ], product_types: [ 'downloads' ],
}, },
} ) ); } ) );
const { queryByText, queryByRole } = render( <Products /> ); const { queryByText, queryAllByRole } = render( <Products /> );
const productTypeList = queryAllByRole( 'menu' )?.[ 0 ];
expect( queryByText( 'Digital product' ) ).toBeInTheDocument(); expect( queryByText( 'Digital product' ) ).toBeInTheDocument();
expect( queryByRole( 'menu' )?.childElementCount ).toBe( 1 ); expect( productTypeList?.childElementCount ).toBe( 1 );
expect( queryByText( 'View more product types' ) ).toBeInTheDocument(); expect( queryByText( 'View more product types' ) ).toBeInTheDocument();
} ); } );
@ -116,7 +117,9 @@ describe( 'Products', () => {
product_types: [ 'downloads' ], product_types: [ 'downloads' ],
}, },
} ) ); } ) );
const { queryByText, getByRole, queryByRole } = render( <Products /> ); const { queryByText, getByRole, queryAllByRole } = render(
<Products />
);
expect( queryByText( 'View more product types' ) ).toBeInTheDocument(); expect( queryByText( 'View more product types' ) ).toBeInTheDocument();
@ -124,11 +127,13 @@ describe( 'Products', () => {
getByRole( 'button', { name: 'View more product types' } ) getByRole( 'button', { name: 'View more product types' } )
); );
await waitFor( () => await waitFor( () => {
expect( queryByRole( 'menu' )?.childElementCount ).toBe( const productTypeList = queryAllByRole( 'menu' )?.[ 0 ];
expect( productTypeList?.childElementCount ).toBe(
productTypes.length productTypes.length
) );
); } );
userEvent.click( userEvent.click(
getByRole( 'menuitem', { getByRole( 'menuitem', {
name: 'Grouped product A collection of related products.', name: 'Grouped product A collection of related products.',
@ -162,7 +167,9 @@ describe( 'Products', () => {
product_types: [ 'downloads' ], product_types: [ 'downloads' ],
}, },
} ) ); } ) );
const { queryByText, getByRole, queryByRole } = render( <Products /> ); const { queryByText, getByRole, queryAllByRole } = render(
<Products />
);
expect( queryByText( 'View more product types' ) ).toBeInTheDocument(); expect( queryByText( 'View more product types' ) ).toBeInTheDocument();
@ -170,11 +177,12 @@ describe( 'Products', () => {
getByRole( 'button', { name: 'View more product types' } ) getByRole( 'button', { name: 'View more product types' } )
); );
await waitFor( () => await waitFor( () => {
expect( queryByRole( 'menu' )?.childElementCount ).toBe( const productTypeList = queryAllByRole( 'menu' )?.[ 0 ];
expect( productTypeList?.childElementCount ).toBe(
productTypes.length productTypes.length
) );
); } );
expect( queryByText( 'View less product types' ) ).toBeInTheDocument(); expect( queryByText( 'View less product types' ) ).toBeInTheDocument();
} ); } );
@ -237,8 +245,10 @@ describe( 'Products', () => {
it( 'should render stacked layout', async () => { it( 'should render stacked layout', async () => {
const { container } = render( <Products /> ); const { container } = render( <Products /> );
expect( expect(
container.getElementsByClassName( 'woocommerce-products-stack' ) container.getElementsByClassName( 'woocommerce-products-stack' )
).toHaveLength( 1 ); .length
).toBeGreaterThanOrEqual( 1 );
} ); } );
} ); } );

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Update the footer section in "Add products" task

View File

@ -25,7 +25,7 @@
"remote-inbox-notifications": true, "remote-inbox-notifications": true,
"remote-free-extensions": true, "remote-free-extensions": true,
"payment-gateway-suggestions": true, "payment-gateway-suggestions": true,
"printful": false, "printful": true,
"settings": false, "settings": false,
"shipping-label-banner": true, "shipping-label-banner": true,
"subscriptions": true, "subscriptions": true,

View File

@ -23,7 +23,7 @@
"payment-gateway-suggestions": true, "payment-gateway-suggestions": true,
"product-pre-publish-modal": false, "product-pre-publish-modal": false,
"product-custom-fields": true, "product-custom-fields": true,
"printful": false, "printful": true,
"remote-inbox-notifications": true, "remote-inbox-notifications": true,
"remote-free-extensions": true, "remote-free-extensions": true,
"settings": false, "settings": false,