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:
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -96,6 +96,9 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
table.wc-block-cart-items {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.is-medium,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import Title from '@woocommerce/base-components/title';
|
||||
import { Title } from '@woocommerce/blocks-components';
|
||||
|
||||
/**
|
||||
* Step Heading Component
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 = '',
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
} }
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 } />;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 ) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 >;
|
||||
|
||||
@@ -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 ),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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( ',' ) ) );
|
||||
},
|
||||
},
|
||||
},
|
||||
} );
|
||||
@@ -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,
|
||||
} );
|
||||
}
|
||||
@@ -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)',
|
||||
},
|
||||
];
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 >;
|
||||
@@ -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 },
|
||||
] )
|
||||
);
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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 )
|
||||
);
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -75,7 +75,7 @@ export const INNER_BLOCKS_TEMPLATE: InnerBlockTemplate[] = [
|
||||
},
|
||||
},
|
||||
],
|
||||
[ 'core/query-no-results' ],
|
||||
[ 'woocommerce/product-collection-no-results' ],
|
||||
];
|
||||
|
||||
const Edit = ( props: BlockEditProps< ProductCollectionAttributes > ) => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
} );
|
||||
@@ -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 />;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 />
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -69,6 +69,7 @@ export const ProductGalleryBlockSettings = ( {
|
||||
cropImages: ! cropImages,
|
||||
} )
|
||||
}
|
||||
className="wc-block-product-gallery__crop-images"
|
||||
/>
|
||||
<ToggleControl
|
||||
label={ __(
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
"nextPreviousButtonsPosition": "nextPreviousButtonsPosition",
|
||||
"pagerDisplayMode": "pagerDisplayMode",
|
||||
"hoverZoom": "hoverZoom",
|
||||
"fullScreenOnClick": "fullScreenOnClick"
|
||||
"fullScreenOnClick": "fullScreenOnClick",
|
||||
"mode": "mode",
|
||||
"cropImages": "cropImages"
|
||||
},
|
||||
"attributes": {
|
||||
"thumbnailsPosition": {
|
||||
|
||||
@@ -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 ];
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
justify-content: center;
|
||||
list-style: none;
|
||||
gap: $gap-small;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' } } },
|
||||
},
|
||||
],
|
||||
[
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -49,7 +49,6 @@ const FrontendBlock = ( {
|
||||
<ReviewSortSelect
|
||||
value={ sortSelectValue }
|
||||
onChange={ onChangeOrderby }
|
||||
readOnly
|
||||
/>
|
||||
) }
|
||||
<ReviewList attributes={ attributes } reviews={ reviews } />
|
||||
|
||||
@@ -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 ),
|
||||
} ) );
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user