Merged in feature/MAW-855-import-code-into-aws (pull request #2)

code import from pantheon

* code import from pantheon
This commit is contained in:
Tony Volpe
2023-12-04 23:08:14 +00:00
parent 8c9b1312bc
commit 8f4b5efda6
4766 changed files with 185592 additions and 239967 deletions

View File

@@ -11,6 +11,7 @@ import { __ } from '@wordpress/i18n';
import { getSettingWithCoercion } from '@woocommerce/settings';
import {
AttributeObject,
AttributeTerm,
isAttributeQueryCollection,
isAttributeTermCollection,
isBoolean,
@@ -47,7 +48,7 @@ const ActiveAttributeFilters = ( {
displayStyle,
isLoadingCallback,
}: ActiveAttributeFiltersProps ) => {
const { results, isLoading } = useCollection( {
const { results, isLoading } = useCollection< AttributeTerm >( {
namespace: '/wc/store/v1',
resourceName: 'products/attributes/terms',
resourceValues: [ attributeObject.id ],
@@ -73,7 +74,7 @@ const ActiveAttributeFilters = ( {
const attributeLabel = attributeObject.label;
const filteringForPhpTemplate = getSettingWithCoercion(
'is_rendering_php_template',
'isRenderingPhpTemplate',
false,
isBoolean
);

View File

@@ -6,7 +6,7 @@ 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 Label from '@woocommerce/base-components/label';
import { Label } from '@woocommerce/blocks-components';
import {
isAttributeQueryCollection,
isBoolean,
@@ -59,7 +59,7 @@ const ActiveFiltersBlock = ( {
const isMounted = useIsMounted();
const componentHasMounted = isMounted();
const filteringForPhpTemplate = getSettingWithCoercion(
'is_rendering_php_template',
'isRenderingPhpTemplate',
false,
isBoolean
);
@@ -323,7 +323,7 @@ const ActiveFiltersBlock = ( {
);
const hasFilterableProducts = getSettingWithCoercion(
'has_filterable_products',
'hasFilterableProducts',
false,
isBoolean
);

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

@@ -3,8 +3,7 @@
*/
import { __, sprintf } from '@wordpress/i18n';
import { formatPrice } from '@woocommerce/price-format';
import { RemovableChip } from '@woocommerce/base-components/chip';
import Label from '@woocommerce/base-components/label';
import { Label, RemovableChip } from '@woocommerce/blocks-components';
import { getQueryArgs, addQueryArgs, removeQueryArgs } from '@wordpress/url';
import { changeUrl } from '@woocommerce/utils';
import { Icon, closeSmall } from '@wordpress/icons';

View File

@@ -19,6 +19,7 @@ import { getSettingWithCoercion } from '@woocommerce/settings';
import { getQueryArgs, removeQueryArgs } from '@wordpress/url';
import {
AttributeQuery,
AttributeTerm,
isAttributeQueryCollection,
isBoolean,
isString,
@@ -72,19 +73,19 @@ 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
);
@@ -124,11 +125,12 @@ const AttributeFilterBlock = ( {
useQueryStateByKey( 'attributes', [] );
const { results: attributeTerms, isLoading: attributeTermsLoading } =
useCollection( {
useCollection< AttributeTerm >( {
namespace: '/wc/store/v1',
resourceName: 'products/attributes/terms',
resourceValues: [ attributeObject?.id || 0 ],
shouldSelect: blockAttributes.attributeId > 0,
query: { orderby: 'menu_order' },
} );
const { results: filteredCounts, isLoading: filteredCountsLoading } =
@@ -544,9 +546,6 @@ const AttributeFilterBlock = ( {
'single-selection': ! multiple,
'is-loading': isLoading,
} ) }
style={ {
borderStyle: 'none',
} }
suggestions={ displayedOptions
.filter(
( option ) =>

View File

@@ -1,7 +1,8 @@
/**
* External dependencies
*/
import CheckboxList from '@woocommerce/base-components/checkbox-list';
import { CheckboxList } from '@woocommerce/blocks-components';
/**
* Internal dependencies
*/

View File

@@ -69,7 +69,6 @@ const Edit = ( {
}: EditProps ) => {
const {
attributeId,
className,
displayStyle,
heading,
headingLevel,
@@ -353,6 +352,7 @@ const Edit = ( {
href={ getAdminLink(
'edit.php?post_type=product&page=product_attributes'
) }
target="_top"
>
{ __( 'Add new attribute', 'woo-gutenberg-products-block' ) +
' ' }
@@ -362,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>
@@ -419,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';
@@ -27,13 +26,6 @@ registerBlockType( metadata, {
},
supports: {
...metadata.supports,
...( isFeaturePluginBuild() && {
__experimentalBorder: {
radius: false,
color: true,
width: false,
},
} ),
},
attributes: {
...metadata.attributes,

View File

@@ -0,0 +1,33 @@
/**
* External dependencies
*/
import { SVG, Rect } from '@wordpress/primitives';
export const queryPaginationIcon = (
<SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<Rect
x="4"
y="10.5"
width="6"
height="3"
rx="1.5"
fill="currentColor"
/>
<Rect
x="12"
y="10.5"
width="3"
height="3"
rx="1.5"
fill="currentColor"
/>
<Rect
x="17"
y="10.5"
width="3"
height="3"
rx="1.5"
fill="currentColor"
/>
</SVG>
);

View File

@@ -3,13 +3,14 @@
*/
import { registerBlockType } from '@wordpress/blocks';
import { isFeaturePluginBuild } from '@woocommerce/block-settings';
import { Icon, queryPagination } from '@wordpress/icons';
import { Icon } from '@wordpress/icons';
/**
* Internal dependencies
*/
import metadata from './block.json';
import edit from './edit';
import { queryPaginationIcon } from './icon';
import './style.scss';
const featurePluginSupport = {
@@ -32,7 +33,7 @@ registerBlockType( metadata, {
icon: {
src: (
<Icon
icon={ queryPagination }
icon={ queryPaginationIcon }
className="wc-block-editor-components-block-icon"
/>
),

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

@@ -1,22 +1,16 @@
$border-width: 1px;
$border-radius: 5px;
.wc-block-components-express-payment {
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,17 +21,22 @@ $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);
margin-top: calc($universal-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;
@@ -45,17 +44,17 @@ $border-radius: 5px;
left: 0;
position: absolute;
right: 0;
top: -$border-radius;
top: -$universal-border-radius;
vertical-align: middle;
// Pseudo-elements used to show the border before and after the title.
&::before {
border-left: $border-width solid currentColor;
border-top: $border-width solid currentColor;
border-radius: $border-radius 0 0 0;
border-radius: $universal-border-radius 0 0 0;
content: "";
display: block;
height: $border-radius - $border-width;
height: $universal-border-radius - $border-width;
margin-right: $gap-small;
opacity: 0.3;
pointer-events: none;
@@ -65,10 +64,10 @@ $border-radius: 5px;
&::after {
border-right: $border-width solid currentColor;
border-top: $border-width solid currentColor;
border-radius: 0 $border-radius 0 0;
border-radius: 0 $universal-border-radius 0 0;
content: "";
display: block;
height: $border-radius - $border-width;
height: $universal-border-radius - $border-width;
margin-left: $gap-small;
opacity: 0.3;
pointer-events: none;
@@ -83,10 +82,10 @@ $border-radius: 5px;
.wc-block-components-express-payment__content {
@include with-translucent-border(0 $border-width $border-width);
padding: #{$gap-large - $border-radius} $gap-large $gap-large;
padding: #{$gap-large - $universal-border-radius} $gap-large $gap-large;
&::after {
border-radius: 0 0 $border-radius $border-radius;
border-radius: 0 0 $universal-border-radius $universal-border-radius;
}
> p {

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

@@ -8,7 +8,7 @@ import {
import { cloneElement, useCallback } from '@wordpress/element';
import { useEditorContext } from '@woocommerce/base-context';
import classNames from 'classnames';
import RadioControlAccordion from '@woocommerce/base-components/radio-control-accordion';
import { RadioControlAccordion } from '@woocommerce/blocks-components';
import { useDispatch, useSelect } from '@wordpress/data';
import { getPaymentMethods } from '@woocommerce/blocks-registry';

View File

@@ -2,7 +2,7 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import Label from '@woocommerce/base-components/label';
import { Label } from '@woocommerce/blocks-components';
import { useSelect } from '@wordpress/data';
import { PAYMENT_STORE_KEY } from '@woocommerce/block-data';

View File

@@ -4,7 +4,10 @@
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 {
RadioControl,
RadioControlOptionType,
} from '@woocommerce/blocks-components';
import {
usePaymentMethodInterface,
useStoreEvents,
@@ -13,7 +16,6 @@ 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
@@ -87,7 +89,7 @@ const SavedPaymentMethodOptions = () => {
const { removeNotice } = useDispatch( 'core/notices' );
const { dispatchCheckoutEvent } = useStoreEvents();
const options = useMemo< RadioControlOption[] >( () => {
const options = useMemo< RadioControlOptionType[] >( () => {
const types = Object.keys( savedPaymentMethods );
// Get individual payment methods from saved payment methods and put them into a unique array.
@@ -145,7 +147,7 @@ const SavedPaymentMethodOptions = () => {
} );
return mappedOptions.filter(
( option ) => typeof option !== 'undefined'
) as RadioControlOption[];
) as RadioControlOptionType[];
}, [
savedPaymentMethods,
paymentMethods,

View File

@@ -39,7 +39,7 @@
line-height: 1.375; // =22px when font-size is 16px.
background-color: #fff;
padding: em($gap-small) 0 em($gap-small) $gap;
border-radius: 4px;
border-radius: $universal-border-radius;
border: 1px solid $input-border-gray;
width: 100%;
font-family: inherit;
@@ -201,6 +201,16 @@
@include with-translucent-border(1px 1px 0 1px);
}
.wc-block-components-radio-control-accordion-option:first-child::after {
border-top-left-radius: $universal-border-radius;
border-top-right-radius: $universal-border-radius;
}
.wc-block-components-radio-control-accordion-option:last-child::after {
border-bottom-left-radius: $universal-border-radius;
border-bottom-right-radius: $universal-border-radius;
}
.wc-block-components-radio-control__option:last-child::after,
.wc-block-components-radio-control-accordion-option:last-child::after {
border-width: 1px;

View File

@@ -27,19 +27,24 @@ jest.mock( '../saved-payment-method-options', () => ( { onChange } ) => {
);
} );
jest.mock(
'@woocommerce/base-components/radio-control-accordion',
() =>
( { onChange } ) =>
(
<>
<span>Payment method options</span>
<button onClick={ () => onChange( 'credit-card' ) }>
Select new payment
</button>
</>
)
);
jest.mock( '@woocommerce/blocks-components', () => {
const originalModule = jest.requireActual(
'@woocommerce/blocks-components'
);
return {
__esModule: true,
...originalModule,
RadioControlAccordion: ( { onChange } ) => (
<>
<span>Payment method options</span>
<button onClick={ () => onChange( 'credit-card' ) }>
Select new payment
</button>
</>
),
};
} );
const originalSelect = jest.requireActual( '@wordpress/data' ).select;
const selectMock = jest

View File

@@ -12,12 +12,10 @@ 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 { IncompatibleExtensionsNotice } from '@woocommerce/editor-components/incompatible-extension-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;
@@ -39,49 +37,73 @@ const withSidebarNotices = createHigherOrderComponent(
isSelected: isBlockSelected,
} = props;
const isBlockTheme = getSetting( 'isBlockTheme' );
const [
isIncompatiblePaymentGatewaysNoticeDismissed,
setIsIncompatiblePaymentGatewaysNoticeDismissed,
isIncompatibleExtensionsNoticeDismissed,
setIsIncompatibleExtensionsNoticeDismissed,
] = useState( true );
const toggleIncompatiblePaymentGatewaysNoticeDismissedStatus = (
const toggleIncompatibleExtensionsNoticeDismissedStatus = (
isDismissed: boolean
) => {
setIsIncompatiblePaymentGatewaysNoticeDismissed( isDismissed );
setIsIncompatibleExtensionsNoticeDismissed( isDismissed );
};
const { isCart, isCheckout, isPaymentMethodsBlock, hasPaymentMethods } =
useSelect( ( select ) => {
const { getBlockParentsByBlockName, getBlockName } =
select( blockEditorStore );
const parent = getBlockParentsByBlockName( clientId, [
'woocommerce/cart',
'woocommerce/checkout',
] ).map( getBlockName );
const currentBlockName = getBlockName( clientId );
return {
isCart:
parent.includes( 'woocommerce/cart' ) ||
currentBlockName === 'woocommerce/cart',
isCheckout:
parent.includes( 'woocommerce/checkout' ) ||
currentBlockName === 'woocommerce/checkout',
isPaymentMethodsBlock:
currentBlockName ===
'woocommerce/checkout-payment-block',
hasPaymentMethods:
select(
PAYMENT_STORE_KEY
).paymentMethodsInitialized() &&
Object.keys(
select(
PAYMENT_STORE_KEY
).getAvailablePaymentMethods()
).length > 0,
};
} );
const {
isCart,
isCheckout,
isPaymentMethodsBlock,
hasPaymentMethods,
parentId,
} = useSelect( ( select ) => {
const { getBlockParentsByBlockName, getBlockName } =
select( blockEditorStore );
const parents = getBlockParentsByBlockName( clientId, [
'woocommerce/cart',
'woocommerce/checkout',
] ).reduce(
(
accumulator: Record< string, string >,
parentClientId: string
) => {
const parentName = getBlockName( parentClientId );
accumulator[ parentName ] = parentClientId;
return accumulator;
},
{}
);
const currentBlockName = getBlockName( clientId );
const parentBlockIsCart =
Object.keys( parents ).includes( 'woocommerce/cart' );
const parentBlockIsCheckout = Object.keys( parents ).includes(
'woocommerce/checkout'
);
const currentBlockIsCart =
currentBlockName === 'woocommerce/cart' || parentBlockIsCart;
const currentBlockIsCheckout =
currentBlockName === 'woocommerce/checkout' ||
parentBlockIsCheckout;
const targetParentBlock = currentBlockIsCart
? 'woocommerce/cart'
: 'woocommerce/checkout';
return {
isCart: currentBlockIsCart,
isCheckout: currentBlockIsCheckout,
parentId:
currentBlockName === targetParentBlock
? clientId
: parents[ targetParentBlock ],
isPaymentMethodsBlock:
currentBlockName === 'woocommerce/checkout-payment-block',
hasPaymentMethods:
select( PAYMENT_STORE_KEY ).paymentMethodsInitialized() &&
Object.keys(
select( PAYMENT_STORE_KEY ).getAvailablePaymentMethods()
).length > 0,
};
} );
// Show sidebar notices only when a WooCommerce block is selected.
if (
@@ -95,28 +117,19 @@ const withSidebarNotices = createHigherOrderComponent(
return (
<>
<InspectorControls>
<IncompatiblePaymentGatewaysNotice
<IncompatibleExtensionsNotice
toggleDismissedStatus={
toggleIncompatiblePaymentGatewaysNoticeDismissedStatus
toggleIncompatibleExtensionsNoticeDismissedStatus
}
block={
isCheckout
? 'woocommerce/checkout'
: 'woocommerce/cart'
isCart ? 'woocommerce/cart' : 'woocommerce/checkout'
}
clientId={ parentId }
/>
{ isBlockTheme ? (
<TemplateNotice
block={ isCheckout ? 'checkout' : 'cart' }
/>
) : (
<DefaultNotice
block={ isCheckout ? 'checkout' : 'cart' }
/>
) }
<DefaultNotice block={ isCheckout ? 'checkout' : 'cart' } />
{ isIncompatiblePaymentGatewaysNoticeDismissed ? (
{ isIncompatibleExtensionsNoticeDismissed ? (
<CartCheckoutSidebarCompatibilityNotice
block={ isCheckout ? 'checkout' : 'cart' }
/>

View File

@@ -46,6 +46,15 @@ export const useForcedLayout = ( {
const { replaceInnerBlocks } = dispatch( 'core/block-editor' );
return registry.subscribe( () => {
const currentBlock = registry
.select( 'core/block-editor' )
.getBlock( clientId );
// If the block is removed we shouldn't reinsert its inner blocks.
if ( ! currentBlock ) {
return;
}
const innerBlocks = registry
.select( 'core/block-editor' )
.getBlocks( clientId );

View File

@@ -13,6 +13,9 @@ import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundar
import { EditorProvider, CartProvider } from '@woocommerce/base-context';
import { previewCart } from '@woocommerce/resource-previews';
import { SlotFillProvider } from '@woocommerce/blocks-checkout';
import { useEffect, useRef } from '@wordpress/element';
import { getQueryArg } from '@wordpress/url';
import { dispatch, select } from '@wordpress/data';
/**
* Internal dependencies
@@ -37,7 +40,7 @@ const ALLOWED_BLOCKS = [
'woocommerce/empty-cart-block',
];
export const Edit = ( { className, attributes, setAttributes } ) => {
export const Edit = ( { clientId, className, attributes, setAttributes } ) => {
const { hasDarkControls, currentView, isPreview = false } = attributes;
const defaultTemplate = [
[ 'woocommerce/filled-cart-block', {}, [] ],
@@ -49,6 +52,22 @@ export const Edit = ( { className, attributes, setAttributes } ) => {
} ),
} );
// This focuses on the block when a certain query param is found. This is used on the link from the task list.
const focus = useRef( getQueryArg( window.location.href, 'focus' ) );
useEffect( () => {
if (
focus.current === 'cart' &&
! select( 'core/block-editor' ).hasSelectedBlock()
) {
dispatch( 'core/block-editor' ).selectBlock( clientId );
dispatch( 'core/interface' ).enableComplementaryArea(
'core/edit-site',
'edit-site/block-inspector'
);
}
}, [ clientId ] );
return (
<div { ...blockProps }>
<InspectorControls>

View File

@@ -45,6 +45,24 @@ const settings = {
attributes: blockAttributes,
edit: Edit,
save: Save,
transforms: {
to: [
{
type: 'block',
blocks: [ 'woocommerce/classic-shortcode' ],
transform: ( attributes ) => {
return createBlock(
'woocommerce/classic-shortcode',
{
shortcode: 'cart',
align: attributes.align,
},
[]
);
},
},
],
},
// Migrates v1 to v2 checkout.
deprecated: [
{

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

@@ -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

@@ -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,18 +0,0 @@
/**
* External dependencies
*/
import { getSetting } from '@woocommerce/settings';
export default {
isShippingCalculatorEnabled: {
type: 'boolean',
default: getSetting( 'isShippingCalculatorEnabled', true ),
},
lock: {
type: 'object',
default: {
move: false,
remove: true,
},
},
};

View File

@@ -5,14 +5,9 @@ import { TotalsShipping } from '@woocommerce/base-components/cart-checkout';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import { TotalsWrapper } from '@woocommerce/blocks-checkout';
import { getSetting } from '@woocommerce/settings';
const Block = ( {
className,
isShippingCalculatorEnabled,
}: {
className: string;
isShippingCalculatorEnabled: boolean;
} ): JSX.Element | null => {
const Block = ( { className }: { className: string } ): JSX.Element | null => {
const { cartTotals, cartNeedsShipping } = useStoreCart();
if ( ! cartNeedsShipping ) {
@@ -24,7 +19,10 @@ const Block = ( {
return (
<TotalsWrapper className={ className }>
<TotalsShipping
showCalculator={ isShippingCalculatorEnabled }
showCalculator={ getSetting< boolean >(
'isShippingCalculatorEnabled',
true
) }
showRateSelector={ true }
values={ cartTotals }
currency={ totalsCurrency }

View File

@@ -3,8 +3,8 @@
*/
import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, ToggleControl } from '@wordpress/components';
import { getSetting } from '@woocommerce/settings';
import { PanelBody, ExternalLink } from '@wordpress/components';
import { ADMIN_URL, getSetting } from '@woocommerce/settings';
import Noninteractive from '@woocommerce/base-components/noninteractive';
/**
@@ -14,19 +14,16 @@ import Block from './block';
export const Edit = ( {
attributes,
setAttributes,
}: {
attributes: {
isShippingCalculatorEnabled: boolean;
className: string;
lock: {
move: boolean;
remove: boolean;
};
};
setAttributes: ( attributes: Record< string, unknown > ) => void;
} ): JSX.Element => {
const { isShippingCalculatorEnabled, className } = attributes;
const { className } = attributes;
const shippingEnabled = getSetting( 'shippingEnabled', true );
const blockProps = useBlockProps();
@@ -36,35 +33,29 @@ export const Edit = ( {
{ !! shippingEnabled && (
<PanelBody
title={ __(
'Shipping rates',
'Shipping Calculations',
'woo-gutenberg-products-block'
) }
>
<ToggleControl
label={ __(
'Shipping calculator',
<p className="wc-block-checkout__controls-text">
{ __(
'Options that control shipping can be managed in your store settings.',
'woo-gutenberg-products-block'
) }
help={ __(
'Allow customers to estimate shipping by entering their address.',
</p>
<ExternalLink
href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=shipping&section=options` }
>
{ __(
'Manage shipping options',
'woo-gutenberg-products-block'
) }
checked={ isShippingCalculatorEnabled }
onChange={ () =>
setAttributes( {
isShippingCalculatorEnabled:
! isShippingCalculatorEnabled,
} )
}
/>
</ExternalLink>{ ' ' }
</PanelBody>
) }
</InspectorControls>
<Noninteractive>
<Block
className={ className }
isShippingCalculatorEnabled={ isShippingCalculatorEnabled }
/>
<Block className={ className } />
</Noninteractive>
</div>
);

View File

@@ -1,12 +1,6 @@
/**
* External dependencies
*/
import { withFilteredAttributes } from '@woocommerce/shared-hocs';
/**
* Internal dependencies
*/
import Block from './block';
import attributes from './attributes';
export default withFilteredAttributes( attributes )( Block );
export default Block;

View File

@@ -9,7 +9,6 @@ import { registerBlockType } from '@wordpress/blocks';
* Internal dependencies
*/
import { Edit, Save } from './edit';
import attributes from './attributes';
registerBlockType( 'woocommerce/cart-order-summary-shipping-block', {
icon: {
@@ -20,7 +19,6 @@ registerBlockType( 'woocommerce/cart-order-summary-shipping-block', {
/>
),
},
attributes,
edit: Edit,
save: Save,
} );

View File

@@ -65,7 +65,7 @@ const defaultTemplate = [
[
'woocommerce/product-new',
{
columns: 3,
columns: 4,
rows: 1,
},
],

View File

@@ -19,12 +19,15 @@ const FrontendBlock = ( {
} ): JSX.Element | null => {
const { cartItems, cartIsLoading } = useStoreCart();
useEffect( () => {
if ( cartItems.length !== 0 || cartIsLoading ) {
return;
}
dispatchEvent( 'wc-blocks_render_blocks_frontend', {
element: document.body.querySelector(
'.wp-block-woocommerce-cart'
),
} );
}, [] );
}, [ cartIsLoading, cartItems ] );
if ( ! cartIsLoading && cartItems.length === 0 ) {
return <div className={ className }>{ children }</div>;
}

View File

@@ -32,18 +32,18 @@ import OrderSummarySubtotalBlock from '../inner-blocks/cart-order-summary-subtot
import OrderSummaryShippingBlock from '../inner-blocks/cart-order-summary-shipping/frontend';
import OrderSummaryTaxesBlock from '../inner-blocks/cart-order-summary-taxes/frontend';
jest.mock( '@wordpress/compose', () => ( {
...jest.requireActual( '@wordpress/compose' ),
useResizeObserver: jest.fn().mockReturnValue( [ null, { width: 0 } ] ),
} ) );
const CartBlock = ( {
attributes = {
showRateAfterTaxName: false,
isShippingCalculatorEnabled: false,
checkoutPageId: 0,
},
} ) => {
const {
showRateAfterTaxName,
isShippingCalculatorEnabled,
checkoutPageId,
} = attributes;
const { showRateAfterTaxName, checkoutPageId } = attributes;
return (
<Cart attributes={ attributes }>
<FilledCart>
@@ -54,11 +54,7 @@ const CartBlock = ( {
<OrderSummaryBlock>
<OrderSummaryHeadingBlock />
<OrderSummarySubtotalBlock />
<OrderSummaryShippingBlock
isShippingCalculatorEnabled={
isShippingCalculatorEnabled
}
/>
<OrderSummaryShippingBlock />
<OrderSummaryTaxesBlock
showRateAfterTaxName={ showRateAfterTaxName }
/>

View File

@@ -0,0 +1,81 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { ALLOWED_COUNTRIES } from '@woocommerce/block-settings';
import type {
CartShippingAddress,
CartBillingAddress,
} from '@woocommerce/types';
/**
* Internal dependencies
*/
import './style.scss';
const AddressCard = ( {
address,
onEdit,
target,
showPhoneField,
}: {
address: CartShippingAddress | CartBillingAddress;
onEdit: () => void;
target: string;
showPhoneField: boolean;
} ): JSX.Element | null => {
return (
<div className="wc-block-components-address-card">
<address>
<span className="wc-block-components-address-card__address-section">
{ address.first_name + ' ' + address.last_name }
</span>
<div className="wc-block-components-address-card__address-section">
{ [
address.address_1,
address.address_2,
address.city,
address.state,
address.postcode,
ALLOWED_COUNTRIES[ address.country ]
? ALLOWED_COUNTRIES[ address.country ]
: address.country,
]
.filter( ( field ) => !! field )
.map( ( field, index ) => (
<span key={ `address-` + index }>{ field }</span>
) ) }
</div>
{ address.phone && showPhoneField ? (
<div
key={ `address-phone` }
className="wc-block-components-address-card__address-section"
>
{ address.phone }
</div>
) : (
''
) }
</address>
{ onEdit && (
<a
role="button"
href={ '#' + target }
className="wc-block-components-address-card__edit"
aria-label={ __(
'Edit address',
'woo-gutenberg-products-block'
) }
onClick={ ( e ) => {
onEdit();
e.preventDefault();
} }
>
{ __( 'Edit', 'woo-gutenberg-products-block' ) }
</a>
) }
</div>
);
};
export default AddressCard;

View File

@@ -0,0 +1,41 @@
.wc-block-components-address-card {
border: 1px solid $universal-border;
@include font-size(regular);
padding: em($gap);
margin: 0;
border-radius: $universal-border-radius;
display: flex;
justify-content: flex-start;
align-items: flex-start;
address {
margin: 0;
font-style: normal;
.wc-block-components-address-card__address-section {
display: block;
margin: 0 0 2px 0;
span {
display: inline-block;
padding: 0 4px 0 0;
&::after {
content: ", ";
}
&:last-child::after {
content: "";
}
}
&:last-child {
margin-bottom: 0;
}
&:first-child {
font-weight: bold;
}
}
}
}
.wc-block-components-address-card__edit {
margin: 0 0 0 auto;
text-decoration: none;
@include font-size(small);
}

View File

@@ -0,0 +1,43 @@
/**
* External dependencies
*/
import classnames from 'classnames';
/**
* Internal dependencies
*/
import './style.scss';
/**
* Wrapper for address fields which handles the edit/preview transition. Form fields are always rendered so that
* validation can occur.
*/
export const AddressWrapper = ( {
isEditing = false,
addressCard,
addressForm,
}: {
isEditing: boolean;
addressCard: () => JSX.Element;
addressForm: () => JSX.Element;
} ): JSX.Element | null => {
const wrapperClasses = classnames(
'wc-block-components-address-address-wrapper',
{
'is-editing': isEditing,
}
);
return (
<div className={ wrapperClasses }>
<div className="wc-block-components-address-card-wrapper">
{ addressCard() }
</div>
<div className="wc-block-components-address-form-wrapper">
{ addressForm() }
</div>
</div>
);
};
export default AddressWrapper;

View File

@@ -0,0 +1,32 @@
.wc-block-components-address-address-wrapper {
position: relative;
.wc-block-components-address-card-wrapper,
.wc-block-components-address-form-wrapper {
transition: all 300ms ease-in-out;
width: 100%;
}
&.is-editing {
.wc-block-components-address-form-wrapper {
opacity: 1;
}
.wc-block-components-address-card-wrapper {
opacity: 0;
visibility: hidden;
position: absolute;
top: 0;
}
}
&:not(.is-editing) {
.wc-block-components-address-form-wrapper {
opacity: 0;
visibility: hidden;
height: 0;
}
.wc-block-components-address-card-wrapper {
opacity: 1;
}
}
}

View File

@@ -8,6 +8,7 @@
margin: 0 auto 1em;
display: block;
color: inherit;
fill: currentColor;
}
.wc-block-checkout-error__title {
display: block;

View File

@@ -21,6 +21,9 @@ import {
} from '@wordpress/components';
import { SlotFillProvider } from '@woocommerce/blocks-checkout';
import type { TemplateArray } from '@wordpress/blocks';
import { useEffect, useRef } from '@wordpress/element';
import { getQueryArg } from '@wordpress/url';
import { dispatch, select } from '@wordpress/data';
/**
* Internal dependencies
@@ -46,9 +49,11 @@ const ALLOWED_BLOCKS: string[] = [
];
export const Edit = ( {
clientId,
attributes,
setAttributes,
}: {
clientId: string;
attributes: Attributes;
setAttributes: ( attributes: Record< string, unknown > ) => undefined;
} ): JSX.Element => {
@@ -66,6 +71,22 @@ export const Edit = ( {
isPreview = false,
} = attributes;
// This focuses on the block when a certain query param is found. This is used on the link from the task list.
const focus = useRef( getQueryArg( window.location.href, 'focus' ) );
useEffect( () => {
if (
focus.current === 'checkout' &&
! select( 'core/block-editor' ).hasSelectedBlock()
) {
dispatch( 'core/block-editor' ).selectBlock( clientId );
dispatch( 'core/interface' ).enableComplementaryArea(
'core/edit-site',
'edit-site/block-inspector'
);
}
}, [ clientId ] );
const defaultTemplate = [
[ 'woocommerce/checkout-fields-block', {}, [] ],
[ 'woocommerce/checkout-totals-block', {}, [] ],

View File

@@ -31,6 +31,24 @@ const settings = {
},
edit: Edit,
save: Save,
transforms: {
to: [
{
type: 'block',
blocks: [ 'woocommerce/classic-shortcode' ],
transform: ( attributes ) => {
return createBlock(
'woocommerce/classic-shortcode',
{
shortcode: 'checkout',
align: attributes.align,
},
[]
);
},
},
],
},
// Migrates v1 to v2 checkout.
deprecated: [
{

View File

@@ -1,26 +1,27 @@
/**
* External dependencies
*/
import { useMemo, useEffect, Fragment, useState } from '@wordpress/element';
import { useMemo, Fragment } from '@wordpress/element';
import { useEffectOnce } from 'usehooks-ts';
import {
useCheckoutAddress,
useStoreEvents,
useEditorContext,
noticeContexts,
} from '@woocommerce/base-context';
import { AddressForm } from '@woocommerce/base-components/cart-checkout';
import Noninteractive from '@woocommerce/base-components/noninteractive';
import type {
BillingAddress,
ShippingAddress,
AddressField,
AddressFields,
} from '@woocommerce/settings';
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
import { useSelect } from '@wordpress/data';
import { CART_STORE_KEY } from '@woocommerce/block-data';
/**
* Internal dependencies
*/
import PhoneNumber from '../../phone-number';
import CustomerAddress from './customer-address';
const Block = ( {
showCompanyField = false,
@@ -28,48 +29,38 @@ const Block = ( {
showPhoneField = false,
requireCompanyField = false,
requirePhoneField = false,
forceEditing = false,
}: {
showCompanyField: boolean;
showApartmentField: boolean;
showPhoneField: boolean;
requireCompanyField: boolean;
requirePhoneField: boolean;
forceEditing?: boolean;
} ): JSX.Element => {
const {
defaultAddressFields,
billingAddress,
setBillingAddress,
setShippingAddress,
setBillingPhone,
setShippingPhone,
useBillingAsShipping,
} = useCheckoutAddress();
const { dispatchCheckoutEvent } = useStoreEvents();
const { billingAddress, setShippingAddress, useBillingAsShipping } =
useCheckoutAddress();
const { isEditor } = useEditorContext();
// Clears data if fields are hidden.
useEffect( () => {
if ( ! showPhoneField ) {
setBillingPhone( '' );
}
}, [ showPhoneField, setBillingPhone ] );
const [ addressesSynced, setAddressesSynced ] = useState( false );
// Syncs shipping address with billing address if "Force shipping to the customer billing address" is enabled.
useEffect( () => {
if ( addressesSynced ) {
return;
}
useEffectOnce( () => {
if ( useBillingAsShipping ) {
setShippingAddress( billingAddress );
const { email, ...addressValues } = billingAddress;
const syncValues: Partial< ShippingAddress > = {
...addressValues,
};
if ( ! showPhoneField ) {
delete syncValues.phone;
}
if ( showCompanyField ) {
delete syncValues.company;
}
setShippingAddress( syncValues );
}
setAddressesSynced( true );
}, [
addressesSynced,
setShippingAddress,
billingAddress,
useBillingAsShipping,
] );
} );
const addressFieldsConfig = useMemo( () => {
return {
@@ -87,54 +78,31 @@ const Block = ( {
showApartmentField,
] ) as Record< keyof AddressFields, Partial< AddressField > >;
const AddressFormWrapperComponent = isEditor ? Noninteractive : Fragment;
const WrapperComponent = isEditor ? Noninteractive : Fragment;
const noticeContext = useBillingAsShipping
? [ noticeContexts.BILLING_ADDRESS, noticeContexts.SHIPPING_ADDRESS ]
: [ noticeContexts.BILLING_ADDRESS ];
const { cartDataLoaded } = useSelect( ( select ) => {
const store = select( CART_STORE_KEY );
return {
cartDataLoaded: store.hasFinishedResolution( 'getCartData' ),
};
} );
return (
<AddressFormWrapperComponent>
<>
<StoreNoticesContainer context={ noticeContext } />
<AddressForm
id="billing"
type="billing"
onChange={ ( values: Partial< BillingAddress > ) => {
setBillingAddress( values );
if ( useBillingAsShipping ) {
setShippingAddress( values );
dispatchCheckoutEvent( 'set-shipping-address' );
}
dispatchCheckoutEvent( 'set-billing-address' );
} }
values={ billingAddress }
fields={
Object.keys(
defaultAddressFields
) as ( keyof AddressFields )[]
}
fieldConfig={ addressFieldsConfig }
/>
{ showPhoneField && (
<PhoneNumber
id={ 'billing-phone' }
errorId={ 'billing_phone' }
isRequired={ requirePhoneField }
value={ billingAddress.phone }
onChange={ ( value ) => {
setBillingPhone( value );
dispatchCheckoutEvent( 'set-phone-number', {
step: 'billing',
} );
if ( useBillingAsShipping ) {
setShippingPhone( value );
dispatchCheckoutEvent( 'set-phone-number', {
step: 'shipping',
} );
}
} }
/>
) }
</AddressFormWrapperComponent>
<WrapperComponent>
{ cartDataLoaded ? (
<CustomerAddress
addressFieldsConfig={ addressFieldsConfig }
showPhoneField={ showPhoneField }
requirePhoneField={ requirePhoneField }
forceEditing={ forceEditing }
/>
) : null }
</WrapperComponent>
</>
);
};

View File

@@ -0,0 +1,162 @@
/**
* External dependencies
*/
import { useState, useCallback, useEffect } from '@wordpress/element';
import { AddressForm } from '@woocommerce/base-components/cart-checkout';
import { useCheckoutAddress, useStoreEvents } from '@woocommerce/base-context';
import type {
BillingAddress,
AddressField,
AddressFields,
} from '@woocommerce/settings';
import { useSelect } from '@wordpress/data';
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
/**
* Internal dependencies
*/
import AddressWrapper from '../../address-wrapper';
import PhoneNumber from '../../phone-number';
import AddressCard from '../../address-card';
const CustomerAddress = ( {
addressFieldsConfig,
showPhoneField,
requirePhoneField,
forceEditing = false,
}: {
addressFieldsConfig: Record< keyof AddressFields, Partial< AddressField > >;
showPhoneField: boolean;
requirePhoneField: boolean;
forceEditing?: boolean;
} ) => {
const {
defaultAddressFields,
billingAddress,
setShippingAddress,
setBillingAddress,
setBillingPhone,
setShippingPhone,
useBillingAsShipping,
} = useCheckoutAddress();
const { dispatchCheckoutEvent } = useStoreEvents();
const hasAddress = !! (
billingAddress.address_1 &&
( billingAddress.first_name || billingAddress.last_name )
);
const [ editing, setEditing ] = useState( ! hasAddress || forceEditing );
// Forces editing state if store has errors.
const { hasValidationErrors, invalidProps } = useSelect( ( select ) => {
const store = select( VALIDATION_STORE_KEY );
return {
hasValidationErrors: store.hasValidationErrors(),
invalidProps: Object.keys( billingAddress )
.filter( ( key ) => {
return (
store.getValidationError( 'billing_' + key ) !==
undefined
);
} )
.filter( Boolean ),
};
} );
useEffect( () => {
if ( invalidProps.length > 0 && editing === false ) {
setEditing( true );
}
}, [ editing, hasValidationErrors, invalidProps.length ] );
const addressFieldKeys = Object.keys(
defaultAddressFields
) as ( keyof AddressFields )[];
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 renderAddressCardComponent = useCallback(
() => (
<AddressCard
address={ billingAddress }
target="billing"
onEdit={ () => {
setEditing( true );
} }
showPhoneField={ showPhoneField }
/>
),
[ billingAddress, showPhoneField ]
);
const renderAddressFormComponent = useCallback(
() => (
<>
<AddressForm
id="billing"
type="billing"
onChange={ onChangeAddress }
values={ billingAddress }
fields={ addressFieldKeys }
fieldConfig={ addressFieldsConfig }
/>
{ showPhoneField && (
<PhoneNumber
id="billing-phone"
errorId={ 'billing_phone' }
isRequired={ requirePhoneField }
value={ billingAddress.phone }
onChange={ ( value ) => {
setBillingPhone( value );
dispatchCheckoutEvent( 'set-phone-number', {
step: 'billing',
} );
if ( useBillingAsShipping ) {
setShippingPhone( value );
dispatchCheckoutEvent( 'set-phone-number', {
step: 'billing',
} );
}
} }
/>
) }
</>
),
[
addressFieldKeys,
addressFieldsConfig,
billingAddress,
dispatchCheckoutEvent,
onChangeAddress,
requirePhoneField,
setBillingPhone,
setShippingPhone,
showPhoneField,
useBillingAsShipping,
]
);
return (
<AddressWrapper
isEditing={ editing }
addressCard={ renderAddressCardComponent }
addressForm={ renderAddressFormComponent }
/>
);
};
export default CustomerAddress;

View File

@@ -2,8 +2,9 @@
* External dependencies
*/
import classnames from 'classnames';
import { useRef, useEffect } from '@wordpress/element';
import { withFilteredAttributes } from '@woocommerce/shared-hocs';
import { FormStep } from '@woocommerce/base-components/cart-checkout';
import { FormStep } from '@woocommerce/blocks-components';
import { useCheckoutAddress } from '@woocommerce/base-context/hooks';
import { useSelect } from '@wordpress/data';
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
@@ -42,12 +43,26 @@ const FrontendBlock = ( {
showCompanyField,
showPhoneField,
} = useCheckoutBlockContext();
const { showBillingFields, forcedBillingAddress, useBillingAsShipping } =
useCheckoutAddress();
const {
showBillingFields,
forcedBillingAddress,
useBillingAsShipping,
useShippingAsBilling,
} = useCheckoutAddress();
// If initial state was true, force editing to true so address fields are visible if the useShippingAsBilling option is unchecked.
const toggledUseShippingAsBilling = useRef( useShippingAsBilling );
useEffect( () => {
if ( useShippingAsBilling ) {
toggledUseShippingAsBilling.current = true;
}
}, [ useShippingAsBilling ] );
if ( ! showBillingFields && ! useBillingAsShipping ) {
return null;
}
title = getBillingAddresssBlockTitle( title, forcedBillingAddress );
description = getBillingAddresssBlockDescription(
description,
@@ -71,6 +86,7 @@ const FrontendBlock = ( {
showCompanyField={ showCompanyField }
showPhoneField={ showPhoneField }
requirePhoneField={ requirePhoneField }
forceEditing={ toggledUseShippingAsBilling.current }
/>
{ children }
</FormStep>

View File

@@ -49,7 +49,7 @@ export const Edit = ( {
>
<p className="wc-block-checkout__controls-text">
{ __(
'Account creation and guest checkout settings can be managed in the WooCommerce settings.',
'Account creation and guest checkout settings can be managed in your store settings.',
'woo-gutenberg-products-block'
) }
</p>

View File

@@ -3,7 +3,7 @@
*/
import classnames from 'classnames';
import { withFilteredAttributes } from '@woocommerce/shared-hocs';
import { FormStep } from '@woocommerce/base-components/cart-checkout';
import { FormStep } from '@woocommerce/blocks-components';
import { useSelect } from '@wordpress/data';
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';

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

@@ -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-smaller});
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

@@ -3,7 +3,7 @@
*/
import classnames from 'classnames';
import { __ } from '@wordpress/i18n';
import { FormStep } from '@woocommerce/base-components/cart-checkout';
import { FormStep } from '@woocommerce/blocks-components';
import { useShippingData } from '@woocommerce/base-context/hooks';
import { useDispatch, useSelect } from '@wordpress/data';
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';

View File

@@ -15,6 +15,13 @@
.wc-block-checkout__add-note .wc-block-components-textarea {
margin-top: $gap;
&:focus {
background-color: #fff;
color: $input-text-active;
outline: 0;
box-shadow: 0 0 0 1px $input-border-gray;
}
}
.wc-block-components-form .wc-block-checkout__order-notes.wc-block-components-checkout-step {

View File

@@ -4,7 +4,7 @@
import classnames from 'classnames';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import { withFilteredAttributes } from '@woocommerce/shared-hocs';
import { FormStep } from '@woocommerce/base-components/cart-checkout';
import { FormStep } from '@woocommerce/blocks-components';
import { useSelect } from '@wordpress/data';
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';

View File

@@ -10,11 +10,13 @@ import {
} from '@wordpress/element';
import { useShippingData, useStoreCart } from '@woocommerce/base-context/hooks';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
import {
FormattedMonetaryAmount,
RadioControlOptionType,
} from '@woocommerce/blocks-components';
import { decodeEntities } from '@wordpress/html-entities';
import { getSetting } from '@woocommerce/settings';
import { Icon, mapMarker } from '@wordpress/icons';
import type { RadioControlOption } from '@woocommerce/base-components/radio-control/types';
import { CartShippingPackageShippingRate } from '@woocommerce/types';
import {
isPackageRateCollectable,
@@ -67,7 +69,7 @@ const getPickupDetails = (
const renderPickupLocation = (
option: CartShippingPackageShippingRate,
packageCount: number
): RadioControlOption => {
): RadioControlOptionType => {
const priceWithTaxes = getSetting( 'displayCartPricesIncludingTax', false )
? parseInt( option.price, 10 ) + parseInt( option.taxes, 10 )
: option.price;

View File

@@ -3,7 +3,7 @@
*/
import classnames from 'classnames';
import { withFilteredAttributes } from '@woocommerce/shared-hocs';
import { FormStep } from '@woocommerce/base-components/cart-checkout';
import { FormStep } from '@woocommerce/blocks-components';
import { useSelect } from '@wordpress/data';
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
import { LOCAL_PICKUP_ENABLED } from '@woocommerce/block-settings';

View File

@@ -42,7 +42,7 @@
width: 100%;
box-sizing: border-box;
background-color: $gray-100;
border-radius: 4px;
border-radius: $universal-border-radius;
padding: 1px em($gap-small);
margin-top: em($gap-smaller);
@include font-size(regular);

View File

@@ -2,11 +2,10 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useMemo, useEffect, Fragment, useState } from '@wordpress/element';
import { AddressForm } from '@woocommerce/base-components/cart-checkout';
import { useMemo, Fragment } from '@wordpress/element';
import { useEffectOnce } from 'usehooks-ts';
import {
useCheckoutAddress,
useStoreEvents,
useEditorContext,
noticeContexts,
} from '@woocommerce/base-context';
@@ -17,15 +16,16 @@ import {
import Noninteractive from '@woocommerce/base-components/noninteractive';
import type {
BillingAddress,
ShippingAddress,
AddressField,
AddressFields,
} from '@woocommerce/settings';
import { useSelect } from '@wordpress/data';
import { CART_STORE_KEY } from '@woocommerce/block-data';
/**
* Internal dependencies
*/
import PhoneNumber from '../../phone-number';
import CustomerAddress from './customer-address';
const Block = ( {
showCompanyField = false,
@@ -41,52 +41,38 @@ const Block = ( {
requirePhoneField: boolean;
} ): JSX.Element => {
const {
defaultAddressFields,
setShippingAddress,
setBillingAddress,
shippingAddress,
billingAddress,
setShippingPhone,
useShippingAsBilling,
setUseShippingAsBilling,
} = useCheckoutAddress();
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 );
// Syncs the billing address with the shipping address.
const syncBillingWithShipping = () => {
const syncValues: Partial< BillingAddress > = {
...shippingAddress,
};
// Clears data if fields are hidden.
useEffect( () => {
if ( ! showPhoneField ) {
setShippingPhone( '' );
delete syncValues.phone;
}
}, [ showPhoneField, setShippingPhone ] );
// 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, 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,
]
);
if ( showCompanyField ) {
delete syncValues.company;
}
setBillingAddress( syncValues );
};
// Run this on first render to ensure addresses sync if needed (this is not re-ran when toggling the checkbox).
useEffectOnce( () => {
if ( useShippingAsBilling ) {
syncBillingWithShipping();
}
} );
// Create address fields config from block attributes.
const addressFieldsConfig = useMemo( () => {
return {
company: {
@@ -103,63 +89,50 @@ const Block = ( {
showApartmentField,
] ) as Record< keyof AddressFields, Partial< AddressField > >;
const AddressFormWrapperComponent = isEditor ? Noninteractive : Fragment;
const WrapperComponent = isEditor ? Noninteractive : Fragment;
const noticeContext = useShippingAsBilling
? [ noticeContexts.SHIPPING_ADDRESS, noticeContexts.BILLING_ADDRESS ]
: [ noticeContexts.SHIPPING_ADDRESS ];
const hasAddress = !! (
shippingAddress.address_1 &&
( shippingAddress.first_name || shippingAddress.last_name )
);
const { cartDataLoaded } = useSelect( ( select ) => {
const store = select( CART_STORE_KEY );
return {
cartDataLoaded: store.hasFinishedResolution( 'getCartData' ),
};
} );
return (
<>
<AddressFormWrapperComponent>
<StoreNoticesContainer context={ noticeContext } />
<AddressForm
id="shipping"
type="shipping"
onChange={ ( values: Partial< ShippingAddress > ) => {
setShippingAddress( values );
if ( useShippingAsBilling ) {
setBillingAddress( { ...values, email } );
dispatchCheckoutEvent( 'set-billing-address' );
}
dispatchCheckoutEvent( 'set-shipping-address' );
} }
values={ shippingAddress }
fields={
Object.keys(
defaultAddressFields
) as ( keyof AddressFields )[]
}
fieldConfig={ addressFieldsConfig }
/>
{ showPhoneField && (
<PhoneNumber
id="shipping-phone"
errorId={ 'shipping_phone' }
isRequired={ requirePhoneField }
value={ shippingAddress.phone }
onChange={ ( value ) => {
setShippingPhone( value );
dispatchCheckoutEvent( 'set-phone-number', {
step: 'shipping',
} );
} }
<StoreNoticesContainer context={ noticeContext } />
<WrapperComponent>
{ cartDataLoaded ? (
<CustomerAddress
addressFieldsConfig={ addressFieldsConfig }
showPhoneField={ showPhoneField }
requirePhoneField={ requirePhoneField }
/>
) }
</AddressFormWrapperComponent>
<CheckboxControl
className="wc-block-checkout__use-address-for-billing"
label={ __(
'Use same address for billing',
'woo-gutenberg-products-block'
) }
checked={ useShippingAsBilling }
onChange={ ( checked: boolean ) => {
setUseShippingAsBilling( checked );
if ( checked ) {
setBillingAddress( shippingAddress as BillingAddress );
}
} }
/>
) : null }
</WrapperComponent>
{ hasAddress && (
<CheckboxControl
className="wc-block-checkout__use-address-for-billing"
label={ __(
'Use same address for billing',
'woo-gutenberg-products-block'
) }
checked={ useShippingAsBilling }
onChange={ ( checked: boolean ) => {
setUseShippingAsBilling( checked );
if ( checked ) {
syncBillingWithShipping();
}
} }
/>
) }
</>
);
};

View File

@@ -0,0 +1,173 @@
/**
* External dependencies
*/
import { useState, useCallback, useEffect } from '@wordpress/element';
import { AddressForm } from '@woocommerce/base-components/cart-checkout';
import {
useCheckoutAddress,
useStoreEvents,
useEditorContext,
} from '@woocommerce/base-context';
import type {
ShippingAddress,
AddressField,
AddressFields,
} from '@woocommerce/settings';
import { useSelect } from '@wordpress/data';
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
/**
* Internal dependencies
*/
import AddressWrapper from '../../address-wrapper';
import PhoneNumber from '../../phone-number';
import AddressCard from '../../address-card';
const CustomerAddress = ( {
addressFieldsConfig,
showPhoneField,
requirePhoneField,
}: {
addressFieldsConfig: Record< keyof AddressFields, Partial< AddressField > >;
showPhoneField: boolean;
requirePhoneField: boolean;
} ) => {
const {
defaultAddressFields,
shippingAddress,
setShippingAddress,
setBillingAddress,
setShippingPhone,
setBillingPhone,
useShippingAsBilling,
} = useCheckoutAddress();
const { dispatchCheckoutEvent } = useStoreEvents();
const { isEditor } = useEditorContext();
const hasAddress = !! (
shippingAddress.address_1 &&
( shippingAddress.first_name || shippingAddress.last_name )
);
const [ editing, setEditing ] = useState( ! hasAddress || isEditor );
// Forces editing state if store has errors.
const { hasValidationErrors, invalidProps } = useSelect( ( select ) => {
const store = select( VALIDATION_STORE_KEY );
return {
hasValidationErrors: store.hasValidationErrors(),
invalidProps: Object.keys( shippingAddress )
.filter( ( key ) => {
return (
store.getValidationError( 'shipping_' + key ) !==
undefined
);
} )
.filter( Boolean ),
};
} );
useEffect( () => {
if ( invalidProps.length > 0 && editing === false ) {
setEditing( true );
}
}, [ editing, hasValidationErrors, invalidProps.length ] );
const addressFieldKeys = Object.keys(
defaultAddressFields
) as ( keyof AddressFields )[];
const onChangeAddress = useCallback(
( values: Partial< ShippingAddress > ) => {
setShippingAddress( values );
if ( useShippingAsBilling ) {
// Sync billing with shipping. Ensure unwanted properties are omitted.
const { ...syncBilling } = values;
if ( ! showPhoneField ) {
delete syncBilling.phone;
}
setBillingAddress( syncBilling );
dispatchCheckoutEvent( 'set-billing-address' );
}
dispatchCheckoutEvent( 'set-shipping-address' );
},
[
dispatchCheckoutEvent,
setBillingAddress,
setShippingAddress,
useShippingAsBilling,
showPhoneField,
]
);
const renderAddressCardComponent = useCallback(
() => (
<AddressCard
address={ shippingAddress }
target="shipping"
onEdit={ () => {
setEditing( true );
} }
showPhoneField={ showPhoneField }
/>
),
[ shippingAddress, showPhoneField ]
);
const renderAddressFormComponent = useCallback(
() => (
<>
<AddressForm
id="shipping"
type="shipping"
onChange={ onChangeAddress }
values={ shippingAddress }
fields={ addressFieldKeys }
fieldConfig={ addressFieldsConfig }
/>
{ showPhoneField && (
<PhoneNumber
id="shipping-phone"
errorId={ 'shipping_phone' }
isRequired={ requirePhoneField }
value={ shippingAddress.phone }
onChange={ ( value ) => {
setShippingPhone( value );
dispatchCheckoutEvent( 'set-phone-number', {
step: 'shipping',
} );
if ( useShippingAsBilling ) {
setBillingPhone( value );
dispatchCheckoutEvent( 'set-phone-number', {
step: 'billing',
} );
}
} }
/>
) }
</>
),
[
addressFieldKeys,
addressFieldsConfig,
dispatchCheckoutEvent,
onChangeAddress,
requirePhoneField,
setBillingPhone,
setShippingPhone,
shippingAddress,
showPhoneField,
useShippingAsBilling,
]
);
return (
<AddressWrapper
isEditing={ editing }
addressCard={ renderAddressCardComponent }
addressForm={ renderAddressFormComponent }
/>
);
};
export default CustomerAddress;

View File

@@ -3,7 +3,7 @@
*/
import classnames from 'classnames';
import { withFilteredAttributes } from '@woocommerce/shared-hocs';
import { FormStep } from '@woocommerce/base-components/cart-checkout';
import { FormStep } from '@woocommerce/blocks-components';
import { useCheckoutAddress } from '@woocommerce/base-context/hooks';
import { useSelect } from '@wordpress/data';
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';

View File

@@ -44,8 +44,4 @@ export default {
remove: true,
},
},
shippingCostRequiresAddress: {
type: 'boolean',
default: false,
},
};

View File

@@ -19,10 +19,6 @@
"remove": true,
"move": true
}
},
"shippingCostRequiresAddress": {
"type": "boolean",
"default": false
}
},
"parent": [ "woocommerce/checkout-fields-block" ],

View File

@@ -13,6 +13,7 @@ import { useEffect } from '@wordpress/element';
import { CART_STORE_KEY, VALIDATION_STORE_KEY } from '@woocommerce/block-data';
import { useDispatch, useSelect } from '@wordpress/data';
import { isPackageRateCollectable } from '@woocommerce/base-utils';
import { getSetting } from '@woocommerce/settings';
/**
* Internal dependencies
@@ -164,17 +165,19 @@ const Block = ( {
showIcon,
localPickupText,
shippingText,
shippingCostRequiresAddress = false,
}: {
checked: string;
onChange: ( value: string ) => void;
showPrice: boolean;
showIcon: boolean;
shippingCostRequiresAddress: boolean;
localPickupText: string;
shippingText: string;
} ): JSX.Element | null => {
const { shippingRates } = useShippingData();
const shippingCostRequiresAddress = getSetting< boolean >(
'shippingCostRequiresAddress',
false
);
return (
<RadioGroup

View File

@@ -23,8 +23,6 @@ import { innerBlockAreas } from '@woocommerce/blocks-checkout';
import { useDispatch, useSelect } from '@wordpress/data';
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
import ExternalLinkCard from '@woocommerce/editor-components/external-link-card';
import { Attributes } from '@woocommerce/blocks/checkout/types';
import { updateAttributeInSiblingBlock } from '@woocommerce/utils';
/**
* Internal dependencies
@@ -154,9 +152,7 @@ const ShippingSelector = ( {
export const Edit = ( {
attributes,
setAttributes,
clientId,
}: {
clientId: string;
attributes: {
title: string;
description: string;
@@ -167,16 +163,9 @@ export const Edit = ( {
showPrice: boolean;
showIcon: boolean;
className: string;
shippingCostRequiresAddress: boolean;
};
setAttributes: ( attributes: Record< string, unknown > ) => void;
} ): JSX.Element | null => {
const toggleAttribute = ( key: keyof Attributes ): void => {
const newAttributes = {} as Partial< Attributes >;
newAttributes[ key ] = ! ( attributes[ key ] as boolean );
setAttributes( newAttributes );
};
const { setPrefersCollection } = useDispatch( CHECKOUT_STORE_KEY );
const { prefersCollection } = useSelect( ( select ) => {
const checkoutStore = select( CHECKOUT_STORE_KEY );
@@ -221,30 +210,6 @@ export const Edit = ( {
) }
>
<InspectorControls>
<PanelBody
title={ __(
'Calculations',
'woo-gutenberg-products-block'
) }
>
<ToggleControl
label={ __(
'Hide shipping costs until an address is entered',
'woo-gutenberg-products-block'
) }
checked={ attributes.shippingCostRequiresAddress }
onChange={ ( selected ) => {
updateAttributeInSiblingBlock(
clientId,
'shippingCostRequiresAddress',
selected,
'woocommerce/checkout-shipping-methods-block'
);
toggleAttribute( 'shippingCostRequiresAddress' );
} }
/>
</PanelBody>
<PanelBody
title={ __( 'Appearance', 'woo-gutenberg-products-block' ) }
>

View File

@@ -3,7 +3,7 @@
*/
import classnames from 'classnames';
import { withFilteredAttributes } from '@woocommerce/shared-hocs';
import { FormStep } from '@woocommerce/base-components/cart-checkout';
import { FormStep } from '@woocommerce/blocks-components';
import { useDispatch, useSelect } from '@wordpress/data';
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
import { useShippingData } from '@woocommerce/base-context/hooks';
@@ -25,12 +25,10 @@ const FrontendBlock = ( {
showIcon,
shippingText,
localPickupText,
shippingCostRequiresAddress,
}: {
title: string;
description: string;
showStepNumber: boolean;
shippingCostRequiresAddress: boolean;
children: JSX.Element;
className?: string;
showPrice: boolean;
@@ -92,7 +90,6 @@ const FrontendBlock = ( {
showIcon={ showIcon }
localPickupText={ localPickupText }
shippingText={ shippingText }
shippingCostRequiresAddress={ shippingCostRequiresAddress }
/>
{ children }
</FormStep>

View File

@@ -6,7 +6,7 @@ import { __ } from '@wordpress/i18n';
import { getSetting } from '@woocommerce/settings';
import { createInterpolateElement } from '@wordpress/element';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
import { FormattedMonetaryAmount } from '@woocommerce/blocks-components';
import type { CartShippingPackageShippingRate } from '@woocommerce/type-defs/cart';
export const RatePrice = ( {

View File

@@ -22,7 +22,7 @@
border: none;
box-shadow: none !important;
outline: 1px solid currentColor;
border-radius: 0 !important;
border-radius: $universal-border-radius;
&.components-button:hover:not(:disabled),
&.components-button:focus:not(:disabled),
&:focus,

View File

@@ -24,8 +24,4 @@ export default {
remove: true,
},
},
shippingCostRequiresAddress: {
type: 'boolean',
default: false,
},
};

View File

@@ -19,10 +19,6 @@
"remove": true,
"move": true
}
},
"shippingCostRequiresAddress": {
"type": "boolean",
"default": false
}
},
"parent": [ "woocommerce/checkout-fields-block" ],

View File

@@ -13,7 +13,7 @@ import {
isAddressComplete,
} from '@woocommerce/base-utils';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
import { FormattedMonetaryAmount } from '@woocommerce/blocks-components';
import { useEditorContext, noticeContexts } from '@woocommerce/base-context';
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
import { decodeEntities } from '@wordpress/html-entities';

View File

@@ -4,14 +4,12 @@
import classnames from 'classnames';
import { __ } from '@wordpress/i18n';
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import { PanelBody, ExternalLink, ToggleControl } from '@wordpress/components';
import { PanelBody, ExternalLink } from '@wordpress/components';
import { ADMIN_URL, getSetting } from '@woocommerce/settings';
import ExternalLinkCard from '@woocommerce/editor-components/external-link-card';
import { innerBlockAreas } from '@woocommerce/blocks-checkout';
import { useCheckoutAddress } from '@woocommerce/base-context/hooks';
import Noninteractive from '@woocommerce/base-components/noninteractive';
import { Attributes } from '@woocommerce/blocks/checkout/types';
import { updateAttributeInSiblingBlock } from '@woocommerce/utils';
/**
* Internal dependencies
@@ -34,15 +32,12 @@ type shippingAdminLink = {
export const Edit = ( {
attributes,
setAttributes,
clientId,
}: {
clientId: string;
attributes: {
title: string;
description: string;
showStepNumber: boolean;
className: string;
shippingCostRequiresAddress: boolean;
};
setAttributes: ( attributes: Record< string, unknown > ) => void;
} ): JSX.Element | null => {
@@ -59,12 +54,6 @@ export const Edit = ( {
return null;
}
const toggleAttribute = ( key: keyof Attributes ): void => {
const newAttributes = {} as Partial< Attributes >;
newAttributes[ key ] = ! ( attributes[ key ] as boolean );
setAttributes( newAttributes );
};
return (
<FormStepBlock
attributes={ attributes }
@@ -77,26 +66,24 @@ export const Edit = ( {
<InspectorControls>
<PanelBody
title={ __(
'Calculations',
'Shipping Calculations',
'woo-gutenberg-products-block'
) }
>
<ToggleControl
label={ __(
'Hide shipping costs until an address is entered',
<p className="wc-block-checkout__controls-text">
{ __(
'Options that control shipping can be managed in your store settings.',
'woo-gutenberg-products-block'
) }
checked={ attributes.shippingCostRequiresAddress }
onChange={ ( selected ) => {
updateAttributeInSiblingBlock(
clientId,
'shippingCostRequiresAddress',
selected,
'woocommerce/checkout-shipping-method-block'
);
toggleAttribute( 'shippingCostRequiresAddress' );
} }
/>
</p>
<ExternalLink
href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=shipping&section=options` }
>
{ __(
'Manage shipping options',
'woo-gutenberg-products-block'
) }
</ExternalLink>{ ' ' }
</PanelBody>
{ globalShippingMethods.length > 0 && (
<PanelBody
@@ -133,11 +120,14 @@ export const Edit = ( {
) }
{ activeShippingZones.length && (
<PanelBody
title={ __( 'Zones', 'woo-gutenberg-products-block' ) }
title={ __(
'Shipping Zones',
'woo-gutenberg-products-block'
) }
>
<p className="wc-block-checkout__controls-text">
{ __(
'You currently have the following shipping zones active.',
'Shipping Zones can be made managed in your store settings.',
'woo-gutenberg-products-block'
) }
</p>
@@ -151,24 +141,11 @@ export const Edit = ( {
/>
);
} ) }
<ExternalLink
href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=shipping` }
>
{ __(
'Manage shipping zones',
'woo-gutenberg-products-block'
) }
</ExternalLink>
</PanelBody>
) }
</InspectorControls>
<Noninteractive>
<Block
noShippingPlaceholder={ <NoShippingPlaceholder /> }
shippingCostRequiresAddress={
attributes.shippingCostRequiresAddress
}
/>
<Block noShippingPlaceholder={ <NoShippingPlaceholder /> } />
</Noninteractive>
<AdditionalFields block={ innerBlockAreas.SHIPPING_METHODS } />
</FormStepBlock>

View File

@@ -3,7 +3,7 @@
*/
import classnames from 'classnames';
import { withFilteredAttributes } from '@woocommerce/shared-hocs';
import { FormStep } from '@woocommerce/base-components/cart-checkout';
import { FormStep } from '@woocommerce/blocks-components';
import { useCheckoutAddress } from '@woocommerce/base-context/hooks';
import { useSelect } from '@wordpress/data';
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';

View File

@@ -7,7 +7,31 @@ import { render, queryByText } from '@testing-library/react';
* Internal dependencies
*/
import { Edit } from '../edit';
const blockSettingsMock = jest.requireMock( '@woocommerce/block-settings' );
jest.mock( '@wordpress/data', () => ( {
...jest.requireActual( '@wordpress/data' ),
useSelect: jest.fn().mockImplementation( ( fn ) => {
const select = () => {
return {
getSelectionStart: () => ( {
clientId: null,
} ),
getSelectionEnd: () => ( {
clientId: null,
} ),
getFormatTypes: () => [],
};
};
if ( typeof fn === 'function' ) {
return fn( select );
}
return {
isCaretWithinFormattedText: () => false,
};
} ),
} ) );
jest.mock( '@wordpress/block-editor', () => ( {
...jest.requireActual( '@wordpress/block-editor' ),
@@ -21,6 +45,8 @@ jest.mock( '@woocommerce/block-settings', () => ( {
TERMS_URL: '/terms-and-conditions',
} ) );
const blockSettingsMock = jest.requireMock( '@woocommerce/block-settings' );
describe( 'Edit', () => {
it( 'Renders a checkbox if the checkbox attribute is true', async () => {
const { container } = render(

View File

@@ -0,0 +1,29 @@
{
"name": "woocommerce/classic-shortcode",
"version": "1.0.0",
"title": "Classic Shortcode",
"description": "Renders classic WooCommerce shortcodes.",
"category": "woocommerce",
"keywords": [ "WooCommerce" ],
"supports": {
"align": true,
"html": false,
"multiple": false,
"reusable": false,
"inserter": true
},
"attributes": {
"shortcode": {
"type": "string",
"default": "cart",
"enum": [ "cart", "checkout" ]
},
"align": {
"type": "string",
"default": "wide"
}
},
"textdomain": "woocommerce",
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}

View File

@@ -0,0 +1,75 @@
/**
* External dependencies
*/
import { createBlock, type BlockInstance } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import type { OnClickCallbackParameter, InheritedAttributes } from './types';
const isConversionPossible = () => {
return true;
};
const getButtonLabel = () =>
__( 'Transform into blocks', 'woo-gutenberg-products-block' );
const getBlockifiedTemplate = ( inheritedAttributes: InheritedAttributes ) =>
[
createBlock( 'woocommerce/cart', {
...inheritedAttributes,
className: 'wc-block-cart',
} ),
].filter( Boolean ) as BlockInstance[];
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 );
}
};
/**
* Title shown within the block itself.
*/
const getTitle = () => {
return __( 'Classic Cart', 'woo-gutenberg-products-block' );
};
/**
* Description shown within the block itself.
*/
const getDescription = () => {
return __(
'This block will render the classic cart shortcode. You can optionally transform it into blocks for more control over the cart experience.',
'woo-gutenberg-products-block'
);
};
const blockifyConfig = {
getButtonLabel,
onClickCallback,
getBlockifiedTemplate,
};
export { blockifyConfig, isConversionPossible, getDescription, getTitle };

View File

@@ -0,0 +1,69 @@
/**
* External dependencies
*/
import { createBlock, type BlockInstance } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import type { OnClickCallbackParameter, InheritedAttributes } from './types';
const isConversionPossible = () => {
return true;
};
const getButtonLabel = () =>
__( 'Transform into blocks', 'woo-gutenberg-products-block' );
const getBlockifiedTemplate = ( inheritedAttributes: InheritedAttributes ) =>
[
createBlock( 'woocommerce/checkout', {
...inheritedAttributes,
className: 'wc-block-checkout',
} ),
].filter( Boolean ) as BlockInstance[];
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 getTitle = () => {
return __( 'Classic Checkout', 'woo-gutenberg-products-block' );
};
const getDescription = () => {
return __(
'This block will render the classic checkout shortcode. You can optionally transform it into blocks for more control over the checkout experience.',
'woo-gutenberg-products-block'
);
};
const blockifyConfig = {
getButtonLabel,
onClickCallback,
getBlockifiedTemplate,
};
export { blockifyConfig, isConversionPossible, getDescription, getTitle };

View File

@@ -0,0 +1,41 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { TemplateDetails } from './types';
export const TYPES = {
cart: 'cart',
checkout: 'checkout',
};
export const PLACEHOLDERS = {
cart: 'cart',
checkout: 'checkout',
};
export const TEMPLATES: TemplateDetails = {
cart: {
type: TYPES.cart,
// Title shows up in the list view in the site editor.
title: __( 'Cart Shortcode', 'woo-gutenberg-products-block' ),
// Description in the site editor.
description: __(
'Renders the classic cart shortcode.',
'woo-gutenberg-products-block'
),
placeholder: PLACEHOLDERS.cart,
},
checkout: {
type: TYPES.checkout,
title: __( 'Checkout Cart', 'woo-gutenberg-products-block' ),
description: __(
'Renders the classic checkout shortcode.',
'woo-gutenberg-products-block'
),
placeholder: PLACEHOLDERS.checkout,
},
};

View File

@@ -0,0 +1,96 @@
:where(.wp-block-woocommerce-classic-shortcode) {
margin-left: auto;
margin-right: auto;
}
.wp-block-woocommerce-classic-shortcode__placeholder-warning {
border-left: 5px solid #2181d2;
padding-left: em(40px);
}
.wp-block-woocommerce-classic-shortcode__placeholder .components-placeholder__fieldset {
display: grid;
grid-template-columns: 1fr;
}
.wp-block-woocommerce-classic-shortcode__placeholder-wireframe,
.wp-block-woocommerce-classic-shortcode__placeholder-copy {
grid-row-start: 1;
grid-column-start: 1;
transition: 0.3s all ease;
}
.wp-block-woocommerce-classic-shortcode__placeholder-copy {
border: 1px solid $gray-900;
background-color: #fff;
padding: $gap-large $gap-larger;
border-radius: $universal-border-radius;
display: flex;
flex-direction: column;
max-width: 900px;
width: 400px;
margin: auto;
opacity: 0;
z-index: 10;
.wp-block-woocommerce-classic-shortcode__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-shortcode__placeholder-migration-button-container {
justify-content: center;
margin: $gap 0;
}
}
.wp-block-woocommerce-classic-shortcode__placeholder-wireframe {
pointer-events: none;
// Image based placeholders should fill horizontal width.
> img,
> svg {
width: 100%;
height: auto;
color: $universal-border-light;
}
}
.wp-block-woocommerce-classic-shortcode {
.components-placeholder {
box-shadow: none;
padding: 0;
background-color: transparent;
}
}
.is-selected .wp-block-woocommerce-classic-shortcode,
.is-hovered .wp-block-woocommerce-classic-shortcode,
.wp-block-woocommerce-classic-shortcode.is-selected,
.wp-block-woocommerce-classic-shortcode.is-hovered {
.wp-block-woocommerce-classic-shortcode__placeholder-wireframe {
filter: blur(3px);
opacity: 0.5;
* {
color: $universal-border-light !important;
border-color: $universal-border-light !important;
}
}
.wp-block-woocommerce-classic-shortcode__placeholder-copy {
opacity: 1;
}
.components-placeholder {
box-shadow: inherit;
}
}

View File

@@ -0,0 +1,299 @@
/**
* External dependencies
*/
import {
BlockInstance,
createBlock,
registerBlockType,
} from '@wordpress/blocks';
import type { BlockEditProps } from '@wordpress/blocks';
import {
useBlockProps,
BlockPreview,
store as blockEditorStore,
} from '@wordpress/block-editor';
import {
Button,
Placeholder,
Popover,
ExternalLink,
TabbableContainer,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { shortcode, Icon } from '@wordpress/icons';
import { useDispatch, useSelect } from '@wordpress/data';
import { useState, createInterpolateElement } from '@wordpress/element';
import { store as noticesStore } from '@wordpress/notices';
import { woo } from '@woocommerce/icons';
import { findBlock } from '@woocommerce/utils';
/**
* Internal dependencies
*/
import './editor.scss';
import './style.scss';
import { CartPlaceholder, CheckoutPlaceholder } from './placeholder';
import { TEMPLATES, TYPES } from './constants';
import { getTemplateDetailsBySlug } from './utils';
import * as blockifiedCheckout from './checkout';
import * as blockifiedCart from './cart';
import metadata from './block.json';
import type { BlockifiedTemplateConfig } from './types';
type Attributes = {
shortcode: string;
align: string;
};
const blockifiedFallbackConfig = {
isConversionPossible: () => false,
getBlockifiedTemplate: () => [],
getDescription: () => '',
onClickCallback: () => void 0,
};
const conversionConfig: { [ key: string ]: BlockifiedTemplateConfig } = {
[ TYPES.cart ]: blockifiedCart,
[ TYPES.checkout ]: blockifiedCheckout,
fallback: blockifiedFallbackConfig,
};
const ConvertTemplate = ( { blockifyConfig, clientId, attributes } ) => {
const { getButtonLabel, onClickCallback, getBlockifiedTemplate } =
blockifyConfig;
const [ isPopoverOpen, setIsPopoverOpen ] = useState( false );
const { replaceBlock, selectBlock } = useDispatch( blockEditorStore );
const { createInfoNotice } = useDispatch( noticesStore );
const { getBlocks } = useSelect( ( sel ) => {
return {
getBlocks: sel( blockEditorStore ).getBlocks,
};
}, [] );
return (
<TabbableContainer className="wp-block-woocommerce-classic-shortcode__placeholder-migration-button-container">
<Button
variant="primary"
onClick={ () => {
onClickCallback( {
clientId,
getBlocks,
attributes,
replaceBlock,
selectBlock,
} );
createInfoNotice(
__(
'Classic shortcode transformed to blocks.',
'woo-gutenberg-products-block'
),
{
actions: [
{
label: __(
'Undo',
'woo-gutenberg-products-block'
),
onClick: () => {
const targetBlocks = [
'woocommerce/cart',
'woocommerce/checkout',
];
const cartCheckoutBlock = findBlock( {
blocks: getBlocks(),
findCondition: (
foundBlock: BlockInstance
) =>
targetBlocks.includes(
foundBlock.name
),
} );
if ( ! cartCheckoutBlock ) {
return;
}
replaceBlock(
cartCheckoutBlock.clientId,
createBlock(
'woocommerce/classic-shortcode',
{
shortcode:
attributes.shortcode,
}
)
);
},
},
],
type: 'snackbar',
}
);
} }
onMouseEnter={ () => setIsPopoverOpen( true ) }
onMouseLeave={ () => setIsPopoverOpen( false ) }
text={ getButtonLabel ? getButtonLabel() : '' }
tabIndex={ 0 }
>
{ 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,
isPreview: true,
} ) }
viewportWidth={ 1200 }
additionalStyles={ [
{
css: 'body { padding: 20px !important; height: fit-content !important; overflow:hidden}',
},
] }
/>
</div>
</Popover>
) }
</Button>
<Button
variant="secondary"
href="https://woocommerce.com/document/cart-checkout-blocks-support-status/"
target="_blank"
tabIndex={ 0 }
>
{ __( 'Learn more', 'woo-gutenberg-products-block' ) }
</Button>
</TabbableContainer>
);
};
const Edit = ( { clientId, attributes }: BlockEditProps< Attributes > ) => {
const blockProps = useBlockProps();
const templateDetails = getTemplateDetailsBySlug(
attributes.shortcode,
TEMPLATES
);
const templateTitle = attributes.shortcode;
const templatePlaceholder = templateDetails?.placeholder ?? 'cart';
const templateType = templateDetails?.type ?? 'fallback';
const { isConversionPossible, getDescription, getTitle, blockifyConfig } =
conversionConfig[ templateType ];
const canConvert = isConversionPossible();
const placeholderTitle = getTitle
? getTitle()
: __( 'Classic Shortcode Placeholder', 'woo-gutenberg-products-block' );
const placeholderDescription = getDescription( templateTitle, canConvert );
const learnMoreContent = createInterpolateElement(
__(
'You can learn more about the benefits of switching to blocks, compatibility with extensions, and how to switch back to shortcodes <a>in our documentation</a>.',
'woo-gutenberg-products-block'
),
{
a: (
// Suppress the warning as this <a> will be interpolated into the string with content.
// eslint-disable-next-line jsx-a11y/anchor-has-content
<ExternalLink href="https://woocommerce.com/document/cart-checkout-blocks-support-status/" />
),
}
);
return (
<div { ...blockProps }>
<Placeholder className="wp-block-woocommerce-classic-shortcode__placeholder">
<div className="wp-block-woocommerce-classic-shortcode__placeholder-wireframe">
{ templatePlaceholder === 'cart' ? (
<CartPlaceholder />
) : (
<CheckoutPlaceholder />
) }
</div>
<div className="wp-block-woocommerce-classic-shortcode__placeholder-copy">
<div className="wp-block-woocommerce-classic-shortcode__placeholder-copy__icon-container">
<span className="woo-icon">
<Icon icon={ woo } />{ ' ' }
{ __(
'WooCommerce',
'woo-gutenberg-products-block'
) }
</span>
<span>{ placeholderTitle }</span>
</div>
<p
dangerouslySetInnerHTML={ {
__html: placeholderDescription,
} }
/>
<p>{ learnMoreContent }</p>
{ canConvert && blockifyConfig && (
<ConvertTemplate
clientId={ clientId }
blockifyConfig={ blockifyConfig }
attributes={ attributes }
/>
) }
</div>
</Placeholder>
</div>
);
};
const settings = {
icon: (
<Icon
icon={ shortcode }
className="wc-block-editor-components-block-icon"
/>
),
edit: ( {
attributes,
clientId,
setAttributes,
}: BlockEditProps< Attributes > ) => {
return (
<Edit
attributes={ attributes }
setAttributes={ setAttributes }
clientId={ clientId }
/>
);
},
save: () => null,
variations: [
{
name: 'checkout',
title: __( 'Classic Checkout', 'woo-gutenberg-products-block' ),
attributes: {
shortcode: 'checkout',
},
isActive: ( blockAttributes, variationAttributes ) =>
blockAttributes.shortcode === variationAttributes.shortcode,
scope: [ 'inserter' ],
},
{
name: 'cart',
title: __( 'Classic Cart', 'woo-gutenberg-products-block' ),
attributes: {
shortcode: 'cart',
},
isActive: ( blockAttributes, variationAttributes ) =>
blockAttributes.shortcode === variationAttributes.shortcode,
scope: [ 'inserter' ],
isDefault: true,
},
],
};
registerBlockType( metadata, settings );

View File

@@ -0,0 +1,55 @@
/**
* External dependencies
*/
import { Rect, SVG, G } from '@wordpress/primitives';
export const CartPlaceholder = () => (
<SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 892 516">
<G fill="currentColor" transform="translate(-1)">
<Rect width="100" height="20" x="1" rx="2" />
<Rect width="100" height="20" x="421" rx="2" />
<Rect width="100" height="20" x="793" rx="2" />
<Rect width="150" height="24" x="125" y="48" rx="2" />
<Rect width="50" height="24" x="125" y="86" rx="2" />
<Rect width="50" height="24" x="457" y="48" rx="2" />
<Rect width="100" height="100" x="15" y="48" rx="2" />
<Rect width="150" height="150" x="1" y="318" rx="2" />
<Rect width="150" height="150" x="373" y="318" rx="2" />
<Rect width="150" height="150" x="187" y="318" rx="2" />
<Rect width="150" height="24" x="125" y="178" rx="2" />
<Rect width="50" height="24" x="125" y="216" rx="2" />
<Rect width="50" height="24" x="457" y="178" rx="2" />
<Rect width="100" height="100" x="15" y="178" rx="2" />
<Rect width="304" height="359" x="588" y="34" rx="2" />
<Rect width="520" height="1" x="1" y="34" rx=".5" />
<Rect width="520" height="1" y="162" rx=".5" />
<Rect width="520" height="1" x="1" y="292" rx=".5" />
<Rect width="304" height="64" x="589" y="407" rx="2" />
<Rect width="100" height="38" x="26" y="478" rx="2" />
<Rect width="100" height="38" x="212" y="478" rx="2" />
<Rect width="100" height="38" x="398" y="478" rx="2" />
</G>
</SVG>
);
export const CheckoutPlaceholder = () => (
<SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 892 726">
<G fill="currentColor" transform="translate(-1)">
<Rect width="203" height="38" x="1" rx="2" />
<Rect width="434" height="38" x="1" y="62" rx="2" />
<Rect width="434" height="38" y="124" rx="2" />
<Rect width="434" height="38" x="1" y="186" rx="2" />
<Rect width="434" height="38" x="2" y="248" rx="2" />
<Rect width="434" height="38" x="3" y="310" rx="2" />
<Rect width="434" height="38" x="3" y="372" rx="2" />
<Rect width="892" height="204" x="2" y="434" rx="2" />
<Rect width="203" height="38" x="231" rx="2" />
<Rect width="203" height="38" x="514" rx="2" />
<Rect width="427" height="100" x="466" y="62" rx="2" />
<Rect width="304" height="64" x="588" y="662" rx="2" />
<Rect width="38" height="38" x="466" rx="2" />
<Rect width="203" height="38" x="48" y="662" rx="2" />
<Rect width="38" height="38" y="662" rx="2" />
</G>
</SVG>
);

View File

@@ -0,0 +1,5 @@
:where(div[data-block-name="woocommerce/classic-shortcode"]) {
margin-left: auto;
margin-right: auto;
max-width: 1000px;
}

View File

@@ -0,0 +1,46 @@
/**
* External dependencies
*/
import { type BlockInstance } from '@wordpress/blocks';
type TemplateDetail = {
type: string;
title: string;
description?: string | undefined;
placeholder: string;
};
export type TemplateDetails = Record< string, TemplateDetail >;
export type InheritedAttributes = {
align?: string;
};
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[];
};
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;
// Returns the title for the placeholder, or can be left blank to use the default fallback text.
getTitle?: ( () => string ) | 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

@@ -0,0 +1,24 @@
/**
* Internal dependencies
*/
import { TemplateDetails } from './types';
// Finds the most appropriate template details object for specific template keys such as single-product-hoodie.
export function getTemplateDetailsBySlug(
parsedTemplate: string,
templates: TemplateDetails
) {
const templateKeys = Object.keys( templates );
let templateDetails = null;
for ( let i = 0; templateKeys.length > i; i++ ) {
const keyToMatch = parsedTemplate.substr( 0, templateKeys[ i ].length );
const maybeTemplate = templates[ keyToMatch ];
if ( maybeTemplate ) {
templateDetails = maybeTemplate;
break;
}
}
return templateDetails;
}

View File

@@ -8,15 +8,15 @@ import {
} from '@wordpress/blocks';
import { isWpVersion } from '@woocommerce/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 { OnClickCallbackParameter, type InheritedAttributes } from './types';

View File

@@ -15,8 +15,6 @@ export const TYPES = {
productTaxonomy: 'product-taxonomy',
productSearchResults: 'product-search-results',
orderConfirmation: 'order-confirmation',
cart: 'cart',
checkout: 'checkout',
checkoutHeader: 'checkout-header',
};
export const PLACEHOLDERS = {
@@ -84,16 +82,6 @@ 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' ),

View File

@@ -21,7 +21,7 @@
border: 1px solid $gray-900;
background-color: #fff;
padding: $gap-large $gap-larger;
border-radius: 3px;
border-radius: $universal-border-radius;
display: flex;
flex-direction: column;
max-width: 900px;

View File

@@ -175,7 +175,10 @@ const ConvertTemplate = ( { blockifyConfig, clientId, attributes } ) => {
} }
>
<BlockPreview
blocks={ getBlockifiedTemplate( attributes ) }
blocks={ getBlockifiedTemplate( {
...attributes,
isPreview: true,
} ) }
viewportWidth={ 1200 }
additionalStyles={ [
{

View File

@@ -1,11 +1,89 @@
/**
* External dependencies
*/
import { createBlock, type BlockInstance } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import type { OnClickCallbackParameter, InheritedAttributes } from './types';
const isConversionPossible = () => {
return false;
return true;
};
const getButtonLabel = () =>
__( 'Transform into blocks', 'woo-gutenberg-products-block' );
const getBlockifiedTemplate = ( inheritedAttributes: InheritedAttributes ) =>
[
createBlock( 'woocommerce/order-confirmation-status', {
...inheritedAttributes,
fontSize: 'large',
} ),
createBlock(
'woocommerce/order-confirmation-summary',
inheritedAttributes
),
createBlock(
'woocommerce/order-confirmation-totals-wrapper',
inheritedAttributes
),
createBlock(
'woocommerce/order-confirmation-downloads-wrapper',
inheritedAttributes
),
createBlock(
'core/columns',
{
...inheritedAttributes,
className: 'woocommerce-order-confirmation-address-wrapper',
},
[
createBlock( 'core/column', inheritedAttributes, [
createBlock(
'woocommerce/order-confirmation-shipping-wrapper',
inheritedAttributes
),
] ),
createBlock( 'core/column', inheritedAttributes, [
createBlock(
'woocommerce/order-confirmation-billing-wrapper',
inheritedAttributes
),
] ),
]
),
createBlock(
'woocommerce/order-confirmation-additional-information',
inheritedAttributes
),
].filter( Boolean ) as BlockInstance[];
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 getDescription = () => {
@@ -150,4 +228,10 @@ const getSkeleton = () => {
);
};
export { isConversionPossible, getDescription, getSkeleton };
const blockifyConfig = {
getButtonLabel,
onClickCallback,
getBlockifiedTemplate,
};
export { blockifyConfig, isConversionPossible, getDescription, getSkeleton };

View File

@@ -9,15 +9,15 @@ import {
} from '@wordpress/blocks';
import { isWpVersion } from '@woocommerce/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 { OnClickCallbackParameter, type InheritedAttributes } from './types';

View File

@@ -6,36 +6,24 @@ import { getTemplateDetailsBySlug } from '../utils';
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 )
).toBeTruthy();
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 )
).toBeTruthy();
expect(
getTemplateDetailsBySlug( 'taxonomy-product_brands', TEMPLATES )
).toStrictEqual( TEMPLATES[ 'taxonomy-product' ] );

View File

@@ -0,0 +1,17 @@
{
"name": "woocommerce/collection-filters",
"version": "1.0.0",
"title": "Collection Filters",
"description": "A block that adds product filters to the product collection.",
"category": "woocommerce",
"keywords": [ "WooCommerce", "Filters" ],
"textdomain": "woocommerce",
"supports": {
"html": false,
"reusable": false
},
"usesContext": [ "query" ],
"ancestor": [ "woocommerce/product-collection" ],
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}

View File

@@ -0,0 +1,13 @@
/**
* External dependencies
*/
import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
const Edit = () => {
const blockProps = useBlockProps();
const innerBlockProps = useInnerBlocksProps( blockProps );
return <nav { ...innerBlockProps } />;
};
export default Edit;

View File

@@ -0,0 +1,28 @@
/**
* External dependencies
*/
import { registerBlockType } from '@wordpress/blocks';
import { Icon, more } from '@wordpress/icons';
import { isExperimentalBuild } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import metadata from './block.json';
import edit from './edit';
import save from './save';
if ( isExperimentalBuild() ) {
registerBlockType( metadata, {
icon: {
src: (
<Icon
icon={ more }
className="wc-block-editor-components-block-icon"
/>
),
},
edit,
save,
} );
}

View File

@@ -0,0 +1,32 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"name": "woocommerce/collection-price-filter",
"version": "1.0.0",
"title": "Collection Price Filter",
"description": "Enable customers to filter the product collection by choosing a price range.",
"category": "woocommerce",
"keywords": [
"WooCommerce"
],
"textdomain": "woocommerce",
"apiVersion": 2,
"viewScript": [
"wc-collection-price-filter-block-frontend"
],
"ancestor": [
"woocommerce/product-collection"
],
"supports": {
"interactivity": true
},
"attributes": {
"showInputFields": {
"type": "boolean",
"default": true
},
"inlineInput": {
"type": "boolean",
"default": false
}
}
}

View File

@@ -0,0 +1,41 @@
/**
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
import { useCollectionData } from '@woocommerce/base-context/hooks';
import { Disabled } from '@wordpress/components';
import FilterResetButton from '@woocommerce/base-components/filter-reset-button';
/**
* Internal dependencies
*/
import { getFormattedPrice } from './utils';
import { EditProps } from './types';
import { PriceSlider } from './price-slider';
const Edit = ( props: EditProps ) => {
const blockProps = useBlockProps();
const { results } = useCollectionData( {
queryPrices: true,
isEditor: true,
queryState: {},
} );
return (
<div { ...blockProps }>
<Disabled>
<div className="controls">
<PriceSlider
{ ...props }
collectionData={ getFormattedPrice( results ) }
/>
</div>
<div className="actions">
<FilterResetButton onClick={ () => false } />
</div>
</Disabled>
</div>
);
};
export default Edit;

View File

@@ -0,0 +1,90 @@
/**
* External dependencies
*/
import { store, navigate } from '@woocommerce/interactivity';
import { formatPrice, getCurrency } from '@woocommerce/price-format';
/**
* Internal dependencies
*/
import { ActionProps, StateProps } from './types';
const getHrefWithFilters = ( { state }: StateProps ) => {
const { minPrice, maxPrice } = state.filters;
const url = new URL( window.location.href );
const { searchParams } = url;
if ( minPrice > 0 ) {
searchParams.set( 'min_price', minPrice.toString() );
} else {
searchParams.delete( 'min_price' );
}
if ( maxPrice < state.filters.maxRange ) {
searchParams.set( 'max_price', maxPrice.toString() );
} else {
searchParams.delete( 'max_price' );
}
searchParams.forEach( ( _, key ) => {
if ( /query-[0-9]+-page/.test( key ) ) searchParams.delete( key );
} );
return url.href;
};
store( {
state: {
filters: {
rangeStyle: ( { state }: StateProps ) => {
const { minPrice, maxPrice, maxRange } = state.filters;
return [
`--low: ${ ( 100 * minPrice ) / maxRange }%`,
`--high: ${ ( 100 * maxPrice ) / maxRange }%`,
].join( ';' );
},
formattedMinPrice: ( { state }: StateProps ) => {
const { minPrice } = state.filters;
return formatPrice( minPrice, getCurrency( { minorUnit: 0 } ) );
},
formattedMaxPrice: ( { state }: StateProps ) => {
const { maxPrice } = state.filters;
return formatPrice( maxPrice, getCurrency( { minorUnit: 0 } ) );
},
},
},
actions: {
filters: {
setMinPrice: ( { state, event }: ActionProps ) => {
const value = parseFloat( event.target.value );
state.filters.minPrice = Math.min(
Number.isNaN( value ) ? state.filters.minRange : value,
state.filters.maxRange - 1
);
state.filters.maxPrice = Math.max(
state.filters.maxPrice,
state.filters.minPrice + 1
);
},
setMaxPrice: ( { state, event }: ActionProps ) => {
const value = parseFloat( event.target.value );
state.filters.maxPrice = Math.max(
Number.isNaN( value ) ? state.filters.maxRange : value,
state.filters.minRange + 1
);
state.filters.minPrice = Math.min(
state.filters.minPrice,
state.filters.maxPrice - 1
);
},
updateProducts: ( { state }: ActionProps ) => {
navigate( getHrefWithFilters( { state } ) );
},
reset: ( { state }: ActionProps ) => {
state.filters.minPrice = 0;
state.filters.maxPrice = state.filters.maxRange;
navigate( getHrefWithFilters( { state } ) );
},
},
},
} );

View File

@@ -0,0 +1,24 @@
/**
* External dependencies
*/
import { registerBlockType } from '@wordpress/blocks';
import { Icon, currencyDollar } from '@wordpress/icons';
/**
* Internal dependencies
*/
import './style.scss';
import metadata from './block.json';
import Edit from './edit';
registerBlockType( metadata, {
icon: {
src: (
<Icon
icon={ currencyDollar }
className="wc-block-editor-components-block-icon"
/>
),
},
edit: Edit,
} );

View File

@@ -0,0 +1,76 @@
/**
* External dependencies
*/
import classNames from 'classnames';
/**
* Internal dependencies
*/
import { FilterComponentProps } from '../types';
import { Inspector } from './inspector';
import './style.scss';
export const PriceSlider = ( {
collectionData,
...editProps
}: FilterComponentProps ) => {
const { showInputFields, inlineInput } = editProps.attributes;
const { minPrice, maxPrice, formattedMinPrice, formattedMaxPrice } =
collectionData;
const onChange = () => null;
const priceMin = showInputFields ? (
<input
className="min"
type="text"
value={ minPrice }
onChange={ onChange }
/>
) : (
<span>{ formattedMinPrice }</span>
);
const priceMax = showInputFields ? (
<input
className="max"
type="text"
value={ maxPrice }
onChange={ onChange }
/>
) : (
<span>{ formattedMaxPrice }</span>
);
return (
<>
<Inspector { ...editProps } />
<div
className={ classNames( 'price-slider', {
'inline-input': inlineInput && showInputFields,
} ) }
>
<div className="range">
<div className="range-bar"></div>
<input
type="range"
className="min"
min={ minPrice }
max={ maxPrice }
value={ minPrice }
onChange={ onChange }
/>
<input
type="range"
className="max"
min={ minPrice }
max={ maxPrice }
value={ maxPrice }
onChange={ onChange }
/>
</div>
<div className="text">
{ priceMin }
{ priceMax }
</div>
</div>
</>
);
};

View File

@@ -0,0 +1,73 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { InspectorControls } from '@wordpress/block-editor';
import {
PanelBody,
ToggleControl,
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
__experimentalToggleGroupControl as ToggleGroupControl,
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
__experimentalToggleGroupControlOption as ToggleGroupControlOption,
} from '@wordpress/components';
/**
* Internal dependencies
*/
import { EditProps } from '../types';
export const Inspector = ( { attributes, setAttributes }: EditProps ) => {
const { showInputFields, inlineInput } = attributes;
return (
<InspectorControls>
<PanelBody
title={ __( 'Settings', 'woo-gutenberg-products-block' ) }
>
<ToggleGroupControl
label={ __(
'Price Slider',
'woo-gutenberg-products-block'
) }
value={ showInputFields ? 'editable' : 'text' }
onChange={ ( value: string ) =>
setAttributes( {
showInputFields: value === 'editable',
} )
}
className="wc-block-price-filter__price-range-toggle"
>
<ToggleGroupControlOption
value="editable"
label={ __(
'Editable',
'woo-gutenberg-products-block'
) }
/>
<ToggleGroupControlOption
value="text"
label={ __( 'Text', 'woo-gutenberg-products-block' ) }
/>
</ToggleGroupControl>
{ showInputFields && (
<ToggleControl
label={ __(
'Inline input fields',
'woo-gutenberg-products-block'
) }
checked={ inlineInput }
onChange={ () =>
setAttributes( {
inlineInput: ! inlineInput,
} )
}
help={ __(
'Show input fields inline with the slider.',
'woo-gutenberg-products-block'
) }
/>
) }
</PanelBody>
</InspectorControls>
);
};

View File

@@ -0,0 +1,216 @@
@mixin thumb {
background: $white;
background-position: 0 0;
box-sizing: content-box;
width: 12px;
height: 12px;
border: 2px solid $gray-900;
border-radius: 100%;
padding: 0;
margin: 0;
vertical-align: top;
cursor: pointer;
z-index: 20;
pointer-events: auto;
transition: transform 0.2s ease-in-out;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
&:hover {
@include thumbFocus;
}
}
@mixin thumbFocus {
background: $gray-900;
border-color: $white;
}
@mixin track {
cursor: default;
height: 1px;
/* Required for Samsung internet based browsers */
outline: 0;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
@mixin reset {
margin: 0;
/* Use !important to prevent theme input styles from breaking the component.
Reference https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/3902
*/
padding: 0 !important;
border: 0 !important;
outline: none;
background: transparent;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
.wp-block-woocommerce-collection-price-filter .price-slider {
.range {
--low: 0%;
--high: 100%;
--range-color: currentColor;
--track-background: linear-gradient(to right, transparent var(--low), var(--range-color) 0, var(--range-color) var(--high), transparent 0) no-repeat 0 100% / 100% 100%;
.rtl & {
--track-background: linear-gradient(to left, transparent var(--low), var(--range-color) 0, var(--range-color) var(--high), transparent 0) no-repeat 0 100% / 100% 100%;
}
@include reset;
background: transparent;
border-radius: 4px;
clear: both;
flex-grow: 1;
height: 4px;
margin: 15px 0;
position: relative;
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: currentColor;
opacity: 0.2;
}
.range-bar {
position: relative;
height: 4px;
background: var(--track-background);
}
input[type="range"] {
@include reset;
width: 100%;
height: 0;
display: block;
pointer-events: none;
outline: none !important;
position: absolute;
left: 0;
top: 0;
&::-webkit-slider-thumb {
@include thumb;
margin: -5px 0 0 0;
}
&::-moz-range-thumb {
@include thumb;
}
&::-ms-thumb {
@include thumb;
}
&:focus {
&::-webkit-slider-thumb {
@include thumbFocus;
}
&::-moz-range-thumb {
@include thumbFocus;
}
&::-ms-thumb {
@include thumbFocus;
}
}
&::-webkit-slider-runnable-track {
@include track;
}
&::-moz-range-track {
@include track;
}
&::-webkit-slider-progress {
@include reset;
}
&::-moz-range-progress {
@include reset;
}
&::-moz-focus-outer {
border: 0;
}
&.min {
&::-webkit-slider-thumb {
margin-left: -2px;
background-position-x: left;
}
&::-moz-range-thumb {
background-position-x: left;
transform: translate(-2px, 2px);
}
&::-ms-thumb {
background-position-x: left;
}
}
&.max {
&::-webkit-slider-thumb {
background-position-x: right;
margin-left: 2px;
}
&::-moz-range-thumb {
background-position-x: right;
transform: translate(2px, 2px);
}
&::-ms-thumb {
background-position-x: right;
}
}
}
input[type="range" i] {
color: -internal-light-dark(rgb(16, 16, 16), rgb(255, 255, 255));
padding: initial;
}
}
.text {
display: flex;
align-items: center;
justify-content: space-between;
margin: 16px 0;
gap: 8px;
input[type="text"] {
padding: 8px;
margin: 0;
width: auto;
max-width: 60px;
min-width: 0;
font-size: 0.875em;
border-width: 1px;
border-style: solid;
border-color: currentColor;
border-radius: 4px;
}
}
&.inline-input {
display: flex;
align-items: center;
gap: 8px;
.text {
display: contents;
}
.text .min {
order: -1;
}
}
}

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