diff --git a/package.json b/package.json index 84bb3b8b7ec..9e50a58b174 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "sass": "^1.49.9", "sass-loader": "^10.2.1", "syncpack": "^9.8.4", - "turbo": "^1.7.0", + "turbo": "^1.8.3", "typescript": "^4.8.3", "url-loader": "^1.1.2", "webpack": "^5.70.0" diff --git a/packages/js/product-editor/changelog/add-37005 b/packages/js/product-editor/changelog/add-37005 new file mode 100644 index 00000000000..2b45e42e13d --- /dev/null +++ b/packages/js/product-editor/changelog/add-37005 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add a product header component to the blocks interface diff --git a/packages/js/product-editor/changelog/add-37096-tests b/packages/js/product-editor/changelog/add-37096-tests new file mode 100644 index 00000000000..a95e829ad3d --- /dev/null +++ b/packages/js/product-editor/changelog/add-37096-tests @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add tests around product block editor tabs diff --git a/packages/js/product-editor/changelog/add-37098_add_product_list_price_block b/packages/js/product-editor/changelog/add-37098_add_product_list_price_block new file mode 100644 index 00000000000..6ee3c274aa8 --- /dev/null +++ b/packages/js/product-editor/changelog/add-37098_add_product_list_price_block @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add new pricing block to the product editor package. diff --git a/packages/js/product-editor/package.json b/packages/js/product-editor/package.json index 677e915c674..ee99b501d5b 100644 --- a/packages/js/product-editor/package.json +++ b/packages/js/product-editor/package.json @@ -36,6 +36,7 @@ "@woocommerce/data": "workspace:^4.1.0", "@woocommerce/navigation": "workspace:^8.1.0", "@woocommerce/number": "workspace:*", + "@woocommerce/settings": "^1.0.0", "@woocommerce/tracks": "workspace:^1.3.0", "@wordpress/block-editor": "^9.8.0", "@wordpress/blocks": "^12.3.0", diff --git a/packages/js/product-editor/src/components/editor/editor.tsx b/packages/js/product-editor/src/components/editor/editor.tsx index ea6ef097811..b789f2539bf 100644 --- a/packages/js/product-editor/src/components/editor/editor.tsx +++ b/packages/js/product-editor/src/components/editor/editor.tsx @@ -12,7 +12,6 @@ import { Product } from '@woocommerce/data'; // @ts-ignore No types for this exist yet. // eslint-disable-next-line @woocommerce/dependency-group import { EntityProvider } from '@wordpress/core-data'; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore No types for this exist yet. // eslint-disable-next-line @woocommerce/dependency-group @@ -30,9 +29,11 @@ import { BlockEditor } from '../block-editor'; import { initBlocks } from './init-blocks'; initBlocks(); + export type ProductEditorSettings = Partial< EditorSettings & EditorBlockListSettings >; + type EditorProps = { product: Product; settings: ProductEditorSettings | undefined; @@ -46,7 +47,12 @@ export function Editor( { product, settings }: EditorProps ) { } + header={ +
+ } content={ { + registerCoreBlocks(); initName(); initSection(); initTab(); + initPricing(); }; diff --git a/packages/js/product-editor/src/components/header/header.tsx b/packages/js/product-editor/src/components/header/header.tsx index 3a202f43cdc..65270bd1346 100644 --- a/packages/js/product-editor/src/components/header/header.tsx +++ b/packages/js/product-editor/src/components/header/header.tsx @@ -1,14 +1,68 @@ /** * External dependencies */ -import { __ } from '@wordpress/i18n'; +import { Product } from '@woocommerce/data'; +import { Button } from '@wordpress/components'; +import { useDispatch, useSelect } from '@wordpress/data'; import { createElement } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { navigateTo, getNewPath } from '@woocommerce/navigation'; + +/** + * Internal dependencies + */ +import { AUTO_DRAFT_NAME, getHeaderTitle } from '../../utils'; export type HeaderProps = { - title: string; + productId: number; + productName: string; }; -export function Header( { title }: HeaderProps ) { +export function Header( { productId, productName }: HeaderProps ) { + const { isProductLocked, isSaving, editedProductName } = useSelect( + ( select ) => { + const { isSavingEntityRecord, getEditedEntityRecord } = + select( 'core' ); + const { isPostSavingLocked } = select( 'core/editor' ); + + const product: Product = getEditedEntityRecord( + 'postType', + 'product', + productId + ); + + return { + isProductLocked: isPostSavingLocked(), + isSaving: isSavingEntityRecord( + 'postType', + 'product', + productId + ), + editedProductName: product?.name, + }; + }, + [ productId ] + ); + + const isDisabled = isProductLocked || isSaving; + const isCreating = productName === AUTO_DRAFT_NAME; + + const { saveEditedEntityRecord } = useDispatch( 'core' ); + + function handleSave() { + saveEditedEntityRecord< Product >( + 'postType', + 'product', + productId + ).then( ( response ) => { + if ( isCreating ) { + navigateTo( { + url: getNewPath( {}, `/product/${ response.id }` ), + } ); + } + } ); + } + return (
-

{ title }

+

+ { getHeaderTitle( editedProductName, productName ) } +

+ +
+ +
); } diff --git a/packages/js/product-editor/src/components/header/style.scss b/packages/js/product-editor/src/components/header/style.scss index 79b59a277ad..a91b0280e7c 100644 --- a/packages/js/product-editor/src/components/header/style.scss +++ b/packages/js/product-editor/src/components/header/style.scss @@ -3,4 +3,8 @@ display: flex; align-items: center; padding: 0 $gap; + + &__actions { + margin-left: auto; + } } diff --git a/packages/js/product-editor/src/components/pricing-block/block.json b/packages/js/product-editor/src/components/pricing-block/block.json new file mode 100644 index 00000000000..9ca3884ba5f --- /dev/null +++ b/packages/js/product-editor/src/components/pricing-block/block.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "woocommerce/product-pricing", + "description": "A product price block with currency display.", + "title": "Product pricing", + "category": "widgets", + "keywords": [ "products", "price" ], + "textdomain": "default", + "attributes": { + "name": { + "type": "string" + }, + "label": { + "type": "string" + }, + "showPricingSection": { + "type": "boolean" + } + }, + "supports": { + "align": false, + "html": false, + "multiple": false, + "reusable": false, + "inserter": false, + "lock": false + } +} diff --git a/packages/js/product-editor/src/components/pricing-block/edit.tsx b/packages/js/product-editor/src/components/pricing-block/edit.tsx new file mode 100644 index 00000000000..b9b01ed1445 --- /dev/null +++ b/packages/js/product-editor/src/components/pricing-block/edit.tsx @@ -0,0 +1,89 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { createElement, useContext, Fragment } from '@wordpress/element'; +import interpolateComponents from '@automattic/interpolate-components'; +import { Link } from '@woocommerce/components'; +import { useBlockProps } from '@wordpress/block-editor'; +import { useEntityProp } from '@wordpress/core-data'; +import { BlockAttributes } from '@wordpress/blocks'; +import { CurrencyContext } from '@woocommerce/currency'; +import { getSetting } from '@woocommerce/settings'; +import { recordEvent } from '@woocommerce/tracks'; +import { + BaseControl, + // @ts-expect-error `__experimentalInputControl` does exist. + __experimentalInputControl as InputControl, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { formatCurrencyDisplayValue } from '../../utils'; +import { useCurrencyInputProps } from '../../hooks/use-currency-input-props'; + +export function Edit( { attributes }: { attributes: BlockAttributes } ) { + const blockProps = useBlockProps(); + const { name, label, showPricingSection = false } = attributes; + const [ regularPrice, setRegularPrice ] = useEntityProp< string >( + 'postType', + 'product', + name + ); + const context = useContext( CurrencyContext ); + const { getCurrencyConfig, formatAmount } = context; + const currencyConfig = getCurrencyConfig(); + const inputProps = useCurrencyInputProps( { + value: regularPrice, + setValue: setRegularPrice, + } ); + + const taxSettingsElement = showPricingSection + ? interpolateComponents( { + mixedString: __( + 'Manage more settings in {{link}}Pricing.{{/link}}', + 'woocommerce' + ), + components: { + link: ( + { + recordEvent( + 'product_pricing_list_price_help_tax_settings_click' + ); + } } + > + <> + + ), + }, + } ) + : null; + + return ( +
+ + + +
+ ); +} diff --git a/packages/js/product-editor/src/components/pricing-block/index.ts b/packages/js/product-editor/src/components/pricing-block/index.ts new file mode 100644 index 00000000000..f0c73e48b2b --- /dev/null +++ b/packages/js/product-editor/src/components/pricing-block/index.ts @@ -0,0 +1,17 @@ +/** + * Internal dependencies + */ +import { initBlock } from '../../utils'; +import metadata from './block.json'; +import { Edit } from './edit'; + +const { name } = metadata; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/js/product-editor/src/components/tab/edit.tsx b/packages/js/product-editor/src/components/tab/edit.tsx index a7bf3586b55..226dbd92ea8 100644 --- a/packages/js/product-editor/src/components/tab/edit.tsx +++ b/packages/js/product-editor/src/components/tab/edit.tsx @@ -17,7 +17,7 @@ export function Edit( { }: { attributes: BlockAttributes; context?: { - selectedTab?: string; + selectedTab?: string | null; }; } ) { const blockProps = useBlockProps(); diff --git a/packages/js/product-editor/src/components/tabs/test/tabs.spec.tsx b/packages/js/product-editor/src/components/tabs/test/tabs.spec.tsx new file mode 100644 index 00000000000..615f50cc46a --- /dev/null +++ b/packages/js/product-editor/src/components/tabs/test/tabs.spec.tsx @@ -0,0 +1,160 @@ +/** + * External dependencies + */ +import { render, fireEvent } from '@testing-library/react'; +import { getQuery, navigateTo } from '@woocommerce/navigation'; +import React, { createElement } from 'react'; +import { SlotFillProvider } from '@wordpress/components'; +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { Tabs } from '../'; +import { Edit as Tab } from '../../tab/edit'; + +jest.mock( '@wordpress/block-editor', () => ( { + ...jest.requireActual( '@wordpress/block-editor' ), + useBlockProps: jest.fn(), +} ) ); + +jest.mock( '@woocommerce/navigation', () => ( { + ...jest.requireActual( '@woocommerce/navigation' ), + navigateTo: jest.fn(), + getQuery: jest.fn().mockReturnValue( {} ), +} ) ); + +function MockTabs( { onChange = jest.fn() } ) { + const [ selected, setSelected ] = useState< string | null >( null ); + const mockContext = { + selectedTab: selected, + }; + + return ( + + { + setSelected( tabId ); + onChange( tabId ); + } } + /> + + + + + ); +} + +describe( 'Tabs', () => { + beforeEach( () => { + ( getQuery as jest.Mock ).mockReturnValue( { + tab: null, + } ); + } ); + + it( 'should render tab buttons added to the slot', () => { + const { queryByText } = render( ); + expect( queryByText( 'Test button 1' ) ).toBeInTheDocument(); + expect( queryByText( 'Test button 2' ) ).toBeInTheDocument(); + } ); + + it( 'should set the first tab as active initially', () => { + const { queryByText } = render( ); + expect( queryByText( 'Test button 1' ) ).toHaveAttribute( + 'aria-selected', + 'true' + ); + expect( queryByText( 'Test button 2' ) ).toHaveAttribute( + 'aria-selected', + 'false' + ); + } ); + + it( 'should navigate to a new URL when a tab is clicked', () => { + const { getByText } = render( ); + const button = getByText( 'Test button 2' ); + fireEvent.click( button ); + + expect( navigateTo ).toHaveBeenLastCalledWith( { + url: 'admin.php?page=wc-admin&tab=test2', + } ); + } ); + + it( 'should select the tab provided in the URL initially', () => { + ( getQuery as jest.Mock ).mockReturnValue( { + tab: 'test2', + } ); + + const { getByText } = render( ); + + expect( getByText( 'Test button 2' ) ).toHaveAttribute( + 'aria-selected', + 'true' + ); + } ); + + it( 'should select the tab provided on URL change', () => { + const { getByText, rerender } = render( ); + + ( getQuery as jest.Mock ).mockReturnValue( { + tab: 'test3', + } ); + + rerender( ); + + expect( getByText( 'Test button 3' ) ).toHaveAttribute( + 'aria-selected', + 'true' + ); + } ); + + it( 'should call the onChange props when changing', async () => { + const mockOnChange = jest.fn(); + const { rerender } = render( ); + + expect( mockOnChange ).toHaveBeenCalledWith( 'test1' ); + + ( getQuery as jest.Mock ).mockReturnValue( { + tab: 'test2', + } ); + + rerender( ); + + expect( mockOnChange ).toHaveBeenCalledWith( 'test2' ); + } ); + + it( 'should add a class to the initially selected tab panel', async () => { + const { getByRole } = render( ); + const panel1 = getByRole( 'tabpanel', { name: 'Test button 1' } ); + const panel2 = getByRole( 'tabpanel', { name: 'Test button 2' } ); + + expect( panel1.classList ).toContain( 'is-selected' ); + expect( panel2.classList ).not.toContain( 'is-selected' ); + } ); + + it( 'should add a class to the newly selected tab panel', async () => { + const { getByText, getByRole, rerender } = render( ); + const button = getByText( 'Test button 2' ); + fireEvent.click( button ); + const panel1 = getByRole( 'tabpanel', { name: 'Test button 1' } ); + const panel2 = getByRole( 'tabpanel', { name: 'Test button 2' } ); + + ( getQuery as jest.Mock ).mockReturnValue( { + tab: 'test2', + } ); + + rerender( ); + + expect( panel1.classList ).not.toContain( 'is-selected' ); + expect( panel2.classList ).toContain( 'is-selected' ); + } ); +} ); diff --git a/packages/js/product-editor/src/hooks/index.ts b/packages/js/product-editor/src/hooks/index.ts index 700bcf73864..6899f9e1335 100644 --- a/packages/js/product-editor/src/hooks/index.ts +++ b/packages/js/product-editor/src/hooks/index.ts @@ -1,2 +1,3 @@ export { useProductHelper as __experimentalUseProductHelper } from './use-product-helper'; export { useVariationsOrder as __experimentalUseVariationsOrder } from './use-variations-order'; +export { useCurrencyInputProps as __experimentalUseCurrencyInputProps } from './use-currency-input-props'; diff --git a/packages/js/product-editor/src/hooks/use-currency-input-props.ts b/packages/js/product-editor/src/hooks/use-currency-input-props.ts new file mode 100644 index 00000000000..da6b53d573a --- /dev/null +++ b/packages/js/product-editor/src/hooks/use-currency-input-props.ts @@ -0,0 +1,77 @@ +/** + * External dependencies + */ +import { CurrencyContext } from '@woocommerce/currency'; +import { useContext } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { useProductHelper } from './use-product-helper'; + +export type CurrencyInputProps = { + prefix: string; + className: string; + sanitize: ( value: string | number ) => string; + onFocus: ( event: React.FocusEvent< HTMLInputElement > ) => void; + onKeyUp: ( event: React.KeyboardEvent< HTMLInputElement > ) => void; +}; + +type Props = { + value: string; + setValue: ( value: string ) => void; + onFocus?: ( event: React.FocusEvent< HTMLInputElement > ) => void; + onKeyUp?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void; +}; + +export const useCurrencyInputProps = ( { + value, + setValue, + onFocus, + onKeyUp, +}: Props ) => { + const { sanitizePrice } = useProductHelper(); + + const context = useContext( CurrencyContext ); + const { getCurrencyConfig } = context; + const currencyConfig = getCurrencyConfig(); + + const currencyInputProps: CurrencyInputProps = { + prefix: currencyConfig.symbol, + className: 'half-width-field components-currency-control', + sanitize: ( val: string | number ) => { + return sanitizePrice( String( val ) ); + }, + onFocus( event: React.FocusEvent< HTMLInputElement > ) { + // In some browsers like safari .select() function inside + // the onFocus event doesn't work as expected because it + // conflicts with onClick the first time user click the + // input. Using setTimeout defers the text selection and + // avoid the unexpected behaviour. + setTimeout( + function deferSelection( element: HTMLInputElement ) { + element.select(); + }, + 0, + event.currentTarget + ); + if ( onFocus ) { + onFocus( event ); + } + }, + onKeyUp( event: React.KeyboardEvent< HTMLInputElement > ) { + const amount = Number.parseFloat( sanitizePrice( value || '0' ) ); + const step = Number( event.currentTarget.step || '1' ); + if ( event.code === 'ArrowUp' ) { + setValue( String( amount + step ) ); + } + if ( event.code === 'ArrowDown' ) { + setValue( String( amount - step ) ); + } + if ( onKeyUp ) { + onKeyUp( event ); + } + }, + }; + return currencyInputProps; +}; diff --git a/packages/js/product-editor/src/utils/constants.ts b/packages/js/product-editor/src/utils/constants.ts index 0aa72fd516a..6c0b603d436 100644 --- a/packages/js/product-editor/src/utils/constants.ts +++ b/packages/js/product-editor/src/utils/constants.ts @@ -7,3 +7,4 @@ export const ADD_NEW_SHIPPING_CLASS_OPTION_VALUE = export const UNCATEGORIZED_CATEGORY_SLUG = 'uncategorized'; export const PRODUCT_VARIATION_TITLE_LIMIT = 32; export const STANDARD_RATE_TAX_CLASS_SLUG = 'standard'; +export const AUTO_DRAFT_NAME = 'AUTO-DRAFT'; diff --git a/packages/js/product-editor/src/utils/get-header-title.ts b/packages/js/product-editor/src/utils/get-header-title.ts new file mode 100644 index 00000000000..a0c6d712855 --- /dev/null +++ b/packages/js/product-editor/src/utils/get-header-title.ts @@ -0,0 +1,35 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { AUTO_DRAFT_NAME } from './constants'; + +/** + * Get the header title using the product name. + * + * @param editedProductName Name value entered for the product. + * @param initialProductName Name already persisted to the database. + * @return The new title + */ +export const getHeaderTitle = ( + editedProductName: string, + initialProductName: string +): string => { + const isProductNameNotEmpty = Boolean( editedProductName ); + const isProductNameDirty = editedProductName !== initialProductName; + const isCreating = initialProductName === AUTO_DRAFT_NAME; + + if ( isProductNameNotEmpty && isProductNameDirty ) { + return editedProductName; + } + + if ( isCreating ) { + return __( 'Add new product', 'woocommerce' ); + } + + return initialProductName; +}; diff --git a/packages/js/product-editor/src/utils/get-product-stock-status.ts b/packages/js/product-editor/src/utils/get-product-stock-status.ts index 88ba69a46eb..9cc8f5419f3 100644 --- a/packages/js/product-editor/src/utils/get-product-stock-status.ts +++ b/packages/js/product-editor/src/utils/get-product-stock-status.ts @@ -51,7 +51,9 @@ export const getProductStockStatus = ( } if ( product.stock_status ) { - return PRODUCT_STOCK_STATUS_LABELS[ product.stock_status ]; + return PRODUCT_STOCK_STATUS_LABELS[ + product.stock_status as PRODUCT_STOCK_STATUS_KEYS + ]; } return PRODUCT_STOCK_STATUS_LABELS.instock; @@ -77,6 +79,8 @@ export const getProductStockStatusClass = ( return PRODUCT_STOCK_STATUS_CLASSES.outofstock; } return product.stock_status - ? PRODUCT_STOCK_STATUS_CLASSES[ product.stock_status ] + ? PRODUCT_STOCK_STATUS_CLASSES[ + product.stock_status as PRODUCT_STOCK_STATUS_KEYS + ] : ''; }; diff --git a/packages/js/product-editor/src/utils/get-product-title.ts b/packages/js/product-editor/src/utils/get-product-title.ts index 2f92e9d81b9..dc090527ca0 100644 --- a/packages/js/product-editor/src/utils/get-product-title.ts +++ b/packages/js/product-editor/src/utils/get-product-title.ts @@ -3,7 +3,10 @@ */ import { __ } from '@wordpress/i18n'; -export const AUTO_DRAFT_NAME = 'AUTO-DRAFT'; +/** + * Internal dependencies + */ +import { AUTO_DRAFT_NAME } from './constants'; /** * Get the product title for use in the header. diff --git a/packages/js/product-editor/src/utils/index.ts b/packages/js/product-editor/src/utils/index.ts index b2ddb029f36..67a0014effc 100644 --- a/packages/js/product-editor/src/utils/index.ts +++ b/packages/js/product-editor/src/utils/index.ts @@ -1,16 +1,18 @@ /** * Internal dependencies */ +import { AUTO_DRAFT_NAME } from './constants'; import { formatCurrencyDisplayValue } from './format-currency-display-value'; import { getCheckboxTracks } from './get-checkbox-tracks'; import { getCurrencySymbolProps } from './get-currency-symbol-props'; import { getDerivedProductType } from './get-derived-product-type'; +import { getHeaderTitle } from './get-header-title'; import { getProductStatus, PRODUCT_STATUS_LABELS } from './get-product-status'; import { getProductStockStatus, getProductStockStatusClass, } from './get-product-stock-status'; -import { getProductTitle, AUTO_DRAFT_NAME } from './get-product-title'; +import { getProductTitle } from './get-product-title'; import { getProductVariationTitle, getTruncatedProductVariationTitle, @@ -27,6 +29,7 @@ export { getCheckboxTracks, getCurrencySymbolProps, getDerivedProductType, + getHeaderTitle, getProductStatus, getProductStockStatus, getProductStockStatusClass, diff --git a/packages/js/product-editor/typings/global.d.ts b/packages/js/product-editor/typings/global.d.ts index 69abbdeff5e..3a27ae17ad8 100644 --- a/packages/js/product-editor/typings/global.d.ts +++ b/packages/js/product-editor/typings/global.d.ts @@ -6,4 +6,3 @@ declare global { /*~ If your module exports nothing, you'll need this line. Otherwise, delete it */ export {}; - diff --git a/packages/js/product-editor/typings/index.d.ts b/packages/js/product-editor/typings/index.d.ts new file mode 100644 index 00000000000..fa38181628c --- /dev/null +++ b/packages/js/product-editor/typings/index.d.ts @@ -0,0 +1,18 @@ +declare module '@woocommerce/settings' { + export declare function getAdminLink( path: string ): string; + export declare function getSetting< T >( + name: string, + fallback?: unknown, + filter = ( val: unknown, fb: unknown ) => + typeof val !== 'undefined' ? val : fb + ): T; +} + +declare module '@wordpress/core-data' { + function useEntityProp< T = unknown >( + kind: string, + name: string, + prop: string, + id?: string + ): [ T, ( value: T ) => void, T ]; +} diff --git a/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.tsx b/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.tsx index c7044810222..d6f811b5796 100644 --- a/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.tsx +++ b/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.tsx @@ -64,7 +64,7 @@ const CollapsibleCard: React.FC< CollapsibleCardProps > = ( { { ! collapsed && ( <> { children } - { footer && { footer } } + { !! footer && { footer } } ) } diff --git a/plugins/woocommerce-admin/client/marketing/components/CreateNewCampaignModal/CreateNewCampaignModal.tsx b/plugins/woocommerce-admin/client/marketing/components/CreateNewCampaignModal/CreateNewCampaignModal.tsx index 3d67dd541cb..8bfcac186ef 100644 --- a/plugins/woocommerce-admin/client/marketing/components/CreateNewCampaignModal/CreateNewCampaignModal.tsx +++ b/plugins/woocommerce-admin/client/marketing/components/CreateNewCampaignModal/CreateNewCampaignModal.tsx @@ -105,7 +105,7 @@ export const CreateNewCampaignModal = ( props: CreateCampaignModalProps ) => { { __( 'Create', 'woocommerce' ) } - { isExternalURL( el.createUrl ) && ( + { !! isExternalURL( el.createUrl ) && ( - { post.image && ( + { !! post.image && (
@@ -89,7 +89,7 @@ const KnowledgeBase = ( {

{ __( 'By', 'woocommerce' ) + ' ' } { post.author_name } - { post.author_avatar && ( + { !! post.author_avatar && ( { return (

- { shouldShowExtensions && ( + { !! shouldShowExtensions && ( { { el.title } - { el.description && ( + { !! el.description && ( { el.description } @@ -170,14 +170,14 @@ export const Campaigns = () => { > { __( 'Create new campaign', 'woocommerce' ) } - { isModalOpen && ( + { !! isModalOpen && ( setModalOpen( false ) } /> ) } { getContent() } - { total && total > perPage && ( + { !! ( total && total > perPage ) && ( = ( { { /* Recommended channels section. */ } { recommendedChannels.length >= 1 && (
- { hasRegisteredChannels && ( + { !! hasRegisteredChannels && ( <> @@ -98,7 +98,7 @@ export const Channels: React.FC< ChannelsProps > = ( { ) } - { expanded && + { !! expanded && recommendedChannels.map( ( el, idx ) => { return ( diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.tsx index 2d4d9c007fa..ff80123cfc4 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.tsx @@ -31,7 +31,7 @@ export const RegisteredChannelCardBody: React.FC< registeredChannel.description ) : (
- { registeredChannel.syncStatus && ( + { !! registeredChannel.syncStatus && ( <>
diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/LearnMarketing/PostTile.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/LearnMarketing/PostTile.tsx index c5404557db5..7dc38070cd3 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/LearnMarketing/PostTile.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/LearnMarketing/PostTile.tsx @@ -27,7 +27,7 @@ export const PostTile: React.FC< PostTileProps > = ( { post } ) => { } } >
- { post.image && } + { !! post.image && }
{ post.title } @@ -37,7 +37,7 @@ export const PostTile: React.FC< PostTileProps > = ( { post } ) => { // translators: %s: author's name. sprintf( __( 'By %s', 'woocommerce' ), post.author_name ) } - { post.author_avatar && ( + { !! post.author_avatar && ( { /> ) } { !! dataRegistered?.length && } - { dataRegistered && - dataRecommended && + { !! ( dataRegistered && dataRecommended ) && !! ( dataRegistered.length || dataRecommended.length ) && ( { /> ) } - { shouldShowExtensions && } + { !! shouldShowExtensions && }
); diff --git a/plugins/woocommerce-admin/client/marketing/overview/index.js b/plugins/woocommerce-admin/client/marketing/overview/index.js index 966254141ed..6a69a18cb3c 100644 --- a/plugins/woocommerce-admin/client/marketing/overview/index.js +++ b/plugins/woocommerce-admin/client/marketing/overview/index.js @@ -27,7 +27,7 @@ const MarketingOverview = () => { - { shouldShowExtensions && ( + { !! shouldShowExtensions && ( ) } diff --git a/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx b/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx index babef4e8679..1425d26fab0 100644 --- a/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx +++ b/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx @@ -193,7 +193,14 @@ const PaymentRecommendations: React.FC = () => { ), before: ( - + ), }; } ); diff --git a/plugins/woocommerce-admin/client/products/product-block-page.scss b/plugins/woocommerce-admin/client/products/product-block-page.scss new file mode 100644 index 00000000000..9dc12e60198 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/product-block-page.scss @@ -0,0 +1,47 @@ +.woocommerce-product-block-editor { + .components-input-control { + &__prefix { + margin-left: $gap-smaller; + } + + &__suffix { + margin-right: $gap-smaller; + } + } + + .components-currency-control { + .components-input-control__prefix { + color: $gray-700; + } + + .components-input-control__input { + text-align: right; + } + } + + .woocommerce-product-form { + &__custom-label-input { + display: flex; + flex-direction: column; + + label { + display: block; + margin-bottom: $gap-smaller; + } + } + + &__optional-input { + color: $gray-700; + } + } + + .wp-block-columns { + gap: $gap-large; + } + + .wp-block-woocommerce-product-section { + > .block-editor-inner-blocks > .block-editor-block-list__layout > .wp-block:not(:first-child) { + margin-top: $gap-large; + } + } +} diff --git a/plugins/woocommerce-admin/client/products/product-page.tsx b/plugins/woocommerce-admin/client/products/product-page.tsx index 5f3eb9873c8..e62c91a6dce 100644 --- a/plugins/woocommerce-admin/client/products/product-page.tsx +++ b/plugins/woocommerce-admin/client/products/product-page.tsx @@ -16,6 +16,7 @@ import { useParams } from 'react-router-dom'; * Internal dependencies */ import './product-page.scss'; +import './product-block-page.scss'; declare const productBlockEditorSettings: ProductEditorSettings; @@ -34,18 +35,23 @@ const ProductEditor: React.FC< { product: Product | undefined } > = ( { ); }; -const EditProductEditor: React.FC< { productId: string } > = ( { +const EditProductEditor: React.FC< { productId: number } > = ( { productId, } ) => { - const { product } = useSelect( ( select: typeof WPSelect ) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore Missing types. - const { getEditedEntityRecord } = select( 'core' ); + const { product } = useSelect( + ( select: typeof WPSelect ) => { + const { getEntityRecord } = select( 'core' ); - return { - product: getEditedEntityRecord( 'postType', 'product', productId ), - }; - } ); + return { + product: getEntityRecord( + 'postType', + 'product', + productId + ) as Product, + }; + }, + [ productId ] + ); return ; }; @@ -74,7 +80,9 @@ export default function ProductPage() { const { productId } = useParams(); if ( productId ) { - return ; + return ( + + ); } return ; diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/Item.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/Item.js index 80bdede39d2..25ceb1c4557 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/Item.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/Item.js @@ -31,6 +31,7 @@ export const Item = ( { isRecommended, markConfigured, paymentGateway } ) => { settingsUrl: manageUrl, is_local_partner: isLocalPartner, external_link: externalLink, + transaction_processors: transactionProcessors, } = paymentGateway; const connectSlot = useSlot( @@ -88,6 +89,21 @@ export const Item = ( { isRecommended, markConfigured, paymentGateway } ) => {
{ content }
+ { transactionProcessors && ( +
+ { Object.keys( transactionProcessors ).map( + ( key ) => { + return ( + { + ); + } + ) } +
+ ) }
e.preventDefault() } - > - ), - }, - } ), - before: , - onClick: () => { - recordEvent( 'tasklist_add_product', { method: 'migrate' } ); - window - .open( - 'https://woocommerce.com/products/cart2cart/?utm_medium=product', - '_blank' - ) - ?.focus(); - }, - }, ]; diff --git a/plugins/woocommerce-admin/client/tasks/fills/import-products/test/index.tsx b/plugins/woocommerce-admin/client/tasks/fills/import-products/test/index.tsx index 981543421f5..6bf835fea08 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/import-products/test/index.tsx +++ b/plugins/woocommerce-admin/client/tasks/fills/import-products/test/index.tsx @@ -57,24 +57,6 @@ describe( 'Products', () => { ); } ); - test( 'should fire "tasklist_add_product" event when the cart2cart option clicked', async () => { - const { getByRole } = render( ); - - userEvent.click( - getByRole( 'menuitem', { - name: 'FROM CART2CART Migrate all store data like products, customers, and orders in no time with this 3rd party plugin. Learn more (opens in a new tab)', - } ) - ); - await waitFor( () => - expect( recordEvent ).toHaveBeenCalledWith( - 'tasklist_add_product', - { - method: 'migrate', - } - ) - ); - } ); - test( 'should fire "task_completion_time" event when an option clicked', async () => { Object.defineProperty( window, 'performance', { value: { diff --git a/plugins/woocommerce-admin/client/tasks/fills/products/footer.tsx b/plugins/woocommerce-admin/client/tasks/fills/products/footer.tsx index 2538012b50b..86dba20973c 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/products/footer.tsx +++ b/plugins/woocommerce-admin/client/tasks/fills/products/footer.tsx @@ -4,7 +4,6 @@ import { __ } from '@wordpress/i18n'; import interpolateComponents from '@automattic/interpolate-components'; import { Text } from '@woocommerce/experimental'; -import { ExternalLink } from '@wordpress/components'; import { Link } from '@woocommerce/components'; import { getAdminLink } from '@woocommerce/settings'; import { recordEvent } from '@woocommerce/tracks'; @@ -25,7 +24,7 @@ const Footer: React.FC = () => { { interpolateComponents( { mixedString: __( - '{{importCSVLink}}Import your products from a CSV file{{/importCSVLink}} or {{_3rdLink}}use a 3rd party migration plugin{{/_3rdLink}}.', + '{{importCSVLink}}Import your products from a CSV file{{/importCSVLink}}.', 'woocommerce' ), components: { @@ -47,20 +46,6 @@ const Footer: React.FC = () => { <> ), - _3rdLink: ( - { - recordEvent( 'tasklist_add_product', { - method: 'migrate', - } ); - recordCompletionTime(); - } } - href="https://woocommerce.com/products/cart2cart/?utm_medium=product" - type="external" - > - <> - - ), }, } ) } diff --git a/plugins/woocommerce-admin/client/tasks/fills/products/test/footer.tsx b/plugins/woocommerce-admin/client/tasks/fills/products/test/footer.tsx index f042aa61be2..b75e20d3107 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/products/test/footer.tsx +++ b/plugins/woocommerce-admin/client/tasks/fills/products/test/footer.tsx @@ -16,9 +16,9 @@ describe( 'Footer', () => { beforeEach( () => { ( recordEvent as jest.Mock ).mockClear(); } ); - it( 'should render footer with two links', () => { + it( 'should render footer with one links', () => { const { queryAllByRole } = render(