rebase code on oct-10-2023

This commit is contained in:
Rachit Bhargava
2023-10-10 17:51:46 -04:00
parent b16ad94b69
commit 8f1a2c3a66
2197 changed files with 184921 additions and 35568 deletions

View File

@@ -5,11 +5,8 @@
* Description: A robust scheduling library for use in WordPress plugins.
* Author: Automattic
* Author URI: https://automattic.com/
* Version: 3.6.2
* Version: 3.6.1
* License: GPLv3
* Tested up to: 6.3
* Requires at least: 5.2
* Requires PHP: 5.6
*
* Copyright 2019 Automattic, Inc. (https://automattic.com/contact/)
*
@@ -29,27 +26,27 @@
* @package ActionScheduler
*/
if ( ! function_exists( 'action_scheduler_register_3_dot_6_dot_2' ) && function_exists( 'add_action' ) ) { // WRCS: DEFINED_VERSION.
if ( ! function_exists( 'action_scheduler_register_3_dot_6_dot_1' ) && function_exists( 'add_action' ) ) { // WRCS: DEFINED_VERSION.
if ( ! class_exists( 'ActionScheduler_Versions', false ) ) {
require_once __DIR__ . '/classes/ActionScheduler_Versions.php';
add_action( 'plugins_loaded', array( 'ActionScheduler_Versions', 'initialize_latest_version' ), 1, 0 );
}
add_action( 'plugins_loaded', 'action_scheduler_register_3_dot_6_dot_2', 0, 0 ); // WRCS: DEFINED_VERSION.
add_action( 'plugins_loaded', 'action_scheduler_register_3_dot_6_dot_1', 0, 0 ); // WRCS: DEFINED_VERSION.
/**
* Registers this version of Action Scheduler.
*/
function action_scheduler_register_3_dot_6_dot_2() { // WRCS: DEFINED_VERSION.
function action_scheduler_register_3_dot_6_dot_1() { // WRCS: DEFINED_VERSION.
$versions = ActionScheduler_Versions::instance();
$versions->register( '3.6.2', 'action_scheduler_initialize_3_dot_6_dot_2' ); // WRCS: DEFINED_VERSION.
$versions->register( '3.6.1', 'action_scheduler_initialize_3_dot_6_dot_1' ); // WRCS: DEFINED_VERSION.
}
/**
* Initializes this version of Action Scheduler.
*/
function action_scheduler_initialize_3_dot_6_dot_2() { // WRCS: DEFINED_VERSION.
function action_scheduler_initialize_3_dot_6_dot_1() { // WRCS: DEFINED_VERSION.
// A final safety check is required even here, because historic versions of Action Scheduler
// followed a different pattern (in some unusual cases, we could reach this point and the
// ActionScheduler class is already defined—so we need to guard against that).
@@ -61,7 +58,7 @@ if ( ! function_exists( 'action_scheduler_register_3_dot_6_dot_2' ) && function_
// Support usage in themes - load this version if no plugin has loaded a version yet.
if ( did_action( 'plugins_loaded' ) && ! doing_action( 'plugins_loaded' ) && ! class_exists( 'ActionScheduler', false ) ) {
action_scheduler_initialize_3_dot_6_dot_2(); // WRCS: DEFINED_VERSION.
action_scheduler_initialize_3_dot_6_dot_1(); // WRCS: DEFINED_VERSION.
do_action( 'action_scheduler_pre_theme_init' );
ActionScheduler_Versions::initialize_latest_version();
}

View File

@@ -502,20 +502,7 @@ class ActionScheduler_ListTable extends ActionScheduler_Abstract_ListTable {
*/
protected function bulk_delete( array $ids, $ids_sql ) {
foreach ( $ids as $id ) {
try {
$this->store->delete_action( $id );
} catch ( Exception $e ) {
// A possible reason for an exception would include a scenario where the same action is deleted by a
// concurrent request.
error_log(
sprintf(
/* translators: 1: action ID 2: exception message. */
__( 'Action Scheduler was unable to delete action %1$d. Reason: %2$s', 'woocommerce' ),
$id,
$e->getMessage()
)
);
}
$this->store->delete_action( $id );
}
}

View File

@@ -24,37 +24,7 @@ class ActionScheduler_OptionLock extends ActionScheduler_Lock {
* @bool True if lock value has changed, false if not or if set failed.
*/
public function set( $lock_type ) {
global $wpdb;
$lock_key = $this->get_key( $lock_type );
$existing_lock_value = $this->get_existing_lock( $lock_type );
$new_lock_value = $this->new_lock_value( $lock_type );
// The lock may not exist yet, or may have been deleted.
if ( empty( $existing_lock_value ) ) {
return (bool) $wpdb->insert(
$wpdb->options,
array(
'option_name' => $lock_key,
'option_value' => $new_lock_value,
'autoload' => 'no',
)
);
}
if ( $this->get_expiration_from( $existing_lock_value ) >= time() ) {
return false;
}
// Otherwise, try to obtain the lock.
return (bool) $wpdb->update(
$wpdb->options,
array( 'option_value' => $new_lock_value ),
array(
'option_name' => $lock_key,
'option_value' => $existing_lock_value,
)
);
return update_option( $this->get_key( $lock_type ), time() + $this->get_duration( $lock_type ) );
}
/**
@@ -64,30 +34,7 @@ class ActionScheduler_OptionLock extends ActionScheduler_Lock {
* @return bool|int False if no lock is set, otherwise the timestamp for when the lock is set to expire.
*/
public function get_expiration( $lock_type ) {
return $this->get_expiration_from( $this->get_existing_lock( $lock_type ) );
}
/**
* Given the lock string, derives the lock expiration timestamp (or false if it cannot be determined).
*
* @param string $lock_value String containing a timestamp, or pipe-separated combination of unique value and timestamp.
*
* @return false|int
*/
private function get_expiration_from( $lock_value ) {
$lock_string = explode( '|', $lock_value );
// Old style lock?
if ( count( $lock_string ) === 1 && is_numeric( $lock_string[0] ) ) {
return (int) $lock_string[0];
}
// New style lock?
if ( count( $lock_string ) === 2 && is_numeric( $lock_string[1] ) ) {
return (int) $lock_string[1];
}
return false;
return get_option( $this->get_key( $lock_type ) );
}
/**
@@ -99,37 +46,4 @@ class ActionScheduler_OptionLock extends ActionScheduler_Lock {
protected function get_key( $lock_type ) {
return sprintf( 'action_scheduler_lock_%s', $lock_type );
}
/**
* Supplies the existing lock value, or an empty string if not set.
*
* @param string $lock_type A string to identify different lock types.
*
* @return string
*/
private function get_existing_lock( $lock_type ) {
global $wpdb;
// Now grab the existing lock value, if there is one.
return (string) $wpdb->get_var(
$wpdb->prepare(
"SELECT option_value FROM $wpdb->options WHERE option_name = %s",
$this->get_key( $lock_type )
)
);
}
/**
* Supplies a lock value consisting of a unique value and the current timestamp, which are separated by a pipe
* character.
*
* Example: (string) "649de012e6b262.09774912|1688068114"
*
* @param string $lock_type A string to identify different lock types.
*
* @return string
*/
private function new_lock_value( $lock_type ) {
return uniqid( '', true ) . '|' . ( time() + $this->get_duration( $lock_type ) );
}
}

View File

@@ -103,12 +103,9 @@ class ActionScheduler_QueueRunner extends ActionScheduler_Abstract_QueueRunner {
* should dispatch a request to process pending actions.
*/
public function maybe_dispatch_async_request() {
// Only start an async queue at most once every 60 seconds.
if (
is_admin()
&& ! ActionScheduler::lock()->is_locked( 'async-request-runner' )
&& ActionScheduler::lock()->set( 'async-request-runner' )
) {
if ( is_admin() && ! ActionScheduler::lock()->is_locked( 'async-request-runner' ) ) {
// Only start an async queue at most once every 60 seconds
ActionScheduler::lock()->set( 'async-request-runner' );
$this->async_request->maybe_dispatch();
}
}

View File

@@ -26,8 +26,6 @@ abstract class ActionScheduler_Lock {
/**
* Set a lock.
*
* To prevent race conditions, implementations should avoid setting the lock if the lock is already held.
*
* @param string $lock_type A string to identify different lock types.
* @return bool
*/

View File

@@ -935,17 +935,7 @@ AND `group_id` = %d
$sql = $wpdb->prepare( "{$update} {$where} {$order} LIMIT %d", $params ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders
$rows_affected = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
if ( false === $rows_affected ) {
$error = empty( $wpdb->last_error )
? _x( 'unknown', 'database error', 'woocommerce' )
: $wpdb->last_error;
throw new \RuntimeException(
sprintf(
/* translators: %s database error. */
__( 'Unable to claim actions. Database error: %s.', 'woocommerce' ),
$error
)
);
throw new \RuntimeException( __( 'Unable to claim actions. Database error.', 'woocommerce' ) );
}
return (int) $rows_affected;

View File

@@ -1,9 +1,11 @@
=== Action Scheduler ===
Contributors: Automattic, wpmuguru, claudiosanches, peterfabian1000, vedjain, jamosova, obliviousharmony, konamiman, sadowski, royho, barryhughes-1
Tags: scheduler, cron
Stable tag: 3.6.2
Requires at least: 5.2
Tested up to: 6.0
Stable tag: 3.6.1
License: GPLv3
Tested up to: 6.3
Requires PHP: 5.6
Action Scheduler - Job Queue for WordPress
@@ -45,13 +47,6 @@ Collaboration is cool. We'd love to work with you to improve Action Scheduler. [
== Changelog ==
= 3.6.2 - 2023-08-09 =
* Add guidance about passing arguments.
* Atomic option locking.
* Improve bulk delete handling.
* Include database error in the exception message.
* Tweak - WP 6.3 compatibility.
= 3.6.1 - 2023-06-14 =
* Document new optional `$priority` arg for various API functions.
* Document the new `--exclude-groups` WP CLI option.

View File

@@ -92,6 +92,7 @@ $fontSizes: (
word-wrap: normal !important;
padding: 0;
position: absolute !important;
width: 1px;
}
@mixin visually-hidden-focus-reveal() {

View File

@@ -8,10 +8,6 @@
.wc-block-grid__product {
margin: 0 0 $gap-large 0;
.wc-block-grid__product-onsale {
position: absolute;
}
}
}

View File

@@ -139,8 +139,7 @@
}
}
}
.wc-block-grid__product-image .wc-block-grid__product-onsale,
.wc-block-grid .wc-block-grid__product-onsale {
.wc-block-grid__product-onsale {
@include font-size(small);
padding: em($gap-smallest) em($gap-small);
display: inline-block;
@@ -153,10 +152,7 @@
text-transform: uppercase;
font-weight: 600;
z-index: 9;
position: absolute;
top: 4px;
right: 4px;
left: auto;
position: relative;
}
// Element spacing.

View File

@@ -54,24 +54,6 @@ registerBlockComponent( {
),
} );
registerBlockComponent( {
blockName: 'woocommerce/product-rating-counter',
component: lazy( () =>
import(
/* webpackChunkName: "product-rating-counter" */ './product-elements/rating-counter/block'
)
),
} );
registerBlockComponent( {
blockName: 'woocommerce/product-average-rating',
component: lazy( () =>
import(
/* webpackChunkName: "product-average-rating" */ './product-elements/average-rating/block'
)
),
} );
registerBlockComponent( {
blockName: 'woocommerce/product-button',
component: lazy( () =>

View File

@@ -6,8 +6,6 @@ import './product-elements/price';
import './product-elements/image';
import './product-elements/rating';
import './product-elements/rating-stars';
import './product-elements/rating-counter';
import './product-elements/average-rating';
import './product-elements/button';
import './product-elements/summary';
import './product-elements/sale-badge';

View File

@@ -1,17 +0,0 @@
{
"name": "woocommerce/product-average-rating",
"version": "1.0.0",
"title": "Product Average Rating (Beta)",
"description": "Display the average rating of a product",
"attributes": {
"textAlign": {
"type": "string"
}
},
"category": "woocommerce",
"keywords": [ "WooCommerce" ],
"ancestor": [ "woocommerce/single-product" ],
"textdomain": "woocommerce",
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}

View File

@@ -1,37 +0,0 @@
/**
* External dependencies
*/
import classnames from 'classnames';
import { useProductDataContext } from '@woocommerce/shared-context';
import { useStyleProps } from '@woocommerce/base-hooks';
import { __ } from '@wordpress/i18n';
import { withProductDataContext } from '@woocommerce/shared-hocs';
type ProductAverageRatingProps = {
className?: string;
textAlign?: string;
};
export const Block = ( props: ProductAverageRatingProps ): JSX.Element => {
const { textAlign } = props;
const styleProps = useStyleProps( props );
const { product } = useProductDataContext();
const className = classnames(
styleProps.className,
'wc-block-components-product-average-rating',
{
[ `has-text-align-${ textAlign }` ]: textAlign,
}
);
return (
<div className={ className } style={ styleProps.style }>
{ Number( product.average_rating ) > 0
? product.average_rating
: __( 'No ratings', 'woo-gutenberg-products-block' ) }
</div>
);
};
export default withProductDataContext( Block );

View File

@@ -1,43 +0,0 @@
/**
* External dependencies
*/
import {
AlignmentToolbar,
BlockControls,
useBlockProps,
} from '@wordpress/block-editor';
import type { BlockEditProps } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import Block from './block';
export interface BlockAttributes {
textAlign: string;
}
const Edit = ( props: BlockEditProps< BlockAttributes > ): JSX.Element => {
const { attributes, setAttributes } = props;
const blockProps = useBlockProps( {
className: 'wp-block-woocommerce-product-average-rating',
} );
return (
<>
<BlockControls>
<AlignmentToolbar
value={ attributes.textAlign }
onChange={ ( newAlign ) => {
setAttributes( { textAlign: newAlign || '' } );
} }
/>
</BlockControls>
<div { ...blockProps }>
<Block { ...attributes } />
</div>
</>
);
};
export default Edit;

View File

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

View File

@@ -1,26 +0,0 @@
/* eslint-disable @wordpress/no-unsafe-wp-apis */
/**
* External dependencies
*/
import { isFeaturePluginBuild } from '@woocommerce/block-settings';
export const supports = {
...( isFeaturePluginBuild() && {
color: {
text: true,
background: true,
__experimentalSkipSerialization: true,
},
spacing: {
margin: true,
padding: true,
__experimentalSkipSerialization: true,
},
typography: {
fontSize: true,
__experimentalFontWeight: true,
__experimentalSkipSerialization: true,
},
__experimentalSelector: '.wc-block-components-product-average-rating',
} ),
};

View File

@@ -34,7 +34,6 @@
"background": false,
"link": true
},
"interactivity": true,
"html": false,
"typography": {
"fontSize": true,
@@ -58,9 +57,6 @@
"label": "Outline"
}
],
"viewScript": [
"wc-product-button-interactivity-frontend"
],
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}

View File

@@ -1,279 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* External dependencies
*/
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import { store as interactivityStore } from '@woocommerce/interactivity';
import { dispatch, select, subscribe } from '@wordpress/data';
import { Cart } from '@woocommerce/type-defs/cart';
import { createRoot } from '@wordpress/element';
import NoticeBanner from '@woocommerce/base-components/notice-banner';
type Context = {
woocommerce: {
isLoading: boolean;
addToCartText: string;
productId: number;
displayViewCart: boolean;
quantityToAdd: number;
temporaryNumberOfItems: number;
animationStatus: AnimationStatus;
};
};
enum AnimationStatus {
IDLE = 'IDLE',
SLIDE_OUT = 'SLIDE-OUT',
SLIDE_IN = 'SLIDE-IN',
}
type State = {
woocommerce: {
cart: Cart | undefined;
inTheCartText: string;
};
};
type Store = {
state: State;
context: Context;
selectors: any;
ref: HTMLElement;
};
const storeNoticeClass = '.wc-block-store-notices';
const createNoticeContainer = () => {
const noticeContainer = document.createElement( 'div' );
noticeContainer.classList.add( storeNoticeClass.replace( '.', '' ) );
return noticeContainer;
};
const injectNotice = ( domNode: Element, errorMessage: string ) => {
const root = createRoot( domNode );
root.render(
<NoticeBanner status="error" onRemove={ () => root.unmount() }>
{ errorMessage }
</NoticeBanner>
);
domNode?.scrollIntoView( {
behavior: 'smooth',
inline: 'nearest',
} );
};
const getProductById = ( cartState: Cart | undefined, productId: number ) => {
return cartState?.items.find( ( item ) => item.id === productId );
};
const getTextButton = ( {
addToCartText,
inTheCartText,
numberOfItems,
}: {
addToCartText: string;
inTheCartText: string;
numberOfItems: number;
} ) => {
if ( numberOfItems === 0 ) {
return addToCartText;
}
return inTheCartText.replace( '###', numberOfItems.toString() );
};
const productButtonSelectors = {
woocommerce: {
addToCartText: ( store: Store ) => {
const { context, state, selectors } = store;
// We use the temporary number of items when there's no animation, or the
// second part of the animation hasn't started.
if (
context.woocommerce.animationStatus === AnimationStatus.IDLE ||
context.woocommerce.animationStatus ===
AnimationStatus.SLIDE_OUT
) {
return getTextButton( {
addToCartText: context.woocommerce.addToCartText,
inTheCartText: state.woocommerce.inTheCartText,
numberOfItems: context.woocommerce.temporaryNumberOfItems,
} );
}
return getTextButton( {
addToCartText: context.woocommerce.addToCartText,
inTheCartText: state.woocommerce.inTheCartText,
numberOfItems:
selectors.woocommerce.numberOfItemsInTheCart( store ),
} );
},
displayViewCart: ( store: Store ) => {
const { context, selectors } = store;
if ( ! context.woocommerce.displayViewCart ) return false;
if ( ! selectors.woocommerce.hasCartLoaded( store ) ) {
return context.woocommerce.temporaryNumberOfItems > 0;
}
return selectors.woocommerce.numberOfItemsInTheCart( store ) > 0;
},
hasCartLoaded: ( { state }: { state: State } ) => {
return state.woocommerce.cart !== undefined;
},
numberOfItemsInTheCart: ( { state, context }: Store ) => {
const product = getProductById(
state.woocommerce.cart,
context.woocommerce.productId
);
return product?.quantity || 0;
},
slideOutAnimation: ( { context }: Store ) =>
context.woocommerce.animationStatus === AnimationStatus.SLIDE_OUT,
slideInAnimation: ( { context }: Store ) =>
context.woocommerce.animationStatus === AnimationStatus.SLIDE_IN,
},
};
interactivityStore(
// @ts-expect-error: Store function isn't typed.
{
selectors: productButtonSelectors,
actions: {
woocommerce: {
addToCart: async ( store: Store ) => {
const { context, selectors, ref } = store;
if ( ! ref.classList.contains( 'ajax_add_to_cart' ) ) {
return;
}
context.woocommerce.isLoading = true;
// Allow 3rd parties to validate and quit early.
// https://github.com/woocommerce/woocommerce/blob/154dd236499d8a440edf3cde712511b56baa8e45/plugins/woocommerce/client/legacy/js/frontend/add-to-cart.js/#L74-L77
const event = new CustomEvent(
'should_send_ajax_request.adding_to_cart',
{ detail: [ ref ], cancelable: true }
);
const shouldSendRequest =
document.body.dispatchEvent( event );
if ( shouldSendRequest === false ) {
const ajaxNotSentEvent = new CustomEvent(
'ajax_request_not_sent.adding_to_cart',
{ detail: [ false, false, ref ] }
);
document.body.dispatchEvent( ajaxNotSentEvent );
return true;
}
try {
await dispatch( storeKey ).addItemToCart(
context.woocommerce.productId,
context.woocommerce.quantityToAdd
);
// After the cart has been updated, sync the temporary number of
// items again.
context.woocommerce.temporaryNumberOfItems =
selectors.woocommerce.numberOfItemsInTheCart(
store
);
} catch ( error ) {
const storeNoticeBlock =
document.querySelector( storeNoticeClass );
if ( ! storeNoticeBlock ) {
document
.querySelector( '.entry-content' )
?.prepend( createNoticeContainer() );
}
const domNode =
storeNoticeBlock ??
document.querySelector( storeNoticeClass );
if ( domNode ) {
injectNotice( domNode, error.message );
}
// We don't care about errors blocking execution, but will
// console.error for troubleshooting.
// eslint-disable-next-line no-console
console.error( error );
} finally {
context.woocommerce.displayViewCart = true;
context.woocommerce.isLoading = false;
}
},
handleAnimationEnd: (
store: Store & { event: AnimationEvent }
) => {
const { event, context, selectors } = store;
if ( event.animationName === 'slideOut' ) {
// When the first part of the animation (slide-out) ends, we move
// to the second part (slide-in).
context.woocommerce.animationStatus =
AnimationStatus.SLIDE_IN;
} else if ( event.animationName === 'slideIn' ) {
// When the second part of the animation ends, we update the
// temporary number of items to sync it with the cart and reset the
// animation status so it can be triggered again.
context.woocommerce.temporaryNumberOfItems =
selectors.woocommerce.numberOfItemsInTheCart(
store
);
context.woocommerce.animationStatus =
AnimationStatus.IDLE;
}
},
},
},
effects: {
woocommerce: {
startAnimation: ( store: Store ) => {
const { context, selectors } = store;
// We start the animation if the cart has loaded, the temporary number
// of items is out of sync with the number of items in the cart, the
// button is not loading (because that means the user started the
// interaction) and the animation hasn't started yet.
if (
selectors.woocommerce.hasCartLoaded( store ) &&
context.woocommerce.temporaryNumberOfItems !==
selectors.woocommerce.numberOfItemsInTheCart(
store
) &&
! context.woocommerce.isLoading &&
context.woocommerce.animationStatus ===
AnimationStatus.IDLE
) {
context.woocommerce.animationStatus =
AnimationStatus.SLIDE_OUT;
}
},
},
},
},
{
afterLoad: ( store: Store ) => {
const { state, selectors } = store;
// Subscribe to changes in Cart data.
subscribe( () => {
const cartData = select( storeKey ).getCartData();
const isResolutionFinished =
select( storeKey ).hasFinishedResolution( 'getCartData' );
if ( isResolutionFinished ) {
state.woocommerce.cart = cartData;
}
}, storeKey );
// This selector triggers a fetch of the Cart data. It is done in a
// `requestIdleCallback` to avoid potential performance issues.
requestIdleCallback( () => {
if ( ! selectors.woocommerce.hasCartLoaded( store ) ) {
select( storeKey ).getCartData();
}
} );
},
}
);

View File

@@ -1,79 +1,15 @@
.wp-block-button.wc-block-components-product-button {
word-break: break-word;
white-space: normal;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
gap: $gap-small;
.wp-block-button__link {
word-break: break-word;
white-space: normal;
display: inline-flex;
justify-content: center;
text-align: center;
// Set button font size and padding so it inherits from parent.
padding: 0.5em 1em;
font-size: 1em;
&.loading {
opacity: 0.25;
}
&.loading::after {
font-family: WooCommerce; /* stylelint-disable-line */
content: "\e031";
animation: spin 2s linear infinite;
margin-left: 0.5em;
display: inline-block;
width: auto;
height: auto;
}
}
a[hidden] {
display: none;
}
@keyframes slideOut {
from {
transform: translateY(0);
}
to {
transform: translateY(-100%);
}
}
@keyframes slideIn {
from {
transform: translateY(90%);
opacity: 0;
}
to {
transform: translate(0);
opacity: 1;
}
}
.wc-block-components-product-button__button {
border-style: none;
display: inline-flex;
justify-content: center;
margin-right: auto;
margin-left: auto;
white-space: normal;
word-break: break-word;
width: 150px;
overflow: hidden;
span {
&.wc-block-slide-out {
animation: slideOut 0.1s linear 1 normal forwards;
}
&.wc-block-slide-in {
animation: slideIn 0.1s linear 1 normal;
}
}
}
.wc-block-components-product-button__button--placeholder {

View File

@@ -54,7 +54,7 @@ const Edit = ( {
const blockProps = useBlockProps( { style: { width, height } } );
const isDescendentOfQueryLoop = Number.isFinite( context.queryId );
const isBlockThemeEnabled = getSettingWithCoercion(
'isBlockThemeEnabled',
'is_block_theme_enabled',
false,
isBoolean
);

View File

@@ -3,14 +3,11 @@
"version": "1.0.0",
"icon": "info",
"title": "Product Details",
"description": "Display a product's description, attributes, and reviews.",
"description": "Display a products description, attributes, and reviews.",
"category": "woocommerce",
"keywords": [ "WooCommerce" ],
"supports": {
"align": true,
"spacing": {
"margin": true
}
"align": true
},
"textdomain": "woocommerce",
"apiVersion": 2,

View File

@@ -1,38 +0,0 @@
{
"name": "woocommerce/product-rating-counter",
"version": "1.0.0",
"title": "Product Rating Counter",
"description": "Display the review count of a product",
"attributes": {
"productId": {
"type": "number",
"default": 0
},
"isDescendentOfQueryLoop": {
"type": "boolean",
"default": false
},
"textAlign": {
"type": "string",
"default": ""
},
"isDescendentOfSingleProductBlock": {
"type": "boolean",
"default": false
},
"isDescendentOfSingleProductTemplate": {
"type": "boolean",
"default": false
}
},
"usesContext": [ "query", "queryId", "postId" ],
"category": "woocommerce",
"keywords": [ "WooCommerce" ],
"supports": {
"align": true
},
"ancestor": [ "woocommerce/single-product" ],
"textdomain": "woocommerce",
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}

View File

@@ -1,88 +0,0 @@
/**
* External dependencies
*/
import { __, _n, sprintf } from '@wordpress/i18n';
import classnames from 'classnames';
import {
useInnerBlockLayoutContext,
useProductDataContext,
} from '@woocommerce/shared-context';
import { useStyleProps } from '@woocommerce/base-hooks';
import { withProductDataContext } from '@woocommerce/shared-hocs';
import { isNumber, ProductResponseItem } from '@woocommerce/types';
import { Disabled } from '@wordpress/components';
const getRatingCount = ( product: ProductResponseItem ) => {
const count = isNumber( product.review_count )
? product.review_count
: parseInt( product.review_count, 10 );
return Number.isFinite( count ) && count > 0 ? count : 0;
};
const ReviewsCount = ( props: { reviews: number } ): JSX.Element => {
const { reviews } = props;
const reviewsCount = reviews
? sprintf(
/* translators: %s is referring to the total of reviews for a product */
_n(
'(%s customer review)',
'(%s customer reviews)',
reviews,
'woo-gutenberg-products-block'
),
reviews
)
: __( '(X customer reviews)', 'woo-gutenberg-products-block' );
return (
<span className="wc-block-components-product-rating-counter__reviews_count">
<Disabled>
<a href="/">{ reviewsCount }</a>
</Disabled>
</span>
);
};
type ProductRatingCounterProps = {
className?: string;
textAlign?: string;
isDescendentOfSingleProductBlock: boolean;
isDescendentOfQueryLoop: boolean;
postId: number;
productId: number;
shouldDisplayMockedReviewsWhenProductHasNoReviews: boolean;
};
export const Block = (
props: ProductRatingCounterProps
): JSX.Element | undefined => {
const { textAlign, shouldDisplayMockedReviewsWhenProductHasNoReviews } =
props;
const styleProps = useStyleProps( props );
const { parentClassName } = useInnerBlockLayoutContext();
const { product } = useProductDataContext();
const reviews = getRatingCount( product );
const className = classnames(
styleProps.className,
'wc-block-components-product-rating-counter',
{
[ `${ parentClassName }__product-rating` ]: parentClassName,
[ `has-text-align-${ textAlign }` ]: textAlign,
}
);
if ( reviews || shouldDisplayMockedReviewsWhenProductHasNoReviews ) {
return (
<div className={ className } style={ styleProps.style }>
<div className="wc-block-components-product-rating-counter__container">
<ReviewsCount reviews={ reviews } />
</div>
</div>
);
}
};
export default withProductDataContext( Block );

View File

@@ -1,75 +0,0 @@
/**
* External dependencies
*/
import {
AlignmentToolbar,
BlockControls,
useBlockProps,
} from '@wordpress/block-editor';
import type { BlockEditProps } from '@wordpress/blocks';
import { useEffect } from '@wordpress/element';
import { ProductQueryContext as Context } from '@woocommerce/blocks/product-query/types';
/**
* Internal dependencies
*/
import Block from './block';
import { BlockAttributes } from './types';
import { useIsDescendentOfSingleProductBlock } from '../shared/use-is-descendent-of-single-product-block';
import { useIsDescendentOfSingleProductTemplate } from '../shared/use-is-descendent-of-single-product-template';
const Edit = (
props: BlockEditProps< BlockAttributes > & { context: Context }
): JSX.Element => {
const { attributes, setAttributes, context } = props;
const blockProps = useBlockProps( {
className: 'wp-block-woocommerce-product-rating-counter',
} );
const blockAttrs = {
...attributes,
...context,
shouldDisplayMockedReviewsWhenProductHasNoReviews: true,
};
const isDescendentOfQueryLoop = Number.isFinite( context.queryId );
const { isDescendentOfSingleProductBlock } =
useIsDescendentOfSingleProductBlock( {
blockClientId: blockProps?.id,
} );
let { isDescendentOfSingleProductTemplate } =
useIsDescendentOfSingleProductTemplate();
if ( isDescendentOfQueryLoop || isDescendentOfSingleProductBlock ) {
isDescendentOfSingleProductTemplate = false;
}
useEffect( () => {
setAttributes( {
isDescendentOfQueryLoop,
isDescendentOfSingleProductBlock,
isDescendentOfSingleProductTemplate,
} );
}, [
setAttributes,
isDescendentOfQueryLoop,
isDescendentOfSingleProductBlock,
isDescendentOfSingleProductTemplate,
] );
return (
<>
<BlockControls>
<AlignmentToolbar
value={ attributes.textAlign }
onChange={ ( newAlign ) => {
setAttributes( { textAlign: newAlign || '' } );
} }
/>
</BlockControls>
<div { ...blockProps }>
<Block { ...blockAttrs } />
</div>
</>
);
};
export default Edit;

View File

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

View File

@@ -1,24 +0,0 @@
/* eslint-disable @wordpress/no-unsafe-wp-apis */
/**
* External dependencies
*/
import { isFeaturePluginBuild } from '@woocommerce/block-settings';
export const supports = {
...( isFeaturePluginBuild() && {
color: {
text: false,
background: false,
link: true,
},
spacing: {
margin: true,
padding: true,
},
typography: {
fontSize: true,
__experimentalSkipSerialization: true,
},
__experimentalSelector: '.wc-block-components-product-rating-counter',
} ),
};

View File

@@ -1,7 +0,0 @@
export interface BlockAttributes {
productId: number;
isDescendentOfQueryLoop: boolean;
isDescendentOfSingleProductBlock: boolean;
isDescendentOfSingleProductTemplate: boolean;
textAlign: string;
}

View File

@@ -1,6 +1,7 @@
{
"name": "woocommerce/product-rating-stars",
"version": "1.0.0",
"icon": "info",
"title": "Product Rating Stars",
"description": "Display the average rating of a product with stars",
"attributes": {
@@ -31,7 +32,6 @@
"supports": {
"align": true
},
"ancestor": [ "woocommerce/single-product" ],
"textdomain": "woocommerce",
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"

View File

@@ -50,14 +50,12 @@ const NoRating = ( { parentClassName }: { parentClassName: string } ) => {
return (
<div
className={ classnames(
'wc-block-components-product-rating-stars__norating-container',
`${ parentClassName }-product-rating-stars__norating-container`
'wc-block-components-product-rating__norating-container',
`${ parentClassName }-product-rating__norating-container`
) }
>
<div
className={
'wc-block-components-product-rating-stars__norating'
}
className={ 'wc-block-components-product-rating__norating' }
role="img"
>
<span style={ starStyle } />
@@ -94,8 +92,8 @@ const Rating = ( props: RatingProps ): JSX.Element => {
return (
<div
className={ classnames(
'wc-block-components-product-rating-stars__stars',
`${ parentClassName }__product-rating-stars__stars`
'wc-block-components-product-rating__stars',
`${ parentClassName }__product-rating__stars`
) }
role="img"
aria-label={ ratingText }
@@ -126,7 +124,7 @@ export const Block = ( props: ProductRatingStarsProps ): JSX.Element | null => {
const className = classnames(
styleProps.className,
'wc-block-components-product-rating-stars',
'wc-block-components-product-rating',
{
[ `${ parentClassName }__product-rating` ]: parentClassName,
[ `has-text-align-${ textAlign }` ]: textAlign,

View File

@@ -0,0 +1,20 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Icon, starFilled } from '@wordpress/icons';
export const BLOCK_TITLE: string = __(
'Product Rating Stars',
'woo-gutenberg-products-block'
);
export const BLOCK_ICON: JSX.Element = (
<Icon
icon={ starFilled }
className="wc-block-editor-components-block-icon"
/>
);
export const BLOCK_DESCRIPTION: string = __(
'Display the average rating of a product with stars',
'woo-gutenberg-products-block'
);

View File

@@ -1,25 +1,36 @@
/**
* External dependencies
*/
import { registerBlockType } from '@wordpress/blocks';
import { Icon, starFilled } from '@wordpress/icons';
import { BlockConfiguration } from '@wordpress/blocks';
import { registerBlockSingleProductTemplate } from '@woocommerce/atomic-utils';
import { isExperimentalBuild } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import metadata from './block.json';
import edit from './edit';
import sharedConfig from '../shared/config';
import { supports } from './support';
import { BLOCK_ICON } from './constants';
registerBlockType( metadata, {
icon: {
src: (
<Icon
icon={ starFilled }
className="wc-block-editor-components-block-icon"
/>
),
},
const blockConfig: BlockConfiguration = {
...sharedConfig,
ancestor: [
'woocommerce/all-products',
'woocommerce/single-product',
'core/post-template',
'woocommerce/product-template',
],
icon: { src: BLOCK_ICON },
supports,
edit,
} );
};
if ( isExperimentalBuild() ) {
registerBlockSingleProductTemplate( {
blockName: 'woocommerce/product-rating-stars',
blockMetadata: metadata,
blockSettings: blockConfig,
} );
}

View File

@@ -1,4 +1,4 @@
.wc-block-components-product-rating-stars {
.wc-block-components-product-rating {
display: block;
line-height: 1;
@@ -13,7 +13,6 @@
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
font-family: star;
font-weight: 400;
text-align: left;
&::before {
content: "\53\53\53\53\53";

View File

@@ -3,6 +3,7 @@
* External dependencies
*/
import { isFeaturePluginBuild } from '@woocommerce/block-settings';
import { __experimentalGetSpacingClassesAndStyles } from '@wordpress/block-editor';
export const supports = {
...( isFeaturePluginBuild() && {
@@ -22,4 +23,10 @@ export const supports = {
},
__experimentalSelector: '.wc-block-components-product-rating',
} ),
...( ! isFeaturePluginBuild() &&
typeof __experimentalGetSpacingClassesAndStyles === 'function' && {
spacing: {
margin: true,
},
} ),
};

View File

@@ -11,11 +11,6 @@ import { useStyleProps } from '@woocommerce/base-hooks';
import { withProductDataContext } from '@woocommerce/shared-hocs';
import { isNumber, ProductResponseItem } from '@woocommerce/types';
/**
* Internal dependencies
*/
import './style.scss';
type RatingProps = {
reviews: number;
rating: number;

View File

@@ -1,12 +0,0 @@
.wc-block-components-product-rating {
.wc-block-components-product-rating__container {
> * {
vertical-align: middle;
}
}
.wc-block-components-product-rating__stars {
display: inline-block;
margin: 0;
}
}

View File

@@ -1,11 +1,7 @@
/**
* External dependencies
*/
import {
ValidatedTextInput,
isPostcode,
type ValidatedTextInputHandle,
} from '@woocommerce/blocks-checkout';
import { ValidatedTextInput, isPostcode } from '@woocommerce/blocks-checkout';
import {
BillingCountryInput,
ShippingCountryInput,
@@ -14,110 +10,195 @@ import {
BillingStateInput,
ShippingStateInput,
} from '@woocommerce/base-components/state-input';
import { useEffect, useMemo, useRef } from '@wordpress/element';
import { useEffect, useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { withInstanceId } from '@wordpress/compose';
import { useShallowEqual } from '@woocommerce/base-hooks';
import { defaultAddressFields } from '@woocommerce/settings';
import isShallowEqual from '@wordpress/is-shallow-equal';
import {
AddressField,
AddressFields,
AddressType,
defaultAddressFields,
ShippingAddress,
} from '@woocommerce/settings';
import { useSelect, useDispatch, dispatch } from '@wordpress/data';
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
import { FieldValidationStatus } from '@woocommerce/types';
/**
* Internal dependencies
*/
import {
AddressFormProps,
FieldType,
FieldConfig,
AddressFormFields,
} from './types';
import prepareAddressFields from './prepare-address-fields';
import validateShippingCountry from './validate-shipping-country';
import customValidationHandler from './custom-validation-handler';
const defaultFields = Object.keys(
defaultAddressFields
) as unknown as FieldType[];
// If it's the shipping address form and the user starts entering address
// values without having set the country first, show an error.
const validateShippingCountry = (
values: ShippingAddress,
setValidationErrors: (
errors: Record< string, FieldValidationStatus >
) => void,
clearValidationError: ( error: string ) => void,
hasValidationError: boolean
): void => {
const validationErrorId = 'shipping_country';
if (
! hasValidationError &&
! values.country &&
( values.city || values.state || values.postcode )
) {
setValidationErrors( {
[ validationErrorId ]: {
message: __(
'Please select a country to calculate rates.',
'woo-gutenberg-products-block'
),
hidden: false,
},
} );
}
if ( hasValidationError && values.country ) {
clearValidationError( validationErrorId );
}
};
interface AddressFormProps {
// Id for component.
id?: string;
// Unique id for form.
instanceId: string;
// Array of fields in form.
fields: ( keyof AddressFields )[];
// Field configuration for fields in form.
fieldConfig?: Record< keyof AddressFields, Partial< AddressField > >;
// Function to all for an form onChange event.
onChange: ( newValue: ShippingAddress ) => void;
// Type of form.
type?: AddressType;
// Values for fields.
values: ShippingAddress;
}
/**
* Checkout address form.
*/
const AddressForm = ( {
id = '',
fields = defaultFields,
fieldConfig = {} as FieldConfig,
fields = Object.keys(
defaultAddressFields
) as unknown as ( keyof AddressFields )[],
fieldConfig = {} as Record< keyof AddressFields, Partial< AddressField > >,
instanceId,
onChange,
type = 'shipping',
values,
}: AddressFormProps ): JSX.Element => {
// Track incoming props.
const validationErrorId = 'shipping_country';
const { setValidationErrors, clearValidationError } =
useDispatch( VALIDATION_STORE_KEY );
const countryValidationError = useSelect( ( select ) => {
const store = select( VALIDATION_STORE_KEY );
return store.getValidationError( validationErrorId );
} );
const currentFields = useShallowEqual( fields );
const currentFieldConfig = useShallowEqual( fieldConfig );
const currentCountry = useShallowEqual( values.country );
// Memoize the address form fields passed in from the parent component.
const addressFormFields = useMemo( (): AddressFormFields => {
const preparedFields = prepareAddressFields(
const addressFormFields = useMemo( () => {
return prepareAddressFields(
currentFields,
currentFieldConfig,
currentCountry
fieldConfig,
values.country
);
return {
fields: preparedFields,
type,
required: preparedFields.filter( ( field ) => field.required ),
hidden: preparedFields.filter( ( field ) => field.hidden ),
};
}, [ currentFields, currentFieldConfig, currentCountry, type ] );
// Stores refs for rendered fields so we can access them later.
const fieldsRef = useRef<
Record< string, ValidatedTextInputHandle | null >
>( {} );
}, [ currentFields, fieldConfig, values.country ] );
// Clear values for hidden fields.
useEffect( () => {
const newValues = {
...values,
...Object.fromEntries(
addressFormFields.hidden.map( ( field ) => [ field.key, '' ] )
),
};
if ( ! isShallowEqual( values, newValues ) ) {
onChange( newValues );
}
}, [ onChange, addressFormFields, values ] );
addressFormFields.forEach( ( field ) => {
if ( field.hidden && values[ field.key ] ) {
onChange( {
...values,
[ field.key ]: '',
} );
}
} );
}, [ addressFormFields, onChange, values ] );
// Clear postcode validation error if postcode is not required.
useEffect( () => {
addressFormFields.forEach( ( field ) => {
if ( field.key === 'postcode' && field.required === false ) {
const store = dispatch( 'wc/store/validation' );
if ( type === 'shipping' ) {
store.clearValidationError( 'shipping_postcode' );
}
if ( type === 'billing' ) {
store.clearValidationError( 'billing_postcode' );
}
}
} );
}, [ addressFormFields, type, clearValidationError ] );
// Maybe validate country when other fields change so user is notified that it's required.
useEffect( () => {
if ( type === 'shipping' ) {
validateShippingCountry( values );
validateShippingCountry(
values,
setValidationErrors,
clearValidationError,
!! countryValidationError?.message &&
! countryValidationError?.hidden
);
}
}, [ values, type ] );
// Changing country may change format for postcodes.
useEffect( () => {
fieldsRef.current?.postcode?.revalidate();
}, [ currentCountry ] );
}, [
values,
countryValidationError?.message,
countryValidationError?.hidden,
setValidationErrors,
clearValidationError,
type,
] );
id = id || instanceId;
/**
* Custom validation handler for fields with field specific handling.
*/
const customValidationHandler = (
inputObject: HTMLInputElement,
field: string,
customValues: {
country: string;
}
): boolean => {
if (
field === 'postcode' &&
customValues.country &&
! isPostcode( {
postcode: inputObject.value,
country: customValues.country,
} )
) {
inputObject.setCustomValidity(
__(
'Please enter a valid postcode',
'woo-gutenberg-products-block'
)
);
return false;
}
return true;
};
return (
<div id={ id } className="wc-block-components-address-form">
{ addressFormFields.fields.map( ( field ) => {
{ addressFormFields.map( ( field ) => {
if ( field.hidden ) {
return null;
}
const fieldProps = {
id: `${ id }-${ field.key }`,
errorId: `${ type }_${ field.key }`,
label: field.required ? field.label : field.optionalLabel,
autoCapitalize: field.autocapitalize,
autoComplete: field.autocomplete,
errorMessage: field.errorMessage,
required: field.required,
className: `wc-block-components-address-form__${ field.key }`,
};
// Create a consistent error ID based on the field key and type
const errorId = `${ type }_${ field.key }`;
if ( field.key === 'country' ) {
const Tag =
@@ -127,26 +208,24 @@ const AddressForm = ( {
return (
<Tag
key={ field.key }
{ ...fieldProps }
id={ `${ id }-${ field.key }` }
errorId={ errorId }
label={
field.required
? field.label
: field.optionalLabel
}
value={ values.country }
onChange={ ( newCountry ) => {
const newValues = {
autoComplete={ field.autocomplete }
onChange={ ( newValue ) =>
onChange( {
...values,
country: newCountry,
country: newValue,
state: '',
};
// Country will impact postcode too. Do we need to clear it?
if (
values.postcode &&
! isPostcode( {
postcode: values.postcode,
country: newCountry,
} )
) {
newValues.postcode = '';
}
onChange( newValues );
} }
} )
}
errorMessage={ field.errorMessage }
required={ field.required }
/>
);
}
@@ -159,15 +238,24 @@ const AddressForm = ( {
return (
<Tag
key={ field.key }
{ ...fieldProps }
id={ `${ id }-${ field.key }` }
errorId={ errorId }
country={ values.country }
label={
field.required
? field.label
: field.optionalLabel
}
value={ values.state }
autoComplete={ field.autocomplete }
onChange={ ( newValue ) =>
onChange( {
...values,
state: newValue,
} )
}
errorMessage={ field.errorMessage }
required={ field.required }
/>
);
}
@@ -175,30 +263,35 @@ const AddressForm = ( {
return (
<ValidatedTextInput
key={ field.key }
ref={ ( el ) =>
( fieldsRef.current[ field.key ] = el )
id={ `${ id }-${ field.key }` }
errorId={ errorId }
className={ `wc-block-components-address-form__${ field.key }` }
label={
field.required ? field.label : field.optionalLabel
}
{ ...fieldProps }
value={ values[ field.key ] }
autoCapitalize={ field.autocapitalize }
autoComplete={ field.autocomplete }
onChange={ ( newValue: string ) =>
onChange( {
...values,
[ field.key ]: newValue,
[ field.key ]:
field.key === 'postcode'
? newValue.trimStart().toUpperCase()
: newValue,
} )
}
customFormatter={ ( value: string ) => {
if ( field.key === 'postcode' ) {
return value.trimStart().toUpperCase();
}
return value;
} }
customValidation={ ( inputObject: HTMLInputElement ) =>
customValidationHandler(
inputObject,
field.key,
values
)
field.required || inputObject.value
? customValidationHandler(
inputObject,
field.key,
values
)
: true
}
errorMessage={ field.errorMessage }
required={ field.required }
/>
);
} ) }

View File

@@ -1,41 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { isPostcode } from '@woocommerce/blocks-checkout';
/**
* Custom validation handler for fields with field specific handling.
*/
const customValidationHandler = (
inputObject: HTMLInputElement,
field: string,
customValues: {
country: string;
}
): boolean => {
// Pass validation if the field is not required and is empty.
if ( ! inputObject.required && ! inputObject.value ) {
return true;
}
if (
field === 'postcode' &&
customValues.country &&
! isPostcode( {
postcode: inputObject.value,
country: customValues.country,
} )
) {
inputObject.setCustomValidity(
__(
'Please enter a valid postcode',
'woo-gutenberg-products-block'
)
);
return false;
}
return true;
};
export default customValidationHandler;

View File

@@ -1,41 +0,0 @@
/**
* External dependencies
*/
import type {
AddressField,
AddressFields,
AddressType,
ShippingAddress,
KeyedAddressField,
} from '@woocommerce/settings';
export type FieldConfig = Record<
keyof AddressFields,
Partial< AddressField >
>;
export type FieldType = keyof AddressFields;
export type AddressFormFields = {
fields: KeyedAddressField[];
type: AddressType;
required: KeyedAddressField[];
hidden: KeyedAddressField[];
};
export interface AddressFormProps {
// Id for component.
id?: string;
// Unique id for form.
instanceId: string;
// Type of form (billing or shipping).
type?: AddressType;
// Array of fields in form.
fields: FieldType[];
// Field configuration for fields in form.
fieldConfig?: FieldConfig;
// Called with the new address data when the address form changes. This is only called when all required fields are filled and there are no validation errors.
onChange: ( newValue: ShippingAddress ) => void;
// Values for fields.
values: ShippingAddress;
}

View File

@@ -1,43 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { type ShippingAddress } from '@woocommerce/settings';
import { select, dispatch } from '@wordpress/data';
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
// If it's the shipping address form and the user starts entering address
// values without having set the country first, show an error.
const validateShippingCountry = ( values: ShippingAddress ): void => {
const validationErrorId = 'shipping_country';
const hasValidationError =
select( VALIDATION_STORE_KEY ).getValidationError( validationErrorId );
if (
! values.country &&
( values.city || values.state || values.postcode )
) {
if ( hasValidationError ) {
dispatch( VALIDATION_STORE_KEY ).showValidationError(
validationErrorId
);
} else {
dispatch( VALIDATION_STORE_KEY ).setValidationErrors( {
[ validationErrorId ]: {
message: __(
'Please select your country',
'woo-gutenberg-products-block'
),
hidden: false,
},
} );
}
}
if ( hasValidationError && values.country ) {
dispatch( VALIDATION_STORE_KEY ).clearValidationError(
validationErrorId
);
}
};
export default validateShippingCountry;

View File

@@ -7,8 +7,12 @@ import { decodeEntities } from '@wordpress/html-entities';
import { Panel } from '@woocommerce/blocks-checkout';
import Label from '@woocommerce/base-components/label';
import { useCallback } from '@wordpress/element';
import { useShippingData } from '@woocommerce/base-context/hooks';
import {
useShippingData,
useStoreEvents,
} from '@woocommerce/base-context/hooks';
import { sanitizeHTML } from '@woocommerce/utils';
import { useDebouncedCallback } from 'use-debounce';
import type { ReactElement } from 'react';
/**
@@ -27,7 +31,8 @@ export const ShippingRatesControlPackage = ( {
collapsible,
showItems,
}: PackageProps ): ReactElement => {
const { selectShippingRate, isSelectingRate } = useShippingData();
const { selectShippingRate } = useShippingData();
const { dispatchCheckoutEvent } = useStoreEvents();
const multiplePackages =
document.querySelectorAll(
'.wc-block-components-shipping-rates-control__package'
@@ -90,32 +95,28 @@ export const ShippingRatesControlPackage = ( {
const onSelectRate = useCallback(
( newShippingRateId: string ) => {
selectShippingRate( newShippingRateId, packageId );
dispatchCheckoutEvent( 'set-selected-shipping-rate', {
shippingRateId: newShippingRateId,
} );
},
[ packageId, selectShippingRate ]
[ dispatchCheckoutEvent, packageId, selectShippingRate ]
);
const debouncedOnSelectRate = useDebouncedCallback( onSelectRate, 1000 );
const packageRatesProps = {
className,
noResultsMessage,
rates: packageData.shipping_rates,
onSelectRate,
onSelectRate: debouncedOnSelectRate,
selectedRate: packageData.shipping_rates.find(
( rate ) => rate.selected
),
renderOption,
disabled: isSelectingRate,
};
if ( shouldBeCollapsible ) {
return (
<Panel
className={ classNames(
'wc-block-components-shipping-rates-control__package',
className,
{
'wc-block-components-shipping-rates-control__package--disabled':
isSelectingRate,
}
) }
className="wc-block-components-shipping-rates-control__package"
// initialOpen remembers only the first value provided to it, so by the
// time we know we have several packages, initialOpen would be hardcoded to true.
// If we're rendering a panel, we're more likely rendering several
@@ -132,11 +133,7 @@ export const ShippingRatesControlPackage = ( {
<div
className={ classNames(
'wc-block-components-shipping-rates-control__package',
className,
{
'wc-block-components-shipping-rates-control__package--disabled':
isSelectingRate,
}
className
) }
>
{ header }

View File

@@ -6,7 +6,6 @@ import RadioControl, {
RadioControlOptionLayout,
} from '@woocommerce/base-components/radio-control';
import type { CartShippingPackageShippingRate } from '@woocommerce/types';
import { usePrevious } from '@woocommerce/base-hooks';
/**
* Internal dependencies
@@ -21,7 +20,6 @@ interface PackageRates {
className?: string;
noResultsMessage: JSX.Element;
selectedRate: CartShippingPackageShippingRate | undefined;
disabled?: boolean;
}
const PackageRates = ( {
@@ -31,37 +29,34 @@ const PackageRates = ( {
rates,
renderOption = renderPackageRateOption,
selectedRate,
disabled = false,
}: PackageRates ): JSX.Element => {
const selectedRateId = selectedRate?.rate_id || '';
const previousSelectedRateId = usePrevious( selectedRateId );
// Store selected rate ID in local state so shipping rates changes are shown in the UI instantly.
const [ selectedOption, setSelectedOption ] = useState( () => {
if ( selectedRateId ) {
return selectedRateId;
}
// Default to first rate if no rate is selected.
return rates[ 0 ]?.rate_id;
} );
const [ selectedOption, setSelectedOption ] = useState( selectedRateId );
// Update the selected option if cart state changes in the data store.
// Update the selected option if cart state changes in the data stores.
useEffect( () => {
if (
selectedRateId &&
selectedRateId !== previousSelectedRateId &&
selectedRateId !== selectedOption
) {
if ( selectedRateId ) {
setSelectedOption( selectedRateId );
}
}, [ selectedRateId, selectedOption, previousSelectedRateId ] );
}, [ selectedRateId ] );
// Update the data store when the local selected rate changes.
// Update the selected option if there is no rate selected on mount.
useEffect( () => {
if ( selectedOption ) {
onSelectRate( selectedOption );
// Check the rates to see if any are marked as selected. At least one should be. If no rate is selected, it could be
// that the user toggled quickly from local pickup back to shipping.
const isRateSelectedInDataStore = rates.some(
( { selected } ) => selected
);
if (
( ! selectedOption && rates[ 0 ] ) ||
! isRateSelectedInDataStore
) {
setSelectedOption( rates[ 0 ]?.rate_id );
onSelectRate( rates[ 0 ]?.rate_id );
}
}, [ onSelectRate, selectedOption ] );
}, [ onSelectRate, rates, selectedOption ] );
if ( rates.length === 0 ) {
return noResultsMessage;
@@ -75,7 +70,6 @@ const PackageRates = ( {
setSelectedOption( value );
onSelectRate( value );
} }
disabled={ disabled }
selected={ selectedOption }
options={ rates.map( renderOption ) }
/>

View File

@@ -44,11 +44,6 @@
.wc-block-components-radio-control__description-group {
@include font-size(smaller);
}
&--disabled {
opacity: 0.5;
transition: opacity 200ms ease;
}
}
.wc-block-components-shipping-rates-control__package-items {

View File

@@ -61,7 +61,6 @@ jest.mock( '@woocommerce/base-context/hooks', () => {
} );
baseContextHooks.useShippingData.mockReturnValue( {
needsShipping: true,
selectShippingRate: jest.fn(),
shippingRates: [
{
package_id: 0,

View File

@@ -24,7 +24,7 @@ export const CountryInput = ( {
required = false,
errorId,
errorMessage = __(
'Please select a country',
'Please select a country.',
'woo-gutenberg-products-block'
),
}: CountryInputWithCountriesProps ): JSX.Element => {

View File

@@ -43,7 +43,7 @@
}
// Legacy notice compatibility.
.wc-forward {
.wc-forward.wp-element-button {
float: right;
color: $gray-800 !important;
background: transparent;
@@ -52,8 +52,6 @@
border: 0;
appearance: none;
opacity: 0.6;
text-decoration-line: underline;
text-underline-position: under;
&:hover,
&:focus,

View File

@@ -169,6 +169,7 @@
width: 100%;
height: 0;
display: block;
position: relative;
pointer-events: none;
outline: none !important;
position: absolute;
@@ -365,6 +366,7 @@
@include ie11() {
.wc-block-components-price-slider__range-input-wrapper {
border: 0;
height: auto;
position: relative;
height: 50px;
}

View File

@@ -54,13 +54,9 @@
}
&__container {
> * {
vertical-align: middle;
}
}
&__stars + &__reviews_count {
margin-left: $gap-smaller;
display: flex;
align-items: center;
column-gap: $gap-smaller;
}
&__norating-container {

View File

@@ -16,7 +16,6 @@ const RadioControl = ( {
selected = '',
onChange,
options = [],
disabled = false,
}: RadioControlProps ): JSX.Element | null => {
const instanceId = useInstanceId( RadioControl );
const radioControlId = id || instanceId;
@@ -44,7 +43,6 @@ const RadioControl = ( {
option.onChange( value );
}
} }
disabled={ disabled }
/>
) ) }
</div>

View File

@@ -14,7 +14,6 @@ const Option = ( {
name,
onChange,
option,
disabled = false,
}: RadioControlOptionProps ): JSX.Element => {
const { value, label, description, secondaryLabel, secondaryDescription } =
option;
@@ -47,7 +46,6 @@ const Option = ( {
[ `${ name }-${ value }__secondary-description` ]:
secondaryDescription,
} ) }
disabled={ disabled }
/>
<OptionLayout
id={ `${ name }-${ value }` }

View File

@@ -3,11 +3,13 @@
@include reset-typography();
display: block;
margin: em($gap) 0;
margin-top: 0;
padding: 0 0 0 em($gap-larger);
position: relative;
cursor: pointer;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
@@ -97,12 +99,6 @@
background: $input-text-dark;
}
}
&[disabled] {
cursor: not-allowed;
opacity: 0.5;
transition: opacity 200ms ease;
}
}
}

View File

@@ -14,8 +14,6 @@ export interface RadioControlProps {
onChange: ( value: string ) => void;
// List of radio control options.
options: RadioControlOption[];
// Is the control disabled.
disabled?: boolean;
}
export interface RadioControlOptionProps {
@@ -23,7 +21,6 @@ export interface RadioControlOptionProps {
name?: string;
onChange: ( value: string ) => void;
option: RadioControlOption;
disabled?: boolean;
}
interface RadioControlOptionContent {

View File

@@ -14,6 +14,7 @@
display: inline-flex;
width: auto;
max-width: 600px;
margin: 0;
pointer-events: all;
border: 1px solid transparent;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);

View File

@@ -55,15 +55,13 @@ const StateInput = ( {
*/
const onChangeState = useCallback(
( stateValue: string ) => {
const newValue =
onChange(
options.length > 0
? optionMatcher( stateValue, options )
: stateValue;
if ( newValue !== value ) {
onChange( newValue );
}
: stateValue
);
},
[ onChange, options, value ]
[ onChange, options ]
);
/**

View File

@@ -124,7 +124,11 @@ export const useShippingData = (): ShippingData => {
processErrorResponse( error );
} );
},
[ dispatchSelectShippingRate, dispatchCheckoutEvent ]
[
hasSelectedLocalPickup,
dispatchSelectShippingRate,
dispatchCheckoutEvent,
]
);
return {

View File

@@ -2,8 +2,12 @@
* External dependencies
*/
import { doAction } from '@wordpress/hooks';
import { select } from '@wordpress/data';
import { useCallback } from '@wordpress/element';
import { useCallback, useRef, useEffect } from '@wordpress/element';
/**
* Internal dependencies
*/
import { useStoreCart } from './cart/use-store-cart';
type StoreEvent = (
eventName: string,
@@ -17,6 +21,15 @@ export const useStoreEvents = (): {
dispatchStoreEvent: StoreEvent;
dispatchCheckoutEvent: StoreEvent;
} => {
const storeCart = useStoreCart();
const currentStoreCart = useRef( storeCart );
// Track the latest version of the cart so we can use the current value in our callback function below without triggering
// other useEffect hooks using dispatchCheckoutEvent as a dependency.
useEffect( () => {
currentStoreCart.current = storeCart;
}, [ storeCart ] );
const dispatchStoreEvent = useCallback( ( eventName, eventParams = {} ) => {
try {
doAction(
@@ -37,7 +50,7 @@ export const useStoreEvents = (): {
`experimental__woocommerce_blocks-checkout-${ eventName }`,
{
...eventParams,
storeCart: select( 'wc/store/cart' ).getCartData(),
storeCart: currentStoreCart.current,
}
);
} catch ( e ) {

View File

@@ -50,8 +50,8 @@ function getBorderClassName( attributes: {
: '';
return classnames( {
'has-border-color': !! borderColor || !! style?.border?.color,
[ borderColorClass ]: !! borderColorClass,
'has-border-color': borderColor || style?.border?.color,
borderColorClass,
} );
}

View File

@@ -14,5 +14,4 @@ export * from './camel-case-keys';
export * from './snake-case-keys';
export * from './debounce';
export * from './keyby';
export * from './pick';
export * from './get-inline-styles';

View File

@@ -1,11 +0,0 @@
/**
* Creates an object composed of the picked object properties.
*/
export const pick = < Type >( object: Type, keys: string[] ): Type => {
return keys.reduce( ( obj, key ) => {
if ( object && object.hasOwnProperty( key ) ) {
obj[ key as keyof Type ] = object[ key as keyof Type ];
}
return obj;
}, {} as Type );
};

View File

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

View File

@@ -59,7 +59,7 @@ const ActiveFiltersBlock = ( {
const isMounted = useIsMounted();
const componentHasMounted = isMounted();
const filteringForPhpTemplate = getSettingWithCoercion(
'isRenderingPhpTemplate',
'is_rendering_php_template',
false,
isBoolean
);
@@ -323,7 +323,7 @@ const ActiveFiltersBlock = ( {
);
const hasFilterableProducts = getSettingWithCoercion(
'hasFilterableProducts',
'has_filterable_products',
false,
isBoolean
);

View File

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

View File

@@ -72,19 +72,19 @@ const AttributeFilterBlock = ( {
getNotice?: GetNotice;
} ) => {
const hasFilterableProducts = getSettingWithCoercion(
'hasFilterableProducts',
'has_filterable_products',
false,
isBoolean
);
const filteringForPhpTemplate = getSettingWithCoercion(
'isRenderingPhpTemplate',
'is_rendering_php_template',
false,
isBoolean
);
const pageUrl = getSettingWithCoercion(
'pageUrl',
'page_url',
window.location.href,
isString
);
@@ -544,6 +544,9 @@ const AttributeFilterBlock = ( {
'single-selection': ! multiple,
'is-loading': isLoading,
} ) }
style={ {
borderStyle: 'none',
} }
suggestions={ displayedOptions
.filter(
( option ) =>

View File

@@ -69,6 +69,7 @@ const Edit = ( {
}: EditProps ) => {
const {
attributeId,
className,
displayStyle,
heading,
headingLevel,
@@ -352,7 +353,6 @@ const Edit = ( {
href={ getAdminLink(
'edit.php?post_type=product&page=product_attributes'
) }
target="_top"
>
{ __( 'Add new attribute', 'woo-gutenberg-products-block' ) +
' ' }
@@ -362,7 +362,6 @@ const Edit = ( {
className="wc-block-attribute-filter__read_more_button"
isTertiary
href="https://docs.woocommerce.com/document/managing-product-taxonomies/"
target="_blank"
>
{ __( 'Learn more', 'woo-gutenberg-products-block' ) }
</Button>
@@ -420,7 +419,12 @@ const Edit = ( {
{ isEditing ? (
renderEditMode()
) : (
<div className={ classnames( 'wc-block-attribute-filter' ) }>
<div
className={ classnames(
className,
'wc-block-attribute-filter'
) }
>
{ heading && (
<BlockTitle
className="wc-block-attribute-filter__title"

View File

@@ -3,6 +3,7 @@
*/
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps } from '@wordpress/block-editor';
import { isFeaturePluginBuild } from '@woocommerce/block-settings';
import { Icon, category } from '@wordpress/icons';
import classNames from 'classnames';
@@ -26,6 +27,13 @@ registerBlockType( metadata, {
},
supports: {
...metadata.supports,
...( isFeaturePluginBuild() && {
__experimentalBorder: {
radius: false,
color: true,
width: false,
},
} ),
},
attributes: {
...metadata.attributes,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,13 +2,7 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import {
useMemo,
useEffect,
Fragment,
useState,
useCallback,
} from '@wordpress/element';
import { useMemo, useEffect, Fragment, useState } from '@wordpress/element';
import { AddressForm } from '@woocommerce/base-components/cart-checkout';
import {
useCheckoutAddress,
@@ -109,24 +103,6 @@ const Block = ( {
showApartmentField,
] ) as Record< keyof AddressFields, Partial< AddressField > >;
const onChangeAddress = useCallback(
( values: Partial< ShippingAddress > ) => {
setShippingAddress( values );
if ( useShippingAsBilling ) {
setBillingAddress( { ...values, email } );
dispatchCheckoutEvent( 'set-billing-address' );
}
dispatchCheckoutEvent( 'set-shipping-address' );
},
[
dispatchCheckoutEvent,
email,
setBillingAddress,
setShippingAddress,
useShippingAsBilling,
]
);
const AddressFormWrapperComponent = isEditor ? Noninteractive : Fragment;
const noticeContext = useShippingAsBilling
? [ noticeContexts.SHIPPING_ADDRESS, noticeContexts.BILLING_ADDRESS ]
@@ -139,7 +115,14 @@ const Block = ( {
<AddressForm
id="shipping"
type="shipping"
onChange={ onChangeAddress }
onChange={ ( values: Partial< ShippingAddress > ) => {
setShippingAddress( values );
if ( useShippingAsBilling ) {
setBillingAddress( { ...values, email } );
dispatchCheckoutEvent( 'set-billing-address' );
}
dispatchCheckoutEvent( 'set-shipping-address' );
} }
values={ shippingAddress }
fields={
Object.keys(

View File

@@ -8,15 +8,15 @@ import {
} from '@wordpress/blocks';
import { isWpVersion } from '@woocommerce/settings';
import { __, sprintf } from '@wordpress/i18n';
import {
INNER_BLOCKS_TEMPLATE as productsInnerBlocksTemplate,
QUERY_DEFAULT_ATTRIBUTES as productsQueryDefaultAttributes,
PRODUCT_QUERY_VARIATION_NAME as productsVariationName,
} from '@woocommerce/blocks/product-query/constants';
/**
* Internal dependencies
*/
import {
INNER_BLOCKS_TEMPLATE as productsInnerBlocksTemplate,
QUERY_DEFAULT_ATTRIBUTES as productsQueryDefaultAttributes,
} from '../product-query/constants';
import { VARIATION_NAME as productsVariationName } from '../product-query/variations/product-query';
import { createArchiveTitleBlock, createRowBlock } from './utils';
import { OnClickCallbackParameter, type InheritedAttributes } from './types';

View File

@@ -9,15 +9,15 @@ import {
} from '@wordpress/blocks';
import { isWpVersion } from '@woocommerce/settings';
import { __, sprintf } from '@wordpress/i18n';
import {
INNER_BLOCKS_TEMPLATE as productsInnerBlocksTemplate,
QUERY_DEFAULT_ATTRIBUTES as productsQueryDefaultAttributes,
PRODUCT_QUERY_VARIATION_NAME as productsVariationName,
} from '@woocommerce/blocks/product-query/constants';
/**
* Internal dependencies
*/
import {
INNER_BLOCKS_TEMPLATE as productsInnerBlocksTemplate,
QUERY_DEFAULT_ATTRIBUTES as productsQueryDefaultAttributes,
} from '../product-query/constants';
import { VARIATION_NAME as productsVariationName } from '../product-query/variations/product-query';
import { createArchiveTitleBlock, createRowBlock } from './utils';
import { OnClickCallbackParameter, type InheritedAttributes } from './types';

View File

@@ -6,24 +6,36 @@ import { getTemplateDetailsBySlug } from '../utils';
describe( 'getTemplateDetailsBySlug', function () {
it( 'should return single-product object when given an exact match', () => {
expect(
getTemplateDetailsBySlug( 'single-product', TEMPLATES )
).toBeTruthy();
expect(
getTemplateDetailsBySlug( 'single-product', TEMPLATES )
).toStrictEqual( TEMPLATES[ 'single-product' ] );
} );
it( 'should return single-product object when given a partial match', () => {
expect(
getTemplateDetailsBySlug( 'single-product-hoodie', TEMPLATES )
).toBeTruthy();
expect(
getTemplateDetailsBySlug( 'single-product-hoodie', TEMPLATES )
).toStrictEqual( TEMPLATES[ 'single-product' ] );
} );
it( 'should return taxonomy-product object when given a partial match', () => {
expect(
getTemplateDetailsBySlug( 'taxonomy-product_tag', TEMPLATES )
).toBeTruthy();
expect(
getTemplateDetailsBySlug( 'taxonomy-product_tag', TEMPLATES )
).toStrictEqual( TEMPLATES[ 'taxonomy-product_tag' ] );
} );
it( 'should return taxonomy-product object when given an exact match', () => {
expect(
getTemplateDetailsBySlug( 'taxonomy-product_brands', TEMPLATES )
).toBeTruthy();
expect(
getTemplateDetailsBySlug( 'taxonomy-product_brands', TEMPLATES )
).toStrictEqual( TEMPLATES[ 'taxonomy-product' ] );

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,7 +27,6 @@ import {
interface WithFeaturedItemConfig extends GenericBlockUIConfig {
emptyMessage: string;
noSelectionButtonLabel: string;
}
export interface FeaturedItemRequiredAttributes {
@@ -45,7 +44,6 @@ export interface FeaturedItemRequiredAttributes {
overlayGradient: string;
showDesc: boolean;
showPrice: boolean;
editMode: boolean;
}
interface FeaturedCategoryRequiredAttributes
@@ -94,12 +92,7 @@ type FeaturedItemProps< T extends EditorBlock< T > > =
| ( T & FeaturedProductProps< T > );
export const withFeaturedItem =
( {
emptyMessage,
icon,
label,
noSelectionButtonLabel,
}: WithFeaturedItemConfig ) =>
( { emptyMessage, icon, label }: WithFeaturedItemConfig ) =>
< T extends EditorBlock< T > >( Component: ComponentType< T > ) =>
( props: FeaturedItemProps< T > ) => {
const [ isEditingImage ] = props.useEditingImage;
@@ -147,29 +140,13 @@ export const withFeaturedItem =
);
};
const renderNoItemButton = () => {
return (
<>
<p>{ emptyMessage }</p>
<div style={ { flexBasis: '100%', height: '0' } }></div>
<button
type="button"
className="components-button is-secondary"
onClick={ () => setAttributes( { editMode: true } ) }
>
{ noSelectionButtonLabel }
</button>
</>
);
};
const renderNoItem = () => (
<Placeholder
className={ className }
icon={ <Icon icon={ icon } /> }
label={ label }
>
{ isLoading ? <Spinner /> : renderNoItemButton() }
{ isLoading ? <Spinner /> : emptyMessage }
</Placeholder>
);

View File

@@ -25,7 +25,7 @@ registerBlockType( metadata, {
...metadata.attributes,
columns: {
type: 'number',
default: getSetting( 'defaultColumns', 3 ),
default: getSetting( 'default_columns', 3 ),
},
},

View File

@@ -32,8 +32,8 @@ export const HandpickedProductsInspectorControls = (
onChange={ ( value ) =>
setAttributes( { columns: value } )
}
min={ getSetting( 'minColumns', 1 ) }
max={ getSetting( 'maxColumns', 6 ) }
min={ getSetting( 'min_columns', 1 ) }
max={ getSetting( 'max_columns', 6 ) }
/>
<ToggleControl
label={ __(

View File

@@ -1,65 +0,0 @@
{
"name": "woocommerce/mini-cart",
"version": "1.0.0",
"title": "Mini-Cart",
"icon": "miniCartAlt",
"description": "Display a button for shoppers to quickly view their cart.",
"category": "woocommerce",
"keywords": [ "WooCommerce" ],
"textdomain": "woocommerce",
"supports": {
"html": false,
"multiple": false,
"typography": {
"fontSize": true
}
},
"example": {
"attributes": {
"isPreview": true,
"className": "wc-block-mini-cart--preview"
}
},
"attributes": {
"isPreview": {
"type": "boolean",
"default": false
},
"miniCartIcon": {
"type": "string",
"default": "cart"
},
"addToCartBehaviour": {
"type": "string",
"default": "none"
},
"hasHiddenPrice": {
"type": "boolean",
"default": false
},
"cartAndCheckoutRenderStyle": {
"type": "string",
"default": "hidden"
},
"priceColor": {
"type": "object"
},
"priceColorValue": {
"type": "string"
},
"iconColor": {
"type": "object"
},
"iconColorValue": {
"type": "string"
},
"productCountColor": {
"type": "object"
},
"productCountColorValue": {
"type": "string"
}
},
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}

View File

@@ -41,7 +41,6 @@ import {
blockName,
attributes as miniCartContentsAttributes,
} from './mini-cart-contents/attributes';
import { defaultColorItem } from './utils/defaults';
type Props = BlockAttributes;
@@ -59,9 +58,9 @@ const MiniCartBlock = ( attributes: Props ): JSX.Element => {
miniCartIcon,
addToCartBehaviour = 'none',
hasHiddenPrice = false,
priceColor = defaultColorItem,
iconColor = defaultColorItem,
productCountColor = defaultColorItem,
priceColorValue,
iconColorValue,
productCountColorValue,
} = attributes;
const {
@@ -260,7 +259,7 @@ const MiniCartBlock = ( attributes: Props ): JSX.Element => {
{ ! hasHiddenPrice && (
<span
className="wc-block-mini-cart__amount"
style={ { color: priceColor.color } }
style={ { color: priceColorValue } }
>
{ formatPrice(
subTotal,
@@ -271,7 +270,7 @@ const MiniCartBlock = ( attributes: Props ): JSX.Element => {
{ taxLabel !== '' && subTotal !== 0 && ! hasHiddenPrice && (
<small
className="wc-block-mini-cart__tax-label"
style={ { color: priceColor.color } }
style={ { color: priceColorValue } }
>
{ taxLabel }
</small>
@@ -279,8 +278,8 @@ const MiniCartBlock = ( attributes: Props ): JSX.Element => {
<QuantityBadge
count={ cartItemsCount }
icon={ miniCartIcon }
iconColor={ iconColor }
productCountColor={ productCountColor }
iconColor={ iconColorValue }
productCountColor={ productCountColorValue }
/>
</button>
<Drawer

View File

@@ -50,15 +50,9 @@ const renderMiniCartFrontend = () => {
miniCartIcon: el.dataset.miniCartIcon,
addToCartBehaviour: el.dataset.addToCartBehaviour || 'none',
hasHiddenPrice: el.dataset.hasHiddenPrice,
priceColor: el.dataset.priceColor
? JSON.parse( el.dataset.priceColor )
: {},
iconColor: el.dataset.iconColor
? JSON.parse( el.dataset.iconColor )
: {},
productCountColor: el.dataset.productCountColor
? JSON.parse( el.dataset.productCountColor )
: {},
priceColorValue: el.dataset.priceColorValue,
iconColorValue: el.dataset.iconColorValue,
productCountColorValue: el.dataset.productCountColorValue,
contents:
el.querySelector( '.wc-block-mini-cart__template-part' )
?.innerHTML ?? '',

View File

@@ -1,7 +1,13 @@
/**
* External dependencies
*/
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import {
InspectorControls,
useBlockProps,
withColors,
__experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown,
__experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients,
} from '@wordpress/block-editor';
import { formatPrice } from '@woocommerce/price-format';
import {
PanelBody,
@@ -17,28 +23,28 @@ import Noninteractive from '@woocommerce/base-components/noninteractive';
import { isSiteEditorPage } from '@woocommerce/utils';
import type { ReactElement } from 'react';
import { select } from '@wordpress/data';
import classNames from 'classnames';
import { cartOutline, bag, bagAlt } from '@woocommerce/icons';
import { Icon } from '@wordpress/icons';
import { WC_BLOCKS_IMAGE_URL } from '@woocommerce/block-settings';
import { ColorPanel } from '@woocommerce/editor-components/color-panel';
import type { ColorPaletteOption } from '@woocommerce/editor-components/color-panel/types';
/**
* Internal dependencies
*/
import QuantityBadge from './quantity-badge';
import { defaultColorItem } from './utils/defaults';
import { migrateAttributesToColorPanel } from './utils/data';
import './editor.scss';
export interface Attributes {
interface Attributes {
miniCartIcon: 'cart' | 'bag' | 'bag-alt';
addToCartBehaviour: string;
hasHiddenPrice: boolean;
cartAndCheckoutRenderStyle: boolean;
priceColor: ColorPaletteOption;
iconColor: ColorPaletteOption;
productCountColor: ColorPaletteOption;
priceColor: string;
iconColor: string;
productCountColor: string;
priceColorValue: string;
iconColorValue: string;
productCountColorValue: string;
}
interface Props {
@@ -50,36 +56,33 @@ interface Props {
setProductCountColor: ( colorValue: string | undefined ) => void;
}
const Edit = ( { attributes, setAttributes }: Props ): ReactElement => {
const Edit = ( {
attributes,
setAttributes,
clientId,
setPriceColor,
setIconColor,
setProductCountColor,
}: Props ): ReactElement => {
const {
cartAndCheckoutRenderStyle,
addToCartBehaviour,
hasHiddenPrice,
priceColor = defaultColorItem,
iconColor = defaultColorItem,
productCountColor = defaultColorItem,
priceColorValue,
iconColorValue,
productCountColorValue,
miniCartIcon,
} = migrateAttributesToColorPanel( attributes );
} = attributes;
const miniCartColorAttributes = {
priceColor: {
label: __( 'Price', 'woo-gutenberg-products-block' ),
context: 'price-color',
},
iconColor: {
label: __( 'Icon', 'woo-gutenberg-products-block' ),
context: 'icon-color',
},
productCountColor: {
label: __( 'Product Count', 'woo-gutenberg-products-block' ),
context: 'product-count-color',
},
};
const blockProps = useBlockProps( {
className: 'wc-block-mini-cart',
const className = classNames( {
'wc-block-mini-cart': true,
'has-price-color': priceColorValue,
'has-icon-color': iconColorValue,
'has-product-count-color': productCountColorValue,
} );
const blockProps = useBlockProps( { className } );
const isSiteEditor = isSiteEditorPage( select( 'core/edit-site' ) );
const templatePartEditUri = getSetting(
@@ -89,6 +92,48 @@ const Edit = ( { attributes, setAttributes }: Props ): ReactElement => {
const productCount = 0;
const productTotal = 0;
const colorGradientSettings = useMultipleOriginColorsAndGradients();
const colorSettings = [
{
value: priceColorValue,
onChange: ( colorValue: string ) => {
setPriceColor( colorValue );
setAttributes( { priceColorValue: colorValue } );
},
label: __( 'Price', 'woo-gutenberg-products-block' ),
resetAllFilter: () => {
setPriceColor( undefined );
setAttributes( { priceColorValue: undefined } );
},
},
{
value: iconColorValue,
onChange: ( colorValue: string ) => {
setIconColor( colorValue );
setAttributes( { iconColorValue: colorValue } );
},
label: __( 'Icon', 'woo-gutenberg-products-block' ),
resetAllFilter: () => {
setIconColor( undefined );
setAttributes( { iconColorValue: undefined } );
},
},
{
value: productCountColorValue,
onChange: ( colorValue: string ) => {
setProductCountColor( colorValue );
setAttributes( { productCountColorValue: colorValue } );
},
label: __( 'Product count', 'woo-gutenberg-products-block' ),
resetAllFilter: () => {
setProductCountColor( undefined );
setAttributes( { productCountColorValue: undefined } );
},
},
];
return (
<div { ...blockProps }>
<InspectorControls>
@@ -242,21 +287,45 @@ const Edit = ( { attributes, setAttributes }: Props ): ReactElement => {
</BaseControl>
</PanelBody>
</InspectorControls>
<ColorPanel colorTypes={ miniCartColorAttributes } />
{ colorGradientSettings.hasColorsOrGradients && (
// @ts-to-do: Fix outdated InspectorControls type definitions in DefinitelyTyped and/or Gutenberg.
<InspectorControls group="color">
{ colorSettings.map(
( { onChange, label, value, resetAllFilter } ) => (
<ColorGradientSettingsDropdown
key={ `mini-cart-color-${ label }` }
__experimentalIsRenderedInSidebar
settings={ [
{
colorValue: value,
label,
onColorChange: onChange,
isShownByDefault: true,
resetAllFilter,
enableAlpha: true,
},
] }
panelId={ clientId }
{ ...colorGradientSettings }
/>
)
) }
</InspectorControls>
) }
<Noninteractive>
<button className="wc-block-mini-cart__button">
{ ! hasHiddenPrice && (
<span
className="wc-block-mini-cart__amount"
style={ { color: priceColor.color } }
style={ { color: priceColorValue } }
>
{ formatPrice( productTotal ) }
</span>
) }
<QuantityBadge
count={ productCount }
iconColor={ iconColor }
productCountColor={ productCountColor }
iconColor={ iconColorValue }
productCountColor={ productCountColorValue }
icon={ miniCartIcon }
/>
</button>
@@ -265,4 +334,16 @@ const Edit = ( { attributes, setAttributes }: Props ): ReactElement => {
);
};
export default Edit;
const miniCartColorAttributes = {
priceColor: 'price-color',
iconColor: 'icon-color',
productCountColor: 'product-count-color',
};
// @ts-expect-error: TypeScript doesn't resolve the shared React dependency and cannot resolve the type returned by `withColors`.
// Similar issue example: https://github.com/microsoft/TypeScript/issues/47663
const EditWithColors: JSX.Element = withColors( miniCartColorAttributes )(
Edit
);
export default EditWithColors;

View File

@@ -1,30 +1,22 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { miniCartAlt } from '@woocommerce/icons';
import { Icon } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
import type { BlockConfiguration } from '@wordpress/blocks';
import { isFeaturePluginBuild } from '@woocommerce/block-settings';
import { addFilter } from '@wordpress/hooks';
/**
* Internal dependencies
*/
import metadata from './block.json';
import edit from './edit';
import './style.scss';
const featurePluginSupport = {
...metadata.supports,
...( isFeaturePluginBuild() && {
typography: {
...metadata.supports.typography,
__experimentalFontFamily: true,
__experimentalFontWeight: true,
},
} ),
};
registerBlockType( metadata, {
const settings: BlockConfiguration = {
apiVersion: 2,
title: __( 'Mini-Cart', 'woo-gutenberg-products-block' ),
icon: {
src: (
<Icon
@@ -33,20 +25,81 @@ registerBlockType( metadata, {
/>
),
},
category: 'woocommerce',
keywords: [ __( 'WooCommerce', 'woo-gutenberg-products-block' ) ],
description: __(
'Display a button for shoppers to quickly view their cart.',
'woo-gutenberg-products-block'
),
providesContext: {
priceColorValue: 'priceColorValue',
iconColorValue: 'iconColorValue',
productCountColorValue: 'productCountColorValue',
},
supports: {
...featurePluginSupport,
html: false,
multiple: false,
typography: {
fontSize: true,
...( isFeaturePluginBuild() && {
__experimentalFontFamily: true,
__experimentalFontWeight: true,
} ),
},
},
example: {
...metadata.example,
attributes: {
isPreview: true,
className: 'wc-block-mini-cart--preview',
},
},
attributes: {
...metadata.attributes,
isPreview: {
type: 'boolean',
default: false,
},
miniCartIcon: {
type: 'string',
default: 'cart',
},
addToCartBehaviour: {
type: 'string',
default: 'none',
},
hasHiddenPrice: {
type: 'boolean',
default: false,
},
cartAndCheckoutRenderStyle: {
type: 'string',
default: 'hidden',
},
priceColor: {
type: 'string',
},
priceColorValue: {
type: 'string',
},
iconColor: {
type: 'string',
},
iconColorValue: {
type: 'string',
},
productCountColor: {
type: 'string',
},
productCountColorValue: {
type: 'string',
},
},
edit,
save() {
return null;
},
} );
};
registerBlockType( 'woocommerce/mini-cart', settings );
// Remove the Mini Cart template part from the block inserter.
addFilter(
@@ -58,7 +111,7 @@ addFilter(
...blockSettings,
variations: blockSettings.variations.map(
( variation: { name: string } ) => {
if ( variation.name === 'mini-cart' ) {
if ( variation.name === 'instance_mini-cart' ) {
return {
...variation,
scope: [],

View File

@@ -47,7 +47,18 @@ const Edit = ( {
}: Props ): ReactElement => {
const { currentView, width } = attributes;
const blockProps = useBlockProps();
const blockProps = useBlockProps( {
/**
* This is a workaround for the Site Editor to calculate the
* correct height of the Mini-Cart template part on the first load.
*
* @see https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5825
*/
style: {
minHeight: '100vh',
width,
},
} );
const defaultTemplate = [
[ 'woocommerce/filled-mini-cart-contents-block', {}, [] ],

View File

@@ -88,12 +88,8 @@
}
}
/* Site Editor preview */
.block-editor-block-preview__content-iframe .editor-styles-wrapper {
.wp-block-woocommerce-mini-cart-contents,
.wp-block-woocommerce-filled-mini-cart-contents-block,
.wp-block-woocommerce-empty-mini-cart-contents-block {
height: 800px;
min-height: none;
}
.editor-styles-wrapper .wp-block-woocommerce-mini-cart-contents,
.editor-styles-wrapper .wp-block-woocommerce-filled-mini-cart-contents-block {
height: auto;
min-height: 500px;
}

View File

@@ -29,7 +29,6 @@ const Block = ( {
<Button
className={ classNames(
className,
'wp-block-button__link',
'wc-block-mini-cart__shopping-button'
) }
variant={ getVariant( className, 'contained' ) }

View File

@@ -8,13 +8,13 @@ import { Icon } from '@wordpress/icons';
* Internal dependencies
*/
import './style.scss';
import { IconType, ColorItem } from '.././types';
import { IconType } from '.././types';
interface Props {
count: number;
icon?: IconType;
iconColor: ColorItem | { color: undefined };
productCountColor: ColorItem | { color: undefined };
iconColor?: string;
productCountColor?: string;
}
const QuantityBadge = ( {
@@ -40,13 +40,13 @@ const QuantityBadge = ( {
<span className="wc-block-mini-cart__quantity-badge">
<Icon
className="wc-block-mini-cart__icon"
color={ iconColor.color }
color={ iconColor }
size={ 20 }
icon={ getIcon( icon ) }
/>
<span
className="wc-block-mini-cart__badge"
style={ { background: productCountColor.color } }
style={ { background: productCountColor } }
>
{ count > 0 ? count : '' }
</span>

View File

@@ -5,12 +5,6 @@ import { CartResponseTotals } from '@woocommerce/types';
export type IconType = 'cart' | 'bag' | 'bag-alt' | undefined;
export interface ColorItem {
color: string;
name?: string;
slug?: string;
class?: string;
}
export interface BlockAttributes {
initialCartItemsCount?: number;
initialCartTotals?: CartResponseTotals;
@@ -21,7 +15,7 @@ export interface BlockAttributes {
miniCartIcon?: IconType;
addToCartBehaviour: string;
hasHiddenPrice: boolean;
priceColor: ColorItem;
iconColor: ColorItem;
productCountColor: ColorItem;
priceColorValue: string;
iconColorValue: string;
productCountColorValue: string;
}

View File

@@ -12,12 +12,6 @@ import {
isBoolean,
} from '@woocommerce/types';
import { getSettingWithCoercion } from '@woocommerce/settings';
import type { ColorPaletteOption } from '@woocommerce/editor-components/color-panel/types';
/**
* Internal dependencies
*/
import { Attributes } from '../edit';
const getPrice = ( totals: CartResponseTotals, showIncludingTax: boolean ) => {
const currency = getCurrencyFromPriceResponse( totals );
@@ -158,45 +152,3 @@ export const getMiniCartTotalsFromServer = async (): Promise<
return undefined;
} );
};
interface MaybeInCompatibleAttributes
extends Omit<
Attributes,
'priceColor' | 'iconColor' | 'productCountColor'
> {
priceColorValue?: string;
iconColorValue?: string;
productCountColorValue?: string;
priceColor: Partial< ColorPaletteOption > | string;
iconColor: Partial< ColorPaletteOption > | string;
productCountColor: Partial< ColorPaletteOption > | string;
}
export function migrateAttributesToColorPanel(
attributes: MaybeInCompatibleAttributes
): Attributes {
const attrs = { ...attributes };
if ( attrs.priceColorValue && ! attrs.priceColor ) {
attrs.priceColor = {
color: attributes.priceColorValue as string,
};
delete attrs.priceColorValue;
}
if ( attrs.iconColorValue && ! attrs.iconColor ) {
attrs.iconColor = {
color: attributes.iconColorValue as string,
};
delete attrs.iconColorValue;
}
if ( attrs.productCountColorValue && ! attrs.productCountColor ) {
attrs.productCountColor = {
color: attributes.productCountColorValue as string,
};
delete attrs.productCountColorValue;
}
return <Attributes>attrs;
}

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