Merge branch 'trunk' into add/orders-statuses-endpoint

This commit is contained in:
Remi Corson 2024-11-07 09:06:09 +01:00 committed by GitHub
commit f648e910db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
192 changed files with 2897 additions and 3093 deletions

View File

@ -1,84 +0,0 @@
name: Mirrors
on:
push:
branches: ["trunk", "release/**"]
workflow_dispatch:
permissions: {}
jobs:
build:
if: github.repository == 'woocommerce/woocommerce'
name: Build WooCommerce zip
runs-on: ubuntu-20.04
permissions:
contents: read
steps:
- uses: actions/checkout@v3
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
with:
pull-package-deps: '@woocommerce/plugin-woocommerce'
- name: Build zip
working-directory: plugins/woocommerce
run: bash bin/build-zip.sh
- name: Upload the zip file as an artifact
uses: actions/upload-artifact@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
name: woocommerce
path: plugins/woocommerce/woocommerce.zip
retention-days: 7
mirror:
if: github.repository == 'woocommerce/woocommerce'
name: Push to Mirror
needs: [build]
runs-on: ubuntu-20.04
permissions:
contents: read
steps:
- name: Create directories
run: |
mkdir -p monorepo
- name: Checkout monorepo
uses: actions/checkout@v3
with:
path: monorepo
- name: Download WooCommerce ZIP
uses: actions/download-artifact@v3
with:
name: woocommerce
path: tmp/woocommerce-build
- name: Extract and replace WooCommerce zip.
working-directory: tmp/woocommerce-build
run: |
mkdir -p woocommerce/woocommerce-production
unzip woocommerce.zip -d woocommerce/woocommerce-production
mv woocommerce/woocommerce-production/woocommerce/* woocommerce/woocommerce-production
rm -rf woocommerce/woocommerce-production/woocommerce
- name: Copy Composer over to production
run: cp monorepo/plugins/woocommerce/composer.json tmp/woocommerce-build/woocommerce/woocommerce-production
- name: Set up mirror
working-directory: tmp/woocommerce-build
run: |
touch mirrors.txt
echo "woocommerce/woocommerce-production" >> mirrors.txt
- name: Push to mirror
uses: Automattic/action-push-to-mirrors@v1
with:
source-directory: ${{ github.workspace }}/monorepo
token: ${{ secrets.API_TOKEN_GITHUB }}
username: matticbot
working-directory: ${{ github.workspace }}/tmp/woocommerce-build
timeout-minutes: 5 # 2021-01-18: Successful runs seem to take about half a minute.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
A fix a bug where users need to click Give feedback twice.

View File

@ -8,6 +8,7 @@ import { createElement, useState } from '@wordpress/element';
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
import { __ } from '@wordpress/i18n';
import { recordEvent } from '@woocommerce/tracks';
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
@ -38,7 +39,6 @@ import { getStoreAgeInWeeks } from '../../utils';
* @param {boolean} props.allowTracking Whether tracking is allowed or not.
* @param {boolean} props.resolving Are values still being resolved.
* @param {number} props.storeAgeInWeeks The age of the store in weeks.
* @param {Function} props.updateOptions Function to update options.
* @param {Function} props.createNotice Function to create a snackbar.
*/
function _CustomerEffortScoreTracks( {
@ -55,7 +55,6 @@ function _CustomerEffortScoreTracks( {
allowTracking,
resolving,
storeAgeInWeeks,
updateOptions,
createNotice,
} ) {
const [ modalShown, setModalShown ] = useState( false );
@ -91,12 +90,17 @@ function _CustomerEffortScoreTracks( {
ces_location: 'inside',
...trackProps,
} );
if ( ! cesShownForActions || ! cesShownForActions.includes( action ) ) {
updateOptions( {
[ SHOWN_FOR_ACTIONS_OPTION_NAME ]: [
action,
...( cesShownForActions || [] ),
],
apiFetch( {
path: 'wc-admin/options',
method: 'POST',
data: {
[ SHOWN_FOR_ACTIONS_OPTION_NAME ]: [
action,
...( cesShownForActions || [] ),
],
},
} );
}
};
@ -247,11 +251,9 @@ export const CustomerEffortScoreTracks = compose(
};
} ),
withDispatch( ( dispatch ) => {
const { updateOptions } = dispatch( OPTIONS_STORE_NAME );
const { createNotice } = dispatch( 'core/notices' );
return {
updateOptions,
createNotice,
};
} )

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Comment: Adds layout to Settings, which is unreleased.

View File

@ -40,6 +40,10 @@ export { PostTypeContext } from './contexts/post-type-context';
*/
export * from './products';
export { default as SiteHub } from './products-app/site-hub';
export { default as SidebarContent } from './products-app/sidebar';
export { unlock } from './lock-unlock';
// Init the store
registerProductEditorUiStore();

View File

@ -3,8 +3,19 @@
*/
import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis';
export const { lock, unlock } =
__dangerousOptInToUnstableAPIsOnlyForCoreModules(
'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.',
'@wordpress/edit-site'
);
/**
* Internal dependencies
*/
import { getGutenbergVersion } from './utils/get-gutenberg-version';
const isGutenbergEnabled = getGutenbergVersion() > 0;
const noop = () => {};
const { lock, unlock } = isGutenbergEnabled
? __dangerousOptInToUnstableAPIsOnlyForCoreModules(
'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.',
'@wordpress/edit-site'
)
: { lock: noop, unlock: noop };
export { lock, unlock };

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Comment: Adds layout to Settings, which is unreleased.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: add
Create a warning or notice to add Gutenberg

View File

@ -39,6 +39,7 @@
"@types/wordpress__blocks": "11.0.7",
"@woocommerce/settings": "^1.0.0",
"@woocommerce/tracks": "workspace:^",
"@woocommerce/product-editor": "workspace:^",
"@wordpress/api-fetch": "wp-6.0",
"@wordpress/components": "wp-6.0",
"@wordpress/compose": "wp-6.0",
@ -244,6 +245,10 @@
"node_modules/@woocommerce/eslint-plugin/configs",
"node_modules/@woocommerce/eslint-plugin/rules",
"node_modules/@woocommerce/eslint-plugin/index.js",
"node_modules/@woocommerce/product-editor/build",
"node_modules/@woocommerce/product-editor/build-module",
"node_modules/@woocommerce/product-editor/build-style",
"node_modules/@woocommerce/product-editor/build-types",
"node_modules/@woocommerce/tracks/build",
"node_modules/@woocommerce/tracks/build-module",
"node_modules/@woocommerce/tracks/build-types",

View File

@ -2,7 +2,38 @@
* External dependencies
*/
import { createElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { isGutenbergVersionAtLeast } from './utils';
import { Layout } from './layout';
const Sidebar = <div>Sidebar content goes here</div>;
export const SettingsEditor = () => {
return <div style={ { padding: '20px' } }>Settings Editor</div>;
const isRequiredGutenbergVersion = isGutenbergVersionAtLeast( 19.0 );
if ( ! isRequiredGutenbergVersion ) {
return (
// Temporary during development.
<div style={ { margin: 'auto' } }>
{ __(
'Please enable Gutenberg version 19.0 or higher for this feature',
'woocommerce'
) }
</div>
);
}
return (
<Layout
route={ {
key: 'settings',
areas: { sidebar: Sidebar },
widths: {},
} }
/>
);
};

View File

@ -0,0 +1,127 @@
/**
* External dependencies
*/
import { createElement, Fragment, useRef } from '@wordpress/element';
import { unlock, SiteHub, SidebarContent } from '@woocommerce/product-editor';
import {
useViewportMatch,
useResizeObserver,
useReducedMotion,
} from '@wordpress/compose';
import { __ } from '@wordpress/i18n';
import {
// @ts-expect-error missing type.
EditorSnackbars,
// @ts-expect-error missing type.
privateApis as editorPrivateApis,
} from '@wordpress/editor';
// eslint-disable-next-line @woocommerce/dependency-group
import {
// @ts-expect-error missing type.
__unstableMotion as motion,
// @ts-expect-error missing type.
__unstableAnimatePresence as AnimatePresence,
} from '@wordpress/components';
type Route = {
key: string;
areas: {
sidebar: React.JSX.Element | React.FunctionComponent;
content?: React.JSX.Element | React.FunctionComponent;
edit?: React.JSX.Element | React.FunctionComponent;
mobile?: React.JSX.Element | React.FunctionComponent | boolean;
preview?: boolean;
};
widths?: {
content?: number;
edit?: number;
sidebar?: number;
};
};
const { NavigableRegion } = unlock( editorPrivateApis );
const ANIMATION_DURATION = 0.3;
type LayoutProps = {
route: Route;
};
export function Layout( { route }: LayoutProps ) {
const [ fullResizer ] = useResizeObserver();
const toggleRef = useRef< HTMLAnchorElement >( null );
const isMobileViewport = useViewportMatch( 'medium', '<' );
const disableMotion = useReducedMotion();
const { key: routeKey, areas, widths } = route;
return (
<>
{ fullResizer }
<div className="edit-site-layout">
<div className="edit-site-layout__content">
{ /*
The NavigableRegion must always be rendered and not use
`inert` otherwise `useNavigateRegions` will fail.
*/ }
{ ( ! isMobileViewport || ! areas.mobile ) && (
<NavigableRegion
ariaLabel={ __( 'Navigation', 'woocommerce' ) }
className="edit-site-layout__sidebar-region"
>
<AnimatePresence>
<motion.div
initial={ { opacity: 0 } }
animate={ { opacity: 1 } }
exit={ { opacity: 0 } }
transition={ {
type: 'tween',
duration:
// Disable transition in mobile to emulate a full page transition.
disableMotion || isMobileViewport
? 0
: ANIMATION_DURATION,
ease: 'easeOut',
} }
className="edit-site-layout__sidebar"
>
<SiteHub
ref={ toggleRef }
isTransparent={ false }
/>
<SidebarContent routeKey={ routeKey }>
{ areas.sidebar }
</SidebarContent>
</motion.div>
</AnimatePresence>
</NavigableRegion>
) }
<EditorSnackbars />
{ ! isMobileViewport && areas.content && (
<div
className="edit-site-layout__area"
style={ {
maxWidth: widths?.content,
} }
>
{ areas.content }
</div>
) }
{ ! isMobileViewport && areas.edit && (
<div
className="edit-site-layout__area"
style={ {
maxWidth: widths?.edit,
} }
>
{ areas.edit }
</div>
) }
</div>
</div>
</>
);
}

View File

@ -0,0 +1,12 @@
/**
* External dependencies
*/
import { getSetting } from '@woocommerce/settings';
export function isGutenbergVersionAtLeast( version: number ) {
const adminSettings: { gutenberg_version?: string } = getSetting( 'admin' );
if ( adminSettings.gutenberg_version ) {
return parseFloat( adminSettings?.gutenberg_version ) >= version;
}
return false;
}

View File

@ -0,0 +1,13 @@
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;
export declare function isWpVersion(
version: string,
operator: '>' | '>=' | '=' | '<' | '<='
): boolean;
}

View File

@ -44,6 +44,37 @@ register_woocommerce_admin_test_helper_rest_route(
)
);
register_woocommerce_admin_test_helper_rest_route(
'/options',
'wca_test_helper_update_option',
array(
'methods' => 'POST',
'args' => array(
'options' => array(
'description' => 'Array of options to update.',
'type' => 'array',
'required' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'option_name' => array(
'description' => 'The name of the option to update.',
'type' => 'string',
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
),
'option_value' => array(
'description' => 'The new value for the option.',
'required' => true,
),
),
),
),
),
)
);
/**
* A helper to delete options.
*
@ -99,3 +130,26 @@ function wca_test_helper_get_options( $request ) {
return new WP_REST_Response( $options, 200 );
}
/**
* Update WordPress options. Supports single or batch updates.
*
* @param WP_REST_Request $request The full request data.
* @return WP_REST_Response
*/
function wca_test_helper_update_option( $request ) {
$data = $request->get_json_params();
$response = array();
foreach ( $data['options'] as $option ) {
if ( ! isset( $option['option_name'] ) || ! isset( $option['option_value'] ) ) {
continue;
}
update_option( $option['option_name'], $option['option_value'] );
$response[] = array(
'option_name' => $option['option_name'],
'option_value' => $option['option_value'],
);
}
return new WP_REST_Response( $response, 200 );
}

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Update WC Admin Test Helper's Reset Onboarding Wizard tool

View File

@ -101,14 +101,43 @@ export function* triggerWcaInstall() {
export function* resetOnboardingWizard() {
yield runCommand( 'Reset Onboarding Wizard', function* () {
const optionsToDelete = [
'woocommerce_task_list_tracked_completed_tasks',
'woocommerce_onboarding_profile',
'_transient_wc_onboarding_themes',
'woocommerce_task_list_tracked_completed_tasks',
'woocommerce_private_link',
'woocommerce_share_key',
'woocommerce_store_pages_only',
];
const defaultOptions = {
woocommerce_allow_tracking: 'no',
woocommerce_default_country: 'US:CA',
woocommerce_currency: 'USD',
woocommerce_currency_pos: 'left',
woocommerce_price_thousand_sep: ',',
woocommerce_price_decimal_sep: '.',
woocommerce_price_num_decimals: '2',
woocommerce_coming_soon: 'no',
};
// Delete existing options
yield apiFetch( {
method: 'DELETE',
path: `${ API_NAMESPACE }/options/${ optionsToDelete.join( ',' ) }`,
} );
// Execute batch update of options
yield apiFetch( {
method: 'POST',
path: `${ API_NAMESPACE }/options`,
data: {
options: Object.entries( defaultOptions ).map(
( [ option_name, option_value ] ) => ( {
option_name,
option_value,
} )
),
},
} );
} );
}

View File

@ -121,6 +121,40 @@ const restrictedImports = [
},
];
const coreModules = [
'@woocommerce/block-data',
'@woocommerce/blocks-checkout',
'@woocommerce/blocks-components',
'@woocommerce/price-format',
'@woocommerce/settings',
'@woocommerce/shared-context',
'@woocommerce/shared-hocs',
'@woocommerce/tracks',
'@woocommerce/data',
'@wordpress/a11y',
'@wordpress/api-fetch',
'@wordpress/block-editor',
'@wordpress/compose',
'@wordpress/data',
'@wordpress/core-data',
'@wordpress/editor',
'@wordpress/escape-html',
'@wordpress/hooks',
'@wordpress/keycodes',
'@wordpress/url',
'@woocommerce/blocks-test-utils',
'@woocommerce/e2e-utils',
'babel-jest',
'dotenv',
'jest-environment-puppeteer',
'lodash/kebabCase',
'lodash',
'prop-types',
'react',
'requireindex',
'react-transition-group',
];
module.exports = {
env: {
browser: true,
@ -151,39 +185,7 @@ module.exports = {
// List of modules that are externals in our webpack config.
// This helps the `import/no-extraneous-dependencies` and
//`import/no-unresolved` rules account for them.
'import/core-modules': [
'@woocommerce/block-data',
'@woocommerce/blocks-checkout',
'@woocommerce/blocks-components',
'@woocommerce/price-format',
'@woocommerce/settings',
'@woocommerce/shared-context',
'@woocommerce/shared-hocs',
'@woocommerce/tracks',
'@woocommerce/data',
'@wordpress/a11y',
'@wordpress/api-fetch',
'@wordpress/block-editor',
'@wordpress/compose',
'@wordpress/data',
'@wordpress/core-data',
'@wordpress/editor',
'@wordpress/escape-html',
'@wordpress/hooks',
'@wordpress/keycodes',
'@wordpress/url',
'@woocommerce/blocks-test-utils',
'@woocommerce/e2e-utils',
'babel-jest',
'dotenv',
'jest-environment-puppeteer',
'lodash/kebabCase',
'lodash',
'prop-types',
'react',
'requireindex',
'react-transition-group',
],
'import/core-modules': coreModules,
'import/resolver': {
node: {},
webpack: {},
@ -301,6 +303,7 @@ module.exports = {
typescript: {}, // this loads <rootdir>/tsconfig.json to eslint
},
'import/core-modules': [
...coreModules,
// We should lint these modules imports, but the types are way out of date.
// To support us not inadvertently introducing new import errors this lint exists, but to avoid
// having to fix hundreds of import errors for @wordpress packages we ignore them.

View File

@ -5,6 +5,7 @@
"https://github.com/WP-API/Basic-Auth/archive/master.zip",
"https://downloads.wordpress.org/plugin/wordpress-importer.0.8.zip",
"./tests/mocks/woo-test-helper",
"../woocommerce/tests/e2e-pw/bin/test-helper-apis.php",
"../woocommerce"
],
"env": {

View File

@ -8,11 +8,9 @@ export { default as ProductDetails } from './product-details';
export { default as ProductImage } from './product-image';
export { default as ProductLowStockBadge } from './product-low-stock-badge';
export { default as ProductSummary } from './product-summary';
export { default as PickupLocation } from './pickup-location';
export { default as ProductMetadata } from './product-metadata';
export { default as ProductSaleBadge } from './product-sale-badge';
export { default as ReturnToCartButton } from './return-to-cart-button';
export { default as ShippingLocation } from './shipping-location';
export { default as ShippingRatesControl } from './shipping-rates-control';
export { default as ShippingRatesControlPackage } from './shipping-rates-control-package';
export { default as PaymentMethodIcons } from './payment-method-icons';

View File

@ -13,10 +13,12 @@ import './style.scss';
interface OrderSummaryProps {
cartItems: CartItem[];
disableProductDescriptions: boolean;
}
const OrderSummary = ( {
cartItems = [],
disableProductDescriptions = false,
}: OrderSummaryProps ): null | JSX.Element => {
const { isLarge, hasContainerWidth } = useContainerWidthContext();
@ -34,6 +36,9 @@ const OrderSummary = ( {
{ cartItems.map( ( cartItem ) => {
return (
<OrderSummaryItem
disableProductDescriptions={
disableProductDescriptions
}
key={ cartItem.key }
cartItem={ cartItem }
/>

View File

@ -30,9 +30,13 @@ import ProductMetadata from '../product-metadata';
interface OrderSummaryProps {
cartItem: CartItem;
disableProductDescriptions: boolean;
}
const OrderSummaryItem = ( { cartItem }: OrderSummaryProps ): JSX.Element => {
const OrderSummaryItem = ( {
cartItem,
disableProductDescriptions,
}: OrderSummaryProps ): JSX.Element => {
const {
images,
low_stock_remaining: lowStockRemaining,
@ -122,6 +126,18 @@ const OrderSummaryItem = ( { cartItem }: OrderSummaryProps ): JSX.Element => {
arg,
} );
const productMetaProps = disableProductDescriptions
? {
itemData,
variation,
}
: {
itemData,
variation,
shortDescription,
fullDescription,
};
return (
<div
className={ clsx(
@ -174,12 +190,7 @@ const OrderSummaryItem = ( { cartItem }: OrderSummaryProps ): JSX.Element => {
/>
)
) }
<ProductMetadata
shortDescription={ shortDescription }
fullDescription={ fullDescription }
itemData={ itemData }
variation={ variation }
/>
<ProductMetadata { ...productMetaProps } />
</div>
<span className="screen-reader-text">
{ sprintf(

View File

@ -1,70 +0,0 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { useSelect } from '@wordpress/data';
import { isObject, objectHasProp } from '@woocommerce/types';
import { isPackageRateCollectable } from '@woocommerce/base-utils';
/**
* Shows a formatted pickup location.
*/
const PickupLocation = (): JSX.Element | null => {
const { pickupAddress } = useSelect( ( select ) => {
const cartShippingRates = select( 'wc/store/cart' ).getShippingRates();
const flattenedRates = cartShippingRates.flatMap(
( cartShippingRate ) => cartShippingRate.shipping_rates
);
const selectedCollectableRate = flattenedRates.find(
( rate ) => rate.selected && isPackageRateCollectable( rate )
);
// If the rate has an address specified in its metadata.
if (
isObject( selectedCollectableRate ) &&
objectHasProp( selectedCollectableRate, 'meta_data' )
) {
const selectedRateMetaData = selectedCollectableRate.meta_data.find(
( meta ) => meta.key === 'pickup_address'
);
if (
isObject( selectedRateMetaData ) &&
objectHasProp( selectedRateMetaData, 'value' ) &&
selectedRateMetaData.value
) {
const selectedRatePickupAddress = selectedRateMetaData.value;
return {
pickupAddress: selectedRatePickupAddress,
};
}
}
if ( isObject( selectedCollectableRate ) ) {
return {
pickupAddress: undefined,
};
}
return {
pickupAddress: undefined,
};
} );
// If the method does not contain an address, or the method supporting collection was not found, return early.
if ( typeof pickupAddress === 'undefined' ) {
return null;
}
// Show the pickup method's name if we don't have an address to show.
return (
<span className="wc-block-components-shipping-address">
{ sprintf(
/* translators: %s: shipping method name, e.g. "Amazon Locker" */
__( 'Collection from %s', 'woocommerce' ),
pickupAddress
) + ' ' }
</span>
);
};
export default PickupLocation;

View File

@ -1,93 +0,0 @@
/**
* External dependencies
*/
import { render, screen } from '@testing-library/react';
import { CART_STORE_KEY, CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
import { dispatch } from '@wordpress/data';
import { previewCart } from '@woocommerce/resource-previews';
import PickupLocation from '@woocommerce/base-components/cart-checkout/pickup-location';
jest.mock( '@woocommerce/settings', () => {
const originalModule = jest.requireActual( '@woocommerce/settings' );
return {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore We know @woocommerce/settings is an object.
...originalModule,
getSetting: ( setting: string, ...rest: unknown[] ) => {
if ( setting === 'localPickupEnabled' ) {
return true;
}
if ( setting === 'collectableMethodIds' ) {
return [ 'pickup_location' ];
}
return originalModule.getSetting( setting, ...rest );
},
};
} );
describe( 'PickupLocation', () => {
it( `renders an address if one is set in the methods metadata`, async () => {
dispatch( CHECKOUT_STORE_KEY ).setPrefersCollection( true );
// Deselect the default selected rate and select pickup_location:1 rate.
const currentlySelectedIndex =
previewCart.shipping_rates[ 0 ].shipping_rates.findIndex(
( rate ) => rate.selected
);
previewCart.shipping_rates[ 0 ].shipping_rates[
currentlySelectedIndex
].selected = false;
const pickupRateIndex =
previewCart.shipping_rates[ 0 ].shipping_rates.findIndex(
( rate ) => rate.method_id === 'pickup_location'
);
previewCart.shipping_rates[ 0 ].shipping_rates[
pickupRateIndex
].selected = true;
dispatch( CART_STORE_KEY ).receiveCart( previewCart );
render( <PickupLocation /> );
expect(
screen.getByText(
/Collection from 123 Easy Street, New York, 12345/
)
).toBeInTheDocument();
} );
it( 'renders no address if one is not set in the methods metadata', async () => {
dispatch( CHECKOUT_STORE_KEY ).setPrefersCollection( true );
// Deselect the default selected rate and select pickup_location:1 rate.
const currentlySelectedIndex =
previewCart.shipping_rates[ 0 ].shipping_rates.findIndex(
( rate ) => rate.selected
);
previewCart.shipping_rates[ 0 ].shipping_rates[
currentlySelectedIndex
].selected = false;
const pickupRateIndex =
previewCart.shipping_rates[ 0 ].shipping_rates.findIndex(
( rate ) => rate.rate_id === 'pickup_location:2'
);
previewCart.shipping_rates[ 0 ].shipping_rates[
pickupRateIndex
].selected = true;
// Set the pickup_location metadata value to an empty string in the selected pickup rate.
const addressKeyIndex = previewCart.shipping_rates[ 0 ].shipping_rates[
pickupRateIndex
].meta_data.findIndex(
( metaData ) => metaData.key === 'pickup_address'
);
previewCart.shipping_rates[ 0 ].shipping_rates[
pickupRateIndex
].meta_data[ addressKeyIndex ].value = '';
dispatch( CART_STORE_KEY ).receiveCart( previewCart );
render( <PickupLocation /> );
expect(
screen.queryByText( /Collection from / )
).not.toBeInTheDocument();
} );
} );

View File

@ -31,7 +31,6 @@ export const ShippingCalculatorButton = ( {
<Button
render={ <span /> }
className="wc-block-components-totals-shipping__change-address__link"
id="wc-block-components-totals-shipping__change-address__link"
onClick={ ( e ) => {
e.preventDefault();
setIsShippingCalculatorOpen( ! isShippingCalculatorOpen );

View File

@ -2,6 +2,10 @@
margin-bottom: 0;
}
.wc-block-components-totals-shipping__change-address__link {
white-space: nowrap;
}
.wc-block-components-shipping-calculator-address__button {
width: 100%;
margin-top: em($gap-large);

View File

@ -1,29 +0,0 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
interface ShippingLocationProps {
formattedLocation: string | null;
}
// Shows a formatted shipping location.
const ShippingLocation = ( {
formattedLocation,
}: ShippingLocationProps ): JSX.Element | null => {
if ( ! formattedLocation ) {
return null;
}
return (
<span className="wc-block-components-shipping-address">
{ sprintf(
/* translators: %s location. */
__( 'Delivers to %s', 'woocommerce' ),
formattedLocation
) + ' ' }
</span>
);
};
export default ShippingLocation;

View File

@ -1,165 +1,55 @@
/**
* External dependencies
*/
import clsx from 'clsx';
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import { TotalsItem } from '@woocommerce/blocks-components';
import type { Currency } from '@woocommerce/types';
import { ShippingVia } from '@woocommerce/base-components/cart-checkout/totals/shipping/shipping-via';
import {
isAddressComplete,
isPackageRateCollectable,
} from '@woocommerce/base-utils';
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
import { useSelect } from '@wordpress/data';
import { ShippingCalculatorContext } from '@woocommerce/base-components/cart-checkout/shipping-calculator/context';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import { hasShippingRate } from '@woocommerce/base-utils';
import { useStoreCart } from '@woocommerce/base-context';
/**
* Internal dependencies
*/
import { ShippingCalculator } from '../../shipping-calculator';
import {
hasShippingRate,
getTotalShippingValue,
areShippingMethodsMissing,
} from './utils';
import ShippingPlaceholder from './shipping-placeholder';
import ShippingAddress from './shipping-address';
import ShippingRateSelector from './shipping-rate-selector';
import { ShippingVia } from './shipping-via';
import { ShippingAddress } from './shipping-address';
import { renderShippingTotalValue } from './utils';
import './style.scss';
export interface TotalShippingProps {
currency: Currency;
values: {
total_shipping: string;
total_shipping_tax: string;
}; // Values in use
showCalculator?: boolean; //Whether to display the rate selector below the shipping total.
showRateSelector?: boolean; // Whether to show shipping calculator or not.
className?: string;
isCheckout?: boolean;
label?: string;
placeholder?: React.ReactNode;
collaterals?: React.ReactNode;
}
export const TotalsShipping = ( {
currency,
values,
showCalculator = true,
showRateSelector = true,
isCheckout = false,
className,
}: TotalShippingProps ): JSX.Element => {
const [ isShippingCalculatorOpen, setIsShippingCalculatorOpen ] =
useState( false );
const {
shippingAddress,
cartHasCalculatedShipping,
shippingRates,
isLoadingRates,
} = useStoreCart();
const totalShippingValue = getTotalShippingValue( values );
const hasRates = hasShippingRate( shippingRates ) || totalShippingValue > 0;
const showShippingCalculatorForm =
showCalculator && isShippingCalculatorOpen;
const prefersCollection = useSelect( ( select ) => {
return select( CHECKOUT_STORE_KEY ).prefersCollection();
} );
const selectedShippingRates = shippingRates.flatMap(
( shippingPackage ) => {
return shippingPackage.shipping_rates
.filter(
( rate ) =>
// If the shopper prefers collection, the rate is collectable AND selected.
( prefersCollection &&
isPackageRateCollectable( rate ) &&
rate.selected ) ||
// Or the shopper does not prefer collection and the rate is selected
( ! prefersCollection && rate.selected )
)
.flatMap( ( rate ) => rate.name );
}
);
const addressComplete = isAddressComplete( shippingAddress, [
'state',
'country',
'postcode',
'city',
] );
const shippingMethodsMissing = areShippingMethodsMissing(
hasRates,
prefersCollection,
shippingRates
);
const valueToDisplay =
totalShippingValue === 0 ? (
<strong>{ __( 'Free', 'woocommerce' ) }</strong>
) : (
totalShippingValue
);
label = __( 'Shipping', 'woocommerce' ),
placeholder = null,
collaterals = null,
}: TotalShippingProps ): JSX.Element | null => {
const { cartTotals, shippingRates } = useStoreCart();
const hasRates = hasShippingRate( shippingRates );
return (
<div
className={ clsx(
'wc-block-components-totals-shipping',
className
) }
>
<ShippingCalculatorContext.Provider
value={ {
showCalculator,
shippingCalculatorID: 'shipping-calculator-form-wrapper',
isShippingCalculatorOpen,
setIsShippingCalculatorOpen,
} }
>
<TotalsItem
label={ __( 'Delivery', 'woocommerce' ) }
value={
! shippingMethodsMissing && cartHasCalculatedShipping
? // if address is not complete, display the link to add an address.
valueToDisplay
: ( ! addressComplete || isCheckout ) && (
<ShippingPlaceholder
showCalculator={ showCalculator }
isCheckout={ isCheckout }
addressProvided={ addressComplete }
/>
)
}
description={
( ! shippingMethodsMissing &&
cartHasCalculatedShipping ) ||
// If address is complete, display the shipping address.
( addressComplete && ! isCheckout ) ? (
<>
<ShippingVia
selectedShippingRates={
selectedShippingRates
}
/>
<ShippingAddress
shippingAddress={ shippingAddress }
/>
</>
) : null
}
currency={ currency }
/>
{ showShippingCalculatorForm && <ShippingCalculator /> }
{ showRateSelector &&
cartHasCalculatedShipping &&
! showShippingCalculatorForm && (
<ShippingRateSelector
hasRates={ hasRates }
shippingRates={ shippingRates }
isLoadingRates={ isLoadingRates }
isAddressComplete={ addressComplete }
shippingAddress={ shippingAddress }
/>
) }
</ShippingCalculatorContext.Provider>
<div className="wc-block-components-totals-shipping">
<TotalsItem
label={ label }
value={
hasRates
? renderShippingTotalValue( cartTotals )
: placeholder
}
description={
<>
<ShippingVia />
<ShippingAddress />
{ collaterals && (
<div className="wc-block-components-totals-shipping__collaterals">
{ collaterals }
</div>
) }
</>
}
currency={ getCurrencyFromPriceResponse( cartTotals ) }
/>
</div>
);
};

View File

@ -1,43 +1,46 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import { formatShippingAddress } from '@woocommerce/base-utils';
import { ShippingAddress as ShippingAddressType } from '@woocommerce/settings';
import {
ShippingLocation,
PickupLocation,
ShippingCalculatorButton,
} from '@woocommerce/base-components/cart-checkout';
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
import { useStoreCart } from '@woocommerce/base-context';
import { ShippingCalculatorButton } from '@woocommerce/base-components/cart-checkout';
import { useSelect } from '@wordpress/data';
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
export interface ShippingAddressProps {
shippingAddress: ShippingAddressType;
}
/**
* Internal dependencies
*/
import { getPickupLocation } from './utils';
export const ShippingAddress = ( {
shippingAddress,
}: ShippingAddressProps ): JSX.Element | null => {
export const ShippingAddress = (): JSX.Element => {
const { shippingRates, shippingAddress } = useStoreCart();
const prefersCollection = useSelect( ( select ) =>
select( CHECKOUT_STORE_KEY ).prefersCollection()
);
const hasFormattedAddress = !! formatShippingAddress( shippingAddress );
const formattedAddress = prefersCollection
? getPickupLocation( shippingRates )
: formatShippingAddress( shippingAddress );
const addressLabel = prefersCollection
? /* translators: %s location. */
__( 'Collection from %s', 'woocommerce' )
: /* translators: %s location. */
__( 'Delivers to %s', 'woocommerce' );
const calculatorLabel =
! formattedAddress || prefersCollection
? __( 'Enter address to check delivery options', 'woocommerce' )
: __( 'Change address', 'woocommerce' );
const label = hasFormattedAddress
? __( 'Change address', 'woocommerce' )
: __( 'Enter address to check delivery options', 'woocommerce' );
const formattedLocation = formatShippingAddress( shippingAddress );
return (
<>
{ prefersCollection ? (
<PickupLocation />
) : (
<ShippingLocation formattedLocation={ formattedLocation } />
) }
<ShippingCalculatorButton label={ label } />
</>
<div className="wc-block-components-shipping-address">
{ formattedAddress
? sprintf( addressLabel, formattedAddress ) + ' '
: null }
<ShippingCalculatorButton label={ calculatorLabel } />
</div>
);
};

View File

@ -1,41 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { ShippingCalculatorButton } from '@woocommerce/base-components/cart-checkout';
export interface ShippingPlaceholderProps {
showCalculator: boolean;
isCheckout?: boolean;
addressProvided: boolean;
}
export const ShippingPlaceholder = ( {
showCalculator,
addressProvided,
isCheckout = false,
}: ShippingPlaceholderProps ): JSX.Element => {
if ( ! showCalculator ) {
const label = addressProvided
? __( 'No available delivery option', 'woocommerce' )
: __( 'Enter address to calculate', 'woocommerce' );
return (
<span className="wc-block-components-shipping-placeholder__value">
{ isCheckout
? label
: __( 'Calculated at checkout', 'woocommerce' ) }
</span>
);
}
return (
<ShippingCalculatorButton
label={ __(
'Enter address to check delivery options',
'woocommerce'
) }
/>
);
};
export default ShippingPlaceholder;

View File

@ -2,22 +2,23 @@
* External dependencies
*/
import { decodeEntities } from '@wordpress/html-entities';
import { useStoreCart } from '@woocommerce/base-context';
import { getSelectedShippingRateNames } from '@woocommerce/base-utils';
export const ShippingVia = ( {
selectedShippingRates,
}: {
selectedShippingRates: string[];
} ): JSX.Element => {
return (
<div className="wc-block-components-totals-item__description wc-block-components-totals-shipping__via">
export const ShippingVia = (): JSX.Element | null => {
const { shippingRates } = useStoreCart();
const rateNames = getSelectedShippingRateNames( shippingRates );
return rateNames ? (
<div className="wc-block-components-totals-shipping__via">
{ decodeEntities(
selectedShippingRates
rateNames
.filter(
( item, index ) =>
selectedShippingRates.indexOf( item ) === index
( item, index ) => rateNames.indexOf( item ) === index
)
.join( ', ' )
) }
</div>
);
) : null;
};
export default ShippingVia;

View File

@ -11,6 +11,7 @@
text-transform: uppercase;
}
.wc-block-components-totals-shipping__delivery-options-notice,
.wc-block-components-shipping-address {
margin-top: $gap;
display: block;
@ -54,6 +55,11 @@
.wc-block-components-shipping-placeholder__value {
@include font-size(small);
}
.wc-block-components-totals-shipping__via {
@include font-size(small);
width: 100%;
}
}
// Extra classes for specificity.

View File

@ -3,8 +3,10 @@
*/
import { screen, render } from '@testing-library/react';
import { SlotFillProvider } from '@woocommerce/blocks-checkout';
import { previewCart as mockPreviewCart } from '@woocommerce/resource-previews';
import { ShippingCalculatorContext } from '@woocommerce/base-components/cart-checkout/shipping-calculator/context';
import * as wpData from '@wordpress/data';
import { CartShippingRate } from '@woocommerce/types';
import { previewCart as mockPreviewCart } from '@woocommerce/resource-previews';
import * as baseContextHooks from '@woocommerce/base-context/hooks';
/**
@ -51,6 +53,54 @@ const shippingAddress = {
phone: '+1234567890',
};
const shippingRates = [
{
package_id: 0,
name: 'Initial Shipment',
destination: {
address_1: '30 Test Street',
address_2: 'Apt 1 Shipping',
city: 'Liverpool',
state: '',
postcode: 'L1 0BP',
country: 'GB',
},
items: [
{
key: 'acf4b89d3d503d8252c9c4ba75ddbf6d',
name: 'Test product',
quantity: 1,
},
],
shipping_rates: [
{
rate_id: 'flat_rate:1',
name: 'Shipping',
description: '',
delivery_time: '',
price: '0',
taxes: '0',
instance_id: 13,
method_id: 'flat_rate',
meta_data: [
{
key: 'Items',
value: 'Test product &times; 1',
},
],
selected: false,
currency_code: 'USD',
currency_symbol: '$',
currency_minor_unit: 2,
currency_decimal_separator: '.',
currency_thousand_separator: ',',
currency_prefix: '$',
currency_suffix: '',
},
],
},
] as CartShippingRate[];
jest.mock( '@woocommerce/base-context/hooks', () => {
return {
__esModule: true,
@ -59,117 +109,20 @@ jest.mock( '@woocommerce/base-context/hooks', () => {
useStoreCart: jest.fn(),
};
} );
baseContextHooks.useShippingData.mockReturnValue( {
needsShipping: true,
selectShippingRate: jest.fn(),
shippingRates: [
{
package_id: 0,
name: 'Shipping method',
destination: {
address_1: '',
address_2: '',
city: '',
state: '',
postcode: '',
country: '',
},
items: [
{
key: 'fb0c0a746719a7596f296344b80cb2b6',
name: 'Hoodie - Blue, Yes',
quantity: 1,
},
{
key: '1f0e3dad99908345f7439f8ffabdffc4',
name: 'Beanie',
quantity: 1,
},
],
shipping_rates: [
{
rate_id: 'flat_rate:1',
name: 'Flat rate',
description: '',
delivery_time: '',
price: '500',
taxes: '0',
instance_id: 1,
method_id: 'flat_rate',
meta_data: [
{
key: 'Items',
value: 'Hoodie - Blue, Yes &times; 1, Beanie &times; 1',
},
],
selected: false,
currency_code: 'USD',
currency_symbol: '$',
currency_minor_unit: 2,
currency_decimal_separator: '.',
currency_thousand_separator: ',',
currency_prefix: '$',
currency_suffix: '',
},
{
rate_id: 'local_pickup:2',
name: 'Local pickup',
description: '',
delivery_time: '',
price: '0',
taxes: '0',
instance_id: 2,
method_id: 'local_pickup',
meta_data: [
{
key: 'Items',
value: 'Hoodie - Blue, Yes &times; 1, Beanie &times; 1',
},
],
selected: false,
currency_code: 'USD',
currency_symbol: '$',
currency_minor_unit: 2,
currency_decimal_separator: '.',
currency_thousand_separator: ',',
currency_prefix: '$',
currency_suffix: '',
},
{
rate_id: 'free_shipping:5',
name: 'Free shipping',
description: '',
delivery_time: '',
price: '0',
taxes: '0',
instance_id: 5,
method_id: 'free_shipping',
meta_data: [
{
key: 'Items',
value: 'Hoodie - Blue, Yes &times; 1, Beanie &times; 1',
},
],
selected: true,
currency_code: 'USD',
currency_symbol: '$',
currency_minor_unit: 2,
currency_decimal_separator: '.',
currency_thousand_separator: ',',
currency_prefix: '$',
currency_suffix: '',
},
],
},
],
shippingRates,
} );
baseContextHooks.useStoreCart.mockReturnValue( {
cartItems: mockPreviewCart.items,
cartTotals: [ mockPreviewCart.totals ],
cartTotals: mockPreviewCart.totals,
cartCoupons: mockPreviewCart.coupons,
cartFees: mockPreviewCart.fees,
cartNeedsShipping: mockPreviewCart.needs_shipping,
shippingRates: [],
shippingRates,
shippingAddress,
billingAddress: mockPreviewCart.billing_address,
cartHasCalculatedShipping: mockPreviewCart.has_calculated_shipping,
@ -179,141 +132,73 @@ baseContextHooks.useStoreCart.mockReturnValue( {
describe( 'TotalsShipping', () => {
it( 'shows FREE if shipping cost is 0', () => {
baseContextHooks.useStoreCart.mockReturnValue( {
cartItems: mockPreviewCart.items,
cartTotals: [ mockPreviewCart.totals ],
cartCoupons: mockPreviewCart.coupons,
cartFees: mockPreviewCart.fees,
cartNeedsShipping: mockPreviewCart.needs_shipping,
...baseContextHooks.useStoreCart(),
shippingRates: [
{
package_id: 0,
name: 'Initial Shipment',
destination: {
address_1: '30 Test Street',
address_2: 'Apt 1 Shipping',
city: 'Liverpool',
state: '',
postcode: 'L1 0BP',
country: 'GB',
},
items: [
{
key: 'acf4b89d3d503d8252c9c4ba75ddbf6d',
name: 'Test product',
quantity: 1,
},
],
shipping_rates: [
{
rate_id: 'free_shipping:1',
name: 'Free shipping',
description: '',
delivery_time: '',
price: '0',
taxes: '0',
instance_id: 13,
method_id: 'free_shipping',
meta_data: [
{
key: 'Items',
value: 'Test product &times; 1',
},
],
selected: false,
currency_code: 'USD',
currency_symbol: '$',
currency_minor_unit: 2,
currency_decimal_separator: '.',
currency_thousand_separator: ',',
currency_prefix: '$',
currency_suffix: '',
},
],
},
...shippingRates,
{ ...shippingRates[ 0 ], price: '0' },
],
shippingAddress,
billingAddress: mockPreviewCart.billing_address,
cartHasCalculatedShipping: mockPreviewCart.has_calculated_shipping,
isLoadingRates: false,
cartTotals: {
...mockPreviewCart.totals,
total_shipping: '0',
total_shipping_tax: '0',
},
} );
const { rerender } = render(
<SlotFillProvider>
<TotalsShipping
currency={ {
code: 'USD',
symbol: '$',
minorUnit: 2,
decimalSeparator: '.',
prefix: '',
suffix: '',
thousandSeparator: ', ',
} }
values={ {
total_shipping: '0',
total_shipping_tax: '0',
} }
showCalculator={ true }
showRateSelector={ false }
isCheckout={ false }
className={ '' }
/>
<TotalsShipping />
</SlotFillProvider>
);
expect(
screen.getByText( 'Free', { exact: true } )
).toBeInTheDocument();
expect( screen.queryByText( '0.00' ) ).not.toBeInTheDocument();
baseContextHooks.useStoreCart.mockReturnValue( {
...baseContextHooks.useStoreCart(),
shippingRates: [
...shippingRates,
{ ...shippingRates[ 0 ], price: '5678' },
],
cartTotals: {
...mockPreviewCart.totals,
total_shipping: '5678',
total_shipping_tax: '0',
currency_code: 'USD',
currency_symbol: '$',
currency_minor_unit: 2,
currency_decimal_separator: '.',
currency_prefix: '',
currency_suffix: '',
currency_thousand_separator: ', ',
},
} );
rerender(
<SlotFillProvider>
<TotalsShipping
currency={ {
code: 'USD',
symbol: '$',
minorUnit: 2,
decimalSeparator: '.',
prefix: '',
suffix: '',
thousandSeparator: ', ',
} }
values={ {
total_shipping: '5678',
total_shipping_tax: '0',
} }
showCalculator={ true }
showRateSelector={ false }
isCheckout={ false }
className={ '' }
/>
<TotalsShipping />
</SlotFillProvider>
);
expect( screen.queryByText( 'Free' ) ).not.toBeInTheDocument();
expect( screen.getByText( '56.78' ) ).toBeInTheDocument();
} );
it( 'should show correct calculator button label if address is complete', () => {
render(
<SlotFillProvider>
<TotalsShipping
currency={ {
code: 'USD',
symbol: '$',
minorUnit: 2,
decimalSeparator: '.',
prefix: '',
suffix: '',
thousandSeparator: ', ',
<ShippingCalculatorContext.Provider
value={ {
showCalculator: true,
isShippingCalculatorOpen: false,
setIsShippingCalculatorOpen: jest.fn(),
shippingCalculatorID:
'shipping-calculator-form-wrapper',
} }
values={ {
total_shipping: '0',
total_shipping_tax: '0',
} }
showCalculator={ true }
showRateSelector={ true }
isCheckout={ false }
className={ '' }
/>
>
<TotalsShipping />
</ShippingCalculatorContext.Provider>
</SlotFillProvider>
);
expect(
@ -323,45 +208,31 @@ describe( 'TotalsShipping', () => {
).toBeInTheDocument();
expect( screen.getByText( 'Change address' ) ).toBeInTheDocument();
} );
it( 'should show correct calculator button label if address is incomplete', () => {
baseContextHooks.useStoreCart.mockReturnValue( {
cartItems: mockPreviewCart.items,
cartTotals: [ mockPreviewCart.totals ],
cartCoupons: mockPreviewCart.coupons,
cartFees: mockPreviewCart.fees,
cartNeedsShipping: mockPreviewCart.needs_shipping,
shippingRates: [],
...baseContextHooks.useStoreCart(),
shippingAddress: {
...shippingAddress,
city: '',
country: '',
postcode: '',
},
billingAddress: mockPreviewCart.billing_address,
cartHasCalculatedShipping: mockPreviewCart.has_calculated_shipping,
isLoadingRates: false,
} );
render(
<SlotFillProvider>
<TotalsShipping
currency={ {
code: 'USD',
symbol: '$',
minorUnit: 2,
decimalSeparator: '.',
prefix: '',
suffix: '',
thousandSeparator: ', ',
<ShippingCalculatorContext.Provider
value={ {
showCalculator: true,
isShippingCalculatorOpen: false,
setIsShippingCalculatorOpen: jest.fn(),
shippingCalculatorID:
'shipping-calculator-form-wrapper',
} }
values={ {
total_shipping: '0',
total_shipping_tax: '0',
} }
showCalculator={ true }
showRateSelector={ true }
isCheckout={ false }
className={ '' }
/>
>
<TotalsShipping />
</ShippingCalculatorContext.Provider>
</SlotFillProvider>
);
expect(
@ -371,14 +242,10 @@ describe( 'TotalsShipping', () => {
screen.getByText( 'Enter address to check delivery options' )
).toBeInTheDocument();
} );
it( 'does show the calculator button when default rates are available and has formatted address', () => {
baseContextHooks.useStoreCart.mockReturnValue( {
cartItems: mockPreviewCart.items,
cartTotals: [ mockPreviewCart.totals ],
cartCoupons: mockPreviewCart.coupons,
cartFees: mockPreviewCart.fees,
cartNeedsShipping: mockPreviewCart.needs_shipping,
shippingRates: mockPreviewCart.shipping_rates,
...baseContextHooks.useStoreCart(),
shippingAddress: {
...shippingAddress,
city: '',
@ -386,31 +253,21 @@ describe( 'TotalsShipping', () => {
country: 'US',
postcode: '',
},
billingAddress: mockPreviewCart.billing_address,
cartHasCalculatedShipping: mockPreviewCart.has_calculated_shipping,
isLoadingRates: false,
} );
render(
<SlotFillProvider>
<TotalsShipping
currency={ {
code: 'USD',
symbol: '$',
minorUnit: 2,
decimalSeparator: '.',
prefix: '',
suffix: '',
thousandSeparator: ', ',
<ShippingCalculatorContext.Provider
value={ {
showCalculator: true,
isShippingCalculatorOpen: false,
setIsShippingCalculatorOpen: jest.fn(),
shippingCalculatorID:
'shipping-calculator-form-wrapper',
} }
values={ {
total_shipping: '0',
total_shipping_tax: '0',
} }
showCalculator={ true }
showRateSelector={ true }
isCheckout={ false }
className={ '' }
/>
>
<TotalsShipping />
</ShippingCalculatorContext.Provider>
</SlotFillProvider>
);
expect( screen.queryByText( 'Change address' ) ).toBeInTheDocument();

View File

@ -4,8 +4,10 @@
import { render, screen } from '@testing-library/react';
import ShippingAddress from '@woocommerce/base-components/cart-checkout/totals/shipping/shipping-address';
import { CART_STORE_KEY, CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
import { ShippingCalculatorContext } from '@woocommerce/base-components/cart-checkout';
import { dispatch } from '@wordpress/data';
import { previewCart } from '@woocommerce/resource-previews';
import * as baseContextHooks from '@woocommerce/base-context/hooks';
jest.mock( '@woocommerce/settings', () => {
const originalModule = jest.requireActual( '@woocommerce/settings' );
@ -25,35 +27,66 @@ jest.mock( '@woocommerce/settings', () => {
},
};
} );
describe( 'ShippingAddress', () => {
const testShippingAddress = {
first_name: 'John',
last_name: 'Doe',
company: 'Automattic',
address_1: '123 Main St',
address_2: '',
city: 'San Francisco',
state: 'CA',
postcode: '94107',
country: 'US',
phone: '555-555-5555',
};
it( 'renders ShippingLocation if user does not prefer collection', () => {
jest.mock( '@woocommerce/base-context/hooks', () => {
return {
__esModule: true,
...jest.requireActual( '@woocommerce/base-context/hooks' ),
useStoreCart: jest.fn(),
};
} );
const shippingAddress = {
first_name: 'John',
last_name: 'Doe',
company: 'Automattic',
address_1: '123 Main St',
address_2: '',
city: 'San Francisco',
state: 'CA',
postcode: '94107',
country: 'US',
phone: '555-555-5555',
};
baseContextHooks.useStoreCart.mockReturnValue( {
cartItems: previewCart.items,
cartTotals: previewCart.totals,
cartCoupons: previewCart.coupons,
cartFees: previewCart.fees,
cartNeedsShipping: previewCart.needs_shipping,
shippingRates: previewCart.shipping_rates,
shippingAddress,
billingAddress: previewCart.billing_address,
cartHasCalculatedShipping: previewCart.has_calculated_shipping,
isLoadingRates: false,
} );
describe( 'ShippingAddress', () => {
it( 'Renders shipping address if user does not prefer collection', () => {
render(
<ShippingAddress
showCalculator={ false }
isShippingCalculatorOpen={ false }
setIsShippingCalculatorOpen={ jest.fn() }
shippingAddress={ testShippingAddress }
/>
<ShippingCalculatorContext.Provider
value={ {
showCalculator: true,
isShippingCalculatorOpen: false,
setIsShippingCalculatorOpen: jest.fn(),
shippingCalculatorID: 'shipping-calculator-form-wrapper',
} }
>
<ShippingAddress />
</ShippingCalculatorContext.Provider>
);
expect( screen.getByText( /Delivers to 94107/ ) ).toBeInTheDocument();
expect( screen.getByText( 'Change address' ) ).toBeInTheDocument();
expect(
screen.queryByText( /Collection from/ )
).not.toBeInTheDocument();
expect(
screen.queryByText( 'Enter address to check delivery options' )
).not.toBeInTheDocument();
} );
it( 'renders PickupLocation if shopper prefers collection', async () => {
it( 'Renders pickup location if shopper prefers collection', async () => {
dispatch( CHECKOUT_STORE_KEY ).setPrefersCollection( true );
// Deselect the default selected rate and select pickup_location:1 rate.
@ -75,12 +108,16 @@ describe( 'ShippingAddress', () => {
dispatch( CART_STORE_KEY ).receiveCart( previewCart );
render(
<ShippingAddress
showCalculator={ false }
isShippingCalculatorOpen={ false }
setIsShippingCalculatorOpen={ jest.fn() }
shippingAddress={ testShippingAddress }
/>
<ShippingCalculatorContext.Provider
value={ {
showCalculator: true,
isShippingCalculatorOpen: false,
setIsShippingCalculatorOpen: jest.fn(),
shippingCalculatorID: 'shipping-calculator-form-wrapper',
} }
>
<ShippingAddress />
</ShippingCalculatorContext.Provider>
);
expect(
screen.getByText(
@ -88,4 +125,69 @@ describe( 'ShippingAddress', () => {
)
).toBeInTheDocument();
} );
it( `renders an address if one is set in the methods metadata`, async () => {
dispatch( CHECKOUT_STORE_KEY ).setPrefersCollection( true );
// Deselect the default selected rate and select pickup_location:1 rate.
const currentlySelectedIndex =
previewCart.shipping_rates[ 0 ].shipping_rates.findIndex(
( rate ) => rate.selected
);
previewCart.shipping_rates[ 0 ].shipping_rates[
currentlySelectedIndex
].selected = false;
const pickupRateIndex =
previewCart.shipping_rates[ 0 ].shipping_rates.findIndex(
( rate ) => rate.method_id === 'pickup_location'
);
previewCart.shipping_rates[ 0 ].shipping_rates[
pickupRateIndex
].selected = true;
dispatch( CART_STORE_KEY ).receiveCart( previewCart );
render( <ShippingAddress /> );
expect(
screen.getByText(
/Collection from 123 Easy Street, New York, 12345/
)
).toBeInTheDocument();
} );
it( 'renders no address if one is not set in the methods metadata', async () => {
dispatch( CHECKOUT_STORE_KEY ).setPrefersCollection( true );
// Deselect the default selected rate and select pickup_location:1 rate.
const currentlySelectedIndex =
previewCart.shipping_rates[ 0 ].shipping_rates.findIndex(
( rate ) => rate.selected
);
previewCart.shipping_rates[ 0 ].shipping_rates[
currentlySelectedIndex
].selected = false;
const pickupRateIndex =
previewCart.shipping_rates[ 0 ].shipping_rates.findIndex(
( rate ) => rate.rate_id === 'pickup_location:2'
);
previewCart.shipping_rates[ 0 ].shipping_rates[
pickupRateIndex
].selected = true;
// Set the pickup_location metadata value to an empty string in the selected pickup rate.
const addressKeyIndex = previewCart.shipping_rates[ 0 ].shipping_rates[
pickupRateIndex
].meta_data.findIndex(
( metaData ) => metaData.key === 'pickup_address'
);
previewCart.shipping_rates[ 0 ].shipping_rates[
pickupRateIndex
].meta_data[ addressKeyIndex ].value = '';
dispatch( CART_STORE_KEY ).receiveCart( previewCart );
render( <ShippingAddress /> );
expect(
screen.queryByText( /Collection from / )
).not.toBeInTheDocument();
} );
} );

View File

@ -1,58 +0,0 @@
/**
* External dependencies
*/
import { screen, render } from '@testing-library/react';
/**
* Internal dependencies
*/
import ShippingPlaceholder from '../shipping-placeholder';
const shippingCalculatorID = 'shipping-calculator-form-wrapper';
describe( 'ShippingPlaceholder', () => {
it( 'should show correct text if showCalculator is false and addressProvided is false', () => {
const { rerender } = render(
<ShippingPlaceholder
showCalculator={ false }
addressProvided={ false }
isCheckout={ true }
isShippingCalculatorOpen={ false }
setIsShippingCalculatorOpen={ jest.fn() }
shippingCalculatorID={ shippingCalculatorID }
/>
);
expect(
screen.getByText( 'Enter address to calculate' )
).toBeInTheDocument();
rerender(
<ShippingPlaceholder
showCalculator={ false }
isCheckout={ false }
addressProvided={ false }
isShippingCalculatorOpen={ false }
setIsShippingCalculatorOpen={ jest.fn() }
shippingCalculatorID={ shippingCalculatorID }
/>
);
expect(
screen.getByText( 'Calculated at checkout' )
).toBeInTheDocument();
} );
it( 'should show correct text if showCalculator is false and addressProvided is true', () => {
render(
<ShippingPlaceholder
showCalculator={ false }
addressProvided={ true }
isCheckout={ true }
isShippingCalculatorOpen={ false }
setIsShippingCalculatorOpen={ jest.fn() }
shippingCalculatorID={ shippingCalculatorID }
/>
);
expect(
screen.getByText( 'No available delivery option' )
).toBeInTheDocument();
} );
} );

View File

@ -1,62 +0,0 @@
/**
* External dependencies
*/
import { getSetting } from '@woocommerce/settings';
import type { CartResponseShippingRate } from '@woocommerce/type-defs/cart-response';
import { hasCollectableRate } from '@woocommerce/base-utils';
/**
* Searches an array of packages/rates to see if there are actually any rates
* available.
*
* @param {Array} shippingRatePackages An array of packages and rates.
* @return {boolean} True if a rate exists.
*/
export const hasShippingRate = (
shippingRatePackages: CartResponseShippingRate[]
): boolean => {
return shippingRatePackages.some(
( shippingRatePackage ) => shippingRatePackage.shipping_rates.length
);
};
/**
* Calculates the total shipping value based on store settings.
*/
export const getTotalShippingValue = ( values: {
total_shipping: string;
total_shipping_tax: string;
} ): number => {
return getSetting( 'displayCartPricesIncludingTax', false )
? parseInt( values.total_shipping, 10 ) +
parseInt( values.total_shipping_tax, 10 )
: parseInt( values.total_shipping, 10 );
};
/**
* Checks if no shipping methods are available or if all available shipping methods are local pickup
* only.
*/
export const areShippingMethodsMissing = (
hasRates: boolean,
prefersCollection: boolean | undefined,
shippingRates: CartResponseShippingRate[]
) => {
if ( ! hasRates ) {
// No shipping methods available
return true;
}
// We check for the availability of shipping options if the shopper selected "Shipping"
if ( ! prefersCollection ) {
return shippingRates.some(
( shippingRatePackage ) =>
! shippingRatePackage.shipping_rates.some(
( shippingRate ) =>
! hasCollectableRate( shippingRate.method_id )
)
);
}
return false;
};

View File

@ -0,0 +1,53 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import {
getTotalShippingValue,
isPackageRateCollectable,
} from '@woocommerce/base-utils';
import {
isObject,
objectHasProp,
CartShippingRate,
CartResponseTotals,
} from '@woocommerce/types';
export const renderShippingTotalValue = ( values: CartResponseTotals ) => {
const totalShippingValue = getTotalShippingValue( values );
if ( totalShippingValue === 0 ) {
return <strong>{ __( 'Free', 'woocommerce' ) }</strong>;
}
return totalShippingValue;
};
export const getPickupLocation = (
shippingRates: CartShippingRate[]
): string => {
const flattenedRates = ( shippingRates || [] ).flatMap(
( shippingRate ) => shippingRate.shipping_rates
);
const selectedCollectableRate = flattenedRates.find(
( rate ) => rate.selected && isPackageRateCollectable( rate )
);
// If the rate has an address specified in its metadata.
if (
isObject( selectedCollectableRate ) &&
objectHasProp( selectedCollectableRate, 'meta_data' )
) {
const selectedRateMetaData = selectedCollectableRate.meta_data.find(
( meta ) => meta.key === 'pickup_address'
);
if (
isObject( selectedRateMetaData ) &&
objectHasProp( selectedRateMetaData, 'value' ) &&
selectedRateMetaData.value
) {
return selectedRateMetaData.value;
}
}
return '';
};

View File

@ -3,7 +3,7 @@
*/
import {
defaultFields,
AddressFields,
FormFields,
ShippingAddress,
BillingAddress,
getSetting,
@ -30,7 +30,7 @@ interface CheckoutAddress {
setUseShippingAsBilling: ( useShippingAsBilling: boolean ) => void;
setEditingBillingAddress: ( isEditing: boolean ) => void;
setEditingShippingAddress: ( isEditing: boolean ) => void;
defaultFields: AddressFields;
defaultFields: FormFields;
showShippingFields: boolean;
showBillingFields: boolean;
forcedBillingAddress: boolean;

View File

@ -5,7 +5,7 @@ import { useDispatch, useSelect } from '@wordpress/data';
import { CART_STORE_KEY, CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
import { useEffect } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { hasShippingRate } from '@woocommerce/base-components/cart-checkout/totals/shipping/utils';
import { hasShippingRate } from '@woocommerce/base-utils';
/**
* Internal dependencies

View File

@ -1,12 +1,12 @@
/**
* External dependencies
*/
import {
CartShippingPackageShippingRate,
CartShippingRate,
} from '@woocommerce/type-defs/cart';
import { getSetting } from '@woocommerce/settings';
import { LOCAL_PICKUP_ENABLED } from '@woocommerce/block-settings';
import type {
CartShippingPackageShippingRate,
CartShippingRate,
} from '@woocommerce/types';
/**
* Get the number of packages in a shippingRates array.
@ -59,3 +59,97 @@ export const getShippingRatesRateCount = (
return count + shippingPackage.shipping_rates.length;
}, 0 );
};
/**
* Searches an array of packages/rates to see if there are actually any rates
* available.
*
* @param {Array} shippingRates An array of packages and rates.
* @return {boolean} True if a rate exists.
*/
export const hasShippingRate = (
shippingRates: CartShippingRate[]
): boolean => {
return shippingRates.some(
( shippingRatesPackage ) =>
!! shippingRatesPackage.shipping_rates.length
);
};
/**
* Filters an array of packages/rates based on the shopper's preference for collection.
*/
export const filterShippingRatesByPrefersCollection = (
shippingRates: CartShippingRate[],
prefersCollection: boolean
) => {
return shippingRates.map( ( shippingRatesPackage ) => {
return {
...shippingRatesPackage,
shipping_rates: shippingRatesPackage.shipping_rates.filter(
( rate ) => {
const collectableRate = hasCollectableRate(
rate.method_id
);
if ( prefersCollection ) {
return collectableRate;
}
return ! collectableRate;
}
),
};
} );
};
/**
* Calculates the total shipping value based on store settings.
*/
export const getTotalShippingValue = ( values: {
total_shipping: string;
total_shipping_tax: string;
} ): number => {
return getSetting( 'displayCartPricesIncludingTax', false )
? parseInt( values.total_shipping, 10 ) +
parseInt( values.total_shipping_tax, 10 )
: parseInt( values.total_shipping, 10 );
};
/**
* Get the names of the selected rates in an array of shipping rates.
*/
export const getSelectedShippingRateNames = (
shippingRates: CartShippingRate[]
): string[] => {
return shippingRates.flatMap( ( shippingPackage ) => {
return shippingPackage.shipping_rates
.filter( ( rate ) => rate.selected )
.flatMap( ( rate ) => rate.name );
} );
};
export const selectedRatesAreCollectable = (
shippingRates: CartShippingRate[]
): boolean => {
return hasShippingRate( shippingRates )
? shippingRates.every( ( shippingPackage ) => {
return shippingPackage.shipping_rates.every(
( rate ) =>
! rate.selected || isPackageRateCollectable( rate )
);
} )
: false;
};
export const allRatesAreCollectable = (
shippingRates: CartShippingRate[]
): boolean => {
return hasShippingRate( shippingRates )
? shippingRates.every( ( shippingPackage ) => {
return shippingPackage.shipping_rates.every( ( rate ) =>
isPackageRateCollectable( rate )
);
} )
: false;
};

View File

@ -17,10 +17,12 @@ import './style.scss';
/**
* PaymentMethods component.
*
* @return {*} The rendered component.
*/
const PaymentMethods = () => {
const PaymentMethods = ( {
noPaymentMethods = <NoPaymentMethods />,
}: {
noPaymentMethods?: JSX.Element | undefined;
} ) => {
const [ showPaymentMethodsToggle, setShowPaymentMethodsToggle ] =
useState( false );
const {
@ -50,7 +52,7 @@ const PaymentMethods = () => {
paymentMethodsInitialized &&
Object.keys( availablePaymentMethods ).length === 0
) {
return <NoPaymentMethods />;
return noPaymentMethods;
}
// Show payment methods if the toggle is on or if there are no saved payment methods, or if the active saved token is not set.

View File

@ -8,8 +8,6 @@ import {
} from '@wordpress/block-editor';
import { addFilter, hasFilter } from '@wordpress/hooks';
import type { StoreDescriptor } from '@wordpress/data';
import { NoPaymentMethodsNotice } from '@woocommerce/editor-components/no-payment-methods-notice';
import { PAYMENT_STORE_KEY } from '@woocommerce/block-data';
import { DefaultNotice } from '@woocommerce/editor-components/default-notice';
import { IncompatibleExtensionsNotice } from '@woocommerce/editor-components/incompatible-extension-notice';
import { useSelect } from '@wordpress/data';
@ -35,13 +33,7 @@ const withSidebarNotices = createHigherOrderComponent(
isSelected: isBlockSelected,
} = props;
const {
isCart,
isCheckout,
isPaymentMethodsBlock,
hasPaymentMethods,
parentId,
} = useSelect( ( select ) => {
const { isCart, isCheckout, parentId } = useSelect( ( select ) => {
const { getBlockParentsByBlockName, getBlockName } =
select( blockEditorStore );
@ -82,13 +74,6 @@ const withSidebarNotices = createHigherOrderComponent(
currentBlockName === targetParentBlock
? clientId
: parents[ targetParentBlock ],
isPaymentMethodsBlock:
currentBlockName === 'woocommerce/checkout-payment-block',
hasPaymentMethods:
select( PAYMENT_STORE_KEY ).paymentMethodsInitialized() &&
Object.keys(
select( PAYMENT_STORE_KEY ).getAvailablePaymentMethods()
).length > 0,
};
} );
@ -112,11 +97,6 @@ const withSidebarNotices = createHigherOrderComponent(
/>
<DefaultNotice block={ isCheckout ? 'checkout' : 'cart' } />
{ isPaymentMethodsBlock && ! hasPaymentMethods && (
<NoPaymentMethodsNotice />
) }
<CartCheckoutFeedbackPrompt />
</InspectorControls>
<BlockEdit key="edit" { ...props } />

View File

@ -1,43 +1,104 @@
/**
* External dependencies
*/
import { TotalsShipping } from '@woocommerce/base-components/cart-checkout';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import { useStoreCart, useEditorContext } from '@woocommerce/base-context/';
import { TotalsWrapper } from '@woocommerce/blocks-components';
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import {
TotalsShipping,
ShippingCalculatorButton,
ShippingCalculator,
} from '@woocommerce/base-components/cart-checkout';
import { ShippingCalculatorContext } from '@woocommerce/base-components/cart-checkout/shipping-calculator/context';
import { useEditorContext, useStoreCart } from '@woocommerce/base-context';
import { TotalsWrapper } from '@woocommerce/blocks-checkout';
import {
getShippingRatesPackageCount,
selectedRatesAreCollectable,
allRatesAreCollectable,
} from '@woocommerce/base-utils';
import { getSetting } from '@woocommerce/settings';
import { getShippingRatesPackageCount } from '@woocommerce/base-utils';
import { select } from '@wordpress/data';
/**
* Internal dependencies
*/
import { ShippingRateSelector } from './shipping-rate-selector';
const Block = ( { className }: { className: string } ): JSX.Element | null => {
const { cartTotals, cartNeedsShipping } = useStoreCart();
const { isEditor } = useEditorContext();
const { cartNeedsShipping, shippingRates } = useStoreCart();
const [ isShippingCalculatorOpen, setIsShippingCalculatorOpen ] =
useState( false );
if ( ! cartNeedsShipping ) {
return null;
}
const shippingRates = select( 'wc/store/cart' ).getShippingRates();
const shippingRatesPackageCount =
getShippingRatesPackageCount( shippingRates );
if ( ! shippingRatesPackageCount && isEditor ) {
if ( isEditor && getShippingRatesPackageCount( shippingRates ) === 0 ) {
return null;
}
const totalsCurrency = getCurrencyFromPriceResponse( cartTotals );
const showCalculator = getSetting< boolean >(
'isShippingCalculatorEnabled',
true
);
const hasSelectedCollectionOnly =
selectedRatesAreCollectable( shippingRates );
return (
<TotalsWrapper className={ className }>
<TotalsShipping
showCalculator={ getSetting< boolean >(
'isShippingCalculatorEnabled',
true
) }
showRateSelector={ true }
values={ cartTotals }
currency={ totalsCurrency }
/>
<ShippingCalculatorContext.Provider
value={ {
showCalculator,
shippingCalculatorID: 'shipping-calculator-form-wrapper',
isShippingCalculatorOpen,
setIsShippingCalculatorOpen,
} }
>
<TotalsShipping
label={
hasSelectedCollectionOnly
? __( 'Collection', 'woocommerce' )
: __( 'Delivery', 'woocommerce' )
}
placeholder={
showCalculator ? (
<ShippingCalculatorButton
label={ __(
'Enter address to check delivery options',
'woocommerce'
) }
/>
) : (
<span className="wc-block-components-shipping-placeholder__value">
{ __(
'Calculated on checkout',
'woocommerce'
) }
</span>
)
}
collaterals={
<>
{ isShippingCalculatorOpen && (
<ShippingCalculator />
) }
{ ! isShippingCalculatorOpen && (
<ShippingRateSelector />
) }
{ ! showCalculator &&
allRatesAreCollectable( shippingRates ) && (
<div className="wc-block-components-totals-shipping__delivery-options-notice">
{ __(
'Delivery options will be calculated during checkout',
'woocommerce'
) }
</div>
) }
</>
}
/>
</ShippingCalculatorContext.Provider>
</TotalsWrapper>
);
};

View File

@ -2,45 +2,35 @@
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import type {
CartResponseShippingAddress,
CartResponseShippingRate,
} from '@woocommerce/types';
import NoticeBanner from '@woocommerce/base-components/notice-banner';
import { createInterpolateElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import ShippingRatesControl from '../../shipping-rates-control';
import { formatShippingAddress } from '../../../../utils';
export interface ShippingRateSelectorProps {
hasRates: boolean;
shippingRates: CartResponseShippingRate[];
shippingAddress: CartResponseShippingAddress;
isLoadingRates: boolean;
isAddressComplete: boolean;
}
export const ShippingRateSelector = ( {
hasRates,
shippingRates,
shippingAddress,
isLoadingRates,
import { ShippingRatesControl } from '@woocommerce/base-components/cart-checkout';
import NoticeBanner from '@woocommerce/base-components/notice-banner';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import {
formatShippingAddress,
isAddressComplete,
}: ShippingRateSelectorProps ): JSX.Element => {
const legend = hasRates
? __( 'Shipping options', 'woocommerce' )
: __( 'Choose a shipping option', 'woocommerce' );
} from '@woocommerce/base-utils';
export const ShippingRateSelector = (): JSX.Element => {
const { shippingRates, isLoadingRates, shippingAddress } = useStoreCart();
const hasCompleteAddress = isAddressComplete( shippingAddress, [
'state',
'country',
'postcode',
'city',
] );
return (
<fieldset className="wc-block-components-totals-shipping__fieldset">
<legend className="screen-reader-text">{ legend }</legend>
<legend className="screen-reader-text">
{ __( 'Shipping options', 'woocommerce' ) }
</legend>
<ShippingRatesControl
className="wc-block-components-totals-shipping__options"
noResultsMessage={
<>
{ isAddressComplete && (
{ hasCompleteAddress && (
<NoticeBanner
isDismissible={ false }
className="wc-block-components-shipping-rates-control__no-results-notice"

View File

@ -146,11 +146,9 @@
margin-top: 0;
.wc-block-components-shipping-calculator,
.wc-block-components-shipping-rates-control__package:not(
.wc-block-components-panel
) {
padding-left: $gap;
padding-right: $gap;
.wc-block-components-shipping-rates-control__package {
padding-left: 0;
padding-right: 0;
}
.wc-block-components-totals-item__description.wc-block-components-totals-shipping__via,

View File

@ -0,0 +1,50 @@
/**
* External dependencies
*/
import { Placeholder, Button } from '@wordpress/components';
import { Icon, blockDefault } from '@wordpress/icons';
import PALETTE from '@automattic/color-studio';
/**
* Placeholder component that links to a settings page to configure something.
*/
const ConfigurePlaceholder = ( {
label,
description,
buttonLabel,
buttonHref,
icon = blockDefault,
}: {
label: string;
description: string;
buttonLabel: string;
buttonHref: string;
icon?: Icon;
} ) => {
return (
<Placeholder
icon={ <Icon icon={ icon } /> }
label={ label }
className="wc-block-checkout__configure-placeholder"
>
<span className="wc-block-checkout__configure-placeholder-description">
{ description }
</span>
<Button
variant="primary"
href={ buttonHref }
target="_blank"
rel="noopener noreferrer"
style={ {
backgroundColor: PALETTE.colors[ 'Gray 100' ],
color: PALETTE.colors.White,
pointerEvents: 'all',
} }
>
{ buttonLabel }
</Button>
</Placeholder>
);
};
export default ConfigurePlaceholder;

View File

@ -1,4 +1,4 @@
.components-placeholder.wc-block-checkout__no-shipping-placeholder {
.components-placeholder.wc-block-checkout__configure-placeholder {
margin-bottom: $gap;
* {
@ -13,7 +13,7 @@
color: $white;
}
.wc-block-checkout__no-shipping-placeholder-description {
.wc-block-checkout__configure-placeholder-description {
display: block;
margin: 0.25em 0 1em 0;
}

View File

@ -16,6 +16,10 @@
"type": "string",
"default": ""
},
"disableProductDescriptions": {
"type": "boolean",
"default": false
},
"lock": {
"type": "object",
"default": {
@ -24,10 +28,8 @@
}
}
},
"parent": [
"woocommerce/checkout-order-summary-block"
],
"parent": [ "woocommerce/checkout-order-summary-block" ],
"textdomain": "woocommerce",
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3
}
}

View File

@ -5,12 +5,23 @@ import { OrderSummary } from '@woocommerce/base-components/cart-checkout';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import { TotalsWrapper } from '@woocommerce/blocks-components';
const Block = ( { className = '' }: { className?: string } ): JSX.Element => {
/**
* Internal dependencies
*/
import { BlockAttributes } from './edit';
const Block = ( {
className = '',
disableProductDescriptions = false,
}: BlockAttributes ): JSX.Element => {
const { cartItems } = useStoreCart();
return (
<TotalsWrapper className={ className }>
<OrderSummary cartItems={ cartItems } />
<OrderSummary
cartItems={ cartItems }
disableProductDescriptions={ disableProductDescriptions }
/>
</TotalsWrapper>
);
};

View File

@ -1,26 +1,61 @@
/**
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, ToggleControl } from '@wordpress/components';
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import Block from './block';
export type BlockAttributes = {
className: string;
disableProductDescriptions: boolean;
};
export const Edit = ( {
attributes,
setAttributes,
}: {
attributes: {
className: string;
};
attributes: BlockAttributes;
setAttributes: ( attributes: Record< string, unknown > ) => void;
} ): JSX.Element => {
const { className } = attributes;
const { className, disableProductDescriptions } = attributes;
const blockProps = useBlockProps();
return (
<div { ...blockProps }>
<Block className={ className } />
{ /* For now this setting can only be enabled if you have experimental features enabled. */ }
{ isExperimentalBlocksEnabled() && (
<InspectorControls>
<PanelBody title={ __( 'Settings', 'woocommerce' ) }>
<ToggleControl
label={ __(
'Disable product descriptions',
'woocommerce'
) }
help={ __(
'Disable display of product descriptions.',
'woocommerce'
) }
checked={ disableProductDescriptions }
onChange={ () =>
setAttributes( {
disableProductDescriptions:
! disableProductDescriptions,
} )
}
/>
</PanelBody>
</InspectorControls>
) }
<Block
disableProductDescriptions={ disableProductDescriptions }
className={ className }
/>
</div>
);
};

View File

@ -1,32 +1,68 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { TotalsShipping } from '@woocommerce/base-components/cart-checkout';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import { useStoreCart } from '@woocommerce/base-context';
import { TotalsWrapper } from '@woocommerce/blocks-checkout';
import { useSelect } from '@wordpress/data';
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
import {
filterShippingRatesByPrefersCollection,
isAddressComplete,
selectedRatesAreCollectable,
} from '@woocommerce/base-utils';
const Block = ( {
className = '',
}: {
className?: string;
} ): JSX.Element | null => {
const { cartTotals, cartNeedsShipping } = useStoreCart();
const { cartNeedsShipping, shippingRates, shippingAddress } =
useStoreCart();
const prefersCollection = useSelect( ( select ) =>
select( CHECKOUT_STORE_KEY ).prefersCollection()
);
if ( ! cartNeedsShipping ) {
return null;
}
const totalsCurrency = getCurrencyFromPriceResponse( cartTotals );
const hasSelectedCollectionOnly = selectedRatesAreCollectable(
filterShippingRatesByPrefersCollection(
shippingRates,
prefersCollection ?? false
)
);
const hasCompleteAddress = isAddressComplete( shippingAddress, [
'state',
'country',
'postcode',
'city',
] );
return (
<TotalsWrapper className={ className }>
<TotalsShipping
showCalculator={ false }
showRateSelector={ false }
values={ cartTotals }
currency={ totalsCurrency }
isCheckout={ true }
label={
hasSelectedCollectionOnly
? __( 'Collection', 'woocommerce' )
: __( 'Delivery', 'woocommerce' )
}
placeholder={
<span className="wc-block-components-shipping-placeholder__value">
{ hasCompleteAddress
? __(
'No available delivery option',
'woocommerce'
)
: __(
'Enter address to calculate',
'woocommerce'
) }
</span>
}
/>
</TotalsWrapper>
);

View File

@ -3,8 +3,12 @@
*/
import { PaymentMethods } from '../../../cart-checkout-shared/payment-methods';
const Block = (): JSX.Element | null => {
return <PaymentMethods />;
const Block = ( {
noPaymentMethods,
}: {
noPaymentMethods?: JSX.Element | undefined;
} ): JSX.Element | null => {
return <PaymentMethods noPaymentMethods={ noPaymentMethods } />;
};
export default Block;

View File

@ -5,6 +5,7 @@ import clsx from 'clsx';
import { __ } from '@wordpress/i18n';
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import { PanelBody, ExternalLink } from '@wordpress/components';
import { payment } from '@wordpress/icons';
import { ADMIN_URL, getSetting } from '@woocommerce/settings';
import ExternalLinkCard from '@woocommerce/editor-components/external-link-card';
import { innerBlockAreas } from '@woocommerce/blocks-checkout';
@ -24,6 +25,7 @@ import {
AdditionalFieldsContent,
} from '../../form-step';
import Block from './block';
import ConfigurePlaceholder from '../../configure-placeholder';
export const Edit = ( {
attributes,
@ -119,7 +121,23 @@ export const Edit = ( {
) }
</InspectorControls>
<Noninteractive>
<Block />
<Block
noPaymentMethods={
<ConfigurePlaceholder
icon={ payment }
label={ __( 'Payment options', 'woocommerce' ) }
description={ __(
'Your store does not have any payment methods that support the Checkout block. Once you have configured a compatible payment method it will be displayed here.',
'woocommerce'
) }
buttonLabel={ __(
'Configure Payment Options',
'woocommerce'
) }
buttonHref={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=checkout` }
/>
}
/>
</Noninteractive>
<AdditionalFields block={ innerBlockAreas.PAYMENT_METHODS } />
</FormStepBlock>

View File

@ -60,7 +60,26 @@ const renderShippingRatesControlOption = (
};
};
const Block = ( { noShippingPlaceholder = null } ): ReactElement | null => {
const NoShippingAddressMessage = () => {
return (
<p
role="status"
aria-live="polite"
className="wc-block-components-shipping-rates-control__no-shipping-address-message"
>
{ __(
'Enter a shipping address to view shipping options.',
'woocommerce'
) }
</p>
);
};
const Block = ( {
noShippingPlaceholder = null,
}: {
noShippingPlaceholder?: ReactElement | null;
} ) => {
const { isEditor } = useEditorContext();
const {
@ -95,14 +114,7 @@ const Block = ( { noShippingPlaceholder = null } ): ReactElement | null => {
getShippingRatesPackageCount( shippingRates );
if ( ! hasCalculatedShipping && ! shippingRatesPackageCount ) {
return (
<p>
{ __(
'Shipping options will be displayed here after entering your full shipping address.',
'woocommerce'
) }
</p>
);
return <NoShippingAddressMessage />;
}
const addressComplete = isAddressComplete( shippingAddress );
@ -124,15 +136,12 @@ const Block = ( { noShippingPlaceholder = null } ): ReactElement | null => {
status="warning"
>
{ __(
'There are no shipping options available. Please check your shipping address.',
'No shipping options are available for this address. Please verify the address is correct or try a different address.',
'woocommerce'
) }
</NoticeBanner>
) : (
__(
'Add a shipping address to view shipping options.',
'woocommerce'
)
<NoShippingAddressMessage />
) }
</>
}

View File

@ -5,6 +5,7 @@ import clsx from 'clsx';
import { __ } from '@wordpress/i18n';
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import { PanelBody, ExternalLink } from '@wordpress/components';
import { shipping } from '@wordpress/icons';
import { ADMIN_URL, getSetting } from '@woocommerce/settings';
import ExternalLinkCard from '@woocommerce/editor-components/external-link-card';
import { innerBlockAreas } from '@woocommerce/blocks-checkout';
@ -19,7 +20,7 @@ import {
AdditionalFields,
AdditionalFieldsContent,
} from '../../form-step';
import NoShippingPlaceholder from './no-shipping-placeholder';
import ConfigurePlaceholder from '../../configure-placeholder';
import Block from './block';
import './editor.scss';
@ -126,7 +127,23 @@ export const Edit = ( {
) }
</InspectorControls>
<Noninteractive>
<Block noShippingPlaceholder={ <NoShippingPlaceholder /> } />
<Block
noShippingPlaceholder={
<ConfigurePlaceholder
icon={ shipping }
label={ __( 'Shipping options', 'woocommerce' ) }
description={ __(
'Your store does not have any Shipping Options configured. Once you have added your Shipping Options they will appear here.',
'woocommerce'
) }
buttonLabel={ __(
'Configure Shipping Options',
'woocommerce'
) }
buttonHref={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=shipping` }
/>
}
/>
</Noninteractive>
<AdditionalFields block={ innerBlockAreas.SHIPPING_METHODS } />
</FormStepBlock>

View File

@ -1,39 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Placeholder, Button } from '@wordpress/components';
import { Icon, shipping } from '@wordpress/icons';
import { ADMIN_URL } from '@woocommerce/settings';
/**
* Internal dependencies
*/
import './style.scss';
const NoShippingPlaceholder = () => {
return (
<Placeholder
icon={ <Icon icon={ shipping } /> }
label={ __( 'Shipping options', 'woocommerce' ) }
className="wc-block-checkout__no-shipping-placeholder"
>
<span className="wc-block-checkout__no-shipping-placeholder-description">
{ __(
'Your store does not have any Shipping Options configured. Once you have added your Shipping Options they will appear here.',
'woocommerce'
) }
</span>
<Button
variant="secondary"
href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=shipping` }
target="_blank"
rel="noopener noreferrer"
>
{ __( 'Configure Shipping Options', 'woocommerce' ) }
</Button>
</Placeholder>
);
};
export default NoShippingPlaceholder;

View File

@ -11,4 +11,11 @@
.wc-block-components-shipping-rates-control__no-results-notice {
margin: em($gap-small) 0;
}
.wc-block-components-shipping-rates-control__no-shipping-address-message {
background-color: $gray-200;
color: $gray-700;
padding: em($gap-large);
text-align: center;
}
}

View File

@ -12,8 +12,12 @@ import {
PanelBody,
ToggleControl,
Placeholder,
// @ts-expect-error - no types.
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
__experimentalToggleGroupControl as ToggleGroupControl,
// @ts-expect-error - no types.
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
__experimentalToggleGroupControlOption as ToggleGroupControlOption,
} from '@wordpress/components';
@ -72,6 +76,7 @@ const ProductCategoriesBlock = ( {
>
<ToggleGroupControl
label={ __( 'Display style', 'woocommerce' ) }
isBlock
value={ isDropdown ? 'dropdown' : 'list' }
onChange={ ( value: string ) =>
setAttributes( {

View File

@ -10,7 +10,9 @@ import Block from './block';
import './editor.scss';
import type { ProductCategoriesBlockProps } from './types';
export const Edit = ( props: ProductCategoriesBlockProps ): JSX.Element => {
export default function ProductCategoriesEdit(
props: ProductCategoriesBlockProps
): JSX.Element {
const blockProps = useBlockProps();
return (
@ -18,4 +20,4 @@ export const Edit = ( props: ProductCategoriesBlockProps ): JSX.Element => {
<Block { ...props } />
</div>
);
};
}

View File

@ -7,11 +7,11 @@ import { Icon, listView } from '@wordpress/icons';
/**
* Internal dependencies
*/
import './editor.scss';
import metadata from './block.json';
import './style.scss';
import { Edit } from './edit';
import Edit from './edit';
import type { ProductCategoriesIndexProps } from './types';
import './editor.scss';
import './style.scss';
registerBlockType( metadata, {
icon: {

View File

@ -9,6 +9,8 @@ import { BlockEditProps } from '@wordpress/blocks';
import { Disabled, Tooltip } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { isSiteEditorPage } from '@woocommerce/utils';
import { getSettingWithCoercion } from '@woocommerce/settings';
import { isBoolean } from '@woocommerce/types';
/**
* Internal dependencies
@ -26,8 +28,15 @@ export interface Attributes {
const Edit = ( props: BlockEditProps< Attributes > ) => {
const { setAttributes } = props;
const isStepperLayoutFeatureEnabled = getSettingWithCoercion(
'isStepperLayoutFeatureEnabled',
false,
isBoolean
);
const quantitySelectorStyleClass =
props.attributes.quantitySelectorStyle === QuantitySelectorStyle.Input
props.attributes.quantitySelectorStyle ===
QuantitySelectorStyle.Input || ! isStepperLayoutFeatureEnabled
? 'wc-block-add-to-cart-form--input'
: 'wc-block-add-to-cart-form--stepper';
@ -52,10 +61,14 @@ const Edit = ( props: BlockEditProps< Attributes > ) => {
return (
<>
<Settings
quantitySelectorStyle={ props.attributes.quantitySelectorStyle }
setAttributes={ setAttributes }
/>
{ isStepperLayoutFeatureEnabled && (
<Settings
quantitySelectorStyle={
props.attributes.quantitySelectorStyle
}
setAttributes={ setAttributes }
/>
) }
<div { ...blockProps }>
<Tooltip
text="Customer will see product add-to-cart options in this space, dependent on the product type. "
@ -64,8 +77,9 @@ const Edit = ( props: BlockEditProps< Attributes > ) => {
<div className="wc-block-editor-add-to-cart-form-container">
<Skeleton numberOfLines={ 3 } />
<Disabled>
{ props.attributes.quantitySelectorStyle ===
QuantitySelectorStyle.Input && (
{ ( props.attributes.quantitySelectorStyle ===
QuantitySelectorStyle.Input ||
! isStepperLayoutFeatureEnabled ) && (
<>
<div className="quantity">
<input
@ -98,44 +112,52 @@ const Edit = ( props: BlockEditProps< Attributes > ) => {
</>
) }
{ props.attributes.quantitySelectorStyle ===
QuantitySelectorStyle.Stepper && (
<>
<div className="quantity wc-block-components-quantity-selector">
<button className="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus">
-
QuantitySelectorStyle.Stepper &&
isStepperLayoutFeatureEnabled && (
<>
<div className="quantity wc-block-components-quantity-selector">
<button className="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus">
-
</button>
<input
style={
// In the post editor, the editor isn't in an iframe, so WordPress styles are applied. We need to remove them.
! isSiteEditor
? {
backgroundColor:
'#ffffff',
lineHeight:
'normal',
minHeight:
'unset',
boxSizing:
'unset',
borderRadius:
'unset',
}
: {}
}
type={ 'number' }
value={ '1' }
className={
'input-text qty text'
}
readOnly
/>
<button className="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">
+
</button>
</div>
<button
className={ `single_add_to_cart_button alt wp-element-button` }
>
{ __(
'Add to cart',
'woocommerce'
) }
</button>
<input
style={
// In the post editor, the editor isn't in an iframe, so WordPress styles are applied. We need to remove them.
! isSiteEditor
? {
backgroundColor:
'#ffffff',
lineHeight:
'normal',
minHeight: 'unset',
boxSizing: 'unset',
borderRadius:
'unset',
}
: {}
}
type={ 'number' }
value={ '1' }
className={ 'input-text qty text' }
readOnly
/>
<button className="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">
+
</button>
</div>
<button
className={ `single_add_to_cart_button alt wp-element-button` }
>
{ __( 'Add to cart', 'woocommerce' ) }
</button>
</>
) }
</>
) }
</Disabled>
</div>
</Tooltip>

View File

@ -1,21 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { BlockVariation } from '@wordpress/blocks';
import { Icon, button } from '@wordpress/icons';
const variations: BlockVariation[] = [
{
name: 'product-filters-overlay-navigation-open-trigger',
title: __( 'Overlay Navigation (Experimental)', 'woocommerce' ),
attributes: {
triggerType: 'open-overlay',
},
isDefault: false,
icon: <Icon icon={ button } />,
isActive: [ 'triggerType' ],
},
];
export const blockVariations = variations;

View File

@ -1,106 +0,0 @@
{
"name": "woocommerce/product-filters-overlay-navigation",
"title": "Overlay Navigation (Experimental)",
"description": "Display overlay navigation controls.",
"category": "woocommerce",
"keywords": [ "WooCommerce" ],
"textdomain": "woocommerce",
"ancestor": [ "woocommerce/product-filters-overlay", "woocommerce/product-filters" ],
"attributes": {
"align": {
"type": "string",
"default": "right"
},
"navigationStyle": {
"type": "string",
"default": "label-and-icon"
},
"buttonStyle": {
"type": "string",
"default": "link"
},
"iconSize": {
"type": "number"
},
"overlayMode": {
"type": "string",
"default": "never"
},
"overlayIcon": {
"type": "string",
"default": "filter-icon-1"
},
"style": {
"type": "object",
"default": {
"spacing": {
"blockGap": "1rem"
}
}
},
"triggerType": {
"type": "string",
"default": "close-overlay"
}
},
"supports": {
"interactivity": true,
"align": [ "left", "right", "center"],
"inserter": true,
"color": {
"__experimentalDefaultControls": {
"text": false,
"background": false
}
},
"position": {
"sticky": true
},
"typography": {
"fontSize": true,
"lineHeight": true,
"__experimentalFontWeight": true,
"__experimentalFontFamily": true,
"__experimentalFontStyle": true,
"__experimentalTextTransform": true,
"__experimentalTextDecoration": true,
"__experimentalLetterSpacing": true,
"__experimentalDefaultControls": {
"fontSize": false
}
},
"layout": {
"default": {
"type": "flex",
"orientation": "horizontal",
"flexWrap": "nowrap"
},
"allowEditing": false
},
"spacing": {
"margin": true,
"padding": true,
"blockGap": true,
"__experimentalDefaultControls": {
"margin": false,
"padding": false,
"blockGap": false
}
},
"__experimentalBorder": {
"color": true,
"radius": true,
"style": true,
"width": true,
"__experimentalDefaultControls": {
"color": false,
"radius": false,
"style": false,
"width": false
}
}
},
"usesContext": [ "woocommerce/product-filters/overlay"],
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3
}

View File

@ -1,211 +0,0 @@
/**
* External dependencies
*/
import { useSelect } from '@wordpress/data';
import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
import { BlockEditProps, store as blocksStore } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import clsx from 'clsx';
import { Icon, close, menu, settings } from '@wordpress/icons';
import { filter, filterThreeLines } from '@woocommerce/icons';
/**
* Internal dependencies
*/
import type { BlockAttributes, BlockVariationTriggerType } from './types';
import './editor.scss';
import { Inspector } from './inspector-controls';
const OverlayNavigationLabel = ( {
variation,
}: {
variation: BlockVariationTriggerType;
} ) => {
let label = __( 'Close', 'woocommerce' );
if ( variation === 'open-overlay' ) {
label = __( 'Filters', 'woocommerce' );
}
return <span>{ label }</span>;
};
const OverlayNavigationIcon = ( {
variation,
iconSize,
overlayIcon,
style,
}: {
variation: BlockVariationTriggerType;
iconSize: number | undefined;
overlayIcon: string;
style: BlockAttributes[ 'style' ];
} ) => {
let icon = close;
if ( variation === 'open-overlay' ) {
switch ( overlayIcon ) {
case 'filter-icon-4':
icon = settings;
break;
case 'filter-icon-3':
icon = menu;
break;
case 'filter-icon-2':
icon = filterThreeLines;
break;
case 'filter-icon-1':
icon = filter;
break;
default:
icon = filter;
}
}
return (
<Icon
fill="currentColor"
icon={ icon }
style={ {
width: iconSize || style?.typography?.fontSize || '16px',
height: iconSize || style?.typography?.fontSize || '16px',
} }
/>
);
};
const OverlayNavigationContent = ( {
variation,
iconSize,
style,
overlayIcon,
navigationStyle,
}: {
variation: BlockVariationTriggerType;
iconSize: BlockAttributes[ 'iconSize' ];
style: BlockAttributes[ 'style' ];
overlayIcon: BlockAttributes[ 'overlayIcon' ];
navigationStyle: BlockAttributes[ 'navigationStyle' ];
} ) => {
const overlayNavigationLabel = (
<OverlayNavigationLabel variation={ variation } />
);
const overlayNavigationIcon = (
<OverlayNavigationIcon
variation={ variation }
iconSize={ iconSize }
overlayIcon={ overlayIcon }
style={ style }
/>
);
if ( navigationStyle === 'label-and-icon' ) {
if ( variation === 'open-overlay' ) {
return (
<>
{ overlayNavigationIcon }
{ overlayNavigationLabel }
</>
);
} else if ( variation === 'close-overlay' ) {
return (
<>
{ overlayNavigationLabel }
{ overlayNavigationIcon }
</>
);
}
} else if ( navigationStyle === 'label-only' ) {
return overlayNavigationLabel;
} else if ( navigationStyle === 'icon-only' ) {
return overlayNavigationIcon;
}
return null;
};
type BlockProps = BlockEditProps< BlockAttributes >;
export const Edit = ( { attributes, setAttributes }: BlockProps ) => {
const {
navigationStyle,
buttonStyle,
iconSize,
overlayIcon,
style,
triggerType,
} = attributes;
const blockProps = useBlockProps( {
className: clsx( 'wc-block-product-filters-overlay-navigation', {
'wp-block-button__link wp-element-button': buttonStyle !== 'link',
} ),
} );
const shouldHideBlock = () => {
if ( triggerType === 'open-overlay' ) {
return false;
}
return true;
};
// We need useInnerBlocksProps because Gutenberg only applies layout classes
// to parent block. We don't have any inner blocks but we want to use the
// layout controls.
const innerBlocksProps = useInnerBlocksProps( blockProps );
const buttonBlockStyles = useSelect(
( select ) => select( blocksStore ).getBlockStyles( 'core/button' ),
[]
);
const buttonStyles = [
{ value: 'link', label: __( 'Link', 'woocommerce' ) },
];
buttonBlockStyles.forEach(
( buttonBlockStyle: { name: string; label: string } ) => {
if ( buttonBlockStyle.name === 'link' ) return;
buttonStyles.push( {
value: buttonBlockStyle.name,
label: buttonBlockStyle.label,
} );
}
);
if ( shouldHideBlock() ) {
return (
<Inspector
attributes={ attributes }
setAttributes={ setAttributes }
buttonStyles={ buttonStyles }
/>
);
}
return (
<nav
className={ clsx(
'wc-block-product-filters-overlay-navigation__wrapper',
`is-style-${ buttonStyle }`,
{
'wp-block-button': buttonStyle !== 'link',
}
) }
>
<div { ...innerBlocksProps }>
<OverlayNavigationContent
variation={ triggerType }
iconSize={ iconSize }
navigationStyle={ navigationStyle }
overlayIcon={ overlayIcon }
style={ style }
/>
</div>
<Inspector
attributes={ attributes }
setAttributes={ setAttributes }
buttonStyles={ buttonStyles }
/>
</nav>
);
};

View File

@ -1,33 +0,0 @@
.wc-block-product-filters-overlay-navigation__icon-size-control {
.components-range-control__wrapper {
order: 1;
}
.components-range-control__number {
order: 0;
margin-left: 0 !important;
margin-right: 20px;
.components-input-control__input::-webkit-outer-spin-button,
.components-input-control__input::-webkit-inner-spin-button {
appearance: none;
margin: 0;
}
/* Remove stepper arrows in Firefox */
.components-input-control__input {
appearance: textfield;
width: 40px !important;
}
.components-input-control__container::after {
content: "px";
position: absolute;
top: 6px;
right: 7px;
font-size: 12px;
color: var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9));
}
}
}

View File

@ -1,19 +0,0 @@
/**
* External dependencies
*/
import { store } from '@woocommerce/interactivity';
export interface ProductFiltersContext {
isDialogOpen: boolean;
}
const productFiltersOverlayNavigation = {
state: {},
actions: {},
callbacks: {},
};
store( 'woocommerce/product-filters', productFiltersOverlayNavigation );
export type ProductFiltersOverlayNavigation =
typeof productFiltersOverlayNavigation;

View File

@ -1,25 +0,0 @@
/**
* External dependencies
*/
import { registerBlockType } from '@wordpress/blocks';
import { closeSquareShadow } from '@woocommerce/icons';
import { Icon } from '@wordpress/icons';
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import metadata from './block.json';
import { Edit } from './edit';
import { Save } from './save';
import { blockVariations } from './block-variations';
import './style.scss';
if ( isExperimentalBlocksEnabled() ) {
registerBlockType( metadata, {
edit: Edit,
save: Save,
icon: <Icon icon={ closeSquareShadow } />,
variations: blockVariations,
} );
}

View File

@ -1,176 +0,0 @@
/**
* External dependencies
*/
import { InspectorControls } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { filter, filterThreeLines } from '@woocommerce/icons';
import { Icon, menu, settings } from '@wordpress/icons';
import {
PanelBody,
RadioControl,
SelectControl,
RangeControl,
__experimentalToggleGroupControl as ToggleGroupControl,
__experimentalToggleGroupControlOption as ToggleGroupControlOption,
} from '@wordpress/components';
/**
* Internal dependencies
*/
import type { BlockAttributes } from './types';
interface ButtonStyle {
value: string;
label: string;
}
interface InspectorProps {
attributes: BlockEditProps< BlockAttributes >[ 'attributes' ];
setAttributes: BlockEditProps< BlockAttributes >[ 'setAttributes' ];
buttonStyles: ButtonStyle[];
}
export const Inspector = ( {
attributes,
setAttributes,
buttonStyles,
}: InspectorProps ) => {
const { navigationStyle, buttonStyle, iconSize, overlayIcon, triggerType } =
attributes;
return (
<InspectorControls group="styles">
<PanelBody title={ __( 'Style', 'woocommerce' ) }>
<RadioControl
selected={ navigationStyle }
options={ [
{
label: __( 'Label and icon', 'woocommerce' ),
value: 'label-and-icon',
},
{
label: __( 'Label only', 'woocommerce' ),
value: 'label-only',
},
{
label: __( 'Icon only', 'woocommerce' ),
value: 'icon-only',
},
] }
onChange={ (
value: BlockAttributes[ 'navigationStyle' ]
) =>
setAttributes( {
navigationStyle: value,
} )
}
/>
{ buttonStyles.length <= 3 && (
<ToggleGroupControl
label={ __( 'Button', 'woocommerce' ) }
value={ buttonStyle }
isBlock
onChange={ (
value: BlockAttributes[ 'buttonStyle' ]
) =>
setAttributes( {
buttonStyle: value,
} )
}
>
{ buttonStyles.map( ( option ) => (
<ToggleGroupControlOption
key={ option.value }
label={ option.label }
value={ option.value }
/>
) ) }
</ToggleGroupControl>
) }
{ buttonStyles.length > 3 && (
<SelectControl
label={ __( 'Button', 'woocommerce' ) }
value={ buttonStyle }
options={ buttonStyles }
onChange={ (
value: BlockAttributes[ 'buttonStyle' ]
) =>
setAttributes( {
buttonStyle: value,
} )
}
/>
) }
{ triggerType === 'open-overlay' &&
navigationStyle !== 'label-only' && (
<ToggleGroupControl
label={ __( 'Icon', 'woocommerce' ) }
className="wc-block-editor-product-filters__overlay-button-toggle"
isBlock={ true }
value={ overlayIcon }
onChange={ (
value: BlockAttributes[ 'overlayIcon' ]
) => {
setAttributes( {
overlayIcon: value,
} );
} }
>
<ToggleGroupControlOption
value={ 'filter-icon-1' }
aria-label={ __(
'Filter icon 1',
'woocommerce'
) }
label={ <Icon size={ 32 } icon={ filter } /> }
/>
<ToggleGroupControlOption
value={ 'filter-icon-2' }
aria-label={ __(
'Filter icon 2',
'woocommerce'
) }
label={
<Icon
size={ 32 }
icon={ filterThreeLines }
/>
}
/>
<ToggleGroupControlOption
value={ 'filter-icon-3' }
aria-label={ __(
'Filter icon 3',
'woocommerce'
) }
label={ <Icon size={ 32 } icon={ menu } /> }
/>
<ToggleGroupControlOption
value={ 'filter-icon-4' }
aria-label={ __(
'Filter icon 4',
'woocommerce'
) }
label={ <Icon size={ 32 } icon={ settings } /> }
/>
</ToggleGroupControl>
) }
{ navigationStyle !== 'label-only' && (
<RangeControl
className="wc-block-product-filters-overlay-navigation__icon-size-control"
label={ __( 'Icon Size', 'woocommerce' ) }
value={ iconSize }
onChange={ ( newSize: number ) => {
setAttributes( { iconSize: newSize } );
} }
min={ 0 }
max={ 300 }
/>
) }
</PanelBody>
</InspectorControls>
);
};

View File

@ -1,12 +0,0 @@
/**
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
import clsx from 'clsx';
export const Save = (): JSX.Element => {
const blockProps = useBlockProps.save( {
className: clsx( 'wc-block-product-filters-overlay-navigation' ),
} );
return <div { ...blockProps } />;
};

View File

@ -1,25 +0,0 @@
.wc-block-product-filters-overlay-navigation__wrapper {
display: flex;
}
.wc-block-product-filters-overlay-navigation {
display: flex;
flex-direction: row;
cursor: pointer;
&.alignright {
justify-content: right;
}
&.alignleft {
justify-content: unset;
}
&.aligncenter {
justify-content: center;
}
&.hidden {
display: none;
}
}

View File

@ -1,54 +0,0 @@
type BorderRadius = {
bottomLeft: string;
bottomRight: string;
topLeft: string;
topRight: string;
};
type BorderSide = {
color: string;
width: string;
};
export type BlockVariationTriggerType = 'open-overlay' | 'close-overlay';
export type BlockAttributes = {
navigationStyle: 'label-and-icon' | 'label-only' | 'icon-only';
buttonStyle: string;
iconSize?: number;
triggerType: BlockVariationTriggerType;
overlayIcon: string;
style: {
border?: {
radius?: string | BorderRadius;
width?: string;
top?: BorderSide;
bottom?: BorderSide;
left?: BorderSide;
right?: BorderSide;
};
spacing?: {
blockGap?: string;
margin?: {
top?: string;
right?: string;
bottom?: string;
left?: string;
};
padding?: {
top?: string;
right?: string;
bottom?: string;
left?: string;
};
};
typography?: {
fontSize?: string;
lineHeight?: number;
fontStyle?: string;
fontWeight?: string;
letterSpacing?: string;
textDecoration?: string;
textTransform?: string;
};
};
};

View File

@ -1,53 +0,0 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "woocommerce/product-filters-overlay",
"version": "1.0.0",
"title": "Product Filters Overlay (Experimental)",
"description": "Display product filters in an overlay on top of a page.",
"category": "woocommerce",
"keywords": [
"WooCommerce"
],
"supports": {
"align": true,
"multiple": false,
"inserter": false,
"layout": {
"allowCustomContentAndWideSize": true
},
"color": {},
"typography": {},
"dimensions": {},
"spacing": {
"padding": true,
"blockGap": true
}
},
"textdomain": "woocommerce",
"providesContext": {},
"attributes": {
"overlayStyle": {
"type": "string",
"default": "drawer"
},
"overlayPosition": {
"type": "string",
"default": "left"
},
"style": {
"type": "object",
"default": {
"spacing": {
"padding": {
"top": "1rem",
"right": "1rem",
"bottom": "1rem",
"left": "1rem"
}
}
}
}
},
"example": {}
}

View File

@ -1,124 +0,0 @@
/* eslint-disable @wordpress/no-unsafe-wp-apis */
/**
* External dependencies
*/
import {
InnerBlocks,
useBlockProps,
useInnerBlocksProps,
InspectorControls,
} from '@wordpress/block-editor';
import { BlockEditProps, InnerBlockTemplate } from '@wordpress/blocks';
import {
PanelBody,
__experimentalToggleGroupControlOption as ToggleGroupControlOption,
__experimentalToggleGroupControl as ToggleGroupControl,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { WC_BLOCKS_IMAGE_URL } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import type { ProductFiltersOverlayBlockAttributes } from './types';
const TEMPLATE: InnerBlockTemplate[] = [ [ 'woocommerce/product-filters' ] ];
export const Edit = ( {
setAttributes,
attributes,
}: BlockEditProps< ProductFiltersOverlayBlockAttributes > ) => {
const blockProps = useBlockProps();
return (
<div { ...blockProps }>
<InspectorControls group="styles">
<PanelBody title={ __( 'Style', 'woocommerce' ) }>
<ToggleGroupControl
className="wc-block-editor-product-filters-overlay__overlay-style-toggle"
isBlock={ true }
value={ attributes.overlayStyle }
onChange={ ( value: 'fullscreen' | 'drawer' ) => {
setAttributes( {
overlayStyle: value,
} );
} }
help={
attributes.overlayStyle === 'fullscreen'
? __(
'The overlay will fill up the whole screen.',
'woocommerce'
)
: __(
'The overlay will show on the left or right side of the screen (only on desktop).',
'woocommerce'
)
}
>
<ToggleGroupControlOption
value={ 'fullscreen' }
label={ __( 'Full-Screen', 'woocommerce' ) }
/>
<ToggleGroupControlOption
value={ 'drawer' }
label={ __( 'Drawer', 'woocommerce' ) }
/>
</ToggleGroupControl>
{ attributes.overlayStyle === 'drawer' && (
<ToggleGroupControl
className="wc-block-editor-product-filters-overlay__overlay-position-toggle"
isBlock={ true }
value={ attributes.overlayPosition }
label={ __( 'POSITION', 'woocommerce' ) }
onChange={ ( value: 'left' | 'right' ) => {
setAttributes( {
overlayPosition: value,
} );
} }
>
<ToggleGroupControlOption
value={ 'left' }
label={ __( 'Left', 'woocommerce' ) }
/>
<ToggleGroupControlOption
value={ 'right' }
label={ __( 'Right', 'woocommerce' ) }
/>
</ToggleGroupControl>
) }
{ attributes.overlayStyle === 'drawer' ? (
<img
className="wc-block-editor-product-filters-overlay__drawer-image"
src={
attributes.overlayPosition === 'left'
? `${ WC_BLOCKS_IMAGE_URL }blocks/product-filters-overlay/overlay-drawer-left.svg`
: `${ WC_BLOCKS_IMAGE_URL }blocks/product-filters-overlay/overlay-drawer-right.svg`
}
alt={ __(
'Overlay drawer orientation',
'woocommerce'
) }
/>
) : (
<img
className="wc-block-editor-product-filters-overlay__drawer-image"
src={ `${ WC_BLOCKS_IMAGE_URL }blocks/product-filters-overlay/overlay-drawer-fullscreen.svg` }
alt={ __(
'Overlay drawer orientation',
'woocommerce'
) }
/>
) }
</PanelBody>
</InspectorControls>
<InnerBlocks templateLock={ false } template={ TEMPLATE } />
</div>
);
};
export const Save = () => {
const blockProps = useBlockProps.save();
const innerBlocksProps = useInnerBlocksProps.save( blockProps );
return <div { ...innerBlocksProps } />;
};

View File

@ -1,8 +0,0 @@
/**
* External dependencies
*/
import { Icon, pages } from '@wordpress/icons';
const icon = () => <Icon icon={ pages } />;
export default icon;

View File

@ -1,15 +0,0 @@
/**
* External dependencies
*/
import { registerBlockType } from '@wordpress/blocks';
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import metadata from './block.json';
import { ProductFiltersOverlayBlockSettings } from './settings';
if ( isExperimentalBlocksEnabled() ) {
registerBlockType( metadata, ProductFiltersOverlayBlockSettings );
}

View File

@ -1,17 +0,0 @@
/**
* External dependencies
*/
import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
import clsx from 'clsx';
/**
* Internal dependencies
*/
export const Save = (): JSX.Element => {
const blockProps = useBlockProps.save( {
className: clsx( 'wc-block-product-filters-overlay' ),
} );
const innerBlocksProps = useInnerBlocksProps.save( blockProps );
return <div { ...innerBlocksProps } />;
};

View File

@ -1,12 +0,0 @@
/**
* Internal dependencies
*/
import { Edit } from './edit';
import { Save } from './save';
import icon from './icon';
export const ProductFiltersOverlayBlockSettings = {
icon,
edit: Edit,
save: Save,
};

View File

@ -1,5 +0,0 @@
export interface ProductFiltersOverlayBlockAttributes {
overlayStyle: string;
overlayPosition: string;
setAttributes: ( attributes: ProductFiltersOverlayBlockAttributes ) => void;
}

View File

@ -32,6 +32,7 @@ import { EXCLUDED_BLOCKS } from '../../constants';
import { Notice } from '../../components/notice';
import type { Attributes } from './types';
import './style.scss';
import { InitialDisabled } from '../../components/initial-disabled';
const RatingFilterEdit = ( props: BlockEditProps< Attributes > ) => {
const { attributes, setAttributes } = props;
@ -189,30 +190,32 @@ const RatingFilterEdit = ( props: BlockEditProps< Attributes > ) => {
/>
<div { ...innerBlocksProps }>
{ showNoProductsNotice && (
<Notice>
{ __(
"Your store doesn't have any products with ratings yet. This filter option will display when a product receives a review.",
'woocommerce'
) }
</Notice>
) }
<div
className={ clsx( {
'is-loading': isLoading,
} ) }
>
<BlockContextProvider
value={ {
filterData: {
items: displayedOptions,
isLoading,
},
} }
<InitialDisabled>
{ showNoProductsNotice && (
<Notice>
{ __(
"Your store doesn't have any products with ratings yet. This filter option will display when a product receives a review.",
'woocommerce'
) }
</Notice>
) }
<div
className={ clsx( {
'is-loading': isLoading,
} ) }
>
{ children }
</BlockContextProvider>
</div>
<BlockContextProvider
value={ {
filterData: {
items: displayedOptions,
isLoading,
},
} }
>
{ children }
</BlockContextProvider>
</div>
</InitialDisabled>
</div>
</>
);

View File

@ -1,6 +1,4 @@
.wp-block-woocommerce-product-filter-rating {
display: grid;
.wc-block-components-product-rating {
margin-bottom: 0;
display: flex;

View File

@ -1,7 +0,0 @@
.wc-blocks-no-payment-methods-notice {
margin: 0;
.components-notice__content {
margin: 4px 0;
}
}

View File

@ -1,36 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Notice, ExternalLink } from '@wordpress/components';
import { ADMIN_URL } from '@woocommerce/settings';
/**
* Internal dependencies
*/
import './editor.scss';
export function NoPaymentMethodsNotice() {
const noticeContent = __(
'Your store does not have any payment methods that support the Checkout block. Once you have configured a compatible payment method it will be displayed here.',
'woocommerce'
);
return (
<Notice
className="wc-blocks-no-payment-methods-notice"
status={ 'warning' }
spokenMessage={ noticeContent }
isDismissible={ false }
>
<div className="wc-blocks-no-payment-methods-notice__content">
{ noticeContent }{ ' ' }
<ExternalLink
href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=checkout` }
>
{ __( 'Configure Payment Methods', 'woocommerce' ) }
</ExternalLink>
</div>
</Notice>
);
}

View File

@ -91,14 +91,6 @@ const blocks = {
'product-filters': {
isExperimental: true,
},
'product-filters-overlay': {
isExperimental: true,
customDir: 'product-filters/inner-blocks/overlay',
},
'product-filters-overlay-navigation': {
isExperimental: true,
customDir: 'product-filters/inner-blocks/overlay-navigation',
},
'product-filter-status': {
isExperimental: true,
customDir: 'product-filters/inner-blocks/status-filter',

View File

@ -1,21 +0,0 @@
<?php
/**
* Plugin Name: WooCommerce Blocks Test Enable Experimental Features
* Description: Enable experimental features for WooCommerce Blocks that are behind feature flags.
* Plugin URI: https://github.com/woocommerce/woocommerce
* Author: WooCommerce
*
* @package woocommerce-blocks-test-enable-experimental-features
*/
/**
* Enable experimental features
*
* @param array $features Array of feature slugs.
*/
function enable_experimental_features( $features ) {
// add experimental block features
return array_merge( $features, array( 'experimental-blocks' ) );
}
add_filter( 'woocommerce_admin_features', 'enable_experimental_features', 20, 1 );

View File

@ -191,130 +191,138 @@ test.describe( `${ blockData.name } Block`, () => {
).toBeVisible();
} );
test( 'has the stepper option visible', async ( {
admin,
editor,
blockUtils,
} ) => {
await admin.createNewPost();
await editor.insertBlock( { name: 'woocommerce/single-product' } );
test.describe( 'Stepper Layout', () => {
test.beforeEach( async ( { requestUtils } ) => {
await requestUtils.setFeatureFlag(
'add-to-cart-with-options-stepper-layout',
true
);
} );
test( 'has the stepper option visible', async ( {
admin,
editor,
blockUtils,
} ) => {
await admin.createNewPost();
await editor.insertBlock( { name: 'woocommerce/single-product' } );
await blockUtils.configureSingleProductBlock();
await blockUtils.configureSingleProductBlock();
await blockUtils.enableStepperMode();
await blockUtils.enableStepperMode();
const minusButton = editor.canvas.locator(
blockData.selectors.editor.stepperMinusButton
);
const plusButton = editor.canvas.locator(
blockData.selectors.editor.stepperPlusButton
);
const minusButton = editor.canvas.locator(
blockData.selectors.editor.stepperMinusButton
);
const plusButton = editor.canvas.locator(
blockData.selectors.editor.stepperPlusButton
);
await expect( minusButton ).toBeVisible();
await expect( plusButton ).toBeVisible();
} );
test( 'has the stepper mode working on the frontend', async ( {
admin,
editor,
blockUtils,
page,
} ) => {
await admin.createNewPost();
await editor.insertBlock( { name: 'woocommerce/single-product' } );
const productName = 'Hoodie with Logo';
await blockUtils.configureSingleProductBlock( productName );
await blockUtils.enableStepperMode();
await editor.publishAndVisitPost();
const minusButton = page.getByLabel( `Reduce quantity` );
const plusButton = page.getByLabel( `Increase quantity` );
await expect( minusButton ).toBeVisible();
await expect( plusButton ).toBeVisible();
const input = page.getByLabel( 'Product quantity' );
await expect( input ).toHaveValue( '1' );
await plusButton.click();
await expect( input ).toHaveValue( '2' );
await minusButton.click();
await expect( input ).toHaveValue( '1' );
// Ensure the quantity doesn't go below 1.
await minusButton.click();
await expect( input ).toHaveValue( '1' );
} );
test( "doesn't render stepper when the product is sold individually", async ( {
admin,
editor,
blockUtils,
page,
} ) => {
await blockUtils.createSoldIndividuallyProduct();
await admin.createNewPost();
await editor.insertBlock( { name: 'woocommerce/single-product' } );
const productName = 'Sold Individually';
await blockUtils.configureSingleProductBlock( productName );
await blockUtils.enableStepperMode();
await editor.publishAndVisitPost();
const minusButton = page.getByLabel( `Reduce quantity` );
const plusButton = page.getByLabel( `Increase quantity ` );
await expect( minusButton ).toBeHidden();
await expect( plusButton ).toBeHidden();
} );
test( 'has the stepper mode working on the frontend with min, max, and step attributes', async ( {
admin,
editor,
blockUtils,
page,
} ) => {
await admin.createNewPost();
await editor.insertBlock( { name: 'woocommerce/single-product' } );
const productName = 'Hoodie with Logo';
await blockUtils.configureSingleProductBlock( productName );
await blockUtils.enableStepperMode();
await editor.publishAndVisitPost();
await blockUtils.setMinMaxAndStep( {
min: 2,
max: 10,
step: 2,
await expect( minusButton ).toBeVisible();
await expect( plusButton ).toBeVisible();
} );
const minusButton = page.getByLabel( `Reduce quantity` );
const plusButton = page.getByLabel( `Increase quantity` );
test( 'has the stepper mode working on the frontend', async ( {
admin,
editor,
blockUtils,
page,
} ) => {
await admin.createNewPost();
await editor.insertBlock( { name: 'woocommerce/single-product' } );
await expect( minusButton ).toBeVisible();
await expect( plusButton ).toBeVisible();
const productName = 'Hoodie with Logo';
const input = page.getByLabel( 'Product quantity' );
await blockUtils.configureSingleProductBlock( productName );
await expect( input ).toHaveValue( '2' );
await minusButton.click();
await expect( input ).toHaveValue( '2' );
await plusButton.click();
await expect( input ).toHaveValue( '4' );
await plusButton.click();
await expect( input ).toHaveValue( '6' );
await plusButton.click();
await expect( input ).toHaveValue( '8' );
await plusButton.click();
await expect( input ).toHaveValue( '10' );
await plusButton.click();
await expect( input ).toHaveValue( '10' );
await blockUtils.enableStepperMode();
await editor.publishAndVisitPost();
const minusButton = page.getByLabel( `Reduce quantity` );
const plusButton = page.getByLabel( `Increase quantity` );
await expect( minusButton ).toBeVisible();
await expect( plusButton ).toBeVisible();
const input = page.getByLabel( 'Product quantity' );
await expect( input ).toHaveValue( '1' );
await plusButton.click();
await expect( input ).toHaveValue( '2' );
await minusButton.click();
await expect( input ).toHaveValue( '1' );
// Ensure the quantity doesn't go below 1.
await minusButton.click();
await expect( input ).toHaveValue( '1' );
} );
test( "doesn't render stepper when the product is sold individually", async ( {
admin,
editor,
blockUtils,
page,
} ) => {
await blockUtils.createSoldIndividuallyProduct();
await admin.createNewPost();
await editor.insertBlock( { name: 'woocommerce/single-product' } );
const productName = 'Sold Individually';
await blockUtils.configureSingleProductBlock( productName );
await blockUtils.enableStepperMode();
await editor.publishAndVisitPost();
const minusButton = page.getByLabel( `Reduce quantity` );
const plusButton = page.getByLabel( `Increase quantity ` );
await expect( minusButton ).toBeHidden();
await expect( plusButton ).toBeHidden();
} );
test( 'has the stepper mode working on the frontend with min, max, and step attributes', async ( {
admin,
editor,
blockUtils,
page,
} ) => {
await admin.createNewPost();
await editor.insertBlock( { name: 'woocommerce/single-product' } );
const productName = 'Hoodie with Logo';
await blockUtils.configureSingleProductBlock( productName );
await blockUtils.enableStepperMode();
await editor.publishAndVisitPost();
await blockUtils.setMinMaxAndStep( {
min: 2,
max: 10,
step: 2,
} );
const minusButton = page.getByLabel( `Reduce quantity` );
const plusButton = page.getByLabel( `Increase quantity` );
await expect( minusButton ).toBeVisible();
await expect( plusButton ).toBeVisible();
const input = page.getByLabel( 'Product quantity' );
await expect( input ).toHaveValue( '2' );
await minusButton.click();
await expect( input ).toHaveValue( '2' );
await plusButton.click();
await expect( input ).toHaveValue( '4' );
await plusButton.click();
await expect( input ).toHaveValue( '6' );
await plusButton.click();
await expect( input ).toHaveValue( '8' );
await plusButton.click();
await expect( input ).toHaveValue( '10' );
await plusButton.click();
await expect( input ).toHaveValue( '10' );
} );
} );
} );

View File

@ -78,7 +78,7 @@ test.describe( 'Shopper → Shipping', () => {
await expect(
userPage.getByText(
'Shipping options will be displayed here after entering your full shipping address'
'Enter a shipping address to view shipping options.'
)
).toBeVisible();
@ -86,7 +86,7 @@ test.describe( 'Shopper → Shipping', () => {
await expect(
userPage.getByText(
'Shipping options will be displayed here after entering your full shipping address'
'Enter a shipping address to view shipping options.'
)
).toBeHidden();
} );

View File

@ -155,9 +155,7 @@ test.describe( 'Shopper (guest) → Order Confirmation → Create Account', () =
test.use( { storageState: guestFile } );
test.beforeEach( async ( { frontendUtils, pageObject, requestUtils } ) => {
await requestUtils.activatePlugin(
'woocommerce-blocks-test-enable-experimental-features'
);
await requestUtils.setFeatureFlag( 'experimental-blocks', true );
await frontendUtils.goToShop();
await frontendUtils.addToCart( SIMPLE_PHYSICAL_PRODUCT_NAME );
await frontendUtils.goToCheckout();

View File

@ -18,9 +18,7 @@ const blockData = {
test.describe( `Filters Overlay Navigation`, () => {
test.beforeEach( async ( { admin, requestUtils } ) => {
await requestUtils.activatePlugin(
'woocommerce-blocks-test-enable-experimental-features'
);
await requestUtils.setFeatureFlag( 'experimental-blocks', true );
await admin.visitSiteEditor( {
postId: `woocommerce/woocommerce//${ blockData.templateSlug }`,
postType: blockData.templateType,

View File

@ -14,9 +14,7 @@ const test = base.extend< { templateCompiler: TemplateCompiler } >( {
test.describe( 'woocommerce/product-filter-active - Frontend', () => {
test.beforeEach( async ( { requestUtils } ) => {
await requestUtils.activatePlugin(
'woocommerce-blocks-test-enable-experimental-features'
);
await requestUtils.setFeatureFlag( 'experimental-blocks', true );
} );
test( 'Without any filters selected, active block should not be rendered', async ( {

View File

@ -32,9 +32,7 @@ const test = base.extend< { pageObject: ProductFiltersPage } >( {
test.describe( `${ blockData.name }`, () => {
test.beforeEach( async ( { admin, requestUtils } ) => {
await requestUtils.activatePlugin(
'woocommerce-blocks-test-enable-experimental-features'
);
await requestUtils.setFeatureFlag( 'experimental-blocks', true );
await admin.visitSiteEditor( {
postId: `woocommerce/woocommerce//${ blockData.slug }`,
postType: 'wp_template',

View File

@ -24,9 +24,7 @@ const test = base.extend< { templateCompiler: TemplateCompiler } >( {
test.describe( 'woocommerce/product-filter-attribute - Frontend', () => {
test.describe( 'With default display style', () => {
test.beforeEach( async ( { requestUtils, templateCompiler } ) => {
await requestUtils.activatePlugin(
'woocommerce-blocks-test-enable-experimental-features'
);
await requestUtils.setFeatureFlag( 'experimental-blocks', true );
await templateCompiler.compile( {
attributes: {
attributeId: 1,
@ -139,9 +137,7 @@ test.describe( 'woocommerce/product-filter-attribute - Frontend', () => {
test.describe( 'With show counts enabled', () => {
test.beforeEach( async ( { requestUtils, templateCompiler } ) => {
await requestUtils.activatePlugin(
'woocommerce-blocks-test-enable-experimental-features'
);
await requestUtils.setFeatureFlag( 'experimental-blocks', true );
await templateCompiler.compile( {
attributes: {
attributeId: 1,

View File

@ -15,9 +15,7 @@ const test = base.extend< { templateCompiler: TemplateCompiler } >( {
test.describe.skip( 'Product Filter: Price Filter Block', () => {
test.describe( 'frontend', () => {
test.beforeEach( async ( { requestUtils, templateCompiler } ) => {
await requestUtils.activatePlugin(
'woocommerce-blocks-test-enable-experimental-features'
);
await requestUtils.setFeatureFlag( 'experimental-blocks', true );
await templateCompiler.compile();
} );

View File

@ -47,9 +47,7 @@ const test = base.extend< { pageObject: ProductFiltersPage } >( {
test.describe( `${ blockData.name }`, () => {
test.beforeEach( async ( { admin, requestUtils } ) => {
await requestUtils.activatePlugin(
'woocommerce-blocks-test-enable-experimental-features'
);
await requestUtils.setFeatureFlag( 'experimental-blocks', true );
await admin.visitSiteEditor( {
postId: `woocommerce/woocommerce//${ blockData.slug }`,
postType: 'wp_template',

View File

@ -15,9 +15,7 @@ const test = base.extend< { templateCompiler: TemplateCompiler } >( {
test.describe.skip( 'Product Filter: Rating Filter Block', () => {
test.describe( 'frontend', () => {
test.beforeEach( async ( { requestUtils, templateCompiler } ) => {
await requestUtils.activatePlugin(
'woocommerce-blocks-test-enable-experimental-features'
);
await requestUtils.setFeatureFlag( 'experimental-blocks', true );
await templateCompiler.compile( {
attributes: {
attributeId: 1,

View File

@ -15,9 +15,7 @@ const test = base.extend< { templateCompiler: TemplateCompiler } >( {
test.describe.skip( 'Product Filter: Stock Status Block', () => {
test.describe( 'With default display style', () => {
test.beforeEach( async ( { requestUtils, templateCompiler } ) => {
await requestUtils.activatePlugin(
'woocommerce-blocks-test-enable-experimental-features'
);
await requestUtils.setFeatureFlag( 'experimental-blocks', true );
await templateCompiler.compile();
} );
@ -109,9 +107,7 @@ test.describe.skip( 'Product Filter: Stock Status Block', () => {
test.describe( 'With dropdown display style', () => {
test.beforeEach( async ( { requestUtils, templateCompiler } ) => {
await requestUtils.activatePlugin(
'woocommerce-blocks-test-enable-experimental-features'
);
await requestUtils.setFeatureFlag( 'experimental-blocks', true );
await templateCompiler.compile( {
attributes: {
displayStyle: 'dropdown',

View File

@ -0,0 +1,25 @@
/**
* Internal dependencies
*/
import type { RequestUtils } from './index';
export async function setFeatureFlag(
this: RequestUtils,
flag: string,
value: boolean
) {
return this.rest( {
method: 'POST',
path: '/e2e-feature-flags/update',
data: { [ flag ]: value },
failOnStatusCode: true,
} );
}
export async function resetFeatureFlag( this: RequestUtils ) {
return this.rest( {
method: 'GET',
path: '/e2e-feature-flags/reset',
failOnStatusCode: true,
} );
}

View File

@ -13,6 +13,7 @@ import {
createTemplateFromFile,
TemplateCompiler,
} from './templates';
import { resetFeatureFlag, setFeatureFlag } from './feature-flag';
export class RequestUtils extends CoreRequestUtils {
/** @borrows getTemplates as this.getTemplates */
@ -25,6 +26,10 @@ export class RequestUtils extends CoreRequestUtils {
/** @borrows createTemplateFromFile as this.createTemplateFromFile */
createTemplateFromFile: typeof createTemplateFromFile =
createTemplateFromFile.bind( this );
/** @borrows setFeatureFlag as this.setFeatureFlag */
setFeatureFlag: typeof setFeatureFlag = setFeatureFlag.bind( this );
/** @borrows resetFeatureFlag as this.resetFeatureFlag */
resetFeatureFlag: typeof resetFeatureFlag = resetFeatureFlag.bind( this );
}
export { TemplateCompiler, PostCompiler };

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Comment: Increases frequency of cron job from daily to twice daily.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Adds constants for all legacy order statuses to centralize them, reduce typos, improve code strictness, ease status lookups, and enhance documentation.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: enhancement
Removed CouponPageMoved class.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Prevented phone numbers containing Unicode control characters from failing validation during Checkout.

Some files were not shown because too many files have changed in this diff Show More