rebase on oct-10-2023

This commit is contained in:
Rachit Bhargava
2023-10-10 17:23:21 -04:00
parent d37566ffb6
commit d096058d7d
4789 changed files with 254611 additions and 307223 deletions

View File

@@ -73,7 +73,7 @@ const ActiveAttributeFilters = ( {
const attributeLabel = attributeObject.label;
const filteringForPhpTemplate = getSettingWithCoercion(
'is_rendering_php_template',
'isRenderingPhpTemplate',
false,
isBoolean
);

View File

@@ -1,7 +1,8 @@
/**
* External dependencies
*/
import { useColorProps } from '@woocommerce/base-hooks';
import classnames from 'classnames';
import { useStyleProps } from '@woocommerce/base-hooks';
import { isString } from '@woocommerce/types';
/**
@@ -11,14 +12,18 @@ import Block from './block';
import { parseAttributes } from './utils';
const BlockWrapper = ( props: Record< string, unknown > ) => {
const colorProps = useColorProps( props );
const styleProps = useStyleProps( props );
const parsedBlockAttributes = parseAttributes( props );
return (
<div
className={ isString( props.className ) ? props.className : '' }
style={ { ...colorProps.style } }
className={ classnames(
isString( props.className ) ? props.className : '',
styleProps.className
) }
style={ styleProps.style }
>
<Block isEditor={ false } attributes={ parseAttributes( props ) } />
<Block isEditor={ false } attributes={ parsedBlockAttributes } />
</div>
);
};

View File

@@ -6,7 +6,6 @@ import { useQueryStateByKey } from '@woocommerce/base-context/hooks';
import { getSetting, getSettingWithCoercion } from '@woocommerce/settings';
import { useMemo, useEffect, useState } from '@wordpress/element';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import Label from '@woocommerce/base-components/label';
import {
isAttributeQueryCollection,
@@ -18,6 +17,7 @@ import {
import { getUrlParameter } from '@woocommerce/utils';
import FilterTitlePlaceholder from '@woocommerce/base-components/filter-placeholder';
import { useIsMounted } from '@woocommerce/base-hooks';
import type { BlockAttributes } from '@wordpress/blocks';
/**
* Internal dependencies
@@ -31,31 +31,35 @@ import {
cleanFilterUrl,
maybeUrlContainsFilters,
urlContainsAttributeFilter,
StoreAttributes,
} from './utils';
import ActiveAttributeFilters from './active-attribute-filters';
import FilterPlaceholders from './filter-placeholders';
import { Attributes } from './types';
import { useSetWraperVisibility } from '../filter-wrapper/context';
interface ActiveFiltersBlockProps {
/**
* The attributes for this block.
*/
attributes: BlockAttributes;
/**
* Whether it's in the editor or frontend display.
*/
isEditor: boolean;
}
/**
* Component displaying active filters.
*
* @param {Object} props Incoming props for the component.
* @param {Object} props.attributes Incoming attributes for the block.
* @param {boolean} props.isEditor Whether or not in the editor context.
*/
const ActiveFiltersBlock = ( {
attributes: blockAttributes,
isEditor = false,
}: {
attributes: Attributes;
isEditor?: boolean;
} ) => {
}: ActiveFiltersBlockProps ) => {
const setWrapperVisibility = useSetWraperVisibility();
const isMounted = useIsMounted();
const componentHasMounted = isMounted();
const filteringForPhpTemplate = getSettingWithCoercion(
'is_rendering_php_template',
'isRenderingPhpTemplate',
false,
isBoolean
);
@@ -81,7 +85,7 @@ const ActiveFiltersBlock = ( {
useQueryStateByKey( 'rating' );
const STOCK_STATUS_OPTIONS = getSetting( 'stockStatusOptions', [] );
const STORE_ATTRIBUTES = getSetting( 'attributes', [] );
const STORE_ATTRIBUTES: StoreAttributes[] = getSetting( 'attributes', [] );
const activeStockStatusFilters = useMemo( () => {
if (
shouldShowLoadingPlaceholders ||
@@ -319,7 +323,7 @@ const ActiveFiltersBlock = ( {
);
const hasFilterableProducts = getSettingWithCoercion(
'has_filterable_products',
'hasFilterableProducts',
false,
isBoolean
);
@@ -413,15 +417,4 @@ const ActiveFiltersBlock = ( {
);
};
ActiveFiltersBlock.propTypes = {
/**
* The attributes for this block.
*/
attributes: PropTypes.object.isRequired,
/**
* Whether it's in the editor or frontend display.
*/
isEditor: PropTypes.bool,
};
export default ActiveFiltersBlock;

View File

@@ -0,0 +1,45 @@
/**
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
import classNames from 'classnames';
/**
* Internal dependencies
*/
import { blockAttributes } from './attributes';
import metadata from './block.json';
import { Attributes } from './types';
const v1 = {
attributes: {
...metadata.attributes,
...blockAttributes,
},
save: ( { attributes }: { attributes: Attributes } ) => {
const { className, displayStyle, heading, headingLevel } = attributes;
const data = {
'data-display-style': displayStyle,
'data-heading': heading,
'data-heading-level': headingLevel,
};
return (
<div
{ ...useBlockProps.save( {
className: classNames( 'is-loading', className ),
} ) }
{ ...data }
>
<span
aria-hidden
className="wc-block-active-filters__placeholder"
/>
</div>
);
},
};
const deprecated = [ v1 ];
export default deprecated;

View File

@@ -14,6 +14,7 @@ import edit from './edit';
import metadata from './block.json';
import { blockAttributes } from './attributes';
import { Attributes } from './types';
import deprecated from './deprecated';
registerBlockType( metadata, {
icon: {
@@ -31,19 +32,13 @@ registerBlockType( metadata, {
edit,
// Save the props to post content.
save( { attributes }: { attributes: Attributes } ) {
const { className, displayStyle, heading, headingLevel } = attributes;
const data = {
'data-display-style': displayStyle,
'data-heading': heading,
'data-heading-level': headingLevel,
};
const { className } = attributes;
return (
<div
{ ...useBlockProps.save( {
className: classNames( 'is-loading', className ),
} ) }
{ ...data }
>
<span
aria-hidden
@@ -52,4 +47,5 @@ registerBlockType( metadata, {
</div>
);
},
deprecated,
} );

View File

@@ -160,7 +160,6 @@
height: 16px;
width: 16px;
line-height: 16px;
padding: 0;
margin: 0 0.5em 0 0;
color: currentColor;

View File

@@ -248,7 +248,7 @@ export const maybeUrlContainsFilters = (): boolean => {
return maybeHasFilter;
};
interface StoreAttributes {
export interface StoreAttributes {
attribute_id: string;
attribute_label: string;
attribute_name: string;

View File

@@ -1,7 +1,8 @@
/**
* External dependencies
*/
import { useColorProps } from '@woocommerce/base-hooks';
import classnames from 'classnames';
import { useStyleProps } from '@woocommerce/base-hooks';
import { isString } from '@woocommerce/types';
/**
@@ -11,14 +12,18 @@ import Block from './block';
import { parseAttributes } from './utils';
const BlockWrapper = ( props: Record< string, unknown > ) => {
const colorProps = useColorProps( props );
const styleProps = useStyleProps( props );
const parsedBlockAttributes = parseAttributes( props );
return (
<div
className={ isString( props.className ) ? props.className : '' }
style={ { ...colorProps.style } }
className={ classnames(
isString( props.className ) ? props.className : '',
styleProps.className
) }
style={ styleProps.style }
>
<Block isEditor={ false } attributes={ parseAttributes( props ) } />
<Block isEditor={ false } attributes={ parsedBlockAttributes } />
</div>
);
};

View File

@@ -25,7 +25,7 @@
},
"showCounts": {
"type": "boolean",
"default": true
"default": false
},
"queryType": {
"type": "string",

View File

@@ -2,11 +2,7 @@
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import {
usePrevious,
useShallowEqual,
useBorderProps,
} from '@woocommerce/base-hooks';
import { usePrevious, useShallowEqual } from '@woocommerce/base-hooks';
import {
useCollection,
useQueryStateByKey,
@@ -34,7 +30,6 @@ import {
PREFIX_QUERY_ARG_FILTER_TYPE,
PREFIX_QUERY_ARG_QUERY_TYPE,
} from '@woocommerce/utils';
import { difference } from 'lodash';
import FormTokenField from '@woocommerce/base-components/form-token-field';
import FilterTitlePlaceholder from '@woocommerce/base-components/filter-placeholder';
import classnames from 'classnames';
@@ -77,27 +72,23 @@ const AttributeFilterBlock = ( {
getNotice?: GetNotice;
} ) => {
const hasFilterableProducts = getSettingWithCoercion(
'has_filterable_products',
'hasFilterableProducts',
false,
isBoolean
);
const filteringForPhpTemplate = getSettingWithCoercion(
'is_rendering_php_template',
'isRenderingPhpTemplate',
false,
isBoolean
);
const pageUrl = getSettingWithCoercion(
'page_url',
'pageUrl',
window.location.href,
isString
);
const productIds = isEditor
? []
: getSettingWithCoercion( 'product_ids', [], Array.isArray );
const [ hasSetFilterDefaultsFromUrl, setHasSetFilterDefaultsFromUrl ] =
useState( false );
@@ -128,8 +119,6 @@ const AttributeFilterBlock = ( {
: []
);
const borderProps = useBorderProps( blockAttributes );
const [ queryState ] = useQueryStateByContext();
const [ productAttributesQuery, setProductAttributesQuery ] =
useQueryStateByKey( 'attributes', [] );
@@ -142,9 +131,6 @@ const AttributeFilterBlock = ( {
shouldSelect: blockAttributes.attributeId > 0,
} );
const filterAvailableTerms =
blockAttributes.displayStyle !== 'dropdown' &&
blockAttributes.queryType === 'and';
const { results: filteredCounts, isLoading: filteredCountsLoading } =
useCollectionData( {
queryAttribute: {
@@ -153,9 +139,7 @@ const AttributeFilterBlock = ( {
},
queryState: {
...queryState,
attributes: filterAvailableTerms ? queryState.attributes : null,
},
productIds,
isEditor,
} );
@@ -556,14 +540,10 @@ const AttributeFilterBlock = ( {
<>
<FormTokenField
key={ remountKey }
className={ classnames( borderProps.className, {
className={ classnames( {
'single-selection': ! multiple,
'is-loading': isLoading,
} ) }
style={ {
...borderProps.style,
borderStyle: 'none',
} }
suggestions={ displayedOptions
.filter(
( option ) =>
@@ -595,13 +575,19 @@ const AttributeFilterBlock = ( {
: token;
} );
const added = difference( tokens, checked );
const added = [ tokens, checked ].reduce(
( a, b ) =>
a.filter( ( c ) => ! b.includes( c ) )
);
if ( added.length === 1 ) {
return onChange( added[ 0 ] );
}
const removed = difference( checked, tokens );
const removed = [ checked, tokens ].reduce(
( a, b ) =>
a.filter( ( c ) => ! b.includes( c ) )
);
if ( removed.length === 1 ) {
onChange( removed[ 0 ] );
}

View File

@@ -0,0 +1,80 @@
/**
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
import { isFeaturePluginBuild } from '@woocommerce/block-settings';
import classNames from 'classnames';
/**
* Internal dependencies
*/
import type { BlockAttributes } from './types';
import { blockAttributes } from './attributes';
import metadata from './block.json';
const v1 = {
supports: {
...metadata.supports,
...( isFeaturePluginBuild() && {
__experimentalBorder: {
radius: false,
color: true,
width: false,
},
} ),
},
attributes: {
...metadata.attributes,
showCounts: {
type: 'boolean',
default: true,
},
...blockAttributes,
},
save: ( { attributes }: { attributes: BlockAttributes } ) => {
const {
className,
showCounts,
queryType,
attributeId,
heading,
headingLevel,
displayStyle,
showFilterButton,
selectType,
} = attributes;
const data: Record< string, unknown > = {
'data-attribute-id': attributeId,
'data-show-counts': showCounts,
'data-query-type': queryType,
'data-heading': heading,
'data-heading-level': headingLevel,
};
if ( displayStyle !== 'list' ) {
data[ 'data-display-style' ] = displayStyle;
}
if ( showFilterButton ) {
data[ 'data-show-filter-button' ] = showFilterButton;
}
if ( selectType === 'single' ) {
data[ 'data-select-type' ] = selectType;
}
return (
<div
{ ...useBlockProps.save( {
className: classNames( 'is-loading', className ),
} ) }
{ ...data }
>
<span
aria-hidden
className="wc-block-product-attribute-filter__placeholder"
/>
</div>
);
},
};
const deprecated = [ v1 ];
export default deprecated;

View File

@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { sortBy } from 'lodash';
import { sort } from 'fast-sort';
import { __, sprintf, _n } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import {
@@ -69,7 +69,6 @@ const Edit = ( {
}: EditProps ) => {
const {
attributeId,
className,
displayStyle,
heading,
headingLevel,
@@ -158,15 +157,14 @@ const Edit = ( {
),
};
const list = sortBy(
const list = sort(
ATTRIBUTES.map( ( item ) => {
return {
id: parseInt( item.attribute_id, 10 ),
name: item.attribute_label,
};
} ),
'name'
);
} )
).asc( 'name' );
return (
<SearchListControl
@@ -354,6 +352,7 @@ const Edit = ( {
href={ getAdminLink(
'edit.php?post_type=product&page=product_attributes'
) }
target="_top"
>
{ __( 'Add new attribute', 'woo-gutenberg-products-block' ) +
' ' }
@@ -363,6 +362,7 @@ const Edit = ( {
className="wc-block-attribute-filter__read_more_button"
isTertiary
href="https://docs.woocommerce.com/document/managing-product-taxonomies/"
target="_blank"
>
{ __( 'Learn more', 'woo-gutenberg-products-block' ) }
</Button>
@@ -420,12 +420,7 @@ const Edit = ( {
{ isEditing ? (
renderEditMode()
) : (
<div
className={ classnames(
className,
'wc-block-attribute-filter'
) }
>
<div className={ classnames( 'wc-block-attribute-filter' ) }>
{ heading && (
<BlockTitle
className="wc-block-attribute-filter__title"

View File

@@ -3,7 +3,6 @@
*/
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps } from '@wordpress/block-editor';
import { isFeaturePluginBuild } from '@woocommerce/block-settings';
import { Icon, category } from '@wordpress/icons';
import classNames from 'classnames';
@@ -14,6 +13,7 @@ import edit from './edit';
import type { BlockAttributes } from './types';
import { blockAttributes } from './attributes';
import metadata from './block.json';
import deprecated from './deprecated';
registerBlockType( metadata, {
icon: {
@@ -26,13 +26,6 @@ registerBlockType( metadata, {
},
supports: {
...metadata.supports,
...( isFeaturePluginBuild() && {
__experimentalBorder: {
radius: false,
color: true,
width: false,
},
} ),
},
attributes: {
...metadata.attributes,
@@ -41,45 +34,15 @@ registerBlockType( metadata, {
edit,
// Save the props to post content.
save( { attributes }: { attributes: BlockAttributes } ) {
const {
className,
showCounts,
queryType,
attributeId,
heading,
headingLevel,
displayStyle,
showFilterButton,
selectType,
} = attributes;
const data: Record< string, unknown > = {
'data-attribute-id': attributeId,
'data-show-counts': showCounts,
'data-query-type': queryType,
'data-heading': heading,
'data-heading-level': headingLevel,
};
if ( displayStyle !== 'list' ) {
data[ 'data-display-style' ] = displayStyle;
}
if ( showFilterButton ) {
data[ 'data-show-filter-button' ] = showFilterButton;
}
if ( selectType === 'single' ) {
data[ 'data-select-type' ] = selectType;
}
const { className } = attributes;
return (
<div
{ ...useBlockProps.save( {
className: classNames( 'is-loading', className ),
} ) }
{ ...data }
>
<span
aria-hidden
className="wc-block-product-attribute-filter__placeholder"
/>
</div>
/>
);
},
deprecated,
} );

View File

@@ -127,7 +127,7 @@ export const parseAttributes = ( data: Record< string, unknown > ) => {
isString( data?.attributeId ) ? data.attributeId : '0',
10
),
showCounts: data?.showCounts !== 'false',
showCounts: data?.showCounts === 'true',
queryType:
( isString( data?.queryType ) && data.queryType ) ||
metadata.attributes.queryType.default,

View File

@@ -10,6 +10,7 @@ import { Icon, queryPagination } from '@wordpress/icons';
*/
import metadata from './block.json';
import edit from './edit';
import './style.scss';
const featurePluginSupport = {
...metadata.supports,

View File

@@ -0,0 +1,24 @@
const expressIcon = (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
stroke="#1E1E1E"
strokeLinejoin="round"
strokeWidth="1.5"
d="M18.25 12a6.25 6.25 0 1 1-12.5 0 6.25 6.25 0 0 1 12.5 0Z"
/>
<path fill="#1E1E1E" d="M10 3h4v3h-4z" />
<rect width="1.5" height="5" x="11.25" y="8" fill="#1E1E1E" rx=".75" />
<path
fill="#1E1E1E"
d="m15.7 4.816 1.66 1.078-1.114 1.718-1.661-1.078z"
/>
</svg>
);
export default expressIcon;

View File

@@ -91,7 +91,7 @@ const CheckoutExpressPayment = () => {
headingLevel="2"
>
{ __(
'Express checkout',
'Express Checkout',
'woocommerce'
) }
</Title>

View File

@@ -5,18 +5,13 @@ $border-radius: 5px;
margin: auto;
position: relative;
// nested class to avoid conflict with .editor-styles-wrapper ul
.wc-block-components-express-payment__event-buttons {
list-style: none;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(calc(33% - 10px), 1fr));
grid-gap: 10px;
box-sizing: border-box;
width: 100%;
padding: 0;
margin: 0;
overflow: hidden;
text-align: center;
> li {
margin: 0;
width: 100%;
@@ -27,18 +22,23 @@ $border-radius: 5px;
}
}
}
@include breakpoint("<782px") {
.wc-block-components-express-payment__event-buttons {
grid-template-columns: 1fr;
}
}
}
.wc-block-components-express-payment--checkout {
/* stylelint-disable-next-line function-calc-no-unspaced-operator */
margin-top: calc($border-radius * 3);
.wc-block-components-express-payment__event-buttons {
list-style: none;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(calc(33% - 10px), 1fr));
grid-gap: 10px;
@include breakpoint("<782px") {
grid-template-columns: 1fr;
}
}
.wc-block-components-express-payment__title-container {
display: flex;
flex-direction: row;

View File

@@ -4,7 +4,6 @@
import { __ } from '@wordpress/i18n';
import { useEditorContext } from '@woocommerce/base-context';
import { CheckboxControl } from '@woocommerce/blocks-checkout';
import PropTypes from 'prop-types';
import { useSelect, useDispatch } from '@wordpress/data';
import { CHECKOUT_STORE_KEY, PAYMENT_STORE_KEY } from '@woocommerce/block-data';
@@ -23,7 +22,14 @@ import PaymentMethodErrorBoundary from './payment-method-error-boundary';
*
* @return {*} The rendered component.
*/
const PaymentMethodCard = ( { children, showSaveOption } ) => {
interface PaymentMethodCardProps {
showSaveOption: boolean;
children: React.ReactNode;
}
const PaymentMethodCard = ( {
children,
showSaveOption,
}: PaymentMethodCardProps ) => {
const { isEditor } = useEditorContext();
const { shouldSavePaymentMethod, customerId } = useSelect( ( select ) => {
const paymentMethodStore = select( PAYMENT_STORE_KEY );
@@ -44,7 +50,7 @@ const PaymentMethodCard = ( { children, showSaveOption } ) => {
className="wc-block-components-payment-methods__save-card-info"
label={ __(
'Save payment information to my account for future purchases.',
'woocommerce'
'woo-gutenberg-products-block'
) }
checked={ shouldSavePaymentMethod }
onChange={ () =>
@@ -58,9 +64,4 @@ const PaymentMethodCard = ( { children, showSaveOption } ) => {
);
};
PaymentMethodCard.propTypes = {
showSaveOption: PropTypes.bool,
children: PropTypes.node,
};
export default PaymentMethodCard;

View File

@@ -1,68 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import PropTypes from 'prop-types';
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings';
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
import { noticeContexts } from '@woocommerce/base-context';
class PaymentMethodErrorBoundary extends Component {
state = { errorMessage: '', hasError: false };
static getDerivedStateFromError( error ) {
return {
errorMessage: error.message,
hasError: true,
};
}
render() {
const { hasError, errorMessage } = this.state;
const { isEditor } = this.props;
if ( hasError ) {
let errorText = __(
'We are experiencing difficulties with this payment method. Please contact us for assistance.',
'woocommerce'
);
if ( isEditor || CURRENT_USER_IS_ADMIN ) {
if ( errorMessage ) {
errorText = errorMessage;
} else {
errorText = __(
"There was an error with this payment method. Please verify it's configured correctly.",
'woocommerce'
);
}
}
const notices = [
{
id: '0',
content: errorText,
isDismissible: false,
status: 'error',
},
];
return (
<StoreNoticesContainer
additionalNotices={ notices }
context={ noticeContexts.PAYMENTS }
/>
);
}
return this.props.children;
}
}
PaymentMethodErrorBoundary.propTypes = {
isEditor: PropTypes.bool,
};
PaymentMethodErrorBoundary.defaultProps = {
isEditor: false,
};
export default PaymentMethodErrorBoundary;

View File

@@ -0,0 +1,52 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings';
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
import { noticeContexts } from '@woocommerce/base-context';
import { NoticeType } from '@woocommerce/types';
interface PaymentMethodErrorBoundaryProps {
isEditor: boolean;
children: React.ReactNode;
}
const PaymentMethodErrorBoundary = ( {
isEditor,
children,
}: PaymentMethodErrorBoundaryProps ) => {
const [ errorMessage ] = useState( '' );
const [ hasError ] = useState( false );
if ( hasError ) {
let errorText = __(
'We are experiencing difficulties with this payment method. Please contact us for assistance.',
'woo-gutenberg-products-block'
);
if ( isEditor || CURRENT_USER_IS_ADMIN ) {
if ( errorMessage ) {
errorText = errorMessage;
} else {
errorText = __(
"There was an error with this payment method. Please verify it's configured correctly.",
'woo-gutenberg-products-block'
);
}
}
const notices: NoticeType[] = [
{
id: '0',
content: errorText,
isDismissible: false,
status: 'error',
},
];
return (
<StoreNoticesContainer
additionalNotices={ notices }
context={ noticeContexts.PAYMENTS }
/>
);
}
return <>{ children }</>;
};
export default PaymentMethodErrorBoundary;

View File

@@ -12,6 +12,7 @@ import { PAYMENT_STORE_KEY } from '@woocommerce/block-data';
import NoPaymentMethods from './no-payment-methods';
import PaymentMethodOptions from './payment-method-options';
import SavedPaymentMethodOptions from './saved-payment-method-options';
import './style.scss';
/**
* PaymentMethods component.

View File

@@ -1,151 +0,0 @@
/**
* External dependencies
*/
import { useMemo, cloneElement } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { noticeContexts } from '@woocommerce/base-context';
import RadioControl from '@woocommerce/base-components/radio-control';
import {
usePaymentMethodInterface,
useStoreEvents,
} from '@woocommerce/base-context/hooks';
import { PAYMENT_STORE_KEY } from '@woocommerce/block-data';
import { useDispatch, useSelect } from '@wordpress/data';
import { getPaymentMethods } from '@woocommerce/blocks-registry';
/**
* @typedef {import('@woocommerce/type-defs/contexts').CustomerPaymentMethod} CustomerPaymentMethod
* @typedef {import('@woocommerce/type-defs/contexts').PaymentStatusDispatch} PaymentStatusDispatch
*/
/**
* Returns the option object for a cc or echeck saved payment method token.
*
* @param {CustomerPaymentMethod} savedPaymentMethod
* @return {string} label
*/
const getCcOrEcheckLabel = ( { method, expires } ) => {
return sprintf(
/* translators: %1$s is referring to the payment method brand, %2$s is referring to the last 4 digits of the payment card, %3$s is referring to the expiry date. */
__(
'%1$s ending in %2$s (expires %3$s)',
'woocommerce'
),
method.brand,
method.last4,
expires
);
};
/**
* Returns the option object for any non specific saved payment method.
*
* @param {CustomerPaymentMethod} savedPaymentMethod
* @return {string} label
*/
const getDefaultLabel = ( { method } ) => {
/* For saved payment methods with brand & last 4 */
if ( method.brand && method.last4 ) {
return sprintf(
/* translators: %1$s is referring to the payment method brand, %2$s is referring to the last 4 digits of the payment card. */
__( '%1$s ending in %2$s', 'woocommerce' ),
method.brand,
method.last4
);
}
/* For saved payment methods without brand & last 4 */
return sprintf(
/* translators: %s is the name of the payment method gateway. */
__( 'Saved token for %s', 'woocommerce' ),
method.gateway
);
};
const SavedPaymentMethodOptions = () => {
const { activeSavedToken, activePaymentMethod, savedPaymentMethods } =
useSelect( ( select ) => {
const store = select( PAYMENT_STORE_KEY );
return {
activeSavedToken: store.getActiveSavedToken(),
activePaymentMethod: store.getActivePaymentMethod(),
savedPaymentMethods: store.getSavedPaymentMethods(),
};
} );
const { __internalSetActivePaymentMethod } =
useDispatch( PAYMENT_STORE_KEY );
const paymentMethods = getPaymentMethods();
const paymentMethodInterface = usePaymentMethodInterface();
const { removeNotice } = useDispatch( 'core/notices' );
const { dispatchCheckoutEvent } = useStoreEvents();
const options = useMemo( () => {
const types = Object.keys( savedPaymentMethods );
return types
.flatMap( ( type ) => {
const typeMethods = savedPaymentMethods[ type ];
return typeMethods.map( ( paymentMethod ) => {
const isCC = type === 'cc' || type === 'echeck';
const paymentMethodSlug = paymentMethod.method.gateway;
return {
name: `wc-saved-payment-method-token-${ paymentMethodSlug }`,
label: isCC
? getCcOrEcheckLabel( paymentMethod )
: getDefaultLabel( paymentMethod ),
value: paymentMethod.tokenId.toString(),
onChange: ( token ) => {
const savedTokenKey = `wc-${ paymentMethodSlug }-payment-token`;
__internalSetActivePaymentMethod(
paymentMethodSlug,
{
token,
payment_method: paymentMethodSlug,
[ savedTokenKey ]: token.toString(),
isSavedToken: true,
}
);
removeNotice(
'wc-payment-error',
noticeContexts.PAYMENTS
);
dispatchCheckoutEvent(
'set-active-payment-method',
{
paymentMethodSlug,
}
);
},
};
} );
} )
.filter( Boolean );
}, [
savedPaymentMethods,
__internalSetActivePaymentMethod,
removeNotice,
dispatchCheckoutEvent,
] );
const savedPaymentMethodHandler =
!! activeSavedToken &&
paymentMethods[ activePaymentMethod ] &&
paymentMethods[ activePaymentMethod ]?.savedTokenComponent
? cloneElement(
paymentMethods[ activePaymentMethod ]?.savedTokenComponent,
{ token: activeSavedToken, ...paymentMethodInterface }
)
: null;
return options.length > 0 ? (
<>
<RadioControl
id={ 'wc-payment-method-saved-tokens' }
selected={ activeSavedToken }
options={ options }
onChange={ () => void 0 }
/>
{ savedPaymentMethodHandler }
</>
) : null;
};
export default SavedPaymentMethodOptions;

View File

@@ -0,0 +1,184 @@
/**
* External dependencies
*/
import { useMemo, cloneElement } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { noticeContexts } from '@woocommerce/base-context';
import RadioControl from '@woocommerce/base-components/radio-control';
import {
usePaymentMethodInterface,
useStoreEvents,
} from '@woocommerce/base-context/hooks';
import { PAYMENT_STORE_KEY } from '@woocommerce/block-data';
import { useDispatch, useSelect } from '@wordpress/data';
import { getPaymentMethods } from '@woocommerce/blocks-registry';
import { isNull } from '@woocommerce/types';
import { RadioControlOption } from '@woocommerce/base-components/radio-control/types';
/**
* Internal dependencies
*/
import { getCanMakePaymentArg } from '../../../data/payment/utils/check-payment-methods';
import { CustomerPaymentMethodConfiguration } from '../../../data/payment/types';
/**
* Returns the option object for a cc or echeck saved payment method token.
*/
const getCcOrEcheckLabel = ( {
method,
expires,
}: {
method: CustomerPaymentMethodConfiguration;
expires: string;
} ): string => {
return sprintf(
/* translators: %1$s is referring to the payment method brand, %2$s is referring to the last 4 digits of the payment card, %3$s is referring to the expiry date. */
__(
'%1$s ending in %2$s (expires %3$s)',
'woo-gutenberg-products-block'
),
method.brand,
method.last4,
expires
);
};
/**
* Returns the option object for any non specific saved payment method.
*/
const getDefaultLabel = ( {
method,
}: {
method: CustomerPaymentMethodConfiguration;
} ): string => {
/* For saved payment methods with brand & last 4 */
if ( method.brand && method.last4 ) {
return sprintf(
/* translators: %1$s is referring to the payment method brand, %2$s is referring to the last 4 digits of the payment card. */
__( '%1$s ending in %2$s', 'woo-gutenberg-products-block' ),
method.brand,
method.last4
);
}
/* For saved payment methods without brand & last 4 */
return sprintf(
/* translators: %s is the name of the payment method gateway. */
__( 'Saved token for %s', 'woo-gutenberg-products-block' ),
method.gateway
);
};
const SavedPaymentMethodOptions = () => {
const { activeSavedToken, activePaymentMethod, savedPaymentMethods } =
useSelect( ( select ) => {
const store = select( PAYMENT_STORE_KEY );
return {
activeSavedToken: store.getActiveSavedToken(),
activePaymentMethod: store.getActivePaymentMethod(),
savedPaymentMethods: store.getSavedPaymentMethods(),
};
} );
const { __internalSetActivePaymentMethod } =
useDispatch( PAYMENT_STORE_KEY );
const canMakePaymentArg = getCanMakePaymentArg();
const paymentMethods = getPaymentMethods();
const paymentMethodInterface = usePaymentMethodInterface();
const { removeNotice } = useDispatch( 'core/notices' );
const { dispatchCheckoutEvent } = useStoreEvents();
const options = useMemo< RadioControlOption[] >( () => {
const types = Object.keys( savedPaymentMethods );
// Get individual payment methods from saved payment methods and put them into a unique array.
const individualPaymentGateways = new Set(
types.flatMap( ( type ) =>
savedPaymentMethods[ type ].map(
( paymentMethod ) => paymentMethod.method.gateway
)
)
);
const gatewaysThatCanMakePayment = Array.from(
individualPaymentGateways
).filter( ( method ) => {
return paymentMethods[ method ]?.canMakePayment(
canMakePaymentArg
);
} );
const mappedOptions = types.flatMap( ( type ) => {
const typeMethods = savedPaymentMethods[ type ];
return typeMethods.map( ( paymentMethod ) => {
const canMakePayment = gatewaysThatCanMakePayment.includes(
paymentMethod.method.gateway
);
if ( ! canMakePayment ) {
return void 0;
}
const isCC = type === 'cc' || type === 'echeck';
const paymentMethodSlug = paymentMethod.method.gateway;
return {
name: `wc-saved-payment-method-token-${ paymentMethodSlug }`,
label: isCC
? getCcOrEcheckLabel( paymentMethod )
: getDefaultLabel( paymentMethod ),
value: paymentMethod.tokenId.toString(),
onChange: ( token: string ) => {
const savedTokenKey = `wc-${ paymentMethodSlug }-payment-token`;
__internalSetActivePaymentMethod( paymentMethodSlug, {
token,
payment_method: paymentMethodSlug,
[ savedTokenKey ]: token.toString(),
isSavedToken: true,
} );
removeNotice(
'wc-payment-error',
noticeContexts.PAYMENTS
);
dispatchCheckoutEvent( 'set-active-payment-method', {
paymentMethodSlug,
} );
},
};
} );
} );
return mappedOptions.filter(
( option ) => typeof option !== 'undefined'
) as RadioControlOption[];
}, [
savedPaymentMethods,
paymentMethods,
__internalSetActivePaymentMethod,
removeNotice,
dispatchCheckoutEvent,
canMakePaymentArg,
] );
const savedPaymentMethodHandler =
!! activeSavedToken &&
paymentMethods[ activePaymentMethod ] &&
typeof paymentMethods[ activePaymentMethod ]?.savedTokenComponent !==
'undefined' &&
! isNull( paymentMethods[ activePaymentMethod ].savedTokenComponent )
? cloneElement(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - we know for sure that the savedTokenComponent is not null or undefined at this point.
paymentMethods[ activePaymentMethod ].savedTokenComponent,
{ token: activeSavedToken, ...paymentMethodInterface }
)
: null;
return options.length > 0 ? (
<>
<RadioControl
id={ 'wc-payment-method-saved-tokens' }
selected={ activeSavedToken }
options={ options }
onChange={ () => void 0 }
/>
{ savedPaymentMethodHandler }
</>
) : null;
};
export default SavedPaymentMethodOptions;

View File

@@ -59,6 +59,7 @@
}
label {
@include reset-color();
@include reset-typography();
@include font-size(regular);
line-height: 1.375; // =22px when font-size is 16px.

View File

@@ -0,0 +1,138 @@
/**
* External dependencies
*/
import { render, screen } from '@testing-library/react';
import { registerPaymentMethod } from '@woocommerce/blocks-registry';
import * as wpData from '@wordpress/data';
/**
* Internal dependencies
*/
import SavedPaymentMethodOptions from '../saved-payment-method-options';
jest.mock( '@wordpress/data', () => ( {
__esModule: true,
...jest.requireActual( '@wordpress/data' ),
useSelect: jest.fn(),
} ) );
const mockedUseSelect = wpData.useSelect as jest.Mock;
// Mock use select so we can override it when wc/store/checkout is accessed, but return the original select function if any other store is accessed.
mockedUseSelect.mockImplementation(
jest.fn().mockImplementation( ( passedMapSelect ) => {
const mockedSelect = jest.fn().mockImplementation( ( storeName ) => {
if ( storeName === 'wc/store/payment' ) {
return {
...jest
.requireActual( '@wordpress/data' )
.select( storeName ),
getActiveSavedToken: () => 1,
getSavedPaymentMethods: () => {
return {
cc: [
{
tokenId: 1,
expires: '1/2099',
method: {
brand: 'Visa',
gateway:
'can-pay-true-test-payment-method',
last4: '1234',
},
},
{
tokenId: 2,
expires: '1/2099',
method: {
brand: 'Visa',
gateway:
'can-pay-true-test-payment-method',
last4: '2345',
},
},
{
tokenId: 3,
expires: '1/2099',
method: {
brand: 'Visa',
gateway:
'can-pay-true-first-false-second-test-payment-method',
last4: '3456',
},
},
],
};
},
};
}
return jest.requireActual( '@wordpress/data' ).select( storeName );
} );
return passedMapSelect( mockedSelect, {
dispatch: jest.requireActual( '@wordpress/data' ).dispatch,
} );
} )
);
describe( 'SavedPaymentMethodOptions', () => {
it( 'renders saved methods when a registered method exists', () => {
registerPaymentMethod( {
name: 'can-pay-true-test-payment-method',
label: 'Can Pay True Test Payment Method',
edit: <div>edit</div>,
ariaLabel: 'Can Pay True Test Payment Method',
canMakePayment: () => true,
content: <div>content</div>,
supports: {
showSavedCards: true,
showSaveOption: true,
features: [ 'products' ],
},
} );
render( <SavedPaymentMethodOptions /> );
// First saved token for can-pay-true-test-payment-method.
expect(
screen.getByText( 'Visa ending in 1234 (expires 1/2099)' )
).toBeInTheDocument();
// Second saved token for can-pay-true-test-payment-method.
expect(
screen.getByText( 'Visa ending in 2345 (expires 1/2099)' )
).toBeInTheDocument();
// Third saved token for can-pay-false-test-payment-method - this should not show because the method is not registered.
expect(
screen.queryByText( 'Visa ending in 3456 (expires 1/2099)' )
).not.toBeInTheDocument();
} );
it( "does not show saved methods when the method's canPay function returns false", () => {
registerPaymentMethod( {
name: 'can-pay-true-first-false-second-test-payment-method',
label: 'Can Pay True First False Second Test Payment Method',
edit: <div>edit</div>,
ariaLabel: 'Can Pay True First False Second Test Payment Method',
// This mock will return true the first time it runs, then false on subsequent calls.
canMakePayment: jest
.fn()
.mockReturnValueOnce( true )
.mockReturnValue( false ),
content: <div>content</div>,
supports: {
showSavedCards: true,
showSaveOption: true,
features: [ 'products' ],
},
} );
const { rerender } = render( <SavedPaymentMethodOptions /> );
// Saved token for can-pay-true-first-false-second-test-payment-method - this should show because canPay is true on first call.
expect(
screen.queryByText( 'Visa ending in 3456 (expires 1/2099)' )
).toBeInTheDocument();
rerender( <SavedPaymentMethodOptions /> );
// Saved token for can-pay-true-first-false-second-test-payment-method - this should not show because canPay is false on subsequent calls.
expect(
screen.queryByText( 'Visa ending in 3456 (expires 1/2099)' )
).not.toBeInTheDocument();
} );
} );

View File

@@ -12,10 +12,13 @@ import { CartCheckoutSidebarCompatibilityNotice } from '@woocommerce/editor-comp
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 { TemplateNotice } from '@woocommerce/editor-components/template-notice';
import { IncompatiblePaymentGatewaysNotice } from '@woocommerce/editor-components/incompatible-payment-gateways-notice';
import { useSelect } from '@wordpress/data';
import { CartCheckoutFeedbackPrompt } from '@woocommerce/editor-components/feedback-prompt';
import { useState } from '@wordpress/element';
import { getSetting } from '@woocommerce/settings';
declare module '@wordpress/editor' {
let store: StoreDescriptor;
}
@@ -36,6 +39,8 @@ const withSidebarNotices = createHigherOrderComponent(
isSelected: isBlockSelected,
} = props;
const isBlockTheme = getSetting( 'isBlockTheme' );
const [
isIncompatiblePaymentGatewaysNoticeDismissed,
setIsIncompatiblePaymentGatewaysNoticeDismissed,
@@ -101,15 +106,20 @@ const withSidebarNotices = createHigherOrderComponent(
}
/>
{ isBlockTheme ? (
<TemplateNotice
block={ isCheckout ? 'checkout' : 'cart' }
/>
) : (
<DefaultNotice
block={ isCheckout ? 'checkout' : 'cart' }
/>
) }
{ isIncompatiblePaymentGatewaysNoticeDismissed ? (
<>
<DefaultNotice
block={ isCheckout ? 'checkout' : 'cart' }
/>
<CartCheckoutSidebarCompatibilityNotice
block={ isCheckout ? 'checkout' : 'cart' }
/>
</>
<CartCheckoutSidebarCompatibilityNotice
block={ isCheckout ? 'checkout' : 'cart' }
/>
) : null }
{ isPaymentMethodsBlock && ! hasPaymentMethods && (

View File

@@ -5,11 +5,15 @@ import { __ } from '@wordpress/i18n';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import { useEffect } from '@wordpress/element';
import LoadingMask from '@woocommerce/base-components/loading-mask';
import { CartProvider, noticeContexts } from '@woocommerce/base-context';
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings';
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
import { translateJQueryEventToNative } from '@woocommerce/base-utils';
import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top';
import {
CartEventsProvider,
CartProvider,
noticeContexts,
} from '@woocommerce/base-context';
import {
SlotFillProvider,
StoreNoticesContainer,
@@ -85,8 +89,10 @@ const Block = ( { attributes, children, scrollToTop } ) => (
<StoreNoticesContainer context={ noticeContexts.CART } />
<SlotFillProvider>
<CartProvider>
<Cart attributes={ attributes }>{ children }</Cart>
<ScrollOnError scrollToTop={ scrollToTop } />
<CartEventsProvider>
<Cart attributes={ attributes }>{ children }</Cart>
<ScrollOnError scrollToTop={ scrollToTop } />
</CartEventsProvider>
</CartProvider>
</SlotFillProvider>
</BlockErrorBoundary>

View File

@@ -16,7 +16,7 @@ import { Block as ProductRating } from '../../../atomic/blocks/product-elements/
import { Block as ProductSaleBadge } from '../../../atomic/blocks/product-elements/sale-badge/block';
import { Block as ProductPrice } from '../../../atomic/blocks/product-elements/price/block';
import { Block as ProductButton } from '../../../atomic/blocks/product-elements/button/block';
import AddToCartButton from '../../../atomic/blocks/product-elements/add-to-cart/block';
import { ImageSizing } from '../../../atomic/blocks/product-elements/image/types';
interface CrossSellsProductProps {
product: ProductResponseItem;
@@ -44,7 +44,7 @@ const CartCrossSellsProduct = ( {
productId={ product.id }
showProductLink={ false }
saleBadgeAlign={ 'left' }
imageSizing={ 'full-size' }
imageSizing={ ImageSizing.SINGLE }
isDescendentOfQueryLoop={ false }
/>
<ProductName
@@ -59,11 +59,7 @@ const CartCrossSellsProduct = ( {
/>
<ProductPrice />
</div>
{ product.is_in_stock ? (
<AddToCartButton />
) : (
<ProductButton />
) }
<ProductButton />
</ProductDataContextProvider>
</InnerBlockLayoutContextProvider>
</div>

View File

@@ -42,8 +42,8 @@ export const Edit = ( { attributes, setAttributes }: Props ): JSX.Element => {
onChange={ ( value ) =>
setAttributes( { columns: value } )
}
min={ getSetting( 'min_columns', 1 ) }
max={ getSetting( 'max_columns', 6 ) }
min={ getSetting( 'minColumns', 1 ) }
max={ getSetting( 'maxColumns', 6 ) }
/>
</PanelBody>
</InspectorControls>

View File

@@ -8,6 +8,7 @@ import { registerBlockType } from '@wordpress/blocks';
* Internal dependencies
*/
import { Edit, Save } from './edit';
import './style.scss';
registerBlockType( 'woocommerce/cart-cross-sells-products-block', {
icon: {

View File

@@ -2,7 +2,7 @@
"name": "woocommerce/cart-express-payment-block",
"version": "1.0.0",
"title": "Express Checkout",
"description": "Provide an express payment option for your customers.",
"description": "Allow customers to breeze through with quick payment options.",
"category": "woocommerce",
"supports": {
"align": false,

View File

@@ -13,6 +13,11 @@
}
}
.wp-block-woocommerce-proceed-to-checkout-block {
margin-bottom: 28px;
margin-top: 28px;
}
.wp-block-woocommerce-checkout-express-payment-block-placeholder {
* {
pointer-events: all; // Overrides parent disabled component in editor context

View File

@@ -1,19 +1,21 @@
/**
* External dependencies
*/
import { Icon, payment } from '@wordpress/icons';
import { Icon } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import expressIcon from '../../../cart-checkout-shared/icon';
registerBlockType( 'woocommerce/cart-express-payment-block', {
icon: {
src: (
<Icon
icon={ payment }
style={ { fill: 'none' } } // this is needed for this particular svg
icon={ expressIcon }
className="wc-block-editor-components-block-icon"
/>
),

View File

@@ -1,7 +1,6 @@
/**
* External dependencies
*/
import Title from '@woocommerce/base-components/title';
import classnames from 'classnames';
const Block = ( {
@@ -12,12 +11,11 @@ const Block = ( {
content: string;
} ): JSX.Element => {
return (
<Title
headingLevel="2"
<span
className={ classnames( className, 'wc-block-cart__totals-title' ) }
>
{ content }
</Title>
</span>
);
};

View File

@@ -2,7 +2,6 @@
* External dependencies
*/
import { PlainText, useBlockProps } from '@wordpress/block-editor';
import Title from '@woocommerce/base-components/title';
import classnames from 'classnames';
/**
@@ -24,8 +23,7 @@ export const Edit = ( {
const blockProps = useBlockProps();
return (
<div { ...blockProps }>
<Title
headingLevel="2"
<span
className={ classnames(
className,
'wc-block-cart__totals-title'
@@ -39,7 +37,7 @@ export const Edit = ( {
}
style={ { backgroundColor: 'transparent' } }
/>
</Title>
</span>
</div>
);
};

View File

@@ -9,7 +9,6 @@ import type { TemplateArray } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import './style.scss';
import {
useForcedLayout,
getAllowedBlocks,

View File

@@ -8,6 +8,7 @@ import { registerBlockType } from '@wordpress/blocks';
* Internal dependencies
*/
import { Edit, Save } from './edit';
import './style.scss';
registerBlockType( 'woocommerce/cart-totals-block', {
icon: {

View File

@@ -15,7 +15,6 @@ import {
useForcedLayout,
getAllowedBlocks,
} from '../../../cart-checkout-shared';
import './style.scss';
const browseStoreTemplate = SHOP_URL
? [

View File

@@ -9,6 +9,7 @@ import { registerBlockType } from '@wordpress/blocks';
* Internal dependencies
*/
import { Edit, Save } from './edit';
import './style.scss';
registerBlockType( 'woocommerce/empty-cart-block', {
icon: {

View File

@@ -2,7 +2,7 @@
* External dependencies
*/
import classnames from 'classnames';
import { useState, useEffect } from '@wordpress/element';
import { useState, useEffect, useMemo } from '@wordpress/element';
import Button from '@woocommerce/base-components/button';
import { CHECKOUT_URL } from '@woocommerce/block-settings';
import { usePositionRelativeToViewport } from '@woocommerce/base-hooks';
@@ -10,11 +10,12 @@ import { getSetting } from '@woocommerce/settings';
import { useSelect } from '@wordpress/data';
import { CART_STORE_KEY, CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
import { applyCheckoutFilter } from '@woocommerce/blocks-checkout';
import { isErrorResponse } from '@woocommerce/base-context';
import { useCartEventsContext } from '@woocommerce/base-context/providers';
/**
* Internal dependencies
*/
import './style.scss';
import { defaultButtonLabel } from './constants';
/**
@@ -74,18 +75,34 @@ const Block = ( {
arg: { cart },
} );
const { dispatchOnProceedToCheckout } = useCartEventsContext();
const submitContainerContents = (
<Button
className="wc-block-cart__submit-button"
href={ filteredLink }
disabled={ isCalculating }
onClick={ () => setShowSpinner( true ) }
onClick={ ( e ) => {
dispatchOnProceedToCheckout().then( ( observerResponses ) => {
if ( observerResponses.some( isErrorResponse ) ) {
e.preventDefault();
return;
}
setShowSpinner( true );
} );
} }
showSpinner={ showSpinner }
>
{ label }
</Button>
);
// Get the body background color to use as the sticky container background color.
const backgroundColor = useMemo(
() => getComputedStyle( document.body ).backgroundColor,
[]
);
return (
<div className={ classnames( 'wc-block-cart__submit', className ) }>
{ positionReferenceElement }
@@ -95,7 +112,10 @@ const Block = ( {
</div>
{ /* If the positionReferenceElement is below the viewport, display the sticky container. */ }
{ positionRelativeToViewport === 'below' && (
<div className="wc-block-cart__submit-container wc-block-cart__submit-container--sticky">
<div
className="wc-block-cart__submit-container wc-block-cart__submit-container--sticky"
style={ { backgroundColor } }
>
{ submitContainerContents }
</div>
) }

View File

@@ -9,6 +9,7 @@ import { registerBlockType } from '@wordpress/blocks';
*/
import attributes from './attributes';
import { Edit, Save } from './edit';
import './style.scss';
registerBlockType( 'woocommerce/proceed-to-checkout-block', {
icon: {

View File

@@ -30,10 +30,11 @@
position: fixed;
width: 100%;
z-index: 9999;
box-sizing: border-box;
&::before {
box-shadow: 0 -10px 20px 10px currentColor;
color: color.adjust($gray-400, $alpha: -0.5);
color: color.adjust($gray-400, $alpha: -0.7);
content: "";
height: 100%;
left: 0;
@@ -43,4 +44,3 @@
}
}
}

View File

@@ -1,13 +1,17 @@
/**
* External dependencies
*/
import { render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import { registerCheckoutFilters } from '@woocommerce/blocks-checkout';
import { useCartEventsContext } from '@woocommerce/base-context';
import { useEffect } from '@wordpress/element';
/**
* Internal dependencies
*/
import Block from '../block';
import { CartEventsProvider } from '../../../../../base/context/providers';
describe( 'Proceed to checkout block', () => {
it( 'allows the text to be filtered', () => {
@@ -49,4 +53,37 @@ describe( 'Proceed to checkout block', () => {
//@todo When https://github.com/WordPress/gutenberg/issues/22850 is complete use that new matcher here for more specific error message assertion.
expect( console ).toHaveErrored();
} );
it( 'dispatches the onProceedToCheckout event when the button is clicked', async () => {
const mockObserver = jest.fn().mockReturnValue( { type: 'error' } );
const MockObserverComponent = () => {
const { onProceedToCheckout } = useCartEventsContext();
useEffect( () => {
return onProceedToCheckout( mockObserver );
}, [ onProceedToCheckout ] );
return <div>Mock observer</div>;
};
render(
<CartEventsProvider>
<div>
<MockObserverComponent />
<Block
buttonLabel={ 'Proceed to Checkout' }
checkoutPageId={ 0 }
className="test-block"
/>
</div>
</CartEventsProvider>
);
expect( screen.getByText( 'Mock observer' ) ).toBeInTheDocument();
const button = screen.getByText( 'Proceed to Checkout' );
// Forcibly set the button URL to # to prevent JSDOM error: `["Error: Not implemented: navigation (except hash changes)`
button.parentElement?.removeAttribute( 'href' );
button.click();
await waitFor( () => {
expect( mockObserver ).toHaveBeenCalled();
} );
} );
} );

View File

@@ -12,77 +12,17 @@
}
}
}
}
table.wc-block-cart-items,
table.wc-block-cart-items th,
table.wc-block-cart-items td {
// Override Storefront theme gray table background.
background: none !important;
// Remove borders on default themes.
border: 0;
margin: 0 0 2em;
}
.editor-styles-wrapper table.wc-block-cart-items,
table.wc-block-cart-items {
width: 100%;
.wc-block-cart-items__header {
@include font-size( smaller );
text-transform: uppercase;
.wc-block-cart-items__header-image {
width: 100px;
}
.wc-block-cart-items__header-product {
visibility: hidden;
}
.wc-block-cart-items__header-total {
width: 100px;
text-align: right;
}
}
.wc-block-cart-items__row {
.wc-block-cart-item__image img {
width: 100%;
margin: 0;
}
.wc-block-cart-item__quantity {
.wc-block-cart-item__remove-link {
@include link-button;
@include font-size( smaller );
text-transform: none;
white-space: nowrap;
}
}
.wc-block-components-product-name {
display: block;
max-width: max-content;
}
.wc-block-cart-item__total {
@include font-size( regular );
text-align: right;
line-height: inherit;
}
.wc-block-components-product-metadata {
margin-bottom: 0.75em;
}
&.is-disabled {
opacity: 0.5;
pointer-events: none;
transition: opacity 200ms ease;
}
}
}
.wc-block-cart {
.wc-block-components-totals-taxes,
.wc-block-components-totals-footer-item {
margin: 0;
}
table.wc-block-cart-items,
table.wc-block-cart-items th,
table.wc-block-cart-items td {
margin: 0 0 2em;
}
}
// Loading placeholder state.
@@ -168,80 +108,11 @@ table.wc-block-cart-items {
}
}
}
table.wc-block-cart-items {
td {
padding: 0;
}
.wc-block-cart-items__header {
display: none;
}
.wc-block-cart-item__remove-link {
display: none;
}
&:not(.wc-block-mini-cart-items) {
.wc-block-cart-items__row {
@include with-translucent-border( 0 0 1px );
}
}
.wc-block-cart-items__row {
display: grid;
grid-template-columns: 80px 132px;
padding: $gap 0;
.wc-block-cart-item__image {
grid-column-start: 1;
grid-row-start: 1;
padding-right: $gap;
}
.wc-block-cart-item__product {
grid-column-start: 2;
grid-column-end: 4;
grid-row-start: 1;
justify-self: stretch;
padding: 0 $gap $gap 0;
}
.wc-block-cart-item__quantity {
grid-column-start: 1;
grid-row-start: 2;
vertical-align: bottom;
padding-right: $gap;
align-self: end;
padding-top: $gap;
}
.wc-block-cart-item__total {
grid-row-start: 1;
.wc-block-components-formatted-money-amount {
display: inline-block;
}
}
}
}
}
.is-large.wc-block-cart {
margin-bottom: 3em;
.wc-block-cart-items {
@include with-translucent-border( 0 0 1px );
th {
padding: 0.25rem $gap 0.25rem 0;
white-space: nowrap;
}
td {
@include with-translucent-border( 1px 0 0 );
padding: $gap 0 $gap $gap;
vertical-align: top;
}
th:last-child {
padding-right: 0;
}
td:last-child {
padding-right: $gap;
}
}
.wc-block-components-radio-control__input {
left: 0;
margin: 0;

View File

@@ -10,6 +10,7 @@ import { totals } from '@woocommerce/icons';
*/
import metadata from './block.json';
import edit from './edit';
import './style.scss';
registerBlockType( metadata, {
icon: {

View File

@@ -4,7 +4,10 @@
import { __ } from '@wordpress/i18n';
import classnames from 'classnames';
import { createInterpolateElement, useEffect } from '@wordpress/element';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import {
useStoreCart,
useShowShippingTotalWarning,
} from '@woocommerce/base-context/hooks';
import { CheckoutProvider, noticeContexts } from '@woocommerce/base-context';
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
import { SidebarLayout } from '@woocommerce/base-components/sidebar-layout';
@@ -161,6 +164,7 @@ const Block = ( {
children: React.ReactChildren;
scrollToTop: ( props: Record< string, unknown > ) => void;
} ): JSX.Element => {
useShowShippingTotalWarning();
return (
<BlockErrorBoundary
header={ __(

View File

@@ -12,4 +12,8 @@
.wc-block-components-checkout-step__title {
display: flex;
width: 100%;
.block-editor-plain-text {
height: auto !important;
}
}

View File

@@ -17,8 +17,8 @@ import {
/**
* Internal dependencies
*/
import './style.scss';
import { defaultPlaceOrderButtonLabel } from './constants';
import './style.scss';
const Block = ( {
cartPageId,
@@ -53,7 +53,10 @@ const Block = ( {
link={ getSetting( 'page-' + cartPageId, false ) }
/>
) }
<PlaceOrderButton label={ label } />
<PlaceOrderButton
label={ label }
fullWidth={ ! showReturnToCart }
/>
</div>
</div>
);

View File

@@ -1,6 +1,7 @@
/**
* External dependencies
*/
import classnames from 'classnames';
import { useRef } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
@@ -93,23 +94,35 @@ export const Edit = ( {
) }
</InspectorControls>
<div className="wc-block-checkout__actions">
<Noninteractive>
{ showReturnToCart && (
<ReturnToCartButton
link={ getSetting( 'page-' + cartPageId, false ) }
/>
) }
</Noninteractive>
<EditableButton
className="wc-block-cart__submit-button wc-block-components-checkout-place-order-button"
value={ placeOrderButtonLabel }
placeholder={ defaultPlaceOrderButtonLabel }
onChange={ ( content ) => {
setAttributes( {
placeOrderButtonLabel: content,
} );
} }
/>
<div className="wc-block-checkout__actions_row">
<Noninteractive>
{ showReturnToCart && (
<ReturnToCartButton
link={ getSetting(
'page-' + cartPageId,
false
) }
/>
) }
</Noninteractive>
<EditableButton
className={ classnames(
'wc-block-cart__submit-button',
'wc-block-components-checkout-place-order-button',
{
'wc-block-components-checkout-place-order-button--full-width':
! showReturnToCart,
}
) }
value={ placeOrderButtonLabel }
placeholder={ defaultPlaceOrderButtonLabel }
onChange={ ( content ) => {
setAttributes( {
placeOrderButtonLabel: content,
} );
} }
/>
</div>
</div>
</div>
);

View File

@@ -10,6 +10,7 @@ import type { BlockConfiguration } from '@wordpress/blocks';
*/
import attributes from './attributes';
import { Edit, Save } from './edit';
import './style.scss';
const blockConfig: BlockConfiguration = {
icon: {

View File

@@ -9,9 +9,11 @@
padding: 1em;
height: auto;
.wc-block-components-button__text {
line-height: 24px;
&--full-width {
width: 100%;
}
.wc-block-components-button__text {
> svg {
fill: $white;
vertical-align: top;

View File

@@ -1,7 +1,13 @@
/**
* External dependencies
*/
import { useMemo, useEffect, Fragment, useState } from '@wordpress/element';
import {
useMemo,
useEffect,
Fragment,
useState,
useCallback,
} from '@wordpress/element';
import {
useCheckoutAddress,
useStoreEvents,
@@ -87,6 +93,23 @@ const Block = ( {
showApartmentField,
] ) as Record< keyof AddressFields, Partial< AddressField > >;
const onChangeAddress = useCallback(
( values: Partial< BillingAddress > ) => {
setBillingAddress( values );
if ( useBillingAsShipping ) {
setShippingAddress( values );
dispatchCheckoutEvent( 'set-shipping-address' );
}
dispatchCheckoutEvent( 'set-billing-address' );
},
[
dispatchCheckoutEvent,
setBillingAddress,
setShippingAddress,
useBillingAsShipping,
]
);
const AddressFormWrapperComponent = isEditor ? Noninteractive : Fragment;
const noticeContext = useBillingAsShipping
? [ noticeContexts.BILLING_ADDRESS, noticeContexts.SHIPPING_ADDRESS ]
@@ -98,14 +121,7 @@ const Block = ( {
<AddressForm
id="billing"
type="billing"
onChange={ ( values: Partial< BillingAddress > ) => {
setBillingAddress( values );
if ( useBillingAsShipping ) {
setShippingAddress( values );
dispatchCheckoutEvent( 'set-shipping-address' );
}
dispatchCheckoutEvent( 'set-billing-address' );
} }
onChange={ onChangeAddress }
values={ billingAddress }
fields={
Object.keys(

View File

@@ -42,7 +42,10 @@ export const Edit = ( {
>
<InspectorControls>
<PanelBody
title={ __( 'Account', 'woo-gutenberg-products-block' ) }
title={ __(
'Account creation and guest checkout',
'woo-gutenberg-products-block'
) }
>
<p className="wc-block-checkout__controls-text">
{ __(

View File

@@ -2,7 +2,7 @@
"name": "woocommerce/checkout-express-payment-block",
"version": "1.0.0",
"title": "Express Checkout",
"description": "Provide an express payment option for your customers.",
"description": "Allow customers to breeze through with quick payment options.",
"category": "woocommerce",
"supports": {
"align": false,

View File

@@ -1,19 +1,21 @@
/**
* External dependencies
*/
import { Icon, payment } from '@wordpress/icons';
import { Icon } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import expressIcon from '../../../cart-checkout-shared/icon';
import { Edit, Save } from './edit';
registerBlockType( 'woocommerce/checkout-express-payment-block', {
icon: {
src: (
<Icon
icon={ payment }
style={ { fill: 'none' } } // this is needed for this particular svg
icon={ expressIcon }
className="wc-block-editor-components-block-icon"
/>
),

View File

@@ -4,11 +4,6 @@
import classnames from 'classnames';
import { Main } from '@woocommerce/base-components/sidebar-layout';
/**
* Internal dependencies
*/
import './style.scss';
const FrontendBlock = ( {
children,
className,

View File

@@ -8,6 +8,7 @@ import { registerBlockType } from '@wordpress/blocks';
* Internal dependencies
*/
import { Edit, Save } from './edit';
import './style.scss';
registerBlockType( 'woocommerce/checkout-fields-block', {
icon: {

View File

@@ -15,23 +15,15 @@
.wc-block-checkout__shipping-fields,
.wc-block-checkout__billing-fields {
.wc-block-components-address-form {
margin-left: #{-$gap-small * 0.5};
margin-right: #{-$gap-small * 0.5};
&::after {
content: "";
clear: both;
display: block;
}
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.wc-block-components-text-input,
.wc-block-components-country-input,
.wc-block-components-state-input {
float: left;
margin-left: #{$gap-small * 0.5};
margin-right: #{$gap-small * 0.5};
position: relative;
width: calc(50% - #{$gap-small});
flex: 0 0 calc(50% - #{$gap-small});
box-sizing: border-box;
&:nth-of-type(2),
&:first-of-type {
@@ -42,11 +34,7 @@
.wc-block-components-address-form__company,
.wc-block-components-address-form__address_1,
.wc-block-components-address-form__address_2 {
width: calc(100% - #{$gap-small});
}
.wc-block-components-checkbox {
clear: both;
flex: 0 0 100%;
}
}
}

View File

@@ -8,6 +8,7 @@ import { registerBlockType } from '@wordpress/blocks';
* Internal dependencies
*/
import { Edit, Save } from './edit';
import './style.scss';
registerBlockType( 'woocommerce/checkout-order-note-block', {
icon: {

View File

@@ -17,6 +17,6 @@
margin-top: $gap;
}
.wc-block-checkout__order-notes.wc-block-components-checkout-step {
.wc-block-components-form .wc-block-checkout__order-notes.wc-block-components-checkout-step {
padding-left: 0;
}

View File

@@ -1,7 +1,3 @@
/**
* External dependencies
*/
/**
* Internal dependencies
*/

View File

@@ -12,6 +12,8 @@ import Noninteractive from '@woocommerce/base-components/noninteractive';
import { GlobalPaymentMethod } from '@woocommerce/types';
import { useSelect } from '@wordpress/data';
import { PAYMENT_STORE_KEY } from '@woocommerce/block-data';
import { blocksConfig } from '@woocommerce/block-settings';
import { trimCharacters, trimWords } from '@woocommerce/utils';
/**
* Internal dependencies
@@ -49,6 +51,7 @@ export const Edit = ( {
'Incompatible with block-based checkout',
'woo-gutenberg-products-block'
);
const wordCountType = blocksConfig.wordCountType;
return (
<FormStepBlock
@@ -77,12 +80,32 @@ export const Edit = ( {
const isIncompatible =
!! incompatiblePaymentMethods[ method.id ];
let trimmedDescription;
if ( wordCountType === 'words' ) {
trimmedDescription = trimWords(
method.description,
30,
undefined,
false
);
} else {
trimmedDescription = trimCharacters(
method.description,
30,
wordCountType ===
'characters_including_spaces',
undefined,
false
);
}
return (
<ExternalLinkCard
key={ method.id }
href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=checkout&section=${ method.id }` }
title={ method.title }
description={ method.description }
description={ trimmedDescription }
{ ...( isIncompatible
? {
warning:

View File

@@ -26,7 +26,6 @@ import { LocalPickupSelect } from '@woocommerce/base-components/cart-checkout/lo
/**
* Internal dependencies
*/
import './style.scss';
import ShippingRatesControlPackage from '../../../../base/components/cart-checkout/shipping-rates-control-package';
const getPickupLocation = (

View File

@@ -9,6 +9,7 @@ import { registerBlockType } from '@wordpress/blocks';
*/
import { Edit, Save } from './edit';
import attributes from './attributes';
import './style.scss';
registerBlockType( 'woocommerce/checkout-pickup-options-block', {
icon: {

View File

@@ -2,7 +2,13 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useMemo, useEffect, Fragment, useState } from '@wordpress/element';
import {
useMemo,
useEffect,
Fragment,
useState,
useCallback,
} from '@wordpress/element';
import { AddressForm } from '@woocommerce/base-components/cart-checkout';
import {
useCheckoutAddress,
@@ -45,6 +51,7 @@ const Block = ( {
setShippingAddress,
setBillingAddress,
shippingAddress,
billingAddress,
setShippingPhone,
useShippingAsBilling,
setUseShippingAsBilling,
@@ -52,6 +59,7 @@ const Block = ( {
const { dispatchCheckoutEvent } = useStoreEvents();
const { isEditor } = useEditorContext();
const { email } = billingAddress;
// This is used to track whether the "Use shipping as billing" checkbox was checked on first load and if we synced
// the shipping address to the billing address if it was. This is not used on further toggles of the checkbox.
const [ addressesSynced, setAddressesSynced ] = useState( false );
@@ -65,20 +73,25 @@ const Block = ( {
// Run this on first render to ensure addresses sync if needed, there is no need to re-run this when toggling the
// checkbox.
useEffect( () => {
if ( addressesSynced ) {
return;
}
if ( useShippingAsBilling ) {
setBillingAddress( shippingAddress );
}
setAddressesSynced( true );
}, [
addressesSynced,
setBillingAddress,
shippingAddress,
useShippingAsBilling,
] );
useEffect(
() => {
if ( addressesSynced ) {
return;
}
if ( useShippingAsBilling ) {
setBillingAddress( { ...shippingAddress, email } );
}
setAddressesSynced( true );
},
// Skip the `email` dependency since we don't want to re-run if that changes, but we do want to sync it on first render.
// eslint-disable-next-line react-hooks/exhaustive-deps
[
addressesSynced,
setBillingAddress,
shippingAddress,
useShippingAsBilling,
]
);
const addressFieldsConfig = useMemo( () => {
return {
@@ -96,6 +109,24 @@ const Block = ( {
showApartmentField,
] ) as Record< keyof AddressFields, Partial< AddressField > >;
const onChangeAddress = useCallback(
( values: Partial< ShippingAddress > ) => {
setShippingAddress( values );
if ( useShippingAsBilling ) {
setBillingAddress( { ...values, email } );
dispatchCheckoutEvent( 'set-billing-address' );
}
dispatchCheckoutEvent( 'set-shipping-address' );
},
[
dispatchCheckoutEvent,
email,
setBillingAddress,
setShippingAddress,
useShippingAsBilling,
]
);
const AddressFormWrapperComponent = isEditor ? Noninteractive : Fragment;
const noticeContext = useShippingAsBilling
? [ noticeContexts.SHIPPING_ADDRESS, noticeContexts.BILLING_ADDRESS ]
@@ -108,13 +139,7 @@ const Block = ( {
<AddressForm
id="shipping"
type="shipping"
onChange={ ( values: Partial< ShippingAddress > ) => {
setShippingAddress( values );
if ( useShippingAsBilling ) {
setBillingAddress( values );
}
dispatchCheckoutEvent( 'set-shipping-address' );
} }
onChange={ onChangeAddress }
values={ shippingAddress }
fields={
Object.keys(

View File

@@ -10,13 +10,13 @@ import {
import classnames from 'classnames';
import { Icon, store, shipping } from '@wordpress/icons';
import { useEffect } from '@wordpress/element';
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
import { useDispatch } from '@wordpress/data';
import { CART_STORE_KEY, VALIDATION_STORE_KEY } from '@woocommerce/block-data';
import { useDispatch, useSelect } from '@wordpress/data';
import { isPackageRateCollectable } from '@woocommerce/base-utils';
/**
* Internal dependencies
*/
import './style.scss';
import { RatePrice, getLocalPickupPrices, getShippingPrices } from './shared';
import type { minMaxPrices } from './shared';
import { defaultLocalPickupText, defaultShippingText } from './constants';
@@ -92,8 +92,17 @@ const ShippingSelector = ( {
shippingCostRequiresAddress: boolean;
toggleText: string;
} ) => {
const hasShippableRates = useSelect( ( select ) => {
const rates = select( CART_STORE_KEY ).getShippingRates();
return rates.some(
( { shipping_rates: shippingRate } ) =>
! shippingRate.every( isPackageRateCollectable )
);
} );
const rateShouldBeHidden =
shippingCostRequiresAddress && shippingAddressHasValidationErrors();
shippingCostRequiresAddress &&
shippingAddressHasValidationErrors() &&
! hasShippableRates;
const hasShippingPrices = rate.min !== undefined && rate.max !== undefined;
const { setValidationErrors, clearValidationError } =
useDispatch( VALIDATION_STORE_KEY );

View File

@@ -9,6 +9,7 @@ import { registerBlockType } from '@wordpress/blocks';
*/
import { Edit, Save } from './edit';
import attributes from './attributes';
import './style.scss';
registerBlockType( 'woocommerce/checkout-shipping-method-block', {
icon: {

View File

@@ -2,7 +2,10 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useShippingData } from '@woocommerce/base-context/hooks';
import {
useCustomerData,
useShippingData,
} from '@woocommerce/base-context/hooks';
import { ShippingRatesControl } from '@woocommerce/base-components/cart-checkout';
import {
getShippingRatesPackageCount,
@@ -19,15 +22,8 @@ import type {
PackageRateOption,
CartShippingPackageShippingRate,
} from '@woocommerce/types';
import { CART_STORE_KEY } from '@woocommerce/block-data';
import { useSelect } from '@wordpress/data';
import NoticeBanner from '@woocommerce/base-components/notice-banner';
/**
* Internal dependencies
*/
import './style.scss';
import { shippingAddressHasValidationErrors } from '../../../../data/cart/utils';
import type { ReactElement } from 'react';
/**
* Renders a shipping rate control option.
@@ -54,10 +50,7 @@ const renderShippingRatesControlOption = (
};
};
const Block = ( {
noShippingPlaceholder = null,
shippingCostRequiresAddress = false,
} ): React.ReactElement | null => {
const Block = ( { noShippingPlaceholder = null } ): ReactElement | null => {
const { isEditor } = useEditorContext();
const {
@@ -68,9 +61,7 @@ const Block = ( {
isCollectable,
} = useShippingData();
const shippingAddressPushed = useSelect( ( select ) => {
return select( CART_STORE_KEY ).getFullShippingAddressPushed();
} );
const { shippingAddress } = useCustomerData();
const filteredShippingRates = isCollectable
? shippingRates.map( ( shippingRatesPackage ) => {
@@ -86,25 +77,14 @@ const Block = ( {
} )
: shippingRates;
const shippingAddress = useSelect( ( select ) => {
return select( CART_STORE_KEY ).getCustomerData()?.shippingAddress;
} );
if ( ! needsShipping ) {
return null;
}
const shippingAddressHasErrors = ! shippingAddressHasValidationErrors();
const addressComplete = isAddressComplete( shippingAddress );
const shippingRatesPackageCount =
getShippingRatesPackageCount( shippingRates );
if (
( ! hasCalculatedShipping && ! shippingRatesPackageCount ) ||
( shippingCostRequiresAddress &&
( ! shippingAddressPushed || ! shippingAddressHasErrors ) )
) {
if ( ! hasCalculatedShipping && ! shippingRatesPackageCount ) {
return (
<p>
{ __(
@@ -114,6 +94,7 @@ const Block = ( {
</p>
);
}
const addressComplete = isAddressComplete( shippingAddress );
return (
<>

View File

@@ -20,7 +20,6 @@ const FrontendBlock = ( {
showStepNumber,
children,
className,
shippingCostRequiresAddress = false,
}: {
title: string;
description: string;
@@ -32,7 +31,6 @@ const FrontendBlock = ( {
showStepNumber: boolean;
children: JSX.Element;
className?: string;
shippingCostRequiresAddress: boolean;
} ) => {
const checkoutIsProcessing = useSelect( ( select ) =>
select( CHECKOUT_STORE_KEY ).isProcessing()
@@ -55,9 +53,7 @@ const FrontendBlock = ( {
description={ description }
showStepNumber={ showStepNumber }
>
<Block
shippingCostRequiresAddress={ shippingCostRequiresAddress }
/>
<Block />
{ children }
</FormStep>
);

View File

@@ -9,6 +9,7 @@ import { registerBlockType } from '@wordpress/blocks';
*/
import { Edit, Save } from './edit';
import attributes from './attributes';
import './style.scss';
registerBlockType( 'woocommerce/checkout-shipping-methods-block', {
icon: {

View File

@@ -14,7 +14,6 @@ import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
* Internal dependencies
*/
import { termsConsentDefaultText, termsCheckboxDefaultText } from './constants';
import './style.scss';
const FrontendBlock = ( {
text,

View File

@@ -3,10 +3,12 @@
*/
import { Icon, customPostType } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import './style.scss';
registerBlockType( 'woocommerce/checkout-terms-block', {
icon: {

View File

@@ -3,11 +3,7 @@
*/
import classnames from 'classnames';
import { Sidebar } from '@woocommerce/base-components/sidebar-layout';
/**
* Internal dependencies
*/
import './style.scss';
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
const FrontendBlock = ( {
children,
@@ -20,6 +16,9 @@ const FrontendBlock = ( {
<Sidebar
className={ classnames( 'wc-block-checkout__sidebar', className ) }
>
<StoreNoticesContainer
context={ 'woocommerce/checkout-totals-block' }
/>
{ children }
</Sidebar>
);

View File

@@ -8,6 +8,7 @@ import { registerBlockType } from '@wordpress/blocks';
* Internal dependencies
*/
import { Edit, Save } from './edit';
import './style.scss';
registerBlockType( 'woocommerce/checkout-totals-block', {
icon: {

View File

@@ -6,11 +6,6 @@ import { useState } from '@wordpress/element';
import { CheckboxControl } from '@woocommerce/blocks-checkout';
import { Textarea } from '@woocommerce/base-components/textarea';
/**
* Internal dependencies
*/
import './style.scss';
interface CheckoutOrderNotesProps {
disabled: boolean;
onChange: ( orderNotes: string ) => void;

View File

@@ -51,17 +51,11 @@ body.wc-lock-selected-block--remove {
}
}
.wc-block-checkout__controls-text {
color: #999;
font-style: italic;
}
.components-base-control--nested {
padding-left: 52px;
margin-top: -12px;
}
.components-panel__body-title .components-button {
opacity: 1;
}

View File

@@ -51,17 +51,21 @@
.wc-block-components-panel > h2 {
@include font-size(regular);
@include reset-box();
@include reset-color();
@include reset-typography();
.wc-block-components-panel__button {
font-weight: 400;
}
}
.wc-block-components-totals-item,
.wc-block-components-totals-coupon-link,
.wc-block-components-panel {
padding-left: $gap;
padding-right: $gap;
}
.wc-block-components-totals-coupon-link {
margin-left: $gap;
margin-right: $gap;
}
}
}
// Skeleton is shown before mobile classes are appended.
@@ -78,11 +82,14 @@
padding: 0;
width: 100%;
.wc-block-components-totals-item,
.wc-block-components-totals-coupon-link,
.wc-block-components-panel {
padding-left: 0;
padding-right: 0;
}
.wc-block-components-totals-coupon-link {
margin-left: 0;
margin-right: 0;
}
}
}
}

View File

@@ -7,19 +7,18 @@ import {
type BlockInstance,
} from '@wordpress/blocks';
import { isWpVersion } from '@woocommerce/settings';
import { isExperimentalBuild } from '@woocommerce/block-settings';
import { __, sprintf } from '@wordpress/i18n';
import {
INNER_BLOCKS_TEMPLATE as productsInnerBlocksTemplate,
QUERY_DEFAULT_ATTRIBUTES as productsQueryDefaultAttributes,
PRODUCT_QUERY_VARIATION_NAME as productsVariationName,
} from '@woocommerce/blocks/product-query/constants';
/**
* Internal dependencies
*/
import {
INNER_BLOCKS_TEMPLATE as productsInnerBlocksTemplate,
QUERY_DEFAULT_ATTRIBUTES as productsQueryDefaultAttributes,
} from '../product-query/constants';
import { VARIATION_NAME as productsVariationName } from '../product-query/variations/product-query';
import { createArchiveTitleBlock, createRowBlock } from './utils';
import { type InheritedAttributes } from './types';
import { OnClickCallbackParameter, type InheritedAttributes } from './types';
const createProductsBlock = ( inheritedAttributes: InheritedAttributes ) =>
createBlock(
@@ -64,14 +63,14 @@ const getBlockifiedTemplateWithTermDescription = (
const isConversionPossible = () => {
// Blockification is possible for the WP version 6.1 and above,
// which are the versions the Products block supports.
return isExperimentalBuild() && isWpVersion( '6.1', '>=' );
return isWpVersion( '6.1', '>=' );
};
const getDescriptionAllowingConversion = ( templateTitle: string ) =>
sprintf(
/* translators: %s is the template title */
__(
"This block serves as a placeholder for your %s. We recommend upgrading to the Products block for more features to edit your products visually. Don't worry, you can always revert back.",
'Transform this template into multiple blocks so you can add, remove, reorder, and customize your %s template.',
'woo-gutenberg-products-block'
),
templateTitle
@@ -96,18 +95,78 @@ const getDescription = ( templateTitle: string, canConvert: boolean ) => {
};
const getButtonLabel = () =>
__( 'Upgrade to Products block', 'woo-gutenberg-products-block' );
__( 'Transform into blocks', 'woo-gutenberg-products-block' );
const onClickCallback = ( {
clientId,
attributes,
getBlocks,
replaceBlock,
selectBlock,
}: OnClickCallbackParameter ) => {
replaceBlock( clientId, getBlockifiedTemplate( attributes ) );
const blocks = getBlocks();
const groupBlock = blocks.find(
( block ) =>
block.name === 'core/group' &&
block.innerBlocks.some(
( innerBlock ) =>
innerBlock.name === 'woocommerce/store-notices'
)
);
if ( groupBlock ) {
selectBlock( groupBlock.clientId );
}
};
const onClickCallbackWithTermDescription = ( {
clientId,
attributes,
getBlocks,
replaceBlock,
selectBlock,
}: OnClickCallbackParameter ) => {
replaceBlock( clientId, getBlockifiedTemplate( attributes, true ) );
const blocks = getBlocks();
const groupBlock = blocks.find(
( block ) =>
block.name === 'core/group' &&
block.innerBlocks.some(
( innerBlock ) =>
innerBlock.name === 'woocommerce/store-notices'
)
);
if ( groupBlock ) {
selectBlock( groupBlock.clientId );
}
};
const productCatalogBlockifyConfig = {
getButtonLabel,
onClickCallback,
getBlockifiedTemplate,
};
const productTaxonomyBlockifyConfig = {
getButtonLabel,
onClickCallback: onClickCallbackWithTermDescription,
getBlockifiedTemplate: getBlockifiedTemplateWithTermDescription,
};
export const blockifiedProductCatalogConfig = {
getBlockifiedTemplate,
isConversionPossible,
getDescription,
getButtonLabel,
blockifyConfig: productCatalogBlockifyConfig,
};
export const blockifiedProductTaxonomyConfig = {
getBlockifiedTemplate: getBlockifiedTemplateWithTermDescription,
isConversionPossible,
getDescription,
getButtonLabel,
blockifyConfig: productTaxonomyBlockifyConfig,
};

View File

@@ -14,10 +14,16 @@ export const TYPES = {
productCatalog: 'product-catalog',
productTaxonomy: 'product-taxonomy',
productSearchResults: 'product-search-results',
orderConfirmation: 'order-confirmation',
cart: 'cart',
checkout: 'checkout',
checkoutHeader: 'checkout-header',
};
export const PLACEHOLDERS = {
singleProduct: 'single-product',
archiveProduct: 'archive-product',
orderConfirmation: 'fallback',
checkoutHeader: 'checkout-header',
};
export const TEMPLATES: TemplateDetails = {
@@ -61,6 +67,15 @@ export const TEMPLATES: TemplateDetails = {
),
placeholder: PLACEHOLDERS.archiveProduct,
},
// Since that it is a fallback value, it has to be the last one.
'taxonomy-product': {
type: TYPES.productTaxonomy,
title: __(
"WooCommerce Product's Custom Taxonomy Block",
'woo-gutenberg-products-block'
),
placeholder: PLACEHOLDERS.archiveProduct,
},
'product-search-results': {
type: TYPES.productSearchResults,
title: __(
@@ -69,4 +84,24 @@ export const TEMPLATES: TemplateDetails = {
),
placeholder: PLACEHOLDERS.archiveProduct,
},
cart: {
type: TYPES.cart,
title: __( 'WooCommerce Cart Block', 'woo-gutenberg-products-block' ),
placeholder: 'cart',
},
checkout: {
type: TYPES.checkout,
title: __( 'Checkout Block', 'woo-gutenberg-products-block' ),
placeholder: 'checkout',
},
'checkout-header': {
type: TYPES.checkoutHeader,
title: __( 'Checkout Header', 'woo-gutenberg-products-block' ),
placeholder: 'checkout-header',
},
'order-confirmation': {
type: TYPES.orderConfirmation,
title: __( 'Order Confirmation Block', 'woo-gutenberg-products-block' ),
placeholder: PLACEHOLDERS.orderConfirmation,
},
};

View File

@@ -1,48 +1,88 @@
:where(.wp-block-woocommerce-legacy-template) {
margin-left: auto;
margin-right: auto;
max-width: 1000px;
}
.wp-block-woocommerce-classic-template__placeholder-copy {
max-width: 900px;
margin-bottom: 30px;
}
.wp-block-woocommerce-classic-template__placeholder-warning {
border-left: 5px solid #2181d2;
padding-left: em(40px);
}
.wp-block-woocommerce-classic-template__placeholder-wireframe {
width: 100%;
height: 250px;
background: #e5e5e5;
@media only screen and (min-width: 768px) {
height: auto;
background: transparent;
}
.wp-block-woocommerce-classic-template__placeholder .components-placeholder__fieldset {
display: grid;
grid-template-columns: 1fr;
}
.wp-block-woocommerce-classic-template__placeholder .wp-block-woocommerce-classic-template__placeholder-image {
display: none;
width: 100%;
height: auto;
@media only screen and (min-width: 768px) {
display: block;
}
.wp-block-woocommerce-classic-template__placeholder-wireframe,
.wp-block-woocommerce-classic-template__placeholder-copy {
grid-row-start: 1;
grid-column-start: 1;
transition: 0.3s all ease;
}
.wp-block-woocommerce-classic-template__placeholder-wireframe {
.wp-block-woocommerce-classic-template__placeholder-copy {
border: 1px solid $gray-900;
background-color: #fff;
padding: $gap-large $gap-larger;
border-radius: 3px;
display: flex;
flex-direction: column;
max-width: 900px;
width: 400px;
margin: auto;
opacity: 0;
z-index: 10;
.wp-block-woocommerce-classic-template__placeholder-copy__icon-container {
margin: 0 0 $gap;
span {
@include font-size(larger);
display: block;
}
.woo-icon {
color: #{$studio-woocommerce-purple};
@include font-size(large);
svg {
vertical-align: middle;
}
}
}
p {
margin: 0 0 $gap;
}
.wp-block-woocommerce-classic-template__placeholder-migration-button-container {
justify-content: center;
margin: $gap 0;
}
}
.wp-block-woocommerce-classic-template__placeholder-wireframe {
pointer-events: none;
// Image based placeholders should fill horizontal width.
> img {
width: 100%;
}
}
.wp-block-woocommerce-classic-template__placeholder-migration-button-container {
justify-content: center;
align-items: center;
margin: 0 auto;
.wp-block-woocommerce-legacy-template {
.components-placeholder {
box-shadow: none;
padding: 0;
}
}
.wp-block-woocommerce-legacy-template.is-selected {
.wp-block-woocommerce-classic-template__placeholder-wireframe {
filter: blur(3px);
opacity: 0.5;
* {
color: $gray-200 !important;
border-color: $gray-200 !important;
}
}
.wp-block-woocommerce-classic-template__placeholder-copy {
opacity: 1;
}
.components-placeholder {
box-shadow: inherit;
}
}

View File

@@ -2,21 +2,36 @@
* External dependencies
*/
import {
BlockInstance,
createBlock,
getBlockType,
registerBlockType,
unregisterBlockType,
parse,
} from '@wordpress/blocks';
import type { BlockEditProps } from '@wordpress/blocks';
import { WC_BLOCKS_IMAGE_URL } from '@woocommerce/block-settings';
import {
isExperimentalBuild,
WC_BLOCKS_IMAGE_URL,
} from '@woocommerce/block-settings';
import { useBlockProps } from '@wordpress/block-editor';
import { Button, Placeholder } from '@wordpress/components';
useBlockProps,
BlockPreview,
store as blockEditorStore,
} from '@wordpress/block-editor';
import { Button, Placeholder, Popover } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { box, Icon } from '@wordpress/icons';
import { select, useDispatch, subscribe } from '@wordpress/data';
import { useEffect } from '@wordpress/element';
import {
useDispatch,
subscribe,
useSelect,
select,
dispatch,
} from '@wordpress/data';
import { useEffect, useState } from '@wordpress/element';
import { store as noticesStore } from '@wordpress/notices';
import { useEntityRecord } from '@wordpress/core-data';
import { debounce } from '@woocommerce/base-utils';
import { woo } from '@woocommerce/icons';
import { isNumber } from '@woocommerce/types';
/**
* Internal dependencies
@@ -35,6 +50,8 @@ import {
} from './archive-product';
import * as blockifiedSingleProduct from './single-product';
import * as blockifiedProductSearchResults from './product-search-results';
import * as blockifiedOrderConfirmation from './order-confirmation';
import type { BlockifiedTemplateConfig } from './types';
type Attributes = {
@@ -46,7 +63,7 @@ const blockifiedFallbackConfig = {
isConversionPossible: () => false,
getBlockifiedTemplate: () => [],
getDescription: () => '',
getButtonLabel: () => '',
onClickCallback: () => void 0,
};
const conversionConfig: { [ key: string ]: BlockifiedTemplateConfig } = {
@@ -54,22 +71,152 @@ const conversionConfig: { [ key: string ]: BlockifiedTemplateConfig } = {
[ TYPES.productTaxonomy ]: blockifiedProductTaxonomyConfig,
[ TYPES.singleProduct ]: blockifiedSingleProduct,
[ TYPES.productSearchResults ]: blockifiedProductSearchResults,
[ TYPES.orderConfirmation ]: blockifiedOrderConfirmation,
fallback: blockifiedFallbackConfig,
};
const pickBlockClientIds = ( blocks: Array< BlockInstance > ) =>
blocks.reduce< Array< string > >( ( acc, block ) => {
if ( block.name === 'core/template-part' ) {
return acc;
}
return [ ...acc, block.clientId ];
}, [] );
const ConvertTemplate = ( { blockifyConfig, clientId, attributes } ) => {
const { getButtonLabel, onClickCallback, getBlockifiedTemplate } =
blockifyConfig;
const [ isPopoverOpen, setIsPopoverOpen ] = useState( false );
const { replaceBlock, selectBlock, replaceBlocks } =
useDispatch( blockEditorStore );
const { getBlocks } = useSelect( ( sel ) => {
return {
getBlocks: sel( blockEditorStore ).getBlocks,
};
}, [] );
const { createInfoNotice } = useDispatch( noticesStore );
return (
<div className="wp-block-woocommerce-classic-template__placeholder-migration-button-container">
<Button
isPrimary
onClick={ () => {
onClickCallback( {
clientId,
getBlocks,
attributes,
replaceBlock,
selectBlock,
} );
createInfoNotice(
__(
'Template transformed into blocks!',
'woo-gutenberg-products-block'
),
{
actions: [
{
label: __(
'Undo',
'woo-gutenberg-products-block'
),
onClick: () => {
const clientIds = pickBlockClientIds(
getBlocks()
);
replaceBlocks(
clientIds,
createBlock(
'core/group',
{
layout: {
inherit: true,
type: 'constrained',
},
},
[
createBlock(
'woocommerce/legacy-template',
{
template:
attributes.template,
}
),
]
)
);
},
},
],
type: 'snackbar',
}
);
} }
onMouseEnter={ () => setIsPopoverOpen( true ) }
onMouseLeave={ () => setIsPopoverOpen( false ) }
text={ getButtonLabel ? getButtonLabel() : '' }
>
{ isPopoverOpen && (
<Popover resize={ false } placement="right-end">
<div
style={ {
minWidth: '250px',
width: '250px',
maxWidth: '250px',
minHeight: '300px',
height: '300px',
maxHeight: '300px',
cursor: 'pointer',
} }
>
<BlockPreview
blocks={ getBlockifiedTemplate( attributes ) }
viewportWidth={ 1200 }
additionalStyles={ [
{
css: 'body { padding: 20px !important; height: fit-content !important; overflow:hidden}',
},
] }
/>
</div>
</Popover>
) }
</Button>
</div>
);
};
const Edit = ( {
clientId,
attributes,
setAttributes,
}: BlockEditProps< Attributes > ) => {
const { replaceBlock } = useDispatch( 'core/block-editor' );
const blockProps = useBlockProps();
const { editedPostId } = useSelect( ( sel ) => {
return {
editedPostId: sel( 'core/edit-site' ).getEditedPostId(),
};
}, [] );
const template = useEntityRecord< {
slug: string;
title: {
rendered?: string;
row: string;
};
} >( 'postType', 'wp_template', editedPostId );
const templateDetails = getTemplateDetailsBySlug(
attributes.template,
TEMPLATES
);
const templateTitle = templateDetails?.title ?? attributes.template;
const templateTitle =
template.record?.title.rendered?.toLowerCase() ?? attributes.template;
const templatePlaceholder = templateDetails?.placeholder ?? 'fallback';
const templateType = templateDetails?.type ?? 'fallback';
@@ -83,45 +230,65 @@ const Edit = ( {
);
const {
getBlockifiedTemplate,
isConversionPossible,
getDescription,
getButtonLabel,
getSkeleton,
blockifyConfig,
} = conversionConfig[ templateType ];
const skeleton = getSkeleton ? (
getSkeleton()
) : (
<img
className="wp-block-woocommerce-classic-template__placeholder-image"
src={ `${ WC_BLOCKS_IMAGE_URL }template-placeholders/${ templatePlaceholder }.svg` }
alt={ templateTitle }
/>
);
const canConvert = isConversionPossible();
const placeholderDescription = getDescription( templateTitle, canConvert );
return (
<div { ...blockProps }>
<Placeholder
icon={ box }
label={ templateTitle }
className="wp-block-woocommerce-classic-template__placeholder"
>
<div className="wp-block-woocommerce-classic-template__placeholder-copy">
<p>{ placeholderDescription }</p>
</div>
<Placeholder className="wp-block-woocommerce-classic-template__placeholder">
<div className="wp-block-woocommerce-classic-template__placeholder-wireframe">
{ canConvert && (
<div className="wp-block-woocommerce-classic-template__placeholder-migration-button-container">
<Button
isPrimary
onClick={ () => {
replaceBlock(
clientId,
getBlockifiedTemplate( attributes )
);
} }
text={ getButtonLabel() }
/>
</div>
) }
<img
className="wp-block-woocommerce-classic-template__placeholder-image"
src={ `${ WC_BLOCKS_IMAGE_URL }template-placeholders/${ templatePlaceholder }.svg` }
alt={ templateTitle }
{ skeleton }
</div>
<div className="wp-block-woocommerce-classic-template__placeholder-copy">
<div className="wp-block-woocommerce-classic-template__placeholder-copy__icon-container">
<span className="woo-icon">
<Icon icon={ woo } />{ ' ' }
{ __(
'WooCommerce',
'woo-gutenberg-products-block'
) }
</span>
<span>
{ __(
'Classic Template Placeholder',
'woo-gutenberg-products-block'
) }
</span>
</div>
<p
dangerouslySetInnerHTML={ {
__html: placeholderDescription,
} }
/>
<p>
{ __(
'You cannot edit the content of this block. However, you can move it and place other blocks around it.',
'woo-gutenberg-products-block'
) }
</p>
{ canConvert && blockifyConfig && (
<ConvertTemplate
clientId={ clientId }
blockifyConfig={ blockifyConfig }
attributes={ attributes }
/>
) }
</div>
</Placeholder>
</div>
@@ -206,59 +373,102 @@ const registerClassicTemplateBlock = ( {
} );
};
/**
* Attempts to recover the Classic Template block if it fails to render on the Single Product template
* due to the user resetting customizations without refreshing the page.
*
* When the Classic Template block fails to render, it is replaced by the 'core/missing' block, which
* displays an error message stating that the WooCommerce Classic template block is unsupported.
*
* This function replaces the 'core/missing' block with the original Classic Template block that failed
* to render, allowing the block to be displayed correctly.
*
* @see {@link https://github.com/woocommerce/woocommerce-blocks/issues/9637|Issue: Block error is displayed on clearing customizations for Woo Templates}
*
*/
const tryToRecoverClassicTemplateBlockWhenItFailsToRender = debounce( () => {
const blocks = select( 'core/block-editor' ).getBlocks();
const blocksIncludingInnerBlocks = blocks.flatMap( ( block ) => [
block,
...block.innerBlocks,
] );
const classicTemplateThatFailedToRender = blocksIncludingInnerBlocks.find(
( block ) =>
block.name === 'core/missing' &&
block.attributes.originalName === BLOCK_SLUG
);
if ( classicTemplateThatFailedToRender ) {
const blockToReplaceClassicTemplateBlockThatFailedToRender = parse(
classicTemplateThatFailedToRender.attributes.originalContent
);
if ( blockToReplaceClassicTemplateBlockThatFailedToRender ) {
dispatch( 'core/block-editor' ).replaceBlock(
classicTemplateThatFailedToRender.clientId,
blockToReplaceClassicTemplateBlockThatFailedToRender
);
}
}
}, 100 );
// @todo Refactor when there will be possible to show a block according on a template/post with a Gutenberg API. https://github.com/WordPress/gutenberg/pull/41718
let currentTemplateId: string | undefined;
if ( isExperimentalBuild() ) {
subscribe( () => {
const previousTemplateId = currentTemplateId;
const store = select( 'core/edit-site' );
currentTemplateId = store?.getEditedPostId() as string | undefined;
subscribe( () => {
const previousTemplateId = currentTemplateId;
const store = select( 'core/edit-site' );
// With GB 16.3.0 the return type can be a number: https://github.com/WordPress/gutenberg/issues/53230
const editedPostId = store?.getEditedPostId() as
| string
| number
| undefined;
if ( previousTemplateId === currentTemplateId ) {
return;
}
currentTemplateId = isNumber( editedPostId ) ? undefined : editedPostId;
const parsedTemplate = currentTemplateId?.split( '//' )[ 1 ];
const parsedTemplate = currentTemplateId?.split( '//' )[ 1 ];
if ( parsedTemplate === null || parsedTemplate === undefined ) {
return;
}
if ( parsedTemplate === null || parsedTemplate === undefined ) {
return;
}
const block = getBlockType( BLOCK_SLUG );
const block = getBlockType( BLOCK_SLUG );
const isBlockRegistered = Boolean( block );
if (
block !== undefined &&
( ! hasTemplateSupportForClassicTemplateBlock(
parsedTemplate,
TEMPLATES
) ||
isClassicTemplateBlockRegisteredWithAnotherTitle(
block,
parsedTemplate
) )
) {
unregisterBlockType( BLOCK_SLUG );
currentTemplateId = undefined;
return;
}
if (
isBlockRegistered &&
hasTemplateSupportForClassicTemplateBlock( parsedTemplate, TEMPLATES )
) {
tryToRecoverClassicTemplateBlockWhenItFailsToRender();
}
if (
block === undefined &&
hasTemplateSupportForClassicTemplateBlock(
parsedTemplate,
TEMPLATES
)
) {
registerClassicTemplateBlock( {
template: parsedTemplate,
inserter: true,
} );
}
} );
} else {
registerClassicTemplateBlock( {
inserter: false,
} );
}
if ( previousTemplateId === currentTemplateId ) {
return;
}
if (
isBlockRegistered &&
( ! hasTemplateSupportForClassicTemplateBlock(
parsedTemplate,
TEMPLATES
) ||
isClassicTemplateBlockRegisteredWithAnotherTitle(
block,
parsedTemplate
) )
) {
unregisterBlockType( BLOCK_SLUG );
currentTemplateId = undefined;
return;
}
if (
! isBlockRegistered &&
hasTemplateSupportForClassicTemplateBlock( parsedTemplate, TEMPLATES )
) {
registerClassicTemplateBlock( {
template: parsedTemplate,
inserter: true,
} );
}
}, 'core/blocks-editor' );

View File

@@ -0,0 +1,153 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
const isConversionPossible = () => {
return false;
};
const getDescription = () => {
return __(
'This block represents the classic template used to display the order confirmation. The actual rendered template may appear different from this placeholder.',
'woo-gutenberg-products-block'
);
};
const getSkeleton = () => {
return (
<div className="woocommerce-page">
<div className="woocommerce-order">
<h1>
{ __( 'Order received', 'woo-gutenberg-products-block' ) }
</h1>
<p className="woocommerce-notice woocommerce-notice--success woocommerce-thankyou-order-confirmation">
{ __(
'Thank you. Your order has been received.',
'woo-gutenberg-products-block'
) }
</p>
<ul className="woocommerce-order-overview woocommerce-thankyou-order-details order_details">
<li className="woocommerce-order-overview__order order">
{ __( 'Order number', 'woo-gutenberg-products-block' ) }
: <strong>123</strong>
</li>
<li className="woocommerce-order-overview__date date">
{ __( 'Date', 'woo-gutenberg-products-block' ) }:{ ' ' }
<strong>May 25, 2023</strong>
</li>
<li className="woocommerce-order-overview__email email">
{ __( 'Email', 'woo-gutenberg-products-block' ) }:{ ' ' }
<strong>shopper@woo.com</strong>
</li>
<li className="woocommerce-order-overview__total total">
{ __( 'Total', 'woo-gutenberg-products-block' ) }:{ ' ' }
<strong>$20.00</strong>
</li>
</ul>
<section className="woocommerce-order-details">
<h2 className="woocommerce-order-details__title">
{ __(
'Order details',
'woo-gutenberg-products-block'
) }
</h2>
<table className="woocommerce-table woocommerce-table--order-details shop_table order_details">
<thead>
<tr>
<th className="woocommerce-table__product-name product-name">
{ __(
'Product',
'woo-gutenberg-products-block'
) }
</th>
<th className="woocommerce-table__product-table product-total">
{ __(
'Total',
'woo-gutenberg-products-block'
) }
</th>
</tr>
</thead>
<tbody>
<tr className="woocommerce-table__line-item order_item">
<td className="woocommerce-table__product-name product-name">
Sample Product{ ' ' }
<strong className="product-quantity">
×&nbsp;2
</strong>{ ' ' }
</td>
<td className="woocommerce-table__product-total product-total">
$20.00
</td>
</tr>
</tbody>
<tfoot>
<tr>
<th scope="row">
{ __(
'Subtotal',
'woo-gutenberg-products-block'
) }
:
</th>
<td>$20.00</td>
</tr>
<tr>
<th scope="row">
{ __(
'Total',
'woo-gutenberg-products-block'
) }
:
</th>
<td>$20.00</td>
</tr>
</tfoot>
</table>
</section>
<section className="woocommerce-customer-details">
<section className="woocommerce-columns woocommerce-columns--2 woocommerce-columns--addresses col2-set addresses">
<div className="woocommerce-column woocommerce-column--1 woocommerce-column--billing-address col-1">
<h2 className="woocommerce-column__title">
{ __(
'Billing address',
'woo-gutenberg-products-block'
) }
</h2>
<address>
123 Main St
<br />
New York, NY 10001
<br />
United States (US)
</address>
</div>
<div className="woocommerce-column woocommerce-column--2 woocommerce-column--shipping-address col-2">
<h2 className="woocommerce-column__title">
{ __(
'Shipping address',
'woo-gutenberg-products-block'
) }
</h2>
<address>
123 Main St
<br />
New York, NY 10001
<br />
United States (US)
</address>
</div>
</section>
</section>
</div>
</div>
);
};
export { isConversionPossible, getDescription, getSkeleton };

View File

@@ -8,19 +8,18 @@ import {
type InnerBlockTemplate,
} from '@wordpress/blocks';
import { isWpVersion } from '@woocommerce/settings';
import { isExperimentalBuild } from '@woocommerce/block-settings';
import { __, sprintf } from '@wordpress/i18n';
import {
INNER_BLOCKS_TEMPLATE as productsInnerBlocksTemplate,
QUERY_DEFAULT_ATTRIBUTES as productsQueryDefaultAttributes,
PRODUCT_QUERY_VARIATION_NAME as productsVariationName,
} from '@woocommerce/blocks/product-query/constants';
/**
* Internal dependencies
*/
import {
INNER_BLOCKS_TEMPLATE as productsInnerBlocksTemplate,
QUERY_DEFAULT_ATTRIBUTES as productsQueryDefaultAttributes,
} from '../product-query/constants';
import { VARIATION_NAME as productsVariationName } from '../product-query/variations/product-query';
import { createArchiveTitleBlock, createRowBlock } from './utils';
import { type InheritedAttributes } from './types';
import { OnClickCallbackParameter, type InheritedAttributes } from './types';
const createNoResultsParagraph = () =>
createBlock( 'core/paragraph', {
@@ -111,14 +110,14 @@ const getBlockifiedTemplate = ( inheritedAttributes: InheritedAttributes ) =>
const isConversionPossible = () => {
// Blockification is possible for the WP version 6.1 and above,
// which are the versions the Products block supports.
return isExperimentalBuild() && isWpVersion( '6.1', '>=' );
return isWpVersion( '6.1', '>=' );
};
const getDescriptionAllowingConversion = ( templateTitle: string ) =>
sprintf(
/* translators: %s is the template title */
__(
"This block serves as a placeholder for your %s. We recommend upgrading to the Products block for more features to edit your products visually. Don't worry, you can always revert back.",
'Transform this template into multiple blocks so you can add, remove, reorder, and customize your %s template.',
'woo-gutenberg-products-block'
),
templateTitle
@@ -142,12 +141,38 @@ const getDescription = ( templateTitle: string, canConvert: boolean ) => {
return getDescriptionDisallowingConversion( templateTitle );
};
const getButtonLabel = () =>
__( 'Upgrade to Products block', 'woo-gutenberg-products-block' );
const onClickCallback = ( {
clientId,
attributes,
getBlocks,
replaceBlock,
selectBlock,
}: OnClickCallbackParameter ) => {
replaceBlock( clientId, getBlockifiedTemplate( attributes ) );
export {
getBlockifiedTemplate,
isConversionPossible,
getDescription,
getButtonLabel,
const blocks = getBlocks();
const groupBlock = blocks.find(
( block ) =>
block.name === 'core/group' &&
block.innerBlocks.some(
( innerBlock ) =>
innerBlock.name === 'woocommerce/store-notices'
)
);
if ( groupBlock ) {
selectBlock( groupBlock.clientId );
}
};
const getButtonLabel = () =>
__( 'Transform into blocks', 'woo-gutenberg-products-block' );
const blockifyConfig = {
getButtonLabel,
onClickCallback,
getBlockifiedTemplate,
};
export { isConversionPossible, getDescription, blockifyConfig };

View File

@@ -7,6 +7,11 @@ import { BlockInstance, createBlock } from '@wordpress/blocks';
import { VARIATION_NAME as PRODUCT_TITLE_VARIATION_NAME } from '@woocommerce/blocks/product-query/variations/elements/product-title';
import { VARIATION_NAME as PRODUCT_SUMMARY_VARIATION_NAME } from '@woocommerce/blocks/product-query/variations/elements/product-summary';
/**
* Internal dependencies
*/
import { OnClickCallbackParameter } from './types';
const getBlockifiedTemplate = () =>
[
createBlock( 'woocommerce/breadcrumbs' ),
@@ -22,13 +27,16 @@ const getBlockifiedTemplate = () =>
{
type: 'constrained',
justifyContent: 'right',
width: '512px',
},
[ createBlock( 'woocommerce/product-image-gallery' ) ]
),
createBlock( 'core/column', {}, [
createBlock( 'core/post-title', {
__woocommerceNamespace: PRODUCT_TITLE_VARIATION_NAME,
level: 1,
} ),
createBlock( 'woocommerce/product-rating' ),
createBlock( 'woocommerce/product-price', {
fontSize: 'large',
} ),
@@ -58,7 +66,7 @@ const getDescriptionAllowingConversion = ( templateTitle: string ) =>
sprintf(
/* translators: %s is the template title */
__(
"This block serves as a placeholder for your %s. We recommend upgrading to the Single Products block for more features to edit your products visually. Don't worry, you can always revert back.",
'Transform this template into multiple blocks so you can add, remove, reorder, and customize your %s template.',
'woo-gutenberg-products-block'
),
templateTitle
@@ -83,14 +91,34 @@ const getDescription = ( templateTitle: string, canConvert: boolean ) => {
};
const getButtonLabel = () =>
__(
'Upgrade to Blockified Single Product template',
'woo-gutenberg-products-block'
__( 'Transform into blocks', 'woo-gutenberg-products-block' );
const onClickCallback = ( {
clientId,
getBlocks,
replaceBlock,
selectBlock,
}: OnClickCallbackParameter ) => {
replaceBlock( clientId, getBlockifiedTemplate() );
const blocks = getBlocks();
const groupBlock = blocks.find(
( block ) =>
block.name === 'core/group' &&
block.innerBlocks.some(
( innerBlock ) => innerBlock.name === 'woocommerce/breadcrumbs'
)
);
export {
getBlockifiedTemplate,
isConversionPossible,
getDescription,
getButtonLabel,
if ( groupBlock ) {
selectBlock( groupBlock.clientId );
}
};
const blockifyConfig = {
getButtonLabel,
onClickCallback,
getBlockifiedTemplate,
};
export { isConversionPossible, getDescription, blockifyConfig };

View File

@@ -1,46 +1,34 @@
/**
* Internal dependencies
*/
import { TEMPLATES } from '../constants';
import { getTemplateDetailsBySlug } from '../utils';
const TEMPLATES = {
'single-product': {
title: 'Single Product Title',
placeholder: 'Single Product Placeholder',
},
'archive-product': {
title: 'Product Archive Title',
placeholder: 'Product Archive Placeholder',
},
'taxonomy-product_cat': {
title: 'Product Taxonomy Title',
placeholder: 'Product Taxonomy Placeholder',
},
'taxonomy-product_attribute': {
title: 'Product Attribute Title',
placeholder: 'Product Attribute Placeholder',
},
};
describe( 'getTemplateDetailsBySlug', function () {
it( 'should return single-product object when given an exact match', () => {
expect(
getTemplateDetailsBySlug( 'single-product', TEMPLATES )
).toBeTruthy();
expect(
getTemplateDetailsBySlug( 'single-product', TEMPLATES )
).toStrictEqual( TEMPLATES[ 'single-product' ] );
} );
it( 'should return single-product object when given a partial match', () => {
expect(
getTemplateDetailsBySlug( 'single-product-hoodie', TEMPLATES )
).toBeTruthy();
expect(
getTemplateDetailsBySlug( 'single-product-hoodie', TEMPLATES )
).toStrictEqual( TEMPLATES[ 'single-product' ] );
} );
it( 'should return taxonomy-product object when given a partial match', () => {
expect(
getTemplateDetailsBySlug( 'taxonomy-product_tag', TEMPLATES )
).toStrictEqual( TEMPLATES[ 'taxonomy-product_tag' ] );
} );
it( 'should return taxonomy-product object when given an exact match', () => {
expect(
getTemplateDetailsBySlug( 'taxonomy-product_brands', TEMPLATES )
).toStrictEqual( TEMPLATES[ 'taxonomy-product' ] );
} );
it( 'should return null object when given an incorrect match', () => {
expect( getTemplateDetailsBySlug( 'void', TEMPLATES ) ).toBeNull();
} );

View File

@@ -3,17 +3,41 @@
*/
import { type BlockInstance } from '@wordpress/blocks';
export type TemplateDetails = Record< string, Record< string, string > >;
type TemplateDetail = {
type: string;
title: string;
placeholder: string;
};
export type TemplateDetails = Record< string, TemplateDetail >;
export type InheritedAttributes = {
align?: string;
};
export type BlockifiedTemplateConfig = {
export type OnClickCallbackParameter = {
clientId: string;
attributes: Record< string, unknown >;
getBlocks: () => BlockInstance[];
replaceBlock: ( clientId: string, blocks: BlockInstance[] ) => void;
selectBlock: ( clientId: string ) => void;
};
type ConversionConfig = {
onClickCallback: ( params: OnClickCallbackParameter ) => void;
getButtonLabel: () => string;
getBlockifiedTemplate: (
inheritedAttributes: InheritedAttributes
) => BlockInstance[];
isConversionPossible: () => boolean;
getDescription: ( templateTitle: string, canConvert: boolean ) => string;
getButtonLabel: () => string;
};
export type BlockifiedTemplateConfig = {
// Description of the template, shown in the block placeholder.
getDescription: ( templateTitle: string, canConvert: boolean ) => string;
// Returns the skeleton HTML for the template, or can be left blank to use the default fallback image.
getSkeleton?: ( () => JSX.Element ) | undefined;
// Is conversion possible for the template?
isConversionPossible: () => boolean;
// If conversion is possible, returns the config for the template to be blockified.
blockifyConfig?: ConversionConfig | undefined;
};

View File

@@ -4,7 +4,7 @@
"title": "Customer account",
"description": "A block that allows your customers to log in and out of their accounts in your store.",
"category": "woocommerce",
"keywords": [ "WooCommerce" ],
"keywords": [ "WooCommerce", "My Account" ],
"supports": {
"align": true,
"color": {
@@ -13,6 +13,9 @@
"typography": {
"fontSize": true,
"__experimentalFontFamily": true
},
"spacing": {
"margin": true
}
},
"attributes": {

View File

@@ -20,7 +20,7 @@ const Edit = ( {
}: BlockEditProps< Attributes > ) => {
const { className } = attributes;
const blockProps = useBlockProps( {
className: classNames( 'wc-block-customer-account', className ),
className: classNames( 'wc-block-editor-customer-account', className ),
} );
return (

View File

@@ -1,4 +1,23 @@
.wc-block-customer-account__icon-style-toggle {
@import "./style";
.editor-styles-wrapper .is-layout-constrained > .wc-block-editor-customer-account.alignright {
@include flex-justify-content(flex-end);
}
.editor-styles-wrapper .is-layout-constrained > .wc-block-editor-customer-account.alignleft {
@include flex-justify-content(flex-start);
}
.editor-styles-wrapper .is-layout-constrained > .wc-block-editor-customer-account.aligncenter {
@include flex-justify-content(center);
}
.wc-block-editor-customer-account {
display: flex;
padding: em($gap-smaller) em($gap-smaller);
}
.wc-block-editor-customer-account__icon-style-toggle {
width: 100%;
}
@@ -6,7 +25,3 @@
.block-editor-block-card + div > .wc-block-editor-customer-account__link {
padding: 0 $gap $gap 52px;
}
/* In tabbed sidebar (ie: WP >=6.2) */
.wc-block-editor-customer-account__link {
padding: $gap;
}

View File

@@ -11,6 +11,7 @@ import { __ } from '@wordpress/i18n';
*/
import metadata from './block.json';
import edit from './edit';
import './style.scss';
registerBlockType( metadata, {
icon: {

View File

@@ -66,7 +66,9 @@ export const BlockSettings = ( {
return (
<InspectorControls key="inspector">
<AccountSettingsLink />
<PanelBody>
<AccountSettingsLink />
</PanelBody>
<PanelBody
title={ __(
'Display settings',
@@ -123,7 +125,7 @@ export const BlockSettings = ( {
iconStyle: value,
} )
}
className="wc-block-customer-account__icon-style-toggle"
className="wc-block-editor-customer-account__icon-style-toggle"
>
<ToggleGroupControlOption
value={ IconStyle.DEFAULT }
@@ -132,7 +134,7 @@ export const BlockSettings = ( {
icon={ customerAccountStyle }
size={ 16 }
className={ classNames(
'wc-block-customer-account__icon-option',
'wc-block-editor-customer-account__icon-option',
{
active:
iconStyle === IconStyle.DEFAULT,
@@ -148,7 +150,7 @@ export const BlockSettings = ( {
icon={ customerAccountStyleAlt }
size={ 20 }
className={ classNames(
'wc-block-customer-account__icon-option',
'wc-block-editor-customer-account__icon-option',
{
active: iconStyle === IconStyle.ALT,
}

View File

@@ -1,4 +1,23 @@
@mixin flex-justify-content($justification) {
float: none;
justify-content: $justification;
}
.is-layout-constrained > .wp-block-woocommerce-customer-account.alignright {
@include flex-justify-content(flex-end);
}
.is-layout-constrained > .wp-block-woocommerce-customer-account.alignleft {
@include flex-justify-content(flex-start);
}
.is-layout-constrained > .wp-block-woocommerce-customer-account.aligncenter {
@include flex-justify-content(center);
}
.wp-block-woocommerce-customer-account {
display: flex;
a {
text-decoration: none !important;
align-items: center;
@@ -9,11 +28,6 @@
text-decoration: underline !important;
}
.icon + .label,
.wc-block-customer-account__account-icon + .label {
margin-left: $gap-smaller;
}
.icon {
height: em(16px);
width: em(16px);
@@ -22,6 +36,7 @@
.wc-block-customer-account__account-icon {
height: em(23px);
width: em(23px);
padding: em($gap-smaller);
}
}
}

View File

@@ -11,7 +11,6 @@ import { ToolbarButton, ToolbarGroup } from '@wordpress/components';
import { crop } from '@wordpress/icons';
import { WP_REST_API_Category } from 'wp-types';
import { ProductResponseItem } from '@woocommerce/types';
import TextToolbarButton from '@woocommerce/editor-components/text-toolbar-button';
import type { ComponentType, Dispatch, SetStateAction } from 'react';
import type { BlockAlignment } from '@wordpress/blocks';
@@ -112,13 +111,13 @@ export const BlockControls = ( {
allowedTypes={ [ 'image' ] }
/>
{ backgroundImageId && mediaSrc ? (
<TextToolbarButton
<ToolbarButton
onClick={ () =>
setAttributes( { mediaId: 0, mediaSrc: '' } )
}
>
{ __( 'Reset', 'woo-gutenberg-products-block' ) }
</TextToolbarButton>
</ToolbarButton>
) : null }
</ToolbarGroup>
<ToolbarGroup

View File

@@ -4,11 +4,7 @@
import classnames from 'classnames';
import { useState } from '@wordpress/element';
import { ResizableBox } from '@wordpress/components';
/**
* Internal dependencies
*/
import { useThrottle } from '../../utils/useThrottle';
import { useThrottledCallback } from 'use-debounce';
type ResizeCallback = Exclude< ResizableBox.Props[ 'onResize' ], undefined >;
@@ -22,7 +18,7 @@ export const ConstrainedResizable = ( {
const classNames = classnames( className, {
'is-resizing': isResizing,
} );
const throttledResize = useThrottle< ResizeCallback >(
const throttledResize = useThrottledCallback< ResizeCallback >(
( event, direction, elt, _delta ) => {
if ( ! isResizing ) setIsResizing( true );
onResize?.( event, direction, elt, _delta );

View File

@@ -36,6 +36,10 @@ const CONTENT_CONFIG = {
'No product category is selected.',
'woo-gutenberg-products-block'
),
noSelectionButtonLabel: __(
'Select a category',
'woo-gutenberg-products-block'
),
};
const EDIT_MODE_CONFIG = {

View File

@@ -36,6 +36,10 @@ const CONTENT_CONFIG = {
'No product is selected.',
'woo-gutenberg-products-block'
),
noSelectionButtonLabel: __(
'Select a product',
'woo-gutenberg-products-block'
),
};
const EDIT_MODE_CONFIG = {

View File

@@ -64,7 +64,7 @@ export function register(
*/
minHeight: {
type: 'number',
default: getSetting( 'default_height', 500 ),
default: getSetting( 'defaultHeight', 500 ),
},
},
supports: {
@@ -100,7 +100,7 @@ export function register(
editMode: false,
hasParallax: false,
isRepeated: false,
height: getSetting( 'default_height', 500 ),
height: getSetting( 'defaultHeight', 500 ),
mediaSrc: '',
overlayColor: '#000000',
showDesc: true,

View File

@@ -2,7 +2,7 @@
* External dependencies
*/
import type { Block, BlockEditProps } from '@wordpress/blocks';
import { isNumber } from 'lodash';
import { isNumber } from '@woocommerce/types';
export type EditorBlock< T > = Block< T > & BlockEditProps< T >;

View File

@@ -4,14 +4,12 @@
* External dependencies
*/
import type { BlockAlignment } from '@wordpress/blocks';
import { ProductResponseItem } from '@woocommerce/types';
import { __experimentalGetSpacingClassesAndStyles as getSpacingClassesAndStyles } from '@wordpress/block-editor';
import { ProductResponseItem, isEmpty } from '@woocommerce/types';
import { Icon, Placeholder, Spinner } from '@wordpress/components';
import classnames from 'classnames';
import { isEmpty } from 'lodash';
import { useCallback, useState } from '@wordpress/element';
import { WP_REST_API_Category } from 'wp-types';
import { useBorderProps } from '@woocommerce/base-hooks';
import { useStyleProps } from '@woocommerce/base-hooks';
import type { ComponentType, Dispatch, SetStateAction } from 'react';
/**
@@ -29,6 +27,7 @@ import {
interface WithFeaturedItemConfig extends GenericBlockUIConfig {
emptyMessage: string;
noSelectionButtonLabel: string;
}
export interface FeaturedItemRequiredAttributes {
@@ -46,6 +45,7 @@ export interface FeaturedItemRequiredAttributes {
overlayGradient: string;
showDesc: boolean;
showPrice: boolean;
editMode: boolean;
}
interface FeaturedCategoryRequiredAttributes
@@ -94,7 +94,12 @@ type FeaturedItemProps< T extends EditorBlock< T > > =
| ( T & FeaturedProductProps< T > );
export const withFeaturedItem =
( { emptyMessage, icon, label }: WithFeaturedItemConfig ) =>
( {
emptyMessage,
icon,
label,
noSelectionButtonLabel,
}: WithFeaturedItemConfig ) =>
< T extends EditorBlock< T > >( Component: ComponentType< T > ) =>
( props: FeaturedItemProps< T > ) => {
const [ isEditingImage ] = props.useEditingImage;
@@ -142,17 +147,33 @@ export const withFeaturedItem =
);
};
const renderNoItemButton = () => {
return (
<>
<p>{ emptyMessage }</p>
<div style={ { flexBasis: '100%', height: '0' } }></div>
<button
type="button"
className="components-button is-secondary"
onClick={ () => setAttributes( { editMode: true } ) }
>
{ noSelectionButtonLabel }
</button>
</>
);
};
const renderNoItem = () => (
<Placeholder
className={ className }
icon={ <Icon icon={ icon } /> }
label={ label }
>
{ isLoading ? <Spinner /> : emptyMessage }
{ isLoading ? <Spinner /> : renderNoItemButton() }
</Placeholder>
);
const borderProps = useBorderProps( attributes );
const styleProps = useStyleProps( attributes );
const renderItem = () => {
const {
@@ -171,7 +192,7 @@ export const withFeaturedItem =
textColor,
} = attributes;
const classes = classnames(
const containerClass = classnames(
className,
{
'is-selected':
@@ -184,7 +205,8 @@ export const withFeaturedItem =
'is-repeated': isRepeated,
},
dimRatioToClass( dimRatio ),
contentAlign !== 'center' && `has-${ contentAlign }-content`
contentAlign !== 'center' && `has-${ contentAlign }-content`,
styleProps.className
);
const containerStyle = {
@@ -193,11 +215,8 @@ export const withFeaturedItem =
? `var(--wp--preset--color--${ textColor })`
: style?.color?.text,
boxSizing: 'border-box',
};
const wrapperStyle = {
...getSpacingClassesAndStyles( attributes ).style,
minHeight,
...styleProps.style,
};
const isImgElement = ! isRepeated && ! hasParallax;
@@ -223,14 +242,8 @@ export const withFeaturedItem =
showHandle={ isSelected }
style={ { minHeight } }
/>
<div
className={ classes }
style={ { containerStyle, ...borderProps.style } }
>
<div
className={ `${ className }__wrapper` }
style={ wrapperStyle }
>
<div className={ containerClass } style={ containerStyle }>
<div className={ `${ className }__wrapper` }>
<div
className="background-dim__overlay"
style={ overlayStyle }

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