Merged in feature/117-dev-dev01 (pull request #8)

auto-patch  117-dev-dev01-2023-12-15T16_09_06

* auto-patch  117-dev-dev01-2023-12-15T16_09_06
This commit is contained in:
Tony Volpe
2023-12-15 16:10:57 +00:00
parent 0825f6bd5f
commit 3dc9eca989
1424 changed files with 28118 additions and 10097 deletions

View File

@@ -4,7 +4,7 @@
import { __ } from '@wordpress/i18n';
import { useExpressPaymentMethods } from '@woocommerce/base-context/hooks';
import { noticeContexts } from '@woocommerce/base-context';
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
import LoadingMask from '@woocommerce/base-components/loading-mask';
import { useSelect } from '@wordpress/data';
import { CHECKOUT_STORE_KEY, PAYMENT_STORE_KEY } from '@woocommerce/block-data';

View File

@@ -3,8 +3,7 @@
*/
import { __ } from '@wordpress/i18n';
import { useEditorContext, noticeContexts } from '@woocommerce/base-context';
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
import Title from '@woocommerce/base-components/title';
import { Title, StoreNoticesContainer } from '@woocommerce/blocks-components';
import LoadingMask from '@woocommerce/base-components/loading-mask';
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings';
import { CHECKOUT_STORE_KEY, PAYMENT_STORE_KEY } from '@woocommerce/block-data';

View File

@@ -4,7 +4,7 @@
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings';
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
import { noticeContexts } from '@woocommerce/base-context';
import { NoticeType } from '@woocommerce/types';
interface PaymentMethodErrorBoundaryProps {

View File

@@ -14,10 +14,8 @@ import {
CartProvider,
noticeContexts,
} from '@woocommerce/base-context';
import {
SlotFillProvider,
StoreNoticesContainer,
} from '@woocommerce/blocks-checkout';
import { SlotFillProvider } from '@woocommerce/blocks-checkout';
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
/**
* Internal dependencies

View File

@@ -4,7 +4,7 @@
import { TotalsCoupon } from '@woocommerce/base-components/cart-checkout';
import { useStoreCartCoupons } from '@woocommerce/base-context/hooks';
import { getSetting } from '@woocommerce/settings';
import { TotalsWrapper } from '@woocommerce/blocks-checkout';
import { TotalsWrapper } from '@woocommerce/blocks-components';
const Block = ( { className }: { className: string } ): JSX.Element | null => {
const couponsEnabled = getSetting( 'couponsEnabled', true );

View File

@@ -2,15 +2,13 @@
* External dependencies
*/
import { TotalsDiscount } from '@woocommerce/base-components/cart-checkout';
import { TotalsWrapper } from '@woocommerce/blocks-components';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import {
useStoreCartCoupons,
useStoreCart,
} from '@woocommerce/base-context/hooks';
import {
ExperimentalDiscountsMeta,
TotalsWrapper,
} from '@woocommerce/blocks-checkout';
import { ExperimentalDiscountsMeta } from '@woocommerce/blocks-checkout';
const DiscountSlotFill = (): JSX.Element => {
// Prepare props to pass to the ExperimentalOrderMeta slot fill. We need to pluck out receiveCart.

View File

@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { TotalsFees, TotalsWrapper } from '@woocommerce/blocks-checkout';
import { TotalsFees, TotalsWrapper } from '@woocommerce/blocks-components';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import { useStoreCart } from '@woocommerce/base-context/hooks';

View File

@@ -4,7 +4,7 @@
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 { TotalsWrapper } from '@woocommerce/blocks-components';
import { getSetting } from '@woocommerce/settings';
const Block = ( { className }: { className: string } ): JSX.Element | null => {

View File

@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { Subtotal, TotalsWrapper } from '@woocommerce/blocks-checkout';
import { Subtotal, TotalsWrapper } from '@woocommerce/blocks-components';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import { useStoreCart } from '@woocommerce/base-context/hooks';

View File

@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { TotalsTaxes, TotalsWrapper } from '@woocommerce/blocks-checkout';
import { TotalsTaxes, TotalsWrapper } from '@woocommerce/blocks-components';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import { getSetting } from '@woocommerce/settings';

View File

@@ -96,6 +96,9 @@
display: none;
}
}
table.wc-block-cart-items {
margin: 0;
}
}
.is-medium,

View File

@@ -7,6 +7,7 @@ import type {
CartShippingAddress,
CartBillingAddress,
} from '@woocommerce/types';
import { AddressFields, AddressField } from '@woocommerce/settings';
/**
* Internal dependencies
@@ -17,12 +18,12 @@ const AddressCard = ( {
address,
onEdit,
target,
showPhoneField,
fieldConfig,
}: {
address: CartShippingAddress | CartBillingAddress;
onEdit: () => void;
target: string;
showPhoneField: boolean;
fieldConfig: Record< keyof AddressFields, Partial< AddressField > >;
} ): JSX.Element | null => {
return (
<div className="wc-block-components-address-card">
@@ -33,7 +34,7 @@ const AddressCard = ( {
<div className="wc-block-components-address-card__address-section">
{ [
address.address_1,
address.address_2,
! fieldConfig.address_2.hidden && address.address_2,
address.city,
address.state,
address.postcode,
@@ -46,7 +47,7 @@ const AddressCard = ( {
<span key={ `address-` + index }>{ field }</span>
) ) }
</div>
{ address.phone && showPhoneField ? (
{ address.phone && ! fieldConfig.phone.hidden ? (
<div
key={ `address-phone` }
className="wc-block-components-address-card__address-section"

View File

@@ -12,10 +12,8 @@ import { CheckoutProvider, noticeContexts } from '@woocommerce/base-context';
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
import { SidebarLayout } from '@woocommerce/base-components/sidebar-layout';
import { CURRENT_USER_IS_ADMIN, getSetting } from '@woocommerce/settings';
import {
SlotFillProvider,
StoreNoticesContainer,
} from '@woocommerce/blocks-checkout';
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
import { SlotFillProvider } from '@woocommerce/blocks-checkout';
import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top';
import { useDispatch, useSelect } from '@wordpress/data';
import {

View File

@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import Title from '@woocommerce/base-components/title';
import { Title } from '@woocommerce/blocks-components';
/**
* Step Heading Component

View File

@@ -9,10 +9,8 @@ import {
} from '@woocommerce/base-components/cart-checkout';
import { useCheckoutSubmit } from '@woocommerce/base-context/hooks';
import { noticeContexts } from '@woocommerce/base-context';
import {
StoreNoticesContainer,
applyCheckoutFilter,
} from '@woocommerce/blocks-checkout';
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
import { applyCheckoutFilter } from '@woocommerce/blocks-checkout';
/**
* Internal dependencies

View File

@@ -14,9 +14,10 @@ import type {
AddressField,
AddressFields,
} from '@woocommerce/settings';
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
import { useSelect } from '@wordpress/data';
import { CART_STORE_KEY } from '@woocommerce/block-data';
import isShallowEqual from '@wordpress/is-shallow-equal';
/**
* Internal dependencies
@@ -29,17 +30,19 @@ 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 { billingAddress, setShippingAddress, useBillingAsShipping } =
useCheckoutAddress();
const {
shippingAddress,
billingAddress,
setShippingAddress,
useBillingAsShipping,
} = useCheckoutAddress();
const { isEditor } = useEditorContext();
// Syncs shipping address with billing address if "Force shipping to the customer billing address" is enabled.
@@ -71,11 +74,17 @@ const Block = ( {
address_2: {
hidden: ! showApartmentField,
},
phone: {
hidden: ! showPhoneField,
required: requirePhoneField,
},
};
}, [
showCompanyField,
requireCompanyField,
showApartmentField,
showPhoneField,
requirePhoneField,
] ) as Record< keyof AddressFields, Partial< AddressField > >;
const WrapperComponent = isEditor ? Noninteractive : Fragment;
@@ -89,6 +98,20 @@ const Block = ( {
cartDataLoaded: store.hasFinishedResolution( 'getCartData' ),
};
} );
// Default editing state for CustomerAddress component comes from the current address and whether or not we're in the editor.
const hasAddress = !! (
billingAddress.address_1 &&
( billingAddress.first_name || billingAddress.last_name )
);
const { email, ...billingAddressWithoutEmail } = billingAddress;
const billingMatchesShipping = isShallowEqual(
billingAddressWithoutEmail,
shippingAddress
);
const defaultEditingAddress =
isEditor || ! hasAddress || billingMatchesShipping;
return (
<>
<StoreNoticesContainer context={ noticeContext } />
@@ -96,9 +119,7 @@ const Block = ( {
{ cartDataLoaded ? (
<CustomerAddress
addressFieldsConfig={ addressFieldsConfig }
showPhoneField={ showPhoneField }
requirePhoneField={ requirePhoneField }
forceEditing={ forceEditing }
defaultEditing={ defaultEditingAddress }
/>
) : null }
</WrapperComponent>

View File

@@ -16,35 +16,24 @@ 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,
defaultEditing = false,
}: {
addressFieldsConfig: Record< keyof AddressFields, Partial< AddressField > >;
showPhoneField: boolean;
requirePhoneField: boolean;
forceEditing?: boolean;
defaultEditing?: 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 );
const [ editing, setEditing ] = useState( defaultEditing );
// Forces editing state if store has errors.
const { hasValidationErrors, invalidProps } = useSelect( ( select ) => {
@@ -54,8 +43,9 @@ const CustomerAddress = ( {
invalidProps: Object.keys( billingAddress )
.filter( ( key ) => {
return (
key !== 'email' &&
store.getValidationError( 'billing_' + key ) !==
undefined
undefined
);
} )
.filter( Boolean ),
@@ -97,10 +87,10 @@ const CustomerAddress = ( {
onEdit={ () => {
setEditing( true );
} }
showPhoneField={ showPhoneField }
fieldConfig={ addressFieldsConfig }
/>
),
[ billingAddress, showPhoneField ]
[ billingAddress, addressFieldsConfig ]
);
const renderAddressFormComponent = useCallback(
@@ -114,39 +104,13 @@ const CustomerAddress = ( {
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,
]
);

View File

@@ -2,7 +2,6 @@
* External dependencies
*/
import classnames from 'classnames';
import { useRef, useEffect } from '@wordpress/element';
import { withFilteredAttributes } from '@woocommerce/shared-hocs';
import { FormStep } from '@woocommerce/blocks-components';
import { useCheckoutAddress } from '@woocommerce/base-context/hooks';
@@ -43,21 +42,8 @@ const FrontendBlock = ( {
showCompanyField,
showPhoneField,
} = useCheckoutBlockContext();
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 ] );
const { showBillingFields, forcedBillingAddress, useBillingAsShipping } =
useCheckoutAddress();
if ( ! showBillingFields && ! useBillingAsShipping ) {
return null;
@@ -86,7 +72,6 @@ const FrontendBlock = ( {
showCompanyField={ showCompanyField }
showPhoneField={ showPhoneField }
requirePhoneField={ requirePhoneField }
forceEditing={ toggledUseShippingAsBilling.current }
/>
{ children }
</FormStep>

View File

@@ -8,19 +8,15 @@ import {
noticeContexts,
} from '@woocommerce/base-context';
import { getSetting } from '@woocommerce/settings';
import { CheckboxControl } from '@woocommerce/blocks-checkout';
import {
CheckboxControl,
ValidatedTextInput,
StoreNoticesContainer,
} from '@woocommerce/blocks-checkout';
ValidatedTextInput,
} from '@woocommerce/blocks-components';
import { useDispatch, useSelect } from '@wordpress/data';
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
import { isEmail } from '@wordpress/url';
/**
* Internal dependencies
*/
const Block = (): JSX.Element => {
const { customerId, shouldCreateAccount } = useSelect( ( select ) => {
const store = select( CHECKOUT_STORE_KEY );

View File

@@ -3,6 +3,8 @@
*/
import classnames from 'classnames';
import { Main } from '@woocommerce/base-components/sidebar-layout';
import { useStoreEvents } from '@woocommerce/base-context/hooks';
import { useEffect } from '@wordpress/element';
const FrontendBlock = ( {
children,
@@ -11,6 +13,14 @@ const FrontendBlock = ( {
children: JSX.Element;
className?: string;
} ): JSX.Element => {
const { dispatchCheckoutEvent } = useStoreEvents();
// Ignore changes to dispatchCheckoutEvent callback so this is ran on first mount only.
useEffect( () => {
dispatchCheckoutEvent( 'render-checkout-form' );
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [] );
return (
<Main className={ classnames( 'wc-block-checkout__main', className ) }>
<form className="wc-block-components-form wc-block-checkout__form">

View File

@@ -18,11 +18,11 @@
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 0 calc(#{$gap-smaller} * 2); // Required for spacing especially when using flex-grow
.wc-block-components-text-input,
.wc-block-components-country-input,
.wc-block-components-state-input {
flex: 0 0 calc(50% - #{$gap-smaller});
flex: 1 0 calc(50% - #{$gap-smaller}); // "flex-grow = 1" allows the input to grow to fill the space
box-sizing: border-box;
&:nth-of-type(2),
@@ -33,7 +33,8 @@
.wc-block-components-address-form__company,
.wc-block-components-address-form__address_1,
.wc-block-components-address-form__address_2 {
.wc-block-components-address-form__address_2,
.wc-block-components-country-input {
flex: 0 0 100%;
}
}

View File

@@ -3,7 +3,7 @@
*/
import { OrderSummary } from '@woocommerce/base-components/cart-checkout';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import { TotalsWrapper } from '@woocommerce/blocks-checkout';
import { TotalsWrapper } from '@woocommerce/blocks-components';
const Block = ( { className }: { className: string } ): JSX.Element => {
const { cartItems } = useStoreCart();

View File

@@ -4,7 +4,7 @@
import { TotalsCoupon } from '@woocommerce/base-components/cart-checkout';
import { useStoreCartCoupons } from '@woocommerce/base-context/hooks';
import { getSetting } from '@woocommerce/settings';
import { TotalsWrapper } from '@woocommerce/blocks-checkout';
import { TotalsWrapper } from '@woocommerce/blocks-components';
const Block = ( {
className = '',

View File

@@ -2,15 +2,13 @@
* External dependencies
*/
import { TotalsDiscount } from '@woocommerce/base-components/cart-checkout';
import { TotalsWrapper } from '@woocommerce/blocks-components';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import {
useStoreCartCoupons,
useStoreCart,
} from '@woocommerce/base-context/hooks';
import {
ExperimentalDiscountsMeta,
TotalsWrapper,
} from '@woocommerce/blocks-checkout';
import { ExperimentalDiscountsMeta } from '@woocommerce/blocks-checkout';
const DiscountSlotFill = (): JSX.Element => {
// Prepare props to pass to the ExperimentalOrderMeta slot fill. We need to pluck out receiveCart.

View File

@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { TotalsFees, TotalsWrapper } from '@woocommerce/blocks-checkout';
import { TotalsFees, TotalsWrapper } from '@woocommerce/blocks-components';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import { useStoreCart } from '@woocommerce/base-context/hooks';

View File

@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { Subtotal, TotalsWrapper } from '@woocommerce/blocks-checkout';
import { Subtotal, TotalsWrapper } from '@woocommerce/blocks-components';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import { useStoreCart } from '@woocommerce/base-context/hooks';

View File

@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { TotalsTaxes, TotalsWrapper } from '@woocommerce/blocks-checkout';
import { TotalsTaxes, TotalsWrapper } from '@woocommerce/blocks-components';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import { getSetting } from '@woocommerce/settings';

View File

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

View File

@@ -1,66 +1,68 @@
.wc-block-checkout__pickup-options,
.wp-block-woocommerce-checkout-pickup-options-block {
.wc-block-components-radio-control__option {
@include with-translucent-border(0 0 1px);
margin: 0;
padding: em($gap-small) 0 em($gap-small) em($gap-largest);
}
.wc-block-components-shipping-rates-control__no-results-notice {
margin: em($gap-small) 0;
}
.wc-block-components-radio-control .wc-block-components-radio-control__input {
top: auto;
transform: none;
margin-top: 1px;
}
.wc-block-components-radio-control__option-layout {
display: block;
}
.wc-block-components-radio-control__label-group {
width: 100%;
display: flex;
> :last-child {
margin-left: auto;
.wc-block-components-local-pickup-rates-control {
.wc-block-components-radio-control__option {
@include with-translucent-border(0 0 1px);
margin: 0;
padding: em($gap-small) 0 em($gap-small) em($gap-largest);
}
}
.wc-block-components-radio-control__description-group {
display: none;
}
.wc-block-components-radio-control__option-checked {
.wc-block-components-radio-control__description-group {
.wc-block-components-shipping-rates-control__no-results-notice {
margin: em($gap-small) 0;
}
.wc-block-components-radio-control .wc-block-components-radio-control__input {
top: auto;
transform: none;
margin-top: 1px;
}
.wc-block-components-radio-control__option-layout {
display: block;
}
}
.wc-block-components-radio-control__label-group {
em {
text-transform: uppercase;
font-style: inherit;
}
}
.wc-block-components-radio-control__description-group {
width: 100%;
box-sizing: border-box;
background-color: $gray-100;
border-radius: $universal-border-radius;
padding: 1px em($gap-small);
margin-top: em($gap-smaller);
@include font-size(regular);
}
.wc-block-components-radio-control__description,
.wc-block-components-radio-control__secondary-description {
width: 100%;
text-align: left;
margin: em($gap-small) 0;
display: block;
}
.wc-block-components-radio-control__secondary-description {
color: $gray-700;
.wc-block-components-radio-control__label-group {
width: 100%;
display: flex;
> svg {
vertical-align: middle;
margin-top: -4px;
fill: currentColor;
> :last-child {
margin-left: auto;
}
}
.wc-block-components-radio-control__description-group {
display: none;
}
.wc-block-components-radio-control__option-checked {
.wc-block-components-radio-control__description-group {
display: block;
}
}
.wc-block-components-radio-control__label-group {
em {
text-transform: uppercase;
font-style: inherit;
}
}
.wc-block-components-radio-control__description-group {
width: 100%;
box-sizing: border-box;
background-color: $gray-100;
border-radius: $universal-border-radius;
padding: 1px em($gap-small);
margin-top: em($gap-smaller);
@include font-size(regular);
}
.wc-block-components-radio-control__description,
.wc-block-components-radio-control__secondary-description {
width: 100%;
text-align: left;
margin: em($gap-small) 0;
display: block;
}
.wc-block-components-radio-control__secondary-description {
color: $gray-700;
> svg {
vertical-align: middle;
margin-top: -4px;
fill: currentColor;
}
}
}
}

View File

@@ -9,10 +9,8 @@ import {
useEditorContext,
noticeContexts,
} from '@woocommerce/base-context';
import {
CheckboxControl,
StoreNoticesContainer,
} from '@woocommerce/blocks-checkout';
import { CheckboxControl } from '@woocommerce/blocks-checkout';
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
import Noninteractive from '@woocommerce/base-components/noninteractive';
import type {
BillingAddress,
@@ -82,11 +80,17 @@ const Block = ( {
address_2: {
hidden: ! showApartmentField,
},
phone: {
hidden: ! showPhoneField,
required: requirePhoneField,
},
};
}, [
showCompanyField,
requireCompanyField,
showApartmentField,
showPhoneField,
requirePhoneField,
] ) as Record< keyof AddressFields, Partial< AddressField > >;
const WrapperComponent = isEditor ? Noninteractive : Fragment;
@@ -105,6 +109,9 @@ const Block = ( {
};
} );
// Default editing state for CustomerAddress component comes from the current address and whether or not we're in the editor.
const defaultEditingAddress = isEditor || ! hasAddress;
return (
<>
<StoreNoticesContainer context={ noticeContext } />
@@ -112,27 +119,24 @@ const Block = ( {
{ cartDataLoaded ? (
<CustomerAddress
addressFieldsConfig={ addressFieldsConfig }
showPhoneField={ showPhoneField }
requirePhoneField={ requirePhoneField }
defaultEditing={ defaultEditingAddress }
/>
) : 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();
}
} }
/>
) }
<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

@@ -3,11 +3,7 @@
*/
import { useState, useCallback, useEffect } from '@wordpress/element';
import { AddressForm } from '@woocommerce/base-components/cart-checkout';
import {
useCheckoutAddress,
useStoreEvents,
useEditorContext,
} from '@woocommerce/base-context';
import { useCheckoutAddress, useStoreEvents } from '@woocommerce/base-context';
import type {
ShippingAddress,
AddressField,
@@ -20,34 +16,24 @@ 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,
defaultEditing = false,
}: {
addressFieldsConfig: Record< keyof AddressFields, Partial< AddressField > >;
showPhoneField: boolean;
requirePhoneField: boolean;
defaultEditing?: 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 );
const [ editing, setEditing ] = useState( defaultEditing );
// Forces editing state if store has errors.
const { hasValidationErrors, invalidProps } = useSelect( ( select ) => {
@@ -74,19 +60,11 @@ const CustomerAddress = ( {
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 );
setBillingAddress( values );
dispatchCheckoutEvent( 'set-billing-address' );
}
dispatchCheckoutEvent( 'set-shipping-address' );
@@ -96,7 +74,6 @@ const CustomerAddress = ( {
setBillingAddress,
setShippingAddress,
useShippingAsBilling,
showPhoneField,
]
);
@@ -108,56 +85,28 @@ const CustomerAddress = ( {
onEdit={ () => {
setEditing( true );
} }
showPhoneField={ showPhoneField }
fieldConfig={ addressFieldsConfig }
/>
),
[ shippingAddress, showPhoneField ]
[ shippingAddress, addressFieldsConfig ]
);
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',
} );
}
} }
/>
) }
</>
<AddressForm
id="shipping"
type="shipping"
onChange={ onChangeAddress }
values={ shippingAddress }
fields={ addressFieldKeys }
fieldConfig={ addressFieldsConfig }
/>
),
[
addressFieldKeys,
addressFieldsConfig,
dispatchCheckoutEvent,
onChangeAddress,
requirePhoneField,
setBillingPhone,
setShippingPhone,
shippingAddress,
showPhoneField,
useShippingAsBilling,
]
);

View File

@@ -13,9 +13,11 @@ import {
isAddressComplete,
} from '@woocommerce/base-utils';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import { FormattedMonetaryAmount } from '@woocommerce/blocks-components';
import {
FormattedMonetaryAmount,
StoreNoticesContainer,
} from '@woocommerce/blocks-components';
import { useEditorContext, noticeContexts } from '@woocommerce/base-context';
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
import { decodeEntities } from '@wordpress/html-entities';
import { getSetting } from '@woocommerce/settings';
import type {

View File

@@ -3,7 +3,7 @@
*/
import classnames from 'classnames';
import { Sidebar } from '@woocommerce/base-components/sidebar-layout';
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
const FrontendBlock = ( {
children,

View File

@@ -4,7 +4,7 @@
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { CheckboxControl } from '@woocommerce/blocks-checkout';
import { Textarea } from '@woocommerce/base-components/textarea';
import { Textarea } from '@woocommerce/blocks-components';
interface CheckoutOrderNotesProps {
disabled: boolean;

View File

@@ -2,7 +2,7 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { ValidatedTextInput } from '@woocommerce/blocks-checkout';
import { ValidatedTextInput } from '@woocommerce/blocks-components';
/**
* Renders a phone number input.

View File

@@ -4,14 +4,30 @@
"title": "Collection Filters",
"description": "A block that adds product filters to the product collection.",
"category": "woocommerce",
"keywords": [ "WooCommerce", "Filters" ],
"keywords": [
"WooCommerce",
"Filters"
],
"textdomain": "woocommerce",
"supports": {
"html": false,
"reusable": false
},
"usesContext": [ "query" ],
"ancestor": [ "woocommerce/product-collection" ],
"usesContext": [
"query"
],
"providesContext": {
"collectionData": "collectionData"
},
"ancestor": [
"woocommerce/product-collection"
],
"attributes": {
"collectionData": {
"type": "object",
"default": {}
}
},
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}

View File

@@ -2,11 +2,40 @@
* External dependencies
*/
import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
import { useSelect } from '@wordpress/data';
import { useEffect } from '@wordpress/element';
import { useCollection } from '@woocommerce/base-context/hooks';
const Edit = () => {
/**
* Internal dependencies
*/
import { formatQuery, getQueryParams } from './utils';
import type { EditProps } from './type';
const Edit = ( { clientId, setAttributes, context }: EditProps ) => {
const blockProps = useBlockProps();
const innerBlockProps = useInnerBlocksProps( blockProps );
// Get inner blocks by clientId
const currentBlock = useSelect( ( select ) => {
return select( 'core/block-editor' ).getBlock( clientId );
} );
const { results } = useCollection( {
namespace: '/wc/store/v1',
resourceName: 'products/collection-data',
query: {
...formatQuery( context.query ),
...getQueryParams( currentBlock ),
},
} );
useEffect( () => {
setAttributes( {
collectionData: results,
} );
}, [ results, setAttributes ] );
return <nav { ...innerBlockProps } />;
};

View File

@@ -10,16 +10,22 @@
],
"textdomain": "woocommerce",
"apiVersion": 2,
"viewScript": [
"wc-collection-price-filter-block-frontend"
],
"ancestor": [
"woocommerce/product-collection"
"woocommerce/collection-filters"
],
"supports": {
"interactivity": true
},
"usesContext": [
"collectionData"
],
"attributes": {
"queryParam": {
"type": "object",
"default": {
"calculate_price_range": "true"
}
},
"showInputFields": {
"type": "boolean",
"default": true

View File

@@ -0,0 +1,77 @@
/**
* External dependencies
*/
import { InspectorControls } from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
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 type { FilterComponentProps } from '../types';
export const Inspector = ( {
attributes,
setAttributes,
}: Omit< FilterComponentProps, 'collectionData' > ) => {
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,67 @@
/**
* Internal dependencies
*/
import { EditProps } from '../types';
import { getFormattedPrice } from '../utils';
/**
* We pass the whole props from Edit component to <PriceSlider/> so we're
* reusing the EditProps type here.
*/
export const PriceSlider = ( { attributes, context }: EditProps ) => {
const { showInputFields } = attributes;
const { minPrice, maxPrice, formattedMinPrice, formattedMaxPrice } =
getFormattedPrice( context.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 (
<>
<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>
</>
);
};

View File

@@ -2,38 +2,28 @@
* 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';
import classNames from 'classnames';
/**
* Internal dependencies
*/
import { getFormattedPrice } from './utils';
import { EditProps } from './types';
import { PriceSlider } from './price-slider';
import { PriceSlider } from './components/price-slider';
import { Inspector } from './components/inspector';
const Edit = ( props: EditProps ) => {
const blockProps = useBlockProps();
const { results } = useCollectionData( {
queryPrices: true,
isEditor: true,
queryState: {},
const { showInputFields, inlineInput } = props.attributes;
const blockProps = useBlockProps( {
className: classNames( {
'inline-input': inlineInput && showInputFields,
} ),
} );
return (
<div { ...blockProps }>
<Disabled>
<div className="controls">
<PriceSlider
{ ...props }
collectionData={ getFormattedPrice( results ) }
/>
</div>
<div className="actions">
<FilterResetButton onClick={ () => false } />
</div>
</Disabled>
<Inspector { ...props } />
<PriceSlider { ...props } />
</div>
);
};

View File

@@ -37,10 +37,17 @@ store( {
state: {
filters: {
rangeStyle: ( { state }: StateProps ) => {
const { minPrice, maxPrice, maxRange } = state.filters;
const { minPrice, maxPrice, minRange, maxRange } =
state.filters;
return [
`--low: ${ ( 100 * minPrice ) / maxRange }%`,
`--high: ${ ( 100 * maxPrice ) / maxRange }%`,
`--low: ${
( 100 * ( minPrice - minRange ) ) /
( maxRange - minRange )
}%`,
`--high: ${
( 100 * ( maxPrice - minRange ) ) /
( maxRange - minRange )
}%`,
].join( ';' );
},
formattedMinPrice: ( { state }: StateProps ) => {

View File

@@ -1,5 +1,216 @@
.wp-block-woocommerce-collection-price-filter {
.actions {
text-align: right;
@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 {
.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;
}
}
}

View File

@@ -1,17 +1,25 @@
/**
* External dependencies
*/
import { HTMLElementEvent } from '@woocommerce/types';
import { BlockEditProps } from '@wordpress/blocks';
import { HTMLElementEvent } from '@woocommerce/types';
type PriceFilterState = {
export type BlockAttributes = {
showInputFields: boolean;
inlineInput: boolean;
};
export interface EditProps extends BlockEditProps< BlockAttributes > {
context: {
collectionData: unknown[];
};
}
export type PriceFilterState = {
minPrice: number;
maxPrice: number;
minRange: number;
maxRange: number;
rangeStyle: string;
isMinActive: boolean;
isMaxActive: boolean;
formattedMinPrice: string;
formattedMaxPrice: string;
};
@@ -22,16 +30,9 @@ export type StateProps = {
};
};
export type ActionProps = StateProps & {
export interface ActionProps extends StateProps {
event: HTMLElementEvent< HTMLInputElement >;
};
export type BlockAttributes = {
showInputFields: boolean;
inlineInput: boolean;
};
export type EditProps = BlockEditProps< BlockAttributes >;
}
export type FilterComponentProps = BlockEditProps< BlockAttributes > & {
collectionData: Partial< PriceFilterState >;

View File

@@ -13,18 +13,20 @@ import {
isString,
} from '@woocommerce/types';
const formatPriceInt = ( price: string | number, currency: Currency ) => {
function formatPriceInt( price: string | number, currency: Currency ) {
const priceInt = typeof price === 'number' ? price : parseInt( price, 10 );
return priceInt / 10 ** currency.minorUnit;
};
}
export const getFormattedPrice = ( results: unknown[] ) => {
export function getFormattedPrice( results: unknown[] ) {
const currencyWithoutDecimal = getCurrency( { minorUnit: 0 } );
if ( ! objectHasProp( results, 'price_range' ) ) {
return {
minPrice: 0,
maxPrice: 0,
minRange: 0,
maxRange: 0,
formattedMinPrice: formatPrice( 0, currencyWithoutDecimal ),
formattedMaxPrice: formatPrice( 0, currencyWithoutDecimal ),
};
@@ -48,7 +50,9 @@ export const getFormattedPrice = ( results: unknown[] ) => {
return {
minPrice,
maxPrice,
minRange: minPrice,
maxRange: maxPrice,
formattedMinPrice: formatPrice( minPrice, currencyWithoutDecimal ),
formattedMaxPrice: formatPrice( maxPrice, currencyWithoutDecimal ),
};
};
}

View File

@@ -0,0 +1,46 @@
{
"name": "woocommerce/collection-stock-filter",
"version": "1.0.0",
"title": "Stock Filter",
"description": "Enable customers to filter the product collection by stock status.",
"category": "woocommerce",
"keywords": [ "WooCommerce", "filter", "stock" ],
"supports": {
"interactivity": true,
"html": false,
"multiple": false
},
"attributes": {
"className": {
"type": "string",
"default": ""
},
"showCounts": {
"type": "boolean",
"default": false
},
"displayStyle": {
"type": "string",
"default": "list"
},
"selectType": {
"type": "string",
"default": "multiple"
},
"isPreview": {
"type": "boolean",
"default": false
},
"queryParam": {
"type": "object",
"default": {
"calculate_stock_status_counts": "true"
}
}
},
"usesContext": [ "collectionData" ],
"ancestor": [ "woocommerce/collection-filters" ],
"textdomain": "woocommerce",
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}

View File

@@ -0,0 +1,96 @@
/**
* External dependencies
*/
import { InspectorControls } from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
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 { showCounts, selectType, displayStyle } = attributes;
return (
<InspectorControls key="inspector">
<PanelBody
title={ __(
'Display Settings',
'woo-gutenberg-products-block'
) }
>
<ToggleControl
label={ __(
'Display product count',
'woo-gutenberg-products-block'
) }
checked={ showCounts }
onChange={ () =>
setAttributes( {
showCounts: ! showCounts,
} )
}
/>
<ToggleGroupControl
label={ __(
'Allow selecting multiple options?',
'woo-gutenberg-products-block'
) }
value={ selectType || 'multiple' }
onChange={ ( value: string ) =>
setAttributes( {
selectType: value,
} )
}
className="wc-block-attribute-filter__multiple-toggle"
>
<ToggleGroupControlOption
value="multiple"
label={ __(
'Multiple',
'woo-gutenberg-products-block'
) }
/>
<ToggleGroupControlOption
value="single"
label={ __( 'Single', 'woo-gutenberg-products-block' ) }
/>
</ToggleGroupControl>
<ToggleGroupControl
label={ __(
'Display Style',
'woo-gutenberg-products-block'
) }
value={ displayStyle }
onChange={ ( value ) =>
setAttributes( {
displayStyle: value,
} )
}
className="wc-block-attribute-filter__display-toggle"
>
<ToggleGroupControlOption
value="list"
label={ __( 'List', 'woo-gutenberg-products-block' ) }
/>
<ToggleGroupControlOption
value="dropdown"
label={ __(
'Dropdown',
'woo-gutenberg-products-block'
) }
/>
</ToggleGroupControl>
</PanelBody>
</InspectorControls>
);
};

View File

@@ -0,0 +1,131 @@
/**
* External dependencies
*/
import { useMemo } from '@wordpress/element';
import classnames from 'classnames';
import { useBlockProps } from '@wordpress/block-editor';
import { Disabled } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { Icon, chevronDown } from '@wordpress/icons';
import { ProductQueryContext as Context } from '@woocommerce/blocks/product-query/types';
import { CheckboxList } from '@woocommerce/blocks-components';
import Label from '@woocommerce/base-components/filter-element-label';
import FormTokenField from '@woocommerce/base-components/form-token-field';
import type { BlockEditProps } from '@wordpress/blocks';
import { getSetting } from '@woocommerce/settings';
import {
useCollectionData,
useQueryStateByContext,
} from '@woocommerce/base-context/hooks';
/**
* Internal dependencies
*/
import { BlockProps } from './types';
import { Inspector } from './components/inspector';
type CollectionData = {
// attribute_counts: null | unknown;
// price_range: null | unknown;
// rating_counts: null | unknown;
stock_status_counts: StockStatusCount[];
};
type StockStatusCount = {
status: string;
count: number;
};
const Edit = ( props: BlockEditProps< BlockProps > & { context: Context } ) => {
const blockProps = useBlockProps( {
className: classnames(
'wc-block-stock-filter',
props.attributes.className
),
} );
const { showCounts, displayStyle } = props.attributes;
const stockStatusOptions: Record< string, string > = getSetting(
'stockStatusOptions',
{}
);
const [ queryState ] = useQueryStateByContext();
const { results: filteredCounts } = useCollectionData( {
queryStock: true,
queryState,
isEditor: true,
} );
const listOptions = useMemo( () => {
return Object.entries( stockStatusOptions ).map( ( [ key, value ] ) => {
const count =
// @ts-expect-error - there is a fault with useCollectionData types, it can be non-array.
( filteredCounts as CollectionData )?.stock_status_counts?.find(
( item: StockStatusCount ) => item.status === key
)?.count;
return {
value: key,
label: (
<Label
name={ value }
count={ showCounts && count ? Number( count ) : null }
/>
),
};
} );
}, [ stockStatusOptions, filteredCounts, showCounts ] );
return (
<>
{
<div { ...blockProps }>
<Inspector { ...props } />
<Disabled>
<div
className={ classnames(
'wc-block-stock-filter',
`style-${ displayStyle }`,
{
'is-loading': false,
}
) }
>
{ displayStyle === 'dropdown' ? (
<>
<FormTokenField
className={ classnames( {
'single-selection': true,
'is-loading': false,
} ) }
suggestions={ [] }
placeholder={ __(
'Select stock status',
'woo-gutenberg-products-block'
) }
onChange={ () => null }
value={ [] }
/>
<Icon icon={ chevronDown } size={ 30 } />
</>
) : (
<CheckboxList
className={ 'wc-block-stock-filter-list' }
options={ listOptions }
checked={ [] }
onChange={ () => null }
isLoading={ false }
isDisabled={ true }
/>
) }
</div>
</Disabled>
</div>
}
</>
);
};
export default Edit;

View File

@@ -0,0 +1,79 @@
/**
* External dependencies
*/
import {
store as interactivityStore,
navigate,
} from '@woocommerce/interactivity';
import { DropdownContext } from '@woocommerce/interactivity-components/dropdown';
import { HTMLElementEvent } from '@woocommerce/types';
const getUrl = ( activeFilters: string ) => {
const url = new URL( window.location.href );
const { searchParams } = url;
if ( activeFilters !== '' ) {
searchParams.set( 'filter_stock_status', activeFilters );
} else {
searchParams.delete( 'filter_stock_status' );
}
return url.href;
};
type StockFilterState = {
filters: {
stockStatus: string;
activeFilters: string;
showDropdown: boolean;
};
};
type ActionProps = {
state: StockFilterState;
event: HTMLElementEvent< HTMLInputElement >;
};
interactivityStore( {
state: {
filters: {
stockStatus: '',
},
},
actions: {
filters: {
navigate: ( { context }: { context: DropdownContext } ) => {
if ( context.woocommerceDropdown.selectedItem.value ) {
navigate(
getUrl( context.woocommerceDropdown.selectedItem.value )
);
}
},
updateProducts: ( { event }: ActionProps ) => {
// get the active filters from the url:
const url = new URL( window.location.href );
const currentFilters =
url.searchParams.get( 'filter_stock_status' ) || '';
// split out the active filters into an array.
const filtersArr =
currentFilters === '' ? [] : currentFilters.split( ',' );
// if checked and not already in activeFilters, add to activeFilters
// if not checked and in activeFilters, remove from activeFilters.
if ( event.target.checked ) {
if ( ! currentFilters.includes( event.target.value ) ) {
filtersArr.push( event.target.value );
}
} else {
const index = filtersArr.indexOf( event.target.value );
if ( index > -1 ) {
filtersArr.splice( index, 1 );
}
}
navigate( getUrl( filtersArr.join( ',' ) ) );
},
},
},
} );

View File

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

View File

@@ -0,0 +1,25 @@
/**
* External dependencies
*/
import Label from '@woocommerce/base-components/filter-element-label';
export const previewOptions = [
{
value: 'preview-1',
name: 'In Stock',
label: <Label name="In Stock" count={ 3 } />,
textLabel: 'In Stock (3)',
},
{
value: 'preview-2',
name: 'Out of stock',
label: <Label name="Out of stock" count={ 3 } />,
textLabel: 'Out of stock (3)',
},
{
value: 'preview-3',
name: 'On backorder',
label: <Label name="On backorder" count={ 2 } />,
textLabel: 'On backorder (2)',
},
];

View File

@@ -0,0 +1,181 @@
@import "../../../shared/styles/style";
// Import styles we need to render the checkbox list and checkbox control.
@import "../../../../../../packages/components/checkbox-list/style";
@import "../../../../../../packages/checkout/components/checkbox-control/style";
.wp-block-woocommerce-stock-filter {
h1,
h2,
h3,
h4,
h5,
h6 {
text-transform: inherit;
}
}
.wc-block-stock-filter {
&.is-loading {
@include placeholder();
margin-top: $gap;
box-shadow: none;
border-radius: 0;
}
margin-bottom: $gap-large;
.wc-block-stock-filter-list {
margin: 0;
li {
label {
cursor: pointer;
}
input {
cursor: pointer;
display: inline-block;
}
}
}
&.style-dropdown {
@include includeFormTokenFieldFix();
position: relative;
display: flex;
gap: $gap;
align-items: flex-start;
.wc-block-components-filter-submit-button {
height: 36px;
line-height: 1;
}
> svg {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
}
}
.wc-blocks-components-form-token-field-wrapper {
flex-grow: 1;
max-width: unset;
width: 0;
height: max-content;
&:not(.is-loading) {
border: 1px solid $gray-700 !important;
border-radius: 4px;
}
&.is-loading {
border-radius: em(4px);
}
.components-form-token-field {
border-radius: inherit;
}
}
.wc-blocks-components-form-token-field-wrapper .components-form-token-field__input-container {
@include reset-color();
@include reset-typography();
border: 0;
padding: $gap-smaller;
border-radius: inherit;
.components-form-token-field__input {
@include font-size(small);
&::placeholder {
color: $black;
}
}
.components-form-token-field__suggestions-list {
border: 1px solid $gray-700;
border-radius: 4px;
margin-top: $gap-smaller;
max-height: 21em;
.components-form-token-field__suggestion {
color: $black;
border: 1px solid $gray-400;
border-radius: 4px;
margin: $gap-small;
padding: $gap-small;
}
}
.components-form-token-field__token,
.components-form-token-field__suggestion {
@include font-size(small);
}
}
.wc-block-components-product-rating {
margin-bottom: 0;
}
}
.wc-blocks-components-form-token-field-wrapper:not(.single-selection) .components-form-token-field__input-container {
padding: $gap-smallest 30px $gap-smallest $gap-smaller;
.components-form-token-field__token-text {
background-color: $white;
border: 1px solid;
border-right: 0;
border-radius: 25px 0 0 25px;
padding: em($gap-smallest) em($gap-smaller) em($gap-smallest) em($gap-small);
line-height: 22px;
}
> .components-form-token-field__input {
margin: em($gap-smallest) 0;
}
.components-button.components-form-token-field__remove-token {
background-color: $white;
border: 1px solid;
border-left: 0;
border-radius: 0 25px 25px 0;
padding: 1px em($gap-smallest) 0 0;
&.has-icon svg {
background-color: $gray-200;
border-radius: 25px;
}
}
}
.wc-block-stock-filter__actions {
align-items: center;
display: flex;
gap: $gap;
justify-content: flex-end;
margin-top: $gap;
// The specificity here is needed to overwrite the margin-top that is inherited on WC block template pages such as Shop.
button[type="submit"]:not(.wp-block-search__button).wc-block-components-filter-submit-button {
margin-left: 0;
margin-top: 0;
@include font-size(small);
}
.wc-block-stock-filter__button {
margin-top: em($gap-smaller);
padding: em($gap-smaller) em($gap);
@include font-size(small);
}
}
.editor-styles-wrapper .wc-block-stock-filter .wc-block-stock-filter__button {
margin-top: em($gap-smaller);
padding: em($gap-smaller) em($gap);
@include font-size(small);
}

View File

@@ -0,0 +1,27 @@
/**
* External dependencies
*/
import { BlockEditProps } from '@wordpress/blocks';
export interface BlockProps {
className?: string;
showCounts: boolean;
isPreview?: boolean;
displayStyle: string;
selectType: string;
isEditor: boolean;
}
export interface DisplayOption {
value: string;
name: string;
label: JSX.Element;
textLabel: string;
}
export type Current = {
slug: string;
name: string;
};
export type EditProps = BlockEditProps< BlockProps >;

View File

@@ -0,0 +1,71 @@
/**
* External dependencies
*/
import { DEFAULT_QUERY } from '@woocommerce/blocks/product-collection/constants';
/**
* Internal dependencies
*/
import { sharedParams, mappedParams, formatQuery } from '../utils';
describe( 'formatQuery: transform Product Collection Block query to Product Collection Data Store API query', () => {
it( 'shared param is carried over', () => {
const formattedQuery = formatQuery( DEFAULT_QUERY );
sharedParams.forEach( ( key ) => {
expect( formattedQuery ).toHaveProperty(
key,
DEFAULT_QUERY[ key ]
);
} );
} );
it( 'mapped param key is transformed', () => {
const formattedQuery = formatQuery( DEFAULT_QUERY );
mappedParams.forEach( ( { key, map } ) => {
expect( formattedQuery ).toHaveProperty(
map,
DEFAULT_QUERY[ key ]
);
} );
} );
it( 'taxQuery is transformed', () => {
const queryWithTax = Object.assign( {}, DEFAULT_QUERY, {
taxQuery: {
product_cat: [ 1, 2 ],
product_tag: [ 3, 4 ],
custom_taxonomy: [ 5, 6 ],
},
} );
const formattedQuery = formatQuery( queryWithTax );
expect( formattedQuery ).toHaveProperty( 'cat', [ 1, 2 ] );
expect( formattedQuery ).toHaveProperty( 'tag', [ 3, 4 ] );
expect( formattedQuery ).toHaveProperty(
'_unstable_tax_custom_taxonomy',
[ 5, 6 ]
);
} );
it( 'attribute query is transformed', () => {
const woocommerceAttributes = [
{ termId: 11, taxonomy: 'pa_size' },
{ termId: 12, taxonomy: 'pa_color' },
{ termId: 13, taxonomy: 'pa_custom' },
{ termId: 14, taxonomy: 'pa_custom' },
];
const queryWithAttributes = Object.assign( {}, DEFAULT_QUERY, {
woocommerceAttributes,
} );
const formattedQuery = formatQuery( queryWithAttributes );
expect( formattedQuery ).toHaveProperty( 'attributes' );
woocommerceAttributes.forEach( ( { termId, taxonomy } ) => {
expect( formattedQuery.attributes ).toEqual(
expect.arrayContaining( [
{ term_id: termId, attribute: taxonomy },
] )
);
} );
} );
} );

View File

@@ -0,0 +1,15 @@
/**
* External dependencies
*/
import type { BlockEditProps } from '@wordpress/blocks';
import type { ProductCollectionQuery } from '@woocommerce/blocks/product-collection/types';
type BlockAttributes = {
collectionData: unknown[];
};
export interface EditProps extends BlockEditProps< BlockAttributes > {
context: {
query: ProductCollectionQuery;
};
}

View File

@@ -0,0 +1,89 @@
/**
* External dependencies
*/
import type { BlockInstance } from '@wordpress/blocks';
import type { ProductCollectionQuery } from '@woocommerce/blocks/product-collection/types';
function getInnerBlocksParams( block: BlockInstance, initial = {} ) {
return block.innerBlocks.reduce(
( acc, innerBlock ): Record< string, unknown > => {
acc = { ...acc, ...innerBlock.attributes?.queryParam };
return getInnerBlocksParams( innerBlock, acc );
},
initial
);
}
export function getQueryParams( block: BlockInstance | null ) {
if ( ! block ) return {};
return getInnerBlocksParams( block );
}
export const sharedParams: Array< keyof ProductCollectionQuery > = [
'exclude',
'offset',
'search',
];
/**
* There is an open dicussion around the shape of this object. Check it out on GH.
*
* @see {@link https://github.com/woocommerce/woocommerce-blocks/pull/11218#discussion_r1365171167 | #11218 review comment}.
*/
export const mappedParams: {
key: keyof ProductCollectionQuery;
map: string;
}[] = [
{ key: 'woocommerceStockStatus', map: 'stock_status' },
{ key: 'woocommerceOnSale', map: 'on_sale' },
{ key: 'woocommerceHandPickedProducts', map: 'include' },
];
function mapTaxonomy( taxonomy: string ) {
const map = {
product_tag: 'tag',
product_cat: 'cat',
};
return map[ taxonomy as keyof typeof map ] || `_unstable_tax_${ taxonomy }`;
}
function getTaxQueryMap( taxQuery: ProductCollectionQuery[ 'taxQuery' ] ) {
return Object.entries( taxQuery ).map( ( [ taxonomy, terms ] ) => ( {
[ mapTaxonomy( taxonomy ) ]: terms,
} ) );
}
function getAttributeQuery(
woocommerceAttributes: ProductCollectionQuery[ 'woocommerceAttributes' ]
) {
if ( ! woocommerceAttributes ) {
return {};
}
return woocommerceAttributes.map( ( attribute ) => ( {
attribute: attribute.taxonomy,
term_id: attribute.termId,
} ) );
}
export function formatQuery( query: ProductCollectionQuery ) {
if ( ! query ) {
return {};
}
return Object.assign(
{
attributes: getAttributeQuery( query.woocommerceAttributes ),
catalog_visibility: 'visible',
},
...sharedParams.map(
( key ) => key in query && { [ key ]: query[ key ] }
),
...mappedParams.map(
( param ) =>
param.key in query && { [ param.map ]: query[ param.key ] }
),
...getTaxQueryMap( query.taxQuery )
);
}

View File

@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
import { useStoreCart } from '@woocommerce/base-context/hooks';
type FilledMiniCartContentsBlockProps = {

View File

@@ -2,7 +2,7 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { TotalsItem } from '@woocommerce/blocks-checkout';
import { TotalsItem } from '@woocommerce/blocks-components';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import {
usePaymentMethods,

View File

@@ -2,7 +2,7 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { TotalsItem } from '@woocommerce/blocks-checkout';
import { TotalsItem } from '@woocommerce/blocks-components';
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import {

View File

@@ -40,16 +40,17 @@ export const DEFAULT_QUERY: ProductCollectionQuery = {
postType: 'product',
order: 'asc',
orderBy: 'title',
author: '',
search: '',
exclude: [],
inherit: null,
taxQuery: {},
isProductCollectionBlock: true,
featured: false,
woocommerceOnSale: false,
woocommerceStockStatus: getDefaultStockStatuses(),
woocommerceAttributes: [],
woocommerceHandPickedProducts: [],
timeFrame: undefined,
};
export const DEFAULT_ATTRIBUTES: Partial< ProductCollectionAttributes > = {
@@ -87,4 +88,6 @@ export const DEFAULT_FILTERS: Partial< ProductCollectionQuery > = {
woocommerceAttributes: [],
taxQuery: DEFAULT_QUERY.taxQuery,
woocommerceHandPickedProducts: [],
featured: DEFAULT_QUERY.featured,
timeFrame: undefined,
};

View File

@@ -75,7 +75,7 @@ export const INNER_BLOCKS_TEMPLATE: InnerBlockTemplate[] = [
},
},
],
[ 'core/query-no-results' ],
[ 'woocommerce/product-collection-no-results' ],
];
const Edit = ( props: BlockEditProps< ProductCollectionAttributes > ) => {

View File

@@ -0,0 +1,35 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "woocommerce/product-collection-no-results",
"title": "No results",
"version": "1.0.0",
"category": "woocommerce",
"description": "The contents of this block will display when there are no products found.",
"textdomain": "woocommerce",
"keywords": [ "Product Collection" ],
"usesContext": [ "queryId", "query" ],
"ancestor": [ "woocommerce/product-collection" ],
"supports": {
"align": true,
"reusable": false,
"html": false,
"color": {
"gradients": true,
"link": true
},
"typography": {
"fontSize": true,
"lineHeight": true,
"__experimentalFontFamily": true,
"__experimentalFontWeight": true,
"__experimentalFontStyle": true,
"__experimentalTextTransform": true,
"__experimentalTextDecoration": true,
"__experimentalLetterSpacing": true,
"__experimentalDefaultControls": {
"fontSize": true
}
}
}
}

View File

@@ -0,0 +1,65 @@
/**
* External dependencies
*/
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
import { Template } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
const TEMPLATE: Template[] = [
[
'core/group',
{
layout: {
type: 'flex',
orientation: 'vertical',
justifyContent: 'center',
flexWrap: 'wrap',
},
},
[
[
'core/paragraph',
{
textAlign: 'center',
fontSize: 'medium',
content: `<strong>${ __(
'No results found',
'woo-gutenberg-products-block'
) }</strong>`,
},
],
[
'core/paragraph',
{
content: `${ __(
'You can try',
'woo-gutenberg-products-block'
) } <a href="#" class="wc-link-clear-any-filters">${ __(
'clearing any filters',
'woo-gutenberg-products-block'
) }</a> ${ __(
'or head to our',
'woo-gutenberg-products-block'
) } <a href="#" class="wc-link-stores-home">${ __(
"store's home",
'woo-gutenberg-products-block'
) }</a>`,
},
],
],
],
];
const Edit = () => {
const blockProps = useBlockProps( {
className: 'wc-block-product-collection-no-results',
} );
return (
<div { ...blockProps }>
<InnerBlocks template={ TEMPLATE } />
</div>
);
};
export default Edit;

View File

@@ -0,0 +1,21 @@
/**
* External dependencies
*/
import { registerBlockType } from '@wordpress/blocks';
import { loop as loopIcon } from '@wordpress/icons';
/**
* Internal dependencies
*/
import metadata from './block.json';
import edit from './edit';
import save from './save';
registerBlockType( metadata, {
icon: loopIcon,
supports: {
...metadata.supports,
},
edit,
save,
} );

View File

@@ -0,0 +1,9 @@
/**
* External dependencies
*/
import { InnerBlocks } from '@wordpress/block-editor';
export default function NoResultsSave() {
// @ts-expect-error: `InnerBlocks.Content` is a component that is typed in WordPress core
return <InnerBlocks.Content />;
}

View File

@@ -0,0 +1,114 @@
/**
* External dependencies
*/
import { __, _x } from '@wordpress/i18n';
import {
Flex,
FlexItem,
RadioControl,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - Ignoring because `__experimentalToggleGroupControl` is not yet in the type definitions.
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
__experimentalToggleGroupControl as ToggleGroupControl,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - Ignoring because `__experimentalToggleGroupControlOption` is not yet in the type definitions.
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
__experimentalToggleGroupControlOption as ToggleGroupControlOption,
// @ts-expect-error Using experimental features
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
__experimentalToolsPanelItem as ToolsPanelItem,
} from '@wordpress/components';
/**
* Internal dependencies
*/
import { ETimeFrameOperator, QueryControlProps } from '../types';
const CreatedControl = ( props: QueryControlProps ) => {
const { query, setQueryAttribute } = props;
const { timeFrame } = query;
return (
<ToolsPanelItem
label={ __( 'Created', 'woo-gutenberg-products-block' ) }
hasValue={ () => timeFrame?.operator && timeFrame?.value }
onDeselect={ () => {
setQueryAttribute( {
timeFrame: undefined,
} );
} }
>
<Flex direction="column" gap={ 3 }>
<FlexItem>
<ToggleGroupControl
label={ __(
'Created',
'woo-gutenberg-products-block'
) }
isBlock
onChange={ ( value: ETimeFrameOperator ) => {
setQueryAttribute( {
timeFrame: {
...timeFrame,
operator: value,
},
} );
} }
value={ timeFrame?.operator || ETimeFrameOperator.IN }
>
<ToggleGroupControlOption
value={ ETimeFrameOperator.IN }
label={ _x(
'Within',
'Product Collection query operator',
'woo-gutenberg-products-block'
) }
/>
<ToggleGroupControlOption
value={ ETimeFrameOperator.NOT_IN }
label={ _x(
'Before',
'Product Collection query operator',
'woo-gutenberg-products-block'
) }
/>
</ToggleGroupControl>
</FlexItem>
<FlexItem>
<RadioControl
onChange={ ( value: string ) => {
setQueryAttribute( {
timeFrame: {
operator: ETimeFrameOperator.IN,
...timeFrame,
value,
},
} );
} }
options={ [
{
label: 'last 24 hours',
value: '-1 day',
},
{
label: 'last 7 days',
value: '-7 days',
},
{
label: 'last 30 days',
value: '-30 days',
},
{
label: 'last 3 months',
value: '-3 months',
},
] }
selected={ timeFrame?.value }
/>
</FlexItem>
</Flex>
</ToolsPanelItem>
);
};
export default CreatedControl;

View File

@@ -0,0 +1,52 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import {
BaseControl,
ToggleControl,
// @ts-expect-error Using experimental features
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
__experimentalToolsPanelItem as ToolsPanelItem,
} from '@wordpress/components';
/**
* Internal dependencies
*/
import { QueryControlProps } from '../types';
const FeaturedProductsControl = ( props: QueryControlProps ) => {
const { query, setQueryAttribute } = props;
return (
<ToolsPanelItem
label={ __( 'Featured', 'woo-gutenberg-products-block' ) }
hasValue={ () => query.featured === true }
onDeselect={ () => {
setQueryAttribute( {
featured: false,
} );
} }
>
<BaseControl
id="product-collection-featured-products-control"
label={ __( 'Featured', 'woo-gutenberg-products-block' ) }
>
<ToggleControl
label={ __(
'Show only featured products',
'woo-gutenberg-products-block'
) }
checked={ query.featured || false }
onChange={ ( featured ) => {
setQueryAttribute( {
featured,
} );
} }
/>
</BaseControl>
</ToolsPanelItem>
);
};
export default FeaturedProductsControl;

View File

@@ -38,8 +38,9 @@ import KeywordControl from './keyword-control';
import AttributesControl from './attributes-control';
import TaxonomyControls from './taxonomy-controls';
import HandPickedProductsControl from './hand-picked-products-control';
import AuthorControl from './author-control';
import LayoutOptionsControl from './layout-options-control';
import FeaturedProductsControl from './featured-products-control';
import CreatedControl from './created-control';
const ProductCollectionInspectorControls = (
props: BlockEditProps< ProductCollectionAttributes >
@@ -99,7 +100,8 @@ const ProductCollectionInspectorControls = (
<KeywordControl { ...queryControlProps } />
<AttributesControl { ...queryControlProps } />
<TaxonomyControls { ...queryControlProps } />
<AuthorControl { ...queryControlProps } />
<FeaturedProductsControl { ...queryControlProps } />
<CreatedControl { ...queryControlProps } />
</ToolsPanel>
) : null }
<ProductCollectionFeedbackPrompt />

View File

@@ -28,8 +28,17 @@ export interface ProductCollectionDisplayLayout {
shrinkColumns?: boolean;
}
export enum ETimeFrameOperator {
IN = 'in',
NOT_IN = 'not-in',
}
export interface TimeFrame {
operator?: ETimeFrameOperator;
value?: string;
}
export interface ProductCollectionQuery {
author: string;
exclude: string[];
inherit: boolean | null;
offset: number;
@@ -40,6 +49,11 @@ export interface ProductCollectionQuery {
postType: string;
search: string;
taxQuery: Record< string, number[] >;
/**
* If true, show only featured products.
*/
featured: boolean;
timeFrame: TimeFrame | undefined;
woocommerceOnSale: boolean;
/**
* Filter products by their stock status.

View File

@@ -69,6 +69,7 @@ export const ProductGalleryBlockSettings = ( {
cropImages: ! cropImages,
} )
}
className="wc-block-product-gallery__crop-images"
/>
<ToggleControl
label={ __(

View File

@@ -21,7 +21,9 @@
"nextPreviousButtonsPosition": "nextPreviousButtonsPosition",
"pagerDisplayMode": "pagerDisplayMode",
"hoverZoom": "hoverZoom",
"fullScreenOnClick": "fullScreenOnClick"
"fullScreenOnClick": "fullScreenOnClick",
"mode": "mode",
"cropImages": "cropImages"
},
"attributes": {
"thumbnailsPosition": {

View File

@@ -7,12 +7,15 @@ interface State {
[ key: string ]: unknown;
}
interface Context {
export interface ProductGalleryInteractivityApiContext {
woocommerce: {
selectedImage: string;
firstMainImageId: string;
imageId: string;
visibleImagesIds: string[];
dialogVisibleImagesIds: string[];
isDialogOpen: boolean;
productId: string;
};
}
@@ -28,7 +31,9 @@ export interface ProductGallerySelectors {
interface Actions {
woocommerce: {
thumbnails: {
handleClick: ( context: Context ) => void;
handleClick: (
context: ProductGalleryInteractivityApiContext
) => void;
};
handlePreviousImageButtonClick: {
( store: Store ): void;
@@ -36,12 +41,17 @@ interface Actions {
handleNextImageButtonClick: {
( store: Store ): void;
};
dialog: {
handleCloseButtonClick: {
( store: Store ): void;
};
};
};
}
interface Store {
state: State;
context: Context;
context: ProductGalleryInteractivityApiContext;
selectors: ProductGallerySelectors;
actions: Actions;
ref?: HTMLElement;
@@ -63,6 +73,41 @@ interactivityApiStore( {
state: {},
effects: {
woocommerce: {
watchForChangesOnAddToCartForm: ( store: Store ) => {
const variableProductCartForm = document.querySelector(
`form[data-product_id="${ store.context.woocommerce.productId }"]`
);
if ( ! variableProductCartForm ) {
return;
}
const observer = new MutationObserver( function ( mutations ) {
for ( const mutation of mutations ) {
const mutationTarget = mutation.target as HTMLElement;
const currentImageAttribute =
mutationTarget.getAttribute( 'current-image' );
if (
mutation.type === 'attributes' &&
currentImageAttribute &&
store.context.woocommerce.visibleImagesIds.includes(
currentImageAttribute
)
) {
store.context.woocommerce.selectedImage =
currentImageAttribute;
}
}
} );
observer.observe( variableProductCartForm, {
attributes: true,
} );
return () => {
observer.disconnect();
};
},
keyboardAccess: ( store: Store ) => {
const { context, actions } = store;
let allowNavigation = true;
@@ -84,7 +129,9 @@ interactivityApiStore( {
// Check if the esc key is pressed.
if ( event.keyCode === Keys.ESC ) {
context.woocommerce.isDialogOpen = false;
actions.woocommerce.dialog.handleCloseButtonClick(
store
);
}
// Check if left arrow key is pressed.
@@ -136,6 +183,10 @@ interactivityApiStore( {
dialog: {
handleCloseButtonClick: ( { context }: Store ) => {
context.woocommerce.isDialogOpen = false;
// Reset the main image.
context.woocommerce.selectedImage =
context.woocommerce.firstMainImageId;
},
},
handleSelectImage: ( { context }: Store ) => {
@@ -143,30 +194,39 @@ interactivityApiStore( {
},
handleNextImageButtonClick: ( store: Store ) => {
const { context } = store;
const selectedImageIdIndex =
context.woocommerce.visibleImagesIds.indexOf(
context.woocommerce.selectedImage
);
const imagesIds =
context.woocommerce[
context.woocommerce.isDialogOpen
? 'dialogVisibleImagesIds'
: 'visibleImagesIds'
];
const selectedImageIdIndex = imagesIds.indexOf(
context.woocommerce.selectedImage
);
const nextImageIndex = Math.min(
selectedImageIdIndex + 1,
context.woocommerce.visibleImagesIds.length - 1
imagesIds.length - 1
);
context.woocommerce.selectedImage =
context.woocommerce.visibleImagesIds[ nextImageIndex ];
context.woocommerce.selectedImage = imagesIds[ nextImageIndex ];
},
handlePreviousImageButtonClick: ( store: Store ) => {
const { context } = store;
const selectedImageIdIndex =
context.woocommerce.visibleImagesIds.indexOf(
context.woocommerce.selectedImage
);
const imagesIds =
context.woocommerce[
context.woocommerce.isDialogOpen
? 'dialogVisibleImagesIds'
: 'visibleImagesIds'
];
const selectedImageIdIndex = imagesIds.indexOf(
context.woocommerce.selectedImage
);
const previousImageIndex = Math.max(
selectedImageIdIndex - 1,
0
);
context.woocommerce.selectedImage =
context.woocommerce.visibleImagesIds[ previousImageIndex ];
imagesIds[ previousImageIndex ];
},
},
},

View File

@@ -7,10 +7,8 @@ import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { Edit } from './edit';
import { Save } from './save';
import metadata from './block.json';
import icon from './icon';
import { ProductGalleryBlockSettings } from './settings';
import './style.scss';
import './inner-blocks/product-gallery-large-image-next-previous';
import './inner-blocks/product-gallery-pager';
@@ -18,9 +16,5 @@ import './inner-blocks/product-gallery-thumbnails';
if ( isExperimentalBuild() ) {
// @ts-expect-error: `metadata` currently does not have a type definition in WordPress core.
registerBlockType( metadata, {
icon,
edit: Edit,
save: Save,
} );
registerBlockType( metadata, ProductGalleryBlockSettings );
}

View File

@@ -4,7 +4,7 @@
"name": "woocommerce/product-gallery-large-image-next-previous",
"version": "1.0.0",
"title": "Next/Previous Buttons",
"description": "Dispaly next and previous buttons.",
"description": "Display next and previous buttons.",
"category": "woocommerce",
"keywords": [ "WooCommerce" ],
"usesContext": [ "nextPreviousButtonsPosition", "productGalleryClientId", "postId"],

View File

@@ -7,7 +7,7 @@
"description": "Display the Large Image of a product.",
"category": "woocommerce",
"keywords": [ "WooCommerce" ],
"usesContext": [ "nextPreviousButtonsPosition", "postId", "hoverZoom", "fullScreenOnClick"],
"usesContext": [ "nextPreviousButtonsPosition", "postId", "hoverZoom", "fullScreenOnClick", "cropImages"],
"supports": {
"interactivity": true
},

View File

@@ -6,7 +6,10 @@ import { store as interactivityStore } from '@woocommerce/interactivity';
/**
* Internal dependencies
*/
import { ProductGallerySelectors } from '../../frontend';
import {
ProductGalleryInteractivityApiContext,
ProductGallerySelectors,
} from '../../frontend';
type Context = {
woocommerce: {
@@ -20,7 +23,7 @@ type Context = {
| undefined;
isDialogOpen: boolean;
};
};
} & ProductGalleryInteractivityApiContext;
type Store = {
context: Context;
@@ -49,6 +52,13 @@ const productGalleryLargeImageSelectors = {
let isDialogStatusChanged = false;
const resetImageZoom = ( context: Context ) => {
if ( context.woocommerce.styles ) {
context.woocommerce.styles.transform = `scale(1.0)`;
context.woocommerce.styles[ 'transform-origin' ] = '';
}
};
interactivityStore(
// @ts-expect-error: Store function isn't typed.
{
@@ -62,13 +72,23 @@ interactivityStore(
event: MouseEvent;
context: Context;
} ) => {
if ( ( event.target as HTMLElement ).tagName === 'IMG' ) {
const element = event.target as HTMLElement;
const percentageX =
( event.offsetX / element.clientWidth ) * 100;
const percentageY =
( event.offsetY / element.clientHeight ) * 100;
const target = event.target as HTMLElement;
const isMouseEventFromLargeImage =
target.classList.contains(
'wc-block-woocommerce-product-gallery-large-image__image'
);
if ( ! isMouseEventFromLargeImage ) {
resetImageZoom( context );
return;
}
const element = event.target as HTMLElement;
const percentageX =
( event.offsetX / element.clientWidth ) * 100;
const percentageY =
( event.offsetY / element.clientHeight ) * 100;
if ( context.woocommerce.styles ) {
context.woocommerce.styles.transform = `scale(1.3)`;
context.woocommerce.styles[
@@ -77,8 +97,7 @@ interactivityStore(
}
},
handleMouseLeave: ( { context }: { context: Context } ) => {
context.woocommerce.styles.transform = `scale(1.0)`;
context.woocommerce.styles[ 'transform-origin' ] = '';
resetImageZoom( context );
},
handleClick: ( {
context,
@@ -87,7 +106,11 @@ interactivityStore(
context: Context;
event: Event;
} ) => {
if ( ( event.target as HTMLElement ).tagName === 'IMG' ) {
if (
( event.target as HTMLElement ).classList.contains(
'wc-block-product-gallery-dialog-on-click'
)
) {
context.woocommerce.isDialogOpen = true;
}
},

View File

@@ -3,6 +3,8 @@
justify-content: center;
list-style: none;
gap: $gap-small;
margin-top: 0;
margin-bottom: 0;
padding: 0;
}

View File

@@ -7,7 +7,7 @@
"description": "Display the Thumbnails of a product.",
"category": "woocommerce",
"keywords": [ "WooCommerce" ],
"usesContext": [ "postId", "thumbnailsPosition", "thumbnailsNumberOfThumbnails", "productGalleryClientId" ],
"usesContext": [ "postId", "thumbnailsPosition", "thumbnailsNumberOfThumbnails", "productGalleryClientId", "mode" ],
"textdomain": "woocommerce",
"ancestor": [ "woocommerce/product-gallery" ],
"supports": {

View File

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

View File

@@ -216,6 +216,8 @@ $outside-image-max-width: calc(100% - (2 * $outside-image-offset));
justify-content: center;
list-style: none;
gap: $gap-small;
margin-top: 0;
margin-bottom: 0;
padding: 0;
}
@@ -245,6 +247,35 @@ $outside-image-max-width: calc(100% - (2 * $outside-image-offset));
width: 100px;
height: 100px;
margin: 5px;
position: relative;
}
.wc-block-product-gallery-thumbnails__thumbnail__overlay {
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: absolute;
background-color: rgba(0, 0, 0, 0.4);
top: 0;
width: 100%;
height: 100%;
.wc-block-product-gallery-thumbnails__thumbnail__remaining-thumbnails-count {
@include font-size(large);
font-weight: 700;
}
.wc-block-product-gallery-thumbnails__thumbnail__view-all {
@include font-size(smaller);
text-decoration: underline;
}
.wc-block-product-gallery-thumbnails__thumbnail__remaining-thumbnails-count,
.wc-block-product-gallery-thumbnails__thumbnail__view-all {
color: #fff;
}
}
}

View File

@@ -3,7 +3,8 @@
*/
import { __ } from '@wordpress/i18n';
import { createBlock, registerBlockType } from '@wordpress/blocks';
import { Icon, sparkles } from '@wordpress/icons';
import { Icon } from '@wordpress/icons';
import { sparkles } from '@woocommerce/icons';
/**
* Internal dependencies

View File

@@ -57,6 +57,7 @@ export const INNER_BLOCKS_TEMPLATE: InnerBlockTemplate[] = [
{
level: 2,
content: __( 'Related products', 'woo-gutenberg-products-block' ),
style: { spacing: { margin: { top: '1rem', bottom: '1rem' } } },
},
],
[

View File

@@ -77,7 +77,6 @@ const ProductTemplateEdit = ( {
offset = 0,
order,
orderBy,
author,
search,
exclude,
inherit,
@@ -155,9 +154,6 @@ const ProductTemplateEdit = ( {
if ( perPage ) {
query.per_page = perPage;
}
if ( author ) {
query.author = author;
}
if ( search ) {
query.search = search;
}
@@ -186,7 +182,6 @@ const ProductTemplateEdit = ( {
order,
orderBy,
clientId,
author,
search,
postType,
exclude,

View File

@@ -5,7 +5,7 @@ import { Component } from '@wordpress/element';
import { ProductListContainer } from '@woocommerce/base-components/product-list';
import { InnerBlockLayoutContextProvider } from '@woocommerce/shared-context';
import { gridBlockPreview } from '@woocommerce/resource-previews';
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
import { Attributes as ProductListAttributes } from 'assets/js/base/components/product-list/types';
interface BlockProps {

View File

@@ -49,7 +49,6 @@ const FrontendBlock = ( {
<ReviewSortSelect
value={ sortSelectValue }
onChange={ onChangeOrderby }
readOnly
/>
) }
<ReviewList attributes={ attributes } reviews={ reviews } />

View File

@@ -7,7 +7,9 @@ jest.mock( '../utils', () => ( {
jest.mock( '@woocommerce/settings', () => ( {
...jest.requireActual( '@woocommerce/settings' ),
getSetting: jest.fn().mockReturnValue( true ),
getSetting: jest
.fn()
.mockImplementation( ( setting, defaultValue ) => defaultValue ),
} ) );
/**