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

code import from pantheon

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

View File

@@ -5,8 +5,11 @@
* Description: A robust scheduling library for use in WordPress plugins.
* Author: Automattic
* Author URI: https://automattic.com/
* Version: 3.6.1
* Version: 3.6.4
* License: GPLv3
* Tested up to: 6.3
* Requires at least: 5.2
* Requires PHP: 5.6
*
* Copyright 2019 Automattic, Inc. (https://automattic.com/contact/)
*
@@ -26,27 +29,27 @@
* @package ActionScheduler
*/
if ( ! function_exists( 'action_scheduler_register_3_dot_6_dot_1' ) && function_exists( 'add_action' ) ) { // WRCS: DEFINED_VERSION.
if ( ! function_exists( 'action_scheduler_register_3_dot_6_dot_4' ) && 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_1', 0, 0 ); // WRCS: DEFINED_VERSION.
add_action( 'plugins_loaded', 'action_scheduler_register_3_dot_6_dot_4', 0, 0 ); // WRCS: DEFINED_VERSION.
/**
* Registers this version of Action Scheduler.
*/
function action_scheduler_register_3_dot_6_dot_1() { // WRCS: DEFINED_VERSION.
function action_scheduler_register_3_dot_6_dot_4() { // WRCS: DEFINED_VERSION.
$versions = ActionScheduler_Versions::instance();
$versions->register( '3.6.1', 'action_scheduler_initialize_3_dot_6_dot_1' ); // WRCS: DEFINED_VERSION.
$versions->register( '3.6.4', 'action_scheduler_initialize_3_dot_6_dot_4' ); // WRCS: DEFINED_VERSION.
}
/**
* Initializes this version of Action Scheduler.
*/
function action_scheduler_initialize_3_dot_6_dot_1() { // WRCS: DEFINED_VERSION.
function action_scheduler_initialize_3_dot_6_dot_4() { // 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).
@@ -58,7 +61,7 @@ if ( ! function_exists( 'action_scheduler_register_3_dot_6_dot_1' ) && 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_1(); // WRCS: DEFINED_VERSION.
action_scheduler_initialize_3_dot_6_dot_4(); // WRCS: DEFINED_VERSION.
do_action( 'action_scheduler_pre_theme_init' );
ActionScheduler_Versions::initialize_latest_version();
}

View File

@@ -0,0 +1,129 @@
*** Changelog ***
= 3.6.4 - 2023-10-11 =
* 3.6.3 release.
* Fix option lock test.
* Fix: Use orderby => 'none' when bulk cancelling actions.
* Tweak - WP 6.3 compatibility.
* Update PR unit tests matrix.
= 3.6.3 - 2023-09-13 =
* Use `_doing_it_wrong` in initialization check.
= 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.
* Document the new `action_scheduler_init` hook.
* Ensure actions within each claim are executed in the expected order.
* Fix incorrect text domain.
* Remove SHOW TABLES usage when checking if tables exist.
= 3.6.0 - 2023-05-10 =
* Add $unique parameter to function signatures.
* Add a cast-to-int for extra safety before forming new DateTime object.
* Add a hook allowing exceptions for consistently failing recurring actions.
* Add action priorities.
* Add init hook.
* Always raise the time limit.
* Bump minimatch from 3.0.4 to 3.0.8.
* Bump yaml from 2.2.1 to 2.2.2.
* Defensive coding relating to gaps in declared schedule types.
* Do not process an action if it cannot be set to `in-progress`.
* Filter view labels (status names) should be translatable | #919.
* Fix WPCLI progress messages.
* Improve data-store initialization flow.
* Improve error handling across all supported PHP versions.
* Improve logic for flushing the runtime cache.
* Support exclusion of multiple groups.
* Update lint-staged and Node/NPM requirements.
* add CLI clean command.
* add CLI exclude-group filter.
* exclude past-due from list table all filter count.
* throwing an exception if as_schedule_recurring_action interval param is not of type integer.
= 3.5.4 - 2023-01-17 =
* Add pre filters during action registration.
* Async scheduling.
* Calculate timeouts based on total actions.
* Correctly order the parameters for `ActionScheduler_ActionFactory`'s calls to `single_unique`.
* Fetch action in memory first before releasing claim to avoid deadlock.
* PHP 8.2: declare property to fix creation of dynamic property warning.
* PHP 8.2: fix "Using ${var} in strings is deprecated, use {$var} instead".
* Prevent `undefined variable` warning for `$num_pastdue_actions`.
= 3.5.3 - 2022-11-09 =
* Query actions with partial match.
= 3.5.2 - 2022-09-16 =
* Fix - erroneous 3.5.1 release.
= 3.5.1 - 2022-09-13 =
* Maintenance on A/S docs.
* fix: PHP 8.2 deprecated notice.
= 3.5.0 - 2022-08-25 =
* Add - The active view link within the "Tools > Scheduled Actions" screen is now clickable.
* Add - A warning when there are past-due actions.
* Enhancement - Added the ability to schedule unique actions via an atomic operation.
* Enhancement - Improvements to cache invalidation when processing batches (when running on WordPress 6.0+).
* Enhancement - If a recurring action is found to be consistently failing, it will stop being rescheduled.
* Enhancement - Adds a new "Past Due" view to the scheduled actions list table.
= 3.4.2 - 2022-06-08 =
* Fix - Change the include for better linting.
* Fix - update: Added Action scheduler completed action hook.
= 3.4.1 - 2022-05-24 =
* Fix - Change the include for better linting.
* Fix - Fix the documented return type.
= 3.4.0 - 2021-10-29 =
* Enhancement - Number of items per page can now be set for the Scheduled Actions view (props @ovidiul). #771
* Fix - Do not lower the max_execution_time if it is already set to 0 (unlimited) (props @barryhughes). #755
* Fix - Avoid triggering autoloaders during the version resolution process (props @olegabr). #731 & #776
* Dev - ActionScheduler_wcSystemStatus PHPCS fixes (props @ovidiul). #761
* Dev - ActionScheduler_DBLogger.php PHPCS fixes (props @ovidiul). #768
* Dev - Fixed phpcs for ActionScheduler_Schedule_Deprecated (props @ovidiul). #762
* Dev - Improve actions table indicies (props @glagonikas). #774 & #777
* Dev - PHPCS fixes for ActionScheduler_DBStore.php (props @ovidiul). #769 & #778
* Dev - PHPCS Fixes for ActionScheduler_Abstract_ListTable (props @ovidiul). #763 & #779
* Dev - Adds new filter action_scheduler_claim_actions_order_by to allow tuning of the claim query (props @glagonikas). #773
* Dev - PHPCS fixes for ActionScheduler_WpPostStore class (props @ovidiul). #780
= 3.3.0 - 2021-09-15 =
* Enhancement - Adds as_has_scheduled_action() to provide a performant way to test for existing actions. #645
* Fix - Improves compatibility with environments where NO_ZERO_DATE is enabled. #519
* Fix - Adds safety checks to guard against errors when our database tables cannot be created. #645
* Dev - Now supports queries that use multiple statuses. #649
* Dev - Minimum requirements for WordPress and PHP bumped (to 5.2 and 5.6 respectively). #723
= 3.2.1 - 2021-06-21 =
* Fix - Add extra safety/account for different versions of AS and different loading patterns. #714
* Fix - Handle hidden columns (Tools → Scheduled Actions) | #600.
= 3.2.0 - 2021-06-03 =
* Fix - Add "no ordering" option to as_next_scheduled_action().
* Fix - Add secondary scheduled date checks when claiming actions (DBStore) | #634.
* Fix - Add secondary scheduled date checks when claiming actions (wpPostStore) | #634.
* Fix - Adds a new index to the action table, reducing the potential for deadlocks (props: @glagonikas).
* Fix - Fix unit tests infrastructure and adapt tests to PHP 8.
* Fix - Identify in-use data store.
* Fix - Improve test_migration_is_scheduled.
* Fix - PHP notice on list table.
* Fix - Speed up clean up and batch selects.
* Fix - Update pending dependencies.
* Fix - [PHP 8.0] Only pass action arg values through to do_action_ref_array().
* Fix - [PHP 8] Set the PHP version to 7.1 in composer.json for PHP 8 compatibility.
* Fix - add is_initialized() to docs.
* Fix - fix file permissions.
* Fix - fixes #664 by replacing __ with esc_html__.
= 3.1.6 - 2020-05-12 =
* Change log starts.

View File

@@ -502,7 +502,20 @@ class ActionScheduler_ListTable extends ActionScheduler_Abstract_ListTable {
*/
protected function bulk_delete( array $ids, $ids_sql ) {
foreach ( $ids as $id ) {
$this->store->delete_action( $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()
)
);
}
}
}

View File

@@ -24,7 +24,37 @@ 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 ) {
return update_option( $this->get_key( $lock_type ), time() + $this->get_duration( $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,
)
);
}
/**
@@ -34,7 +64,30 @@ 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 get_option( $this->get_key( $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;
}
/**
@@ -46,4 +99,37 @@ 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,9 +103,12 @@ class ActionScheduler_QueueRunner extends ActionScheduler_Abstract_QueueRunner {
* should dispatch a request to process pending actions.
*/
public function maybe_dispatch_async_request() {
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' );
// 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' )
) {
$this->async_request->maybe_dispatch();
}
}

View File

@@ -226,7 +226,7 @@ abstract class ActionScheduler {
__( '%s() was called before the Action Scheduler data store was initialized', 'woocommerce' ),
esc_attr( $function_name )
);
error_log( $message );
_doing_it_wrong( $function_name, $message, '3.1.6' );
}
return self::$data_store_initialized;

View File

@@ -26,6 +26,8 @@ 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

@@ -347,7 +347,7 @@ abstract class ActionScheduler_Store extends ActionScheduler_Store_Deprecated {
'hook' => $hook,
'status' => self::STATUS_PENDING,
'per_page' => 1000,
'orderby' => 'action_id',
'orderby' => 'none',
)
);
@@ -372,7 +372,7 @@ abstract class ActionScheduler_Store extends ActionScheduler_Store_Deprecated {
'group' => $group,
'status' => self::STATUS_PENDING,
'per_page' => 1000,
'orderby' => 'action_id',
'orderby' => 'none',
)
);

View File

@@ -705,7 +705,7 @@ AND `group_id` = %d
array(
'per_page' => 1000,
'status' => self::STATUS_PENDING,
'orderby' => 'action_id',
'orderby' => 'none',
)
);
@@ -935,7 +935,17 @@ 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 ) {
throw new \RuntimeException( __( 'Unable to claim actions. Database error.', 'woocommerce' ) );
$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
)
);
}
return (int) $rows_affected;

View File

@@ -1,11 +1,9 @@
=== Action Scheduler ===
Contributors: Automattic, wpmuguru, claudiosanches, peterfabian1000, vedjain, jamosova, obliviousharmony, konamiman, sadowski, royho, barryhughes-1
Tags: scheduler, cron
Requires at least: 5.2
Tested up to: 6.0
Stable tag: 3.6.1
Stable tag: 3.6.4
License: GPLv3
Requires PHP: 5.6
Tested up to: 6.3
Action Scheduler - Job Queue for WordPress
@@ -47,6 +45,23 @@ Collaboration is cool. We'd love to work with you to improve Action Scheduler. [
== Changelog ==
= 3.6.4 - 2023-10-11 =
* 3.6.3 release.
* Fix option lock test.
* Fix: Use orderby => 'none' when bulk cancelling actions.
* Tweak - WP 6.3 compatibility.
* Update PR unit tests matrix.
= 3.6.3 - 2023-09-13 =
* Use `_doing_it_wrong` in initialization check.
= 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

@@ -15,3 +15,8 @@ $select-dropdown-dark: #1e1e1e;
$select-dropdown-light: #fff;
$select-item-dark: rgba(0, 0, 0, 0.4);
$image-placeholder-border-color: #f2f2f2;
// Universal colors for use on the frontend, currently being applied to checkout blocks.
$universal-border: rgba(17, 17, 17, 0.3); // Used for form step borders.
$universal-border-light: rgba(17, 17, 17, 0.115); // e7e7e7 on white.
$universal-body-low-emphasis: rgba(17, 17, 17, 0.5); // Used for low emphasis text such as input labels.

View File

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

View File

@@ -1,9 +1,13 @@
@import "node_modules/@wordpress/base-styles/variables";
$gap-largest: $grid-unit-50;
$gap-larger: 4.5 * $grid-unit;
$gap-large: $grid-unit-30;
$gap: $grid-unit-20;
$gap-small: $grid-unit-15;
$gap-smaller: $grid-unit-10;
$gap-smallest: $grid-unit-05;
// grid-unit from base-styles is 8px.
$gap-largest: 6 * $grid-unit; // 48px
$gap-larger: 4.5 * $grid-unit; // 36px
$gap-large: 3 * $grid-unit; // 24px
$gap: 2 * $grid-unit; // 16px
$gap-small: 1.5 * $grid-unit; // 12px
$gap-smaller: 1 * $grid-unit; // 8px
$gap-smallest: 0.5 * $grid-unit; // 4px
// Standard border radius for forms.
$universal-border-radius: 4px;

View File

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

View File

@@ -139,20 +139,24 @@
}
}
}
.wc-block-grid__product-onsale {
.wc-block-grid__product-image .wc-block-grid__product-onsale,
.wc-block-grid .wc-block-grid__product-onsale {
@include font-size(small);
padding: em($gap-smallest) em($gap-small);
display: inline-block;
width: auto;
border: 1px solid #43454b;
border-radius: 3px;
border-radius: $universal-border-radius;
color: #43454b;
background: #fff;
text-align: center;
text-transform: uppercase;
font-weight: 600;
z-index: 9;
position: relative;
position: absolute;
top: 4px;
right: 4px;
left: auto;
}
// Element spacing.

View File

@@ -54,6 +54,24 @@ 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,6 +6,8 @@ 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

@@ -25,7 +25,7 @@
padding: 0.618em;
background: $white;
border: 1px solid #ccc;
border-radius: 2px;
border-radius: $universal-border-radius;
color: #43454b;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.125);
text-align: center;

View File

@@ -0,0 +1,17 @@
{
"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

@@ -0,0 +1,37 @@
/**
* 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

@@ -0,0 +1,43 @@
/**
* 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

@@ -0,0 +1,25 @@
/**
* 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

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

View File

@@ -27,22 +27,10 @@ import type {
AddToCartButtonPlaceholderAttributes,
} from './types';
/**
* Product Button Block Component.
*
* @param {Object} props Incoming props.
* @param {Object} [props.product] Product.
* @param {Object} [props.style] Object contains CSS Styles.
* @param {string} [props.className] String contains CSS class.
* @param {Object} [props.textAlign] Text alignment.
*
* @return {*} The component.
*/
const AddToCartButton = ( {
product,
className,
style,
textAlign,
}: AddToCartButtonAttributes ): JSX.Element => {
const {
id,
@@ -117,9 +105,6 @@ const AddToCartButton = ( {
{
loading: addingToCart,
added: addedToCart,
},
{
[ `has-text-align-${ textAlign }` ]: textAlign,
}
) }
style={ style }
@@ -129,15 +114,6 @@ const AddToCartButton = ( {
);
};
/**
* Product Button Block Component.
*
* @param {Object} props Incoming props.
* @param {Object} [props.style] Object contains CSS Styles.
* @param {string} [props.className] String contains CSS class.
*
* @return {*} The component.
*/
const AddToCartButtonPlaceholder = ( {
className,
style,
@@ -158,14 +134,6 @@ const AddToCartButtonPlaceholder = ( {
);
};
/**
* Product Button Block Component.
*
* @param {Object} props Incoming props.
* @param {string} [props.className] CSS Class name for the component.
* @param {string} [props.textAlign] Text alignment.
* @return {*} The component.
*/
export const Block = ( props: BlockAttributes ): JSX.Element => {
const { className, textAlign } = props;
const styleProps = useStyleProps( props );
@@ -181,9 +149,7 @@ export const Block = ( props: BlockAttributes ): JSX.Element => {
{
[ `${ parentClassName }__product-add-to-cart` ]:
parentClassName,
},
{
[ `has-text-align-${ textAlign }` ]: textAlign,
[ `align-${ textAlign }` ]: textAlign,
}
) }
>

View File

@@ -0,0 +1,300 @@
/* 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',
} );
};
// RequestIdleCallback is not available in Safari, so we use setTimeout as an alternative.
const callIdleCallback =
window.requestIdleCallback || ( ( cb ) => setTimeout( cb, 100 ) );
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;
}
},
},
},
init: {
woocommerce: {
syncTemporaryNumberOfItemsOnLoad: ( store: Store ) => {
const { selectors, context } = store;
// If the cart has loaded when we instantiate this element, we sync
// the temporary number of items with the number of items in the cart
// to avoid triggering the animation. We do this only once, but we
// use useLayoutEffect to avoid the useEffect flickering.
if ( selectors.woocommerce.hasCartLoaded( store ) ) {
context.woocommerce.temporaryNumberOfItems =
selectors.woocommerce.numberOfItemsInTheCart(
store
);
}
},
},
},
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.
callIdleCallback( () => {
if ( ! selectors.woocommerce.hasCartLoaded( store ) ) {
select( storeKey ).getCartData();
}
} );
},
}
);

View File

@@ -1,15 +1,87 @@
.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;
}
}
&.align-left {
align-items: flex-start;
}
&.align-right {
align-items: flex-end;
}
.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

@@ -47,6 +47,9 @@ export const blockAttributes: BlockAttributes = {
type: 'string',
default: 'cover',
},
aspectRatio: {
type: 'string',
},
};
export default blockAttributes;

View File

@@ -49,6 +49,7 @@ interface ImageProps {
scale: string;
width?: string | undefined;
height?: string | undefined;
aspectRatio: string | undefined;
}
const Image = ( {
@@ -59,6 +60,7 @@ const Image = ( {
width,
scale,
height,
aspectRatio,
}: ImageProps ): JSX.Element => {
const { thumbnail, src, srcset, sizes, alt } = image || {};
const imageProps = {
@@ -72,6 +74,7 @@ const Image = ( {
height,
width,
objectFit: scale,
aspectRatio,
};
return (
@@ -101,6 +104,7 @@ export const Block = ( props: Props ): JSX.Element | null => {
height,
width,
scale,
aspectRatio,
...restProps
} = props;
const styleProps = useStyleProps( props );
@@ -120,6 +124,7 @@ export const Block = ( props: Props ): JSX.Element | null => {
},
styleProps.className
) }
style={ styleProps.style }
>
<ImagePlaceholder />
</div>
@@ -153,6 +158,7 @@ export const Block = ( props: Props ): JSX.Element | null => {
},
styleProps.className
) }
style={ styleProps.style }
>
<ParentComponent { ...( showProductLink && anchorProps ) }>
{ !! showSaleBadge && (
@@ -169,6 +175,7 @@ export const Block = ( props: Props ): JSX.Element | null => {
width={ width }
height={ height }
scale={ scale }
aspectRatio={ aspectRatio }
/>
</ParentComponent>
</div>

View File

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

View File

@@ -24,4 +24,6 @@ export interface BlockAttributes {
width?: string;
// Image scaling method.
scale: 'cover' | 'contain' | 'fill';
// Aspect ratio of the image.
aspectRatio: string;
}

View File

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

View File

@@ -12,7 +12,7 @@
display: inline-block;
position: relative;
z-index: 0;
border-radius: 4px 4px 0 0;
border-radius: $universal-border-radius $universal-border-radius 0 0;
margin: 0;
padding: 0.5em 1em;

View File

@@ -0,0 +1,38 @@
{
"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

@@ -0,0 +1,88 @@
/**
* 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

@@ -0,0 +1,75 @@
/**
* 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

@@ -0,0 +1,25 @@
/**
* 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

@@ -0,0 +1,24 @@
/* 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

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

View File

@@ -1,7 +1,6 @@
{
"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": {
@@ -32,6 +31,7 @@
"supports": {
"align": true
},
"ancestor": [ "woocommerce/single-product" ],
"textdomain": "woocommerce",
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"

View File

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

View File

@@ -1,20 +0,0 @@
/**
* 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,36 +1,25 @@
/**
* External dependencies
*/
import { BlockConfiguration } from '@wordpress/blocks';
import { registerBlockSingleProductTemplate } from '@woocommerce/atomic-utils';
import { isExperimentalBuild } from '@woocommerce/block-settings';
import { registerBlockType } from '@wordpress/blocks';
import { Icon, starFilled } from '@wordpress/icons';
/**
* 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';
const blockConfig: BlockConfiguration = {
...sharedConfig,
ancestor: [
'woocommerce/all-products',
'woocommerce/single-product',
'core/post-template',
'woocommerce/product-template',
],
icon: { src: BLOCK_ICON },
registerBlockType( metadata, {
icon: {
src: (
<Icon
icon={ starFilled }
className="wc-block-editor-components-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 {
.wc-block-components-product-rating-stars {
display: block;
line-height: 1;
@@ -13,6 +13,7 @@
/* 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,7 +3,6 @@
* External dependencies
*/
import { isFeaturePluginBuild } from '@woocommerce/block-settings';
import { __experimentalGetSpacingClassesAndStyles } from '@wordpress/block-editor';
export const supports = {
...( isFeaturePluginBuild() && {
@@ -23,10 +22,4 @@ export const supports = {
},
__experimentalSelector: '.wc-block-components-product-rating',
} ),
...( ! isFeaturePluginBuild() &&
typeof __experimentalGetSpacingClassesAndStyles === 'function' && {
spacing: {
margin: true,
},
} ),
};

View File

@@ -11,6 +11,11 @@ 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

@@ -0,0 +1,12 @@
.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

@@ -12,6 +12,10 @@ export const blockAttributes: BlockAttributes = {
type: 'boolean',
default: false,
},
isDescendentOfSingleProductTemplate: {
type: 'boolean',
default: false,
},
};
export default blockAttributes;

View File

@@ -3,7 +3,7 @@
*/
import { __ } from '@wordpress/i18n';
import classnames from 'classnames';
import Label from '@woocommerce/base-components/label';
import { Label } from '@woocommerce/blocks-components';
import {
useInnerBlockLayoutContext,
useProductDataContext,
@@ -26,7 +26,10 @@ export const Block = ( props: Props ): JSX.Element | null => {
const { parentClassName } = useInnerBlockLayoutContext();
const { product } = useProductDataContext();
if ( ! product.id || ! product.on_sale ) {
if (
( ! product.id || ! product.on_sale ) &&
! props.isDescendentOfSingleProductTemplate
) {
return null;
}

View File

@@ -10,13 +10,8 @@ import { useEffect } from '@wordpress/element';
* Internal dependencies
*/
import Block from './block';
import withProductSelector from '../shared/with-product-selector';
import {
BLOCK_TITLE as label,
BLOCK_ICON as icon,
BLOCK_DESCRIPTION as description,
} from './constants';
import type { BlockAttributes } from './types';
import { useIsDescendentOfSingleProductTemplate } from '../shared/use-is-descendent-of-single-product-template';
const Edit = ( {
attributes,
@@ -31,9 +26,20 @@ const Edit = ( {
};
const isDescendentOfQueryLoop = Number.isFinite( context.queryId );
const { isDescendentOfSingleProductTemplate } =
useIsDescendentOfSingleProductTemplate();
useEffect(
() => setAttributes( { isDescendentOfQueryLoop } ),
[ setAttributes, isDescendentOfQueryLoop ]
() =>
setAttributes( {
isDescendentOfQueryLoop,
isDescendentOfSingleProductTemplate,
} ),
[
setAttributes,
isDescendentOfQueryLoop,
isDescendentOfSingleProductTemplate,
]
);
return (
@@ -43,4 +49,4 @@ const Edit = ( {
);
};
export default withProductSelector( { icon, label, description } )( Edit );
export default Edit;

View File

@@ -32,6 +32,7 @@ const blockConfig: BlockConfiguration = {
'woocommerce/single-product',
'core/post-template',
'woocommerce/product-template',
'woocommerce/product-gallery',
],
};

View File

@@ -1,11 +1,15 @@
.wp-block-woocommerce-product-sale-badge {
display: flex;
flex-direction: column;
}
.wc-block-components-product-sale-badge {
margin: 0 auto $gap-small;
@include font-size(small);
padding: em($gap-smallest) em($gap-small);
display: inline-block;
width: auto;
width: fit-content;
border: 1px solid #43454b;
border-radius: 3px;
border-radius: $universal-border-radius;
box-sizing: border-box;
color: #43454b;
background: #fff;
@@ -15,6 +19,16 @@
z-index: 9;
position: static;
&--align-left {
align-self: auto;
}
&--align-center {
align-self: center;
}
&--align-right {
align-self: flex-end;
}
span {
color: inherit;
background-color: inherit;

View File

@@ -7,6 +7,7 @@ import { __experimentalGetSpacingClassesAndStyles } from '@wordpress/block-edito
export const supports = {
html: false,
align: true,
...( isFeaturePluginBuild() && {
color: {
gradients: true,
@@ -31,6 +32,7 @@ export const supports = {
width: true,
__experimentalSkipSerialization: true,
},
// @todo: Improve styles support when WordPress 6.4 is released. https://make.wordpress.org/core/2023/07/17/introducing-the-block-selectors-api/
...( typeof __experimentalGetSpacingClassesAndStyles === 'function' && {
spacing: {
margin: true,

View File

@@ -2,4 +2,5 @@ export interface BlockAttributes {
productId: number;
align: 'left' | 'center' | 'right';
isDescendentOfQueryLoop?: boolean | undefined;
isDescendentOfSingleProductTemplate?: boolean;
}

View File

@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { isNumber } from '@woocommerce/types';
import { isNumber, isEmpty } from '@woocommerce/types';
import {
BlockAttributes,
BlockConfiguration,
@@ -80,19 +80,17 @@ export const registerBlockSingleProductTemplate = ( {
if ( ! isBlockRegistered ) {
if ( isVariationBlock ) {
registerBlockVariation( blockName, {
...blockSettings,
// @ts-expect-error: `ancestor` key is typed in WordPress core
ancestor: ! currentTemplateId?.includes( 'single-product' )
? blockSettings?.ancestor
: undefined,
} );
// @ts-expect-error: `registerBlockType` is not typed in WordPress core
registerBlockVariation( blockName, blockSettings );
} else {
// @ts-expect-error: `registerBlockType` is typed in WordPress core
const ancestor = isEmpty( blockSettings?.ancestor )
? [ 'woocommerce/single-product' ]
: blockSettings?.ancestor;
// @ts-expect-error: `registerBlockType` is not typed in WordPress core
registerBlockType( blockMetadata, {
...blockSettings,
ancestor: ! currentTemplateId?.includes( 'single-product' )
? blockSettings?.ancestor
? ancestor
: undefined,
} );
}

View File

@@ -4,12 +4,12 @@
import { Button as WPButton } from 'wordpress-components';
import type { Button as WPButtonType } from '@wordpress/components';
import classNames from 'classnames';
import Spinner from '@woocommerce/base-components/spinner';
/**
* Internal dependencies
*/
import './style.scss';
import Spinner from '../../../../../packages/components/spinner';
export interface ButtonProps
extends Omit< WPButtonType.ButtonProps, 'variant' | 'href' > {

View File

@@ -1,7 +1,11 @@
/**
* External dependencies
*/
import { ValidatedTextInput, isPostcode } from '@woocommerce/blocks-checkout';
import {
ValidatedTextInput,
isPostcode,
type ValidatedTextInputHandle,
} from '@woocommerce/blocks-checkout';
import {
BillingCountryInput,
ShippingCountryInput,
@@ -10,195 +14,111 @@ import {
BillingStateInput,
ShippingStateInput,
} from '@woocommerce/base-components/state-input';
import { useEffect, useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { withInstanceId } from '@wordpress/compose';
import { useEffect, useMemo, useRef } from '@wordpress/element';
import { useInstanceId } from '@wordpress/compose';
import { useShallowEqual } from '@woocommerce/base-hooks';
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';
import { defaultAddressFields } from '@woocommerce/settings';
import isShallowEqual from '@wordpress/is-shallow-equal';
/**
* 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';
// 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;
}
const defaultFields = Object.keys(
defaultAddressFields
) as unknown as FieldType[];
/**
* Checkout address form.
*/
const AddressForm = ( {
id = '',
fields = Object.keys(
defaultAddressFields
) as unknown as ( keyof AddressFields )[],
fieldConfig = {} as Record< keyof AddressFields, Partial< AddressField > >,
instanceId,
fields = defaultFields,
fieldConfig = {} as FieldConfig,
onChange,
type = 'shipping',
values,
}: AddressFormProps ): JSX.Element => {
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 instanceId = useInstanceId( AddressForm );
// Track incoming props.
const currentFields = useShallowEqual( fields );
const currentFieldConfig = useShallowEqual( fieldConfig );
const currentCountry = useShallowEqual( values.country );
const addressFormFields = useMemo( () => {
return prepareAddressFields(
// Memoize the address form fields passed in from the parent component.
const addressFormFields = useMemo( (): AddressFormFields => {
const preparedFields = prepareAddressFields(
currentFields,
fieldConfig,
values.country
currentFieldConfig,
currentCountry
);
}, [ currentFields, 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 >
>( {} );
// Clear values for hidden fields.
useEffect( () => {
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 ] );
const newValues = {
...values,
...Object.fromEntries(
addressFormFields.hidden.map( ( field ) => [ field.key, '' ] )
),
};
if ( ! isShallowEqual( values, newValues ) ) {
onChange( newValues );
}
}, [ onChange, addressFormFields, values ] );
// Maybe validate country when other fields change so user is notified that it's required.
useEffect( () => {
if ( type === 'shipping' ) {
validateShippingCountry(
values,
setValidationErrors,
clearValidationError,
!! countryValidationError?.message &&
! countryValidationError?.hidden
);
validateShippingCountry( values );
}
}, [
values,
countryValidationError?.message,
countryValidationError?.hidden,
setValidationErrors,
clearValidationError,
type,
] );
}, [ values, type ] );
id = id || instanceId;
// Changing country may change format for postcodes.
useEffect( () => {
fieldsRef.current?.postcode?.revalidate();
}, [ currentCountry ] );
/**
* 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;
};
id = id || `${ instanceId }`;
return (
<div id={ id } className="wc-block-components-address-form">
{ addressFormFields.map( ( field ) => {
{ addressFormFields.fields.map( ( field ) => {
if ( field.hidden ) {
return null;
}
// Create a consistent error ID based on the field key and type
const errorId = `${ type }_${ field.key }`;
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 }`,
};
if ( field.key === 'country' ) {
const Tag =
@@ -208,24 +128,26 @@ const AddressForm = ( {
return (
<Tag
key={ field.key }
id={ `${ id }-${ field.key }` }
errorId={ errorId }
label={
field.required
? field.label
: field.optionalLabel
}
{ ...fieldProps }
value={ values.country }
autoComplete={ field.autocomplete }
onChange={ ( newValue ) =>
onChange( {
onChange={ ( newCountry ) => {
const newValues = {
...values,
country: newValue,
country: newCountry,
state: '',
} )
}
errorMessage={ field.errorMessage }
required={ field.required }
};
// Country will impact postcode too. Do we need to clear it?
if (
values.postcode &&
! isPostcode( {
postcode: values.postcode,
country: newCountry,
} )
) {
newValues.postcode = '';
}
onChange( newValues );
} }
/>
);
}
@@ -238,24 +160,15 @@ const AddressForm = ( {
return (
<Tag
key={ field.key }
id={ `${ id }-${ field.key }` }
errorId={ errorId }
{ ...fieldProps }
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 }
/>
);
}
@@ -263,35 +176,30 @@ const AddressForm = ( {
return (
<ValidatedTextInput
key={ field.key }
id={ `${ id }-${ field.key }` }
errorId={ errorId }
className={ `wc-block-components-address-form__${ field.key }` }
label={
field.required ? field.label : field.optionalLabel
ref={ ( el ) =>
( fieldsRef.current[ field.key ] = el )
}
{ ...fieldProps }
value={ values[ field.key ] }
autoCapitalize={ field.autocapitalize }
autoComplete={ field.autocomplete }
onChange={ ( newValue: string ) =>
onChange( {
...values,
[ field.key ]:
field.key === 'postcode'
? newValue.trimStart().toUpperCase()
: newValue,
[ field.key ]: newValue,
} )
}
customFormatter={ ( value: string ) => {
if ( field.key === 'postcode' ) {
return value.trimStart().toUpperCase();
}
return value;
} }
customValidation={ ( inputObject: HTMLInputElement ) =>
field.required || inputObject.value
? customValidationHandler(
inputObject,
field.key,
values
)
: true
customValidationHandler(
inputObject,
field.key,
values
)
}
errorMessage={ field.errorMessage }
required={ field.required }
/>
);
} ) }
@@ -299,4 +207,4 @@ const AddressForm = ( {
);
};
export default withInstanceId( AddressForm );
export default AddressForm;

View File

@@ -0,0 +1,41 @@
/**
* 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

@@ -0,0 +1,39 @@
/**
* 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;
// 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

@@ -0,0 +1,43 @@
/**
* 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

@@ -1,6 +1,5 @@
export * from './address-form';
export { default as CartLineItemsTable } from './cart-line-items-table';
export { default as FormStep } from './form-step';
export { default as OrderSummary } from './order-summary';
export { default as PlaceOrderButton } from './place-order-button';
export { default as Policies } from './policies';

View File

@@ -1,12 +1,11 @@
/**
* External dependencies
*/
import { RadioControlOption } from '@woocommerce/base-components/radio-control/types';
import {
RadioControl,
RadioControlOptionType,
} from '@woocommerce/blocks-components';
import { CartShippingPackageShippingRate } from '@woocommerce/types';
/**
* Internal dependencies
*/
import RadioControl from '../../radio-control';
interface LocalPickupSelectProps {
title?: string | undefined;
@@ -17,7 +16,7 @@ interface LocalPickupSelectProps {
renderPickupLocation: (
location: CartShippingPackageShippingRate,
pickupLocationsCount: number
) => RadioControlOption;
) => RadioControlOptionType;
packageCount: number;
}
/**

View File

@@ -3,7 +3,7 @@
*/
import classnames from 'classnames';
import { sprintf, _n } from '@wordpress/i18n';
import Label from '@woocommerce/base-components/label';
import { Label } from '@woocommerce/blocks-components';
import ProductPrice from '@woocommerce/base-components/product-price';
import ProductName from '@woocommerce/base-components/product-name';
import {

View File

@@ -1,6 +1,6 @@
.wc-block-components-product-badge {
@include font-size(smaller);
border-radius: 2px;
border-radius: $universal-border-radius;
border: 1px solid;
display: inline-block;
font-weight: 600;

View File

@@ -3,7 +3,7 @@
*/
import { createInterpolateElement } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
import { FormattedMonetaryAmount } from '@woocommerce/blocks-components';
import type { Currency } from '@woocommerce/price-format';
/**

View File

@@ -5,14 +5,10 @@ import classNames from 'classnames';
import { _n, sprintf } from '@wordpress/i18n';
import { decodeEntities } from '@wordpress/html-entities';
import { Panel } from '@woocommerce/blocks-checkout';
import Label from '@woocommerce/base-components/label';
import { Label } from '@woocommerce/blocks-components';
import { useCallback } from '@wordpress/element';
import {
useShippingData,
useStoreEvents,
} from '@woocommerce/base-context/hooks';
import { useShippingData } from '@woocommerce/base-context/hooks';
import { sanitizeHTML } from '@woocommerce/utils';
import { useDebouncedCallback } from 'use-debounce';
import type { ReactElement } from 'react';
/**
@@ -31,8 +27,7 @@ export const ShippingRatesControlPackage = ( {
collapsible,
showItems,
}: PackageProps ): ReactElement => {
const { selectShippingRate } = useShippingData();
const { dispatchCheckoutEvent } = useStoreEvents();
const { selectShippingRate, isSelectingRate } = useShippingData();
const multiplePackages =
document.querySelectorAll(
'.wc-block-components-shipping-rates-control__package'
@@ -95,28 +90,32 @@ export const ShippingRatesControlPackage = ( {
const onSelectRate = useCallback(
( newShippingRateId: string ) => {
selectShippingRate( newShippingRateId, packageId );
dispatchCheckoutEvent( 'set-selected-shipping-rate', {
shippingRateId: newShippingRateId,
} );
},
[ dispatchCheckoutEvent, packageId, selectShippingRate ]
[ packageId, selectShippingRate ]
);
const debouncedOnSelectRate = useDebouncedCallback( onSelectRate, 1000 );
const packageRatesProps = {
className,
noResultsMessage,
rates: packageData.shipping_rates,
onSelectRate: debouncedOnSelectRate,
onSelectRate,
selectedRate: packageData.shipping_rates.find(
( rate ) => rate.selected
),
renderOption,
disabled: isSelectingRate,
};
if ( shouldBeCollapsible ) {
return (
<Panel
className="wc-block-components-shipping-rates-control__package"
className={ classNames(
'wc-block-components-shipping-rates-control__package',
className,
{
'wc-block-components-shipping-rates-control__package--disabled':
isSelectingRate,
}
) }
// 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
@@ -133,7 +132,11 @@ export const ShippingRatesControlPackage = ( {
<div
className={ classNames(
'wc-block-components-shipping-rates-control__package',
className
className,
{
'wc-block-components-shipping-rates-control__package--disabled':
isSelectingRate,
}
) }
>
{ header }

View File

@@ -2,10 +2,12 @@
* External dependencies
*/
import { useState, useEffect } from '@wordpress/element';
import RadioControl, {
import {
RadioControl,
RadioControlOptionLayout,
} from '@woocommerce/base-components/radio-control';
} from '@woocommerce/blocks-components';
import type { CartShippingPackageShippingRate } from '@woocommerce/types';
import { usePrevious } from '@woocommerce/base-hooks';
/**
* Internal dependencies
@@ -20,6 +22,7 @@ interface PackageRates {
className?: string;
noResultsMessage: JSX.Element;
selectedRate: CartShippingPackageShippingRate | undefined;
disabled?: boolean;
}
const PackageRates = ( {
@@ -29,34 +32,37 @@ 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( selectedRateId );
// Update the selected option if cart state changes in the data stores.
useEffect( () => {
const [ selectedOption, setSelectedOption ] = useState( () => {
if ( selectedRateId ) {
return selectedRateId;
}
// Default to first rate if no rate is selected.
return rates[ 0 ]?.rate_id;
} );
// Update the selected option if cart state changes in the data store.
useEffect( () => {
if (
selectedRateId &&
selectedRateId !== previousSelectedRateId &&
selectedRateId !== selectedOption
) {
setSelectedOption( selectedRateId );
}
}, [ selectedRateId ] );
}, [ selectedRateId, selectedOption, previousSelectedRateId ] );
// Update the selected option if there is no rate selected on mount.
// Update the data store when the local selected rate changes.
useEffect( () => {
// 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 );
if ( selectedOption ) {
onSelectRate( selectedOption );
}
}, [ onSelectRate, rates, selectedOption ] );
}, [ onSelectRate, selectedOption ] );
if ( rates.length === 0 ) {
return noResultsMessage;
@@ -70,6 +76,7 @@ const PackageRates = ( {
setSelectedOption( value );
onSelectRate( value );
} }
disabled={ disabled }
selected={ selectedOption }
options={ rates.map( renderOption ) }
/>

View File

@@ -3,7 +3,7 @@
*/
import { decodeEntities } from '@wordpress/html-entities';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
import { FormattedMonetaryAmount } from '@woocommerce/blocks-components';
import type { PackageRateOption } from '@woocommerce/types';
import { getSetting } from '@woocommerce/settings';
import { CartShippingPackageShippingRate } from '@woocommerce/types';

View File

@@ -25,10 +25,6 @@
padding-bottom: em($gap-small);
}
.wc-block-components-radio-control {
margin-bottom: em($gap-small);
}
.wc-block-components-radio-control,
.wc-block-components-radio-control__option-layout {
padding-bottom: 0;
@@ -44,6 +40,11 @@
.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

@@ -3,7 +3,7 @@
*/
import { __, sprintf } from '@wordpress/i18n';
import LoadingMask from '@woocommerce/base-components/loading-mask';
import { RemovableChip } from '@woocommerce/base-components/chip';
import { RemovableChip } from '@woocommerce/blocks-components';
import { applyCheckoutFilter, TotalsItem } from '@woocommerce/blocks-checkout';
import { getSetting } from '@woocommerce/settings';
import {

View File

@@ -4,7 +4,7 @@
import { __, sprintf } from '@wordpress/i18n';
import classNames from 'classnames';
import { createInterpolateElement } from '@wordpress/element';
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
import { FormattedMonetaryAmount } from '@woocommerce/blocks-components';
import { applyCheckoutFilter, TotalsItem } from '@woocommerce/blocks-checkout';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import { getSetting } from '@woocommerce/settings';

View File

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

View File

@@ -6,6 +6,7 @@
.components-base-control__field {
@include reset-box();
position: relative;
}
.components-combobox-control__suggestions-container {
@include reset-typography();
@@ -15,7 +16,8 @@
input.components-combobox-control__input {
@include reset-typography();
@include font-size(regular);
padding: em($gap + $gap-smaller) em($gap-smaller) em($gap-smaller);
line-height: em($gap);
box-sizing: border-box;
outline: inherit;
border: 1px solid $input-border-gray;
@@ -24,17 +26,14 @@
color: $input-text-active;
font-family: inherit;
font-weight: normal;
height: 3em;
letter-spacing: inherit;
line-height: 1;
padding: em($gap-large) $gap em($gap-smallest);
text-align: left;
text-overflow: ellipsis;
text-transform: none;
white-space: nowrap;
width: 100%;
opacity: initial;
border-radius: 4px;
border-radius: $universal-border-radius;
&[aria-expanded="true"],
&:focus {
@@ -67,12 +66,14 @@
background-color: $select-dropdown-light;
border: 1px solid $input-border-gray;
border-top: 0;
margin: 3em 0 0 0;
margin: 3em 0 0 -1px;
padding: 0;
max-height: 300px;
min-width: 100%;
overflow: auto;
color: $input-text-active;
border-bottom-left-radius: $universal-border-radius;
border-bottom-right-radius: $universal-border-radius;
.has-dark-controls & {
background-color: $select-dropdown-dark;
@@ -108,14 +109,16 @@
label.components-base-control__label {
@include reset-typography();
@include font-size(regular);
line-height: 1.375; // =22px when font-size is 16px.
position: absolute;
transform: translateY(0.75em);
transform: translateY(em($gap));
line-height: 1.25; // =20px when font-size is 16px.
left: em($gap-smaller);
top: 0;
transform-origin: top left;
transition: all 200ms ease;
color: $gray-700;
color: $universal-body-low-emphasis;
z-index: 1;
margin: 0 0 0 #{$gap + 1px};
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
max-width: calc(100% - #{2 * $gap});
@@ -130,10 +133,16 @@
}
}
.wc-block-components-combobox-control:has(input:-webkit-autofill) {
label {
transform: translateY(em($gap-smaller)) scale(0.875);
}
}
&.is-active,
&:focus-within {
.wc-block-components-combobox-control label.components-base-control__label {
transform: translateY(#{$gap-smallest}) scale(0.75);
transform: translateY(em($gap-smaller)) scale(0.875);
}
}

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

@@ -3,5 +3,10 @@
@import "node_modules/wordpress-components/src/combobox-control/style";
.wc-block-components-country-input {
margin-top: em($gap-large);
margin-top: $gap;
// Fixes width in the editor.
.components-flex {
width: 100%;
}
}

View File

@@ -2,7 +2,7 @@
* External dependencies
*/
import { _n, sprintf } from '@wordpress/i18n';
import Label from '@woocommerce/base-components/label';
import { Label } from '@woocommerce/blocks-components';
/**
* Internal dependencies

View File

@@ -3,7 +3,7 @@
*/
import { __ } from '@wordpress/i18n';
import classNames from 'classnames';
import Label from '@woocommerce/base-components/label';
import { Label } from '@woocommerce/blocks-components';
/**
* Internal dependencies

View File

@@ -3,7 +3,7 @@
*/
import { __ } from '@wordpress/i18n';
import classNames from 'classnames';
import Label from '@woocommerce/base-components/label';
import { Label } from '@woocommerce/blocks-components';
/**
* Internal dependencies

View File

@@ -1,8 +1,6 @@
export * from './block-error-boundary';
export * from './button';
export * from './cart-checkout';
export * from './checkbox-list';
export * from './chip';
export * from './combobox';
export * from './country-input';
export * from './drawer';
@@ -12,7 +10,6 @@ export * from './filter-reset-button';
export * from './filter-submit-button';
export * from './form';
export * from './form-token-field';
export * from './formatted-monetary-amount';
export * from './label';
export * from './load-more-button';
export * from './loading-mask';
@@ -25,14 +22,11 @@ export * from './product-name';
export * from './product-price';
export * from './product-rating';
export * from './quantity-selector';
export * from './radio-control';
export * from './radio-control-accordion';
export * from './read-more';
export * from './reviews';
export * from './sidebar-layout';
export * from './snackbar-list';
export * from './sort-select';
export * from './spinner';
export * from './state-input';
export * from './summary';
export * from './tabs';

View File

@@ -2,13 +2,13 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import Label from '@woocommerce/base-components/label';
import type { MouseEventHandler } from 'react';
/**
* Internal dependencies
*/
import './style.scss';
import Label from '../../../../../packages/components/label'; // Imported like this because importing from the components package loads the data stores unnecessarily - not a problem in the front end but would require a lot of unit test rewrites to prevent breaking tests due to incorrect mocks.
interface LoadMoreButtonProps {
onClick: MouseEventHandler;

View File

@@ -3,12 +3,12 @@
*/
import { __ } from '@wordpress/i18n';
import classNames from 'classnames';
import { Spinner } from '@woocommerce/blocks-components';
/**
* Internal dependencies
*/
import './style.scss';
import Spinner from '../spinner';
interface LoadingMaskProps {
children?: React.ReactNode | React.ReactNode[];

View File

@@ -6,7 +6,7 @@
padding: $gap !important;
gap: $gap-small;
margin: $gap 0;
border-radius: 4px;
border-radius: $universal-border-radius;
border-color: $gray-800;
font-weight: 400;
line-height: 1.5;
@@ -43,7 +43,7 @@
}
// Legacy notice compatibility.
.wc-forward.wp-element-button {
.wc-forward {
float: right;
color: $gray-800 !important;
background: transparent;
@@ -52,6 +52,8 @@
border: 0;
appearance: none;
opacity: 0.6;
text-decoration-line: underline;
text-underline-position: under;
&:hover,
&:focus,

View File

@@ -3,7 +3,7 @@
*/
import { __, sprintf } from '@wordpress/i18n';
import classNames from 'classnames';
import Label from '@woocommerce/base-components/label';
import { Label } from '@woocommerce/blocks-components';
/**
* Internal dependencies

View File

@@ -11,7 +11,7 @@ import {
useLayoutEffect,
} from '@wordpress/element';
import classnames from 'classnames';
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
import { FormattedMonetaryAmount } from '@woocommerce/blocks-components';
import { Currency, isObject } from '@woocommerce/types';
import { useDebouncedCallback } from 'use-debounce';

View File

@@ -169,7 +169,6 @@
width: 100%;
height: 0;
display: block;
position: relative;
pointer-events: none;
outline: none !important;
position: absolute;
@@ -332,11 +331,10 @@
}
.wc-block-components-price-slider__range-input-progress {
--range-color: currentColor;
margin: -$border-width;
}
.wc-block-price-filter__range-input {
background: transparent;
margin: -$border-width;
height: 0;
width: calc(100% + #{$border-width * 2});
&:hover,
&:focus {
@@ -351,13 +349,24 @@
}
}
&::-webkit-slider-thumb {
margin-top: -9px;
background: $white;
margin-top: -6px;
width: 12px;
height: 12px;
}
&.wc-block-components-price-slider__range-input--max::-moz-range-thumb {
transform: translate(2px, 1px);
background: $white;
transform: translate(2px, 2px);
width: 12px;
height: 12px;
box-sizing: content-box;
}
&.wc-block-components-price-slider__range-input--min::-moz-range-thumb {
transform: translate(-2px, 1px);
background: $white;
transform: translate(-2px, 2px);
width: 12px;
height: 12px;
box-sizing: content-box;
}
&::-ms-track {
border-color: transparent !important;
@@ -366,7 +375,6 @@
@include ie11() {
.wc-block-components-price-slider__range-input-wrapper {
border: 0;
height: auto;
position: relative;
height: 50px;
}

View File

@@ -2,7 +2,7 @@
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
import { FormattedMonetaryAmount } from '@woocommerce/blocks-components';
import classNames from 'classnames';
import { formatPrice } from '@woocommerce/price-format';
import { createInterpolateElement } from '@wordpress/element';

View File

@@ -1,6 +1,11 @@
$line-height: 1.618;
.wc-block-components-product-rating {
display: block;
line-height: 1;
span {
line-height: $line-height;
}
&__stars {
display: inline-block;
@@ -8,7 +13,7 @@
position: relative;
width: 5.3em;
height: 1.618em;
line-height: 1.618;
line-height: $line-height;
font-size: 1em;
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
font-family: star;
@@ -20,6 +25,7 @@
&::before {
content: "\53\53\53\53\53";
line-height: $line-height;
top: 0;
left: 0;
right: 0;
@@ -44,6 +50,7 @@
right: 0;
position: absolute;
color: inherit;
line-height: $line-height;
white-space: nowrap;
}
}
@@ -54,9 +61,13 @@
}
&__container {
display: flex;
align-items: center;
column-gap: $gap-smaller;
> * {
vertical-align: middle;
}
}
&__stars + &__reviews_count {
margin-left: $gap-smaller;
}
&__norating-container {
@@ -72,7 +83,7 @@
position: relative;
width: 1.5em;
height: 1.618em;
line-height: 1.618;
line-height: $line-height;
font-size: 1em;
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
font-family: star;

View File

@@ -12,7 +12,7 @@
}
.wc-block-components-quantity-selector {
border-radius: 4px;
border-radius: $universal-border-radius;
// needed so that buttons fill the container.
box-sizing: content-box;
display: flex;
@@ -21,7 +21,7 @@
width: 107px;
&::after {
border-radius: 4px;
border-radius: $universal-border-radius;
border: 1px solid currentColor;
bottom: 0;
content: "";
@@ -89,12 +89,12 @@
}
> .wc-block-components-quantity-selector__button--minus {
border-radius: 4px 0 0 4px;
border-radius: $universal-border-radius 0 0 $universal-border-radius;
order: 1;
}
> .wc-block-components-quantity-selector__button--plus {
border-radius: 0 4px 4px 0;
border-radius: 0 $universal-border-radius $universal-border-radius 0;
order: 3;
}
}

View File

@@ -0,0 +1,157 @@
// Copy-pasted from https://github.com/brankosekulic/trimHtml/blob/master/index.js
// the published npm version of this code contains a bug that causes it throw exceptions.
export function trimHtml( html, options ) {
options = options || {};
const limit = options.limit || 100,
preserveTags =
typeof options.preserveTags !== 'undefined'
? options.preserveTags
: true,
wordBreak =
typeof options.wordBreak !== 'undefined'
? options.wordBreak
: false,
suffix = options.suffix || '...',
moreLink = options.moreLink || '',
moreText = options.moreText || '»',
preserveWhiteSpace = options.preserveWhiteSpace || false;
const arr = html
.replace( /</g, '\n<' )
.replace( />/g, '>\n' )
.replace( /\n\n/g, '\n' )
.replace( /^\n/g, '' )
.replace( /\n$/g, '' )
.split( '\n' );
let sum = 0,
row,
cut,
add,
rowCut,
tagMatch,
tagName,
// eslint-disable-next-line prefer-const
tagStack = [],
more = false;
for ( let i = 0; i < arr.length; i++ ) {
row = arr[ i ];
// count multiple spaces as one character
if ( ! preserveWhiteSpace ) {
rowCut = row.replace( /[ ]+/g, ' ' );
} else {
rowCut = row;
}
if ( ! row.length ) {
continue;
}
const charArr = getCharArr( rowCut );
if ( row[ 0 ] !== '<' ) {
if ( sum >= limit ) {
row = '';
} else if ( sum + charArr.length >= limit ) {
cut = limit - sum;
if ( charArr[ cut - 1 ] === ' ' ) {
while ( cut ) {
cut -= 1;
if ( charArr[ cut - 1 ] !== ' ' ) {
break;
}
}
} else {
add = charArr.slice( cut ).indexOf( ' ' );
// break on halh of word
if ( ! wordBreak ) {
if ( add !== -1 ) {
cut += add;
} else {
cut = row.length;
}
}
}
row = charArr.slice( 0, cut ).join( '' ) + suffix;
if ( moreLink ) {
row +=
'<a href="' +
moreLink +
'" style="display:inline">' +
moreText +
'</a>';
}
sum = limit;
more = true;
} else {
sum += charArr.length;
}
} else if ( ! preserveTags ) {
row = '';
} else if ( sum >= limit ) {
tagMatch = row.match( /[a-zA-Z]+/ );
tagName = tagMatch ? tagMatch[ 0 ] : '';
if ( tagName ) {
if ( row.substring( 0, 2 ) !== '</' ) {
tagStack.push( tagName );
row = '';
} else {
while (
tagStack[ tagStack.length - 1 ] !== tagName &&
tagStack.length
) {
tagStack.pop();
}
if ( tagStack.length ) {
row = '';
}
tagStack.pop();
}
} else {
row = '';
}
}
arr[ i ] = row;
}
return {
html: arr.join( '\n' ).replace( /\n/g, '' ),
more,
};
}
// count symbols like one char
function getCharArr( rowCut ) {
// eslint-disable-next-line prefer-const
let charArr = [],
subRow,
match,
char;
for ( let i = 0; i < rowCut.length; i++ ) {
subRow = rowCut.substring( i );
match = subRow.match( /^&[a-z0-9#]+;/ );
if ( match ) {
char = match[ 0 ];
charArr.push( char );
i += char.length - 1;
} else {
charArr.push( rowCut[ i ] );
}
}
return charArr;
}

View File

@@ -1,8 +1,11 @@
/**
* Internal dependencies
*/
import { trimHtml } from './trim-html';
/**
* External dependencies
*/
import trimHtml from 'trim-html';
type Markers = {
end: number;
middle: number;

View File

@@ -4,7 +4,7 @@
import { __, sprintf } from '@wordpress/i18n';
import classNames from 'classnames';
import ReadMore from '@woocommerce/base-components/read-more';
import type { BlockAttributes } from '@wordpress/blocks';
import { ReviewBlockAttributes } from '@woocommerce/blocks/reviews/attributes';
/**
* Internal dependencies
@@ -154,8 +154,9 @@ function getReviewRating( review: Review ): JSX.Element {
</div>
);
}
interface ReviewListItemProps {
attributes: BlockAttributes;
attributes: ReviewBlockAttributes;
review?: Review;
}

View File

@@ -2,7 +2,7 @@
* External dependencies
*/
import { getSetting } from '@woocommerce/settings';
import type { BlockAttributes } from '@wordpress/blocks';
import { ReviewBlockAttributes } from '@woocommerce/blocks/reviews/attributes';
/**
* Internal dependencies
@@ -10,9 +10,8 @@ import type { BlockAttributes } from '@wordpress/blocks';
import ReviewListItem from '../review-list-item';
import type { Review } from '../types';
import './style.scss';
interface ReviewListProps {
attributes: BlockAttributes;
attributes: ReviewBlockAttributes;
reviews: Review[];
}
@@ -20,8 +19,11 @@ const ReviewList = ( {
attributes,
reviews,
}: ReviewListProps ): JSX.Element => {
const showAvatars = getSetting( 'showAvatars', true );
const reviewRatingsEnabled = getSetting( 'reviewRatingsEnabled', true );
const showAvatars = getSetting< boolean >( 'showAvatars', true );
const reviewRatingsEnabled = getSetting< boolean >(
'reviewRatingsEnabled',
true
);
const showReviewImage =
( showAvatars || attributes.imageType === 'product' ) &&
attributes.showReviewImage;

View File

@@ -5,10 +5,14 @@ import './style.scss';
export interface SkeletonProps {
numberOfLines?: number;
tag?: keyof JSX.IntrinsicElements;
maxWidth?: string;
}
export const Skeleton = ( {
numberOfLines = 1,
tag: Tag = 'div',
maxWidth = '100%',
}: SkeletonProps ): JSX.Element => {
const skeletonLines = Array.from(
{ length: numberOfLines },
@@ -21,6 +25,13 @@ export const Skeleton = ( {
)
);
return (
<div className="wc-block-components-skeleton">{ skeletonLines }</div>
<Tag
className="wc-block-components-skeleton"
style={ {
maxWidth,
} }
>
{ skeletonLines }
</Tag>
);
};

View File

@@ -7,11 +7,11 @@
}
.wc-block-components-skeleton-text-line {
height: 0.8em;
height: 0.85em;
width: 100%;
position: relative;
background: $gray-200;
border-radius: 2em;
background: $universal-border-light;
border-radius: $universal-border-radius;
&:last-child {
width: 80%;

View File

@@ -14,7 +14,6 @@
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

@@ -2,7 +2,6 @@
* External dependencies
*/
import classNames from 'classnames';
import Label from '@woocommerce/base-components/label';
import { withInstanceId } from '@wordpress/compose';
import type { ChangeEventHandler } from 'react';
@@ -10,6 +9,7 @@ import type { ChangeEventHandler } from 'react';
* Internal dependencies
*/
import './style.scss';
import Label from '../../../../../packages/components/label'; // Imported like this because importing from the components package loads the data stores unnecessarily - not a problem in the front end but would require a lot of unit test rewrites to prevent breaking tests due to incorrect mocks.
interface SortSelectProps {
/**

View File

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

View File

@@ -1,3 +1,8 @@
.wc-block-components-state-input {
margin-top: em($gap-large);
margin-top: $gap;
// Fixes width in the editor.
.components-flex {
width: 100%;
}
}

View File

@@ -2,7 +2,7 @@
@include font-size(regular);
background-color: #fff;
border: 1px solid $input-border-gray;
border-radius: 4px;
border-radius: $universal-border-radius;
color: $input-text-active;
font-family: inherit;
line-height: 1.375; // =22px when font-size is 16px.

View File

@@ -147,9 +147,9 @@ describe( 'useStoreCart', () => {
useStoreCart( options );
return (
<div
results={ results }
receiveCart={ receiveCart }
receiveCartContents={ receiveCartContents }
data-results={ results }
data-receiveCart={ receiveCart }
data-receiveCartContents={ receiveCartContents }
/>
);
};
@@ -200,8 +200,11 @@ describe( 'useStoreCart', () => {
);
} );
const { results, receiveCart, receiveCartContents } =
renderer.root.findByType( 'div' ).props; //eslint-disable-line testing-library/await-async-query
const props = renderer.root.findByType( 'div' ).props; //eslint-disable-line testing-library/await-async-query
const results = props[ 'data-results' ];
const receiveCart = props[ 'data-receiveCart' ];
const receiveCartContents = props[ 'data-receiveCartContents' ];
const {
receiveCart: defaultReceiveCart,
receiveCartContents: defaultReceiveCartContents,
@@ -223,8 +226,10 @@ describe( 'useStoreCart', () => {
);
} );
const { results, receiveCart, receiveCartContents } =
renderer.root.findByType( 'div' ).props; //eslint-disable-line testing-library/await-async-query
const props = renderer.root.findByType( 'div' ).props; //eslint-disable-line testing-library/await-async-query
const results = props[ 'data-results' ];
const receiveCart = props[ 'data-receiveCart' ];
const receiveCartContents = props[ 'data-receiveCartContents' ];
expect( results ).toEqual( mockStoreCartData );
expect( receiveCart ).toBeUndefined();
@@ -255,8 +260,10 @@ describe( 'useStoreCart', () => {
);
} );
const { results, receiveCart, receiveCartContents } =
renderer.root.findByType( 'div' ).props; //eslint-disable-line testing-library/await-async-query
const props = renderer.root.findByType( 'div' ).props; //eslint-disable-line testing-library/await-async-query
const results = props[ 'data-results' ];
const receiveCart = props[ 'data-receiveCart' ];
const receiveCartContents = props[ 'data-receiveCartContents' ];
expect( results ).toEqual( previewCartData );
expect( receiveCart ).toEqual( receiveCartMock );

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