Merged in release/release-1.09 (pull request #10)
Release/release 1.09 * Install missing plugins * rs set to 1 * rebase pantheon for aws * rebase pantheon for aws * prod config change * prod config change * fix campaing issue * revert Approved-by: Jay Sharma
This commit is contained in:
committed by
Jay Sharma
parent
779393381f
commit
22f10a9edd
@@ -5,9 +5,9 @@
|
||||
* Description: A robust scheduling library for use in WordPress plugins.
|
||||
* Author: Automattic
|
||||
* Author URI: https://automattic.com/
|
||||
* Version: 3.7.0
|
||||
* Version: 3.6.4
|
||||
* License: GPLv3
|
||||
* Tested up to: 6.4
|
||||
* Tested up to: 6.3
|
||||
* Requires at least: 5.2
|
||||
* Requires PHP: 5.6
|
||||
*
|
||||
@@ -29,27 +29,27 @@
|
||||
* @package ActionScheduler
|
||||
*/
|
||||
|
||||
if ( ! function_exists( 'action_scheduler_register_3_dot_7_dot_0' ) && 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_7_dot_0', 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_7_dot_0() { // WRCS: DEFINED_VERSION.
|
||||
function action_scheduler_register_3_dot_6_dot_4() { // WRCS: DEFINED_VERSION.
|
||||
$versions = ActionScheduler_Versions::instance();
|
||||
$versions->register( '3.7.0', 'action_scheduler_initialize_3_dot_7_dot_0' ); // 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_7_dot_0() { // 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).
|
||||
@@ -61,7 +61,7 @@ if ( ! function_exists( 'action_scheduler_register_3_dot_7_dot_0' ) && 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_7_dot_0(); // 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();
|
||||
}
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
*** Changelog ***
|
||||
|
||||
= 3.7.0 - 2023-11-20 =
|
||||
* Add extended indexes for hook_status_scheduled_date_gmt and status_sheduled_date_gmt.
|
||||
* Catch and log exceptions thrown when actions can't be created, e.g. under a corrupt database schema.
|
||||
* Release/3.6.4.
|
||||
* Tweak - WP 6.4 compatibility.
|
||||
* Update unit tests for upcoming dependency version policy.
|
||||
* make sure hook action_scheduler_failed_execution can access original exception object.
|
||||
* mention dependency version policy in usage.md.
|
||||
|
||||
= 3.6.4 - 2023-10-11 =
|
||||
* Performance improvements when bulk cancelling actions.
|
||||
* Dev-related fixes.
|
||||
* 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.
|
||||
|
||||
@@ -13,7 +13,6 @@ class ActionScheduler_ActionFactory {
|
||||
* @param array $args Args to pass to callbacks when the hook is triggered.
|
||||
* @param ActionScheduler_Schedule $schedule The action's schedule.
|
||||
* @param string $group A group to put the action in.
|
||||
* phpcs:ignore Squiz.Commenting.FunctionComment.ExtraParamComment
|
||||
* @param int $priority The action priority.
|
||||
*
|
||||
* @return ActionScheduler_Action An instance of the stored action.
|
||||
@@ -247,6 +246,7 @@ class ActionScheduler_ActionFactory {
|
||||
* async_unique(), single() or single_unique(), etc.
|
||||
*
|
||||
* @internal Not intended for public use, should not be overriden by subclasses.
|
||||
* @throws Exception May be thrown if invalid options are passed.
|
||||
*
|
||||
* @param array $options {
|
||||
* Describes the action we wish to schedule.
|
||||
@@ -263,7 +263,7 @@ class ActionScheduler_ActionFactory {
|
||||
* @type int $priority Lower values means higher priority. Should be in the range 0-255.
|
||||
* }
|
||||
*
|
||||
* @return int The action ID. Zero if there was an error scheduling the action.
|
||||
* @return int
|
||||
*/
|
||||
public function create( array $options = array() ) {
|
||||
$defaults = array(
|
||||
@@ -307,27 +307,12 @@ class ActionScheduler_ActionFactory {
|
||||
break;
|
||||
|
||||
default:
|
||||
error_log( "Unknown action type '{$options['type']}' specified when trying to create an action for '{$options['hook']}'." );
|
||||
return 0;
|
||||
throw new Exception( "Unknown action type '{$options['type']}' specified when trying to create an action for '{$options['hook']}'." );
|
||||
}
|
||||
|
||||
$action = new ActionScheduler_Action( $options['hook'], $options['arguments'], $schedule, $options['group'] );
|
||||
$action->set_priority( $options['priority'] );
|
||||
|
||||
$action_id = 0;
|
||||
try {
|
||||
$action_id = $options['unique'] ? $this->store_unique_action( $action ) : $this->store( $action );
|
||||
} catch ( Exception $e ) {
|
||||
error_log(
|
||||
sprintf(
|
||||
/* translators: %1$s is the name of the hook to be enqueued, %2$s is the exception message. */
|
||||
__( 'Caught exception while enqueuing action "%1$s": %2$s', 'woocommerce' ),
|
||||
$options['hook'],
|
||||
$e->getMessage()
|
||||
)
|
||||
);
|
||||
}
|
||||
return $action_id;
|
||||
return $options['unique'] ? $this->store_unique_action( $action ) : $this->store( $action );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -91,7 +91,7 @@ abstract class ActionScheduler_Abstract_QueueRunner extends ActionScheduler_Abst
|
||||
} catch ( Throwable $e ) {
|
||||
// Throwable is defined when executing under PHP 7.0 and up. We convert it to an exception, for
|
||||
// compatibility with ActionScheduler_Logger.
|
||||
throw new Exception( $e->getMessage(), $e->getCode(), $e );
|
||||
throw new Exception( $e->getMessage(), $e->getCode(), $e->getPrevious() );
|
||||
}
|
||||
} catch ( Exception $e ) {
|
||||
// This catch block exists for compatibility with PHP 5.6.
|
||||
|
||||
@@ -82,7 +82,7 @@ class ActionScheduler_DBLogger extends ActionScheduler_Logger {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an action's log entries from the database.
|
||||
* Retrieve the an action's log entries from the database.
|
||||
*
|
||||
* @param int $action_id Action ID.
|
||||
*
|
||||
|
||||
@@ -38,7 +38,6 @@ class ActionScheduler_StoreSchema extends ActionScheduler_Abstract_Schema {
|
||||
$table_name = $wpdb->$table;
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
$max_index_length = 191; // @see wp_get_db_schema()
|
||||
$hook_status_scheduled_date_gmt_max_index_length = $max_index_length - 20 - 8; // - status, - scheduled_date_gmt
|
||||
$default_date = self::DEFAULT_DATE;
|
||||
switch ( $table ) {
|
||||
|
||||
@@ -60,8 +59,8 @@ class ActionScheduler_StoreSchema extends ActionScheduler_Abstract_Schema {
|
||||
claim_id bigint(20) unsigned NOT NULL default '0',
|
||||
extended_args varchar(8000) DEFAULT NULL,
|
||||
PRIMARY KEY (action_id),
|
||||
KEY hook_status_scheduled_date_gmt (hook($hook_status_scheduled_date_gmt_max_index_length), status, scheduled_date_gmt),
|
||||
KEY status_scheduled_date_gmt (status, scheduled_date_gmt),
|
||||
KEY hook (hook($max_index_length)),
|
||||
KEY status (status),
|
||||
KEY scheduled_date_gmt (scheduled_date_gmt),
|
||||
KEY args (args($max_index_length)),
|
||||
KEY group_id (group_id),
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* @param bool $unique Whether the action should be unique.
|
||||
* @param int $priority Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255.
|
||||
*
|
||||
* @return int The action ID. Zero if there was an error scheduling the action.
|
||||
* @return int The action ID.
|
||||
*/
|
||||
function as_enqueue_async_action( $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) {
|
||||
if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) {
|
||||
@@ -63,7 +63,7 @@ function as_enqueue_async_action( $hook, $args = array(), $group = '', $unique =
|
||||
* @param bool $unique Whether the action should be unique.
|
||||
* @param int $priority Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255.
|
||||
*
|
||||
* @return int The action ID. Zero if there was an error scheduling the action.
|
||||
* @return int The action ID.
|
||||
*/
|
||||
function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) {
|
||||
if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) {
|
||||
@@ -115,7 +115,7 @@ function as_schedule_single_action( $timestamp, $hook, $args = array(), $group =
|
||||
* @param bool $unique Whether the action should be unique.
|
||||
* @param int $priority Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255.
|
||||
*
|
||||
* @return int The action ID. Zero if there was an error scheduling the action.
|
||||
* @return int The action ID.
|
||||
*/
|
||||
function as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) {
|
||||
if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) {
|
||||
@@ -200,7 +200,7 @@ function as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook,
|
||||
* @param bool $unique Whether the action should be unique.
|
||||
* @param int $priority Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255.
|
||||
*
|
||||
* @return int The action ID. Zero if there was an error scheduling the action.
|
||||
* @return int The action ID.
|
||||
*/
|
||||
function as_schedule_cron_action( $timestamp, $schedule, $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) {
|
||||
if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) {
|
||||
@@ -283,10 +283,9 @@ function as_unschedule_action( $hook, $args = array(), $group = '' ) {
|
||||
ActionScheduler::logger()->log(
|
||||
$action_id,
|
||||
sprintf(
|
||||
/* translators: %1$s is the name of the hook to be cancelled, %2$s is the exception message. */
|
||||
__( 'Caught exception while cancelling action "%1$s": %2$s', 'woocommerce' ),
|
||||
$hook,
|
||||
$exception->getMessage()
|
||||
/* translators: %s is the name of the hook to be cancelled. */
|
||||
__( 'Caught exception while cancelling action: %s', 'woocommerce' ),
|
||||
esc_attr( $hook )
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
=== Action Scheduler ===
|
||||
Contributors: Automattic, wpmuguru, claudiosanches, peterfabian1000, vedjain, jamosova, obliviousharmony, konamiman, sadowski, royho, barryhughes-1
|
||||
Tags: scheduler, cron
|
||||
Stable tag: 3.7.0
|
||||
Stable tag: 3.6.4
|
||||
License: GPLv3
|
||||
Tested up to: 6.4
|
||||
Tested up to: 6.3
|
||||
|
||||
Action Scheduler - Job Queue for WordPress
|
||||
|
||||
@@ -45,18 +45,12 @@ Collaboration is cool. We'd love to work with you to improve Action Scheduler. [
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 3.7.0 - 2023-11-20 =
|
||||
* Add extended indexes for hook_status_scheduled_date_gmt and status_sheduled_date_gmt.
|
||||
* Catch and log exceptions thrown when actions can't be created, e.g. under a corrupt database schema.
|
||||
* Release/3.6.4.
|
||||
* Tweak - WP 6.4 compatibility.
|
||||
* Update unit tests for upcoming dependency version policy.
|
||||
* make sure hook action_scheduler_failed_execution can access original exception object.
|
||||
* mention dependency version policy in usage.md.
|
||||
|
||||
= 3.6.4 - 2023-10-11 =
|
||||
* Performance improvements when bulk cancelling actions.
|
||||
* Dev-related fixes.
|
||||
* 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.
|
||||
|
||||
@@ -171,22 +171,18 @@ $fontSizes: (
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@mixin hover-effect() {
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset <button> style so we can use link style for action buttons in filter blocks
|
||||
@mixin filter-link-button() {
|
||||
@include link-button();
|
||||
@include hover-effect();
|
||||
@include font-size(small);
|
||||
text-decoration: underline;
|
||||
font-weight: normal;
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
// Makes sure long words are broken if they overflow the container.
|
||||
|
||||
@@ -54,7 +54,8 @@
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
// Set button font size so it inherits from parent.
|
||||
// Set button font size and padding so it inherits from parent.
|
||||
padding: 0.5em 1em;
|
||||
font-size: 1em;
|
||||
|
||||
&.loading {
|
||||
@@ -160,13 +161,6 @@
|
||||
|
||||
// Element spacing.
|
||||
.wc-block-grid__product {
|
||||
// Prevent link and image taking the full width unnecessarily, which might cause: https://github.com/woocommerce/woocommerce-blocks/issues/11438
|
||||
.wc-block-grid__product-link,
|
||||
.wc-block-grid__product-image {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Not operator necessary for avoid this problem: https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5925/files#r814043454
|
||||
.wc-block-grid__product-image:not(.wc-block-components-product-image),
|
||||
.wc-block-grid__product-title {
|
||||
@@ -305,6 +299,7 @@
|
||||
|
||||
.wc-block-grid__product-add-to-cart.wp-block-button .wp-block-button__link {
|
||||
@include font-size(smaller);
|
||||
padding: em($gap-smaller);
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 768px) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Disabled, Tooltip } from '@wordpress/components';
|
||||
import { Button, Disabled, Tooltip } from '@wordpress/components';
|
||||
import { Skeleton } from '@woocommerce/base-components/skeleton';
|
||||
import { BlockEditProps } from '@wordpress/blocks';
|
||||
|
||||
@@ -37,28 +37,31 @@ const Edit = ( props: BlockEditProps< Attributes > ) => {
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<Tooltip
|
||||
text="Customer will see product add-to-cart options in this space, dependent on the product type. "
|
||||
text="Customer will see product add-to-cart options in this space, dependend on the product type. "
|
||||
position="bottom right"
|
||||
>
|
||||
<div className="wc-block-editor-add-to-cart-form-container">
|
||||
<div className="wc-block-editor-container">
|
||||
<Skeleton numberOfLines={ 3 } />
|
||||
<Disabled>
|
||||
<div className="quantity">
|
||||
<input
|
||||
type={ 'number' }
|
||||
value={ '1' }
|
||||
className={ 'input-text qty text' }
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className={ `single_add_to_cart_button button alt wp-element-button` }
|
||||
<input
|
||||
type={ 'number' }
|
||||
value={ '1' }
|
||||
className={
|
||||
'wc-block-editor-add-to-cart-form__quantity'
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
<Button
|
||||
variant={ 'primary' }
|
||||
className={
|
||||
'wc-block-editor-add-to-cart-form__button'
|
||||
}
|
||||
>
|
||||
{ __(
|
||||
'Add to cart',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
</button>
|
||||
</Button>
|
||||
</Disabled>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,4 +1,30 @@
|
||||
.wc-block-editor-add-to-cart-form-container {
|
||||
.wc-block-editor-add-to-cart-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: $default-block-margin;
|
||||
}
|
||||
|
||||
input.wc-block-editor-add-to-cart-form__quantity[type="number"] {
|
||||
max-width: 50px;
|
||||
min-height: 23px;
|
||||
float: left;
|
||||
padding: 6px 6px 6px 12px;
|
||||
margin-right: 10px;
|
||||
font-size: 13px;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
button.components-button.wc-block-add-to-cart-form__button {
|
||||
float: left;
|
||||
padding: 20px 30px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.wc-block-editor-container {
|
||||
cursor: help;
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.wc-block-add-to-cart-form {
|
||||
.wp-block-add-to-cart-form {
|
||||
width: unset;
|
||||
/**
|
||||
* This is a base style for the input text element in WooCommerce that prevents inputs from appearing too small.
|
||||
@@ -9,17 +9,4 @@
|
||||
font-size: var(--wp--preset--font-size--small);
|
||||
padding: 0.9rem 1.1rem;
|
||||
}
|
||||
|
||||
.quantity {
|
||||
display: inline-block;
|
||||
float: none;
|
||||
margin-right: 4px;
|
||||
vertical-align: middle;
|
||||
|
||||
.qty {
|
||||
margin-right: 0.5rem;
|
||||
width: 3.631em;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { SelectControl } from 'wordpress-components';
|
||||
import type { SelectControl as SelectControlType } from '@wordpress/components';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import classnames from 'classnames';
|
||||
import { ValidationInputError } from '@woocommerce/blocks-components';
|
||||
import { ValidationInputError } from '@woocommerce/blocks-checkout';
|
||||
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
// Set button font size so it inherits from parent.
|
||||
// Set button font size and padding so it inherits from parent.
|
||||
padding: 0.5em 1em;
|
||||
font-size: 1em;
|
||||
|
||||
&.loading {
|
||||
@@ -24,7 +25,6 @@
|
||||
font-family: WooCommerce; /* stylelint-disable-line */
|
||||
content: "\e031";
|
||||
animation: spin 2s linear infinite;
|
||||
margin-right: 0;
|
||||
margin-left: 0.5em;
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
@@ -70,9 +70,8 @@
|
||||
justify-content: center;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
width: 150px;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
line-height: inherit;
|
||||
|
||||
span {
|
||||
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
BlockConfiguration,
|
||||
registerBlockType,
|
||||
unregisterBlockType,
|
||||
registerBlockVariation,
|
||||
unregisterBlockVariation,
|
||||
BlockVariation,
|
||||
BlockAttributes,
|
||||
} from '@wordpress/blocks';
|
||||
|
||||
export interface BlockRegistrationStrategy {
|
||||
register(
|
||||
blockNameOrMetadata: string | Partial< BlockConfiguration >,
|
||||
blockSettings: Partial< BlockConfiguration >
|
||||
): boolean;
|
||||
unregister( blockName: string, variationName?: string ): boolean;
|
||||
}
|
||||
|
||||
export class BlockTypeStrategy implements BlockRegistrationStrategy {
|
||||
register(
|
||||
blockNameOrMetadata: string | Partial< BlockConfiguration >,
|
||||
blockSettings: Partial< BlockConfiguration >
|
||||
): boolean {
|
||||
return Boolean(
|
||||
// @ts-expect-error: `registerBlockType` is typed in WordPress core
|
||||
registerBlockType( blockNameOrMetadata, blockSettings )
|
||||
);
|
||||
}
|
||||
|
||||
unregister( blockName: string ): boolean {
|
||||
return Boolean( unregisterBlockType( blockName ) );
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy for BlockVariation
|
||||
export class BlockVariationStrategy implements BlockRegistrationStrategy {
|
||||
register(
|
||||
blockName: string,
|
||||
blockSettings: Partial< BlockConfiguration >
|
||||
): boolean {
|
||||
return Boolean(
|
||||
registerBlockVariation(
|
||||
blockName,
|
||||
blockSettings as BlockVariation< BlockAttributes >
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
unregister( blockName: string, variationName: string ): boolean {
|
||||
return Boolean( unregisterBlockVariation( blockName, variationName ) );
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { getBlockType } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
TemplateChangeDetector,
|
||||
TemplateChangeDetectorObserver,
|
||||
} from './template-change-detector';
|
||||
import {
|
||||
BlockRegistrationStrategy,
|
||||
BlockTypeStrategy,
|
||||
BlockVariationStrategy,
|
||||
} from './block-registration-strategy';
|
||||
import { BLOCKS_WITH_RESTRICTION } from './blocks-with-restriction';
|
||||
|
||||
/**
|
||||
* Manages the registration and unregistration of blocks based on template or page restrictions.
|
||||
*
|
||||
* This class implements the TemplateChangeDetectorObserver interface and is responsible for managing the registration and unregistration of blocks based on the restrictions defined in the BLOCKS_WITH_RESTRICTION constant.
|
||||
*
|
||||
* The class maintains a list of unregistered blocks and uses a block registration strategy to register and unregister blocks as needed. The strategy used depends on whether the block is a variation block or a regular block.
|
||||
*
|
||||
* The `run` method is the main entry point for the class. It is called with a TemplateChangeDetector object and registers and unregisters blocks based on the current template and whether the editor is in post or page mode.
|
||||
*/
|
||||
export class BlockRegistrationManager
|
||||
implements TemplateChangeDetectorObserver
|
||||
{
|
||||
private unregisteredBlocks: string[] = [];
|
||||
private blockRegistrationStrategy: BlockRegistrationStrategy;
|
||||
|
||||
constructor() {
|
||||
this.blockRegistrationStrategy = new BlockTypeStrategy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a block should be registered based on the current template or page.
|
||||
*
|
||||
* This method checks whether a block with restrictions should be registered based on the current template ID and
|
||||
* whether the editor is in post or page mode. It checks whether the current template ID starts with any of the
|
||||
* allowed templates or template parts for the block, and whether the block is available in the post or page editor.
|
||||
*
|
||||
* @param {Object} params - The parameters for the method.
|
||||
* @param {string} params.blockWithRestrictionName - The name of the block with restrictions.
|
||||
* @param {string} params.currentTemplateId - The ID of the current template.
|
||||
* @param {boolean} params.isPostOrPage - Whether the editor is in a post or page.
|
||||
* @return {boolean} True if the block should be registered, false otherwise.
|
||||
*/
|
||||
private shouldBlockBeRegistered( {
|
||||
blockWithRestrictionName,
|
||||
currentTemplateId,
|
||||
isPostOrPage,
|
||||
}: {
|
||||
blockWithRestrictionName: string;
|
||||
currentTemplateId: string;
|
||||
isPostOrPage: boolean;
|
||||
} ) {
|
||||
const {
|
||||
allowedTemplates,
|
||||
allowedTemplateParts,
|
||||
availableInPostOrPageEditor,
|
||||
} = BLOCKS_WITH_RESTRICTION[ blockWithRestrictionName ];
|
||||
const shouldBeAvailableOnTemplate = Object.keys(
|
||||
allowedTemplates
|
||||
).some( ( allowedTemplate ) =>
|
||||
currentTemplateId.startsWith( allowedTemplate )
|
||||
);
|
||||
const shouldBeAvailableOnTemplatePart = Object.keys(
|
||||
allowedTemplateParts
|
||||
).some( ( allowedTemplate ) =>
|
||||
currentTemplateId.startsWith( allowedTemplate )
|
||||
);
|
||||
const shouldBeAvailableOnPostOrPageEditor =
|
||||
isPostOrPage && availableInPostOrPageEditor;
|
||||
|
||||
return (
|
||||
shouldBeAvailableOnTemplate ||
|
||||
shouldBeAvailableOnTemplatePart ||
|
||||
shouldBeAvailableOnPostOrPageEditor
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters blocks before entering a restricted area based on the current template or page/post.
|
||||
*
|
||||
* This method iterates over all blocks with restrictions and unregisters them if they should not be registered
|
||||
* based on the current template ID and whether the editor is in a post or page. It uses a block registration
|
||||
* strategy to unregister the blocks, which depends on whether the block is a variation block or a regular block.
|
||||
*
|
||||
* @param {Object} params - The parameters for the method.
|
||||
* @param {string} params.currentTemplateId - The ID of the current template.
|
||||
* @param {boolean} params.isPostOrPage - Whether the editor is in post or page mode.
|
||||
*/
|
||||
unregisterBlocksBeforeEnteringRestrictedArea( {
|
||||
currentTemplateId,
|
||||
isPostOrPage,
|
||||
}: {
|
||||
currentTemplateId: string;
|
||||
isPostOrPage: boolean;
|
||||
} ) {
|
||||
for ( const blockWithRestrictionName of Object.keys(
|
||||
BLOCKS_WITH_RESTRICTION
|
||||
) ) {
|
||||
if (
|
||||
this.shouldBlockBeRegistered( {
|
||||
blockWithRestrictionName,
|
||||
currentTemplateId,
|
||||
isPostOrPage,
|
||||
} )
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! getBlockType( blockWithRestrictionName ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.blockRegistrationStrategy = BLOCKS_WITH_RESTRICTION[
|
||||
blockWithRestrictionName
|
||||
].isVariationBlock
|
||||
? new BlockVariationStrategy()
|
||||
: new BlockTypeStrategy();
|
||||
|
||||
this.blockRegistrationStrategy.unregister(
|
||||
blockWithRestrictionName
|
||||
);
|
||||
this.unregisteredBlocks.push( blockWithRestrictionName );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers blocks after leaving a restricted area.
|
||||
*
|
||||
* This method iterates over all unregistered blocks and registers them if they are not restricted in the current context.
|
||||
* It uses a block registration strategy to register the blocks, which depends on whether the block is a variation block or a regular block.
|
||||
* If the block is successfully registered, it is removed from the list of unregistered blocks.
|
||||
*/
|
||||
registerBlocksAfterLeavingRestrictedArea() {
|
||||
for ( const unregisteredBlockName of this.unregisteredBlocks ) {
|
||||
if ( ! getBlockType( unregisteredBlockName ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const restrictedBlockData =
|
||||
BLOCKS_WITH_RESTRICTION[ unregisteredBlockName ];
|
||||
this.blockRegistrationStrategy = BLOCKS_WITH_RESTRICTION[
|
||||
unregisteredBlockName
|
||||
].isVariationBlock
|
||||
? new BlockVariationStrategy()
|
||||
: new BlockTypeStrategy();
|
||||
const isBlockRegistered = this.blockRegistrationStrategy.register(
|
||||
restrictedBlockData.blockMetadata,
|
||||
restrictedBlockData.blockSettings
|
||||
);
|
||||
this.unregisteredBlocks = isBlockRegistered
|
||||
? this.unregisteredBlocks.filter(
|
||||
( blockName ) => blockName !== unregisteredBlockName
|
||||
)
|
||||
: this.unregisteredBlocks;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the block registration manager.
|
||||
*
|
||||
* This method is the main entry point for the block registration manager. It is called with a TemplateChangeDetector object,
|
||||
* and registers and unregisters blocks based on the current template and whether the editor is in a post or page.
|
||||
*
|
||||
* @param {TemplateChangeDetector} templateChangeDetector - The template change detector object.
|
||||
*/
|
||||
run( templateChangeDetector: TemplateChangeDetector ) {
|
||||
this.registerBlocksAfterLeavingRestrictedArea();
|
||||
this.unregisterBlocksBeforeEnteringRestrictedArea( {
|
||||
currentTemplateId:
|
||||
templateChangeDetector.getCurrentTemplateId() || '',
|
||||
isPostOrPage: templateChangeDetector.getIsPostOrPage(),
|
||||
} );
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { BlockConfiguration } from '@wordpress/blocks';
|
||||
import { ProductGalleryBlockSettings } from '@woocommerce/blocks/product-gallery/settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import productGalleryBlockMetadata from '../../../blocks/product-gallery/block.json';
|
||||
|
||||
export interface BlocksWithRestriction {
|
||||
[ key: string ]: {
|
||||
blockMetadata: Partial< BlockConfiguration >;
|
||||
blockSettings: Partial< BlockConfiguration >;
|
||||
allowedTemplates: {
|
||||
[ key: string ]: boolean;
|
||||
};
|
||||
allowedTemplateParts: {
|
||||
[ key: string ]: boolean;
|
||||
};
|
||||
availableInPostOrPageEditor: boolean;
|
||||
isVariationBlock: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error: `metadata` currently does not have a type definition in WordPress core
|
||||
export const BLOCKS_WITH_RESTRICTION: BlocksWithRestriction = {
|
||||
[ productGalleryBlockMetadata.name ]: {
|
||||
blockMetadata: productGalleryBlockMetadata,
|
||||
blockSettings: ProductGalleryBlockSettings,
|
||||
allowedTemplates: {
|
||||
'single-product': true,
|
||||
},
|
||||
allowedTemplateParts: {
|
||||
'product-gallery': true,
|
||||
},
|
||||
availableInPostOrPageEditor: false,
|
||||
isVariationBlock: false,
|
||||
},
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import domReady from '@wordpress/dom-ready';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { BlockRegistrationManager } from './blocks-registration-manager';
|
||||
import { TemplateChangeDetector } from './template-change-detector';
|
||||
|
||||
domReady( () => {
|
||||
const templateChangeDetector = new TemplateChangeDetector();
|
||||
const blockRegistrationManager = new BlockRegistrationManager();
|
||||
templateChangeDetector.add( blockRegistrationManager );
|
||||
} );
|
||||
@@ -1,115 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { subscribe, select } from '@wordpress/data';
|
||||
import { isNumber } from '@woocommerce/types';
|
||||
|
||||
interface TemplateChangeDetectorSubject {
|
||||
add( observer: TemplateChangeDetectorObserver ): void;
|
||||
getPreviousTemplateId(): string | undefined;
|
||||
getCurrentTemplateId(): string | undefined;
|
||||
notify(): void;
|
||||
}
|
||||
|
||||
export interface TemplateChangeDetectorObserver {
|
||||
run( subject: TemplateChangeDetectorSubject ): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class implements the TemplateChangeDetectorSubject interface and is responsible for detecting changes in the
|
||||
* current template or page and notifying any observers of these changes. It maintains a list of observers and provides methods
|
||||
* to add observers and notify them of changes.
|
||||
*
|
||||
* The class also provides methods to get the previous and current template IDs and whether the editor is in a post or page.
|
||||
*
|
||||
* The `checkIfTemplateHasChangedAndNotifySubscribers` method is the main method of the class. It checks if the current
|
||||
* template has changed and, if so, notifies all observers.
|
||||
*/
|
||||
export class TemplateChangeDetector implements TemplateChangeDetectorSubject {
|
||||
private previousTemplateId: string | undefined;
|
||||
private currentTemplateId: string | undefined;
|
||||
private isPostOrPage: boolean;
|
||||
|
||||
private observers: TemplateChangeDetectorObserver[] = [];
|
||||
|
||||
constructor() {
|
||||
this.isPostOrPage = false;
|
||||
subscribe( () => {
|
||||
this.checkIfTemplateHasChangedAndNotifySubscribers();
|
||||
}, 'core/edit-site' );
|
||||
}
|
||||
|
||||
public add( observer: TemplateChangeDetectorObserver ): void {
|
||||
this.observers.push( observer );
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger an update in each subscriber.
|
||||
*/
|
||||
public notify(): void {
|
||||
for ( const observer of this.observers ) {
|
||||
observer.run( this );
|
||||
}
|
||||
}
|
||||
|
||||
public getPreviousTemplateId() {
|
||||
return this.previousTemplateId;
|
||||
}
|
||||
|
||||
public getCurrentTemplateId() {
|
||||
return this.currentTemplateId;
|
||||
}
|
||||
|
||||
public getIsPostOrPage() {
|
||||
return this.isPostOrPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the template ID.
|
||||
*
|
||||
* This method takes a template or a post ID and returns it parsed in the expected format.
|
||||
*
|
||||
* @param {string | number | undefined} templateId - The template ID to parse.
|
||||
* @return {string | undefined} The parsed template ID.
|
||||
*/
|
||||
private parseTemplateId(
|
||||
templateId: string | number | undefined
|
||||
): string | undefined {
|
||||
if ( isNumber( templateId ) ) {
|
||||
return String( templateId );
|
||||
}
|
||||
return templateId?.split( '//' )[ 1 ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current template or page has changed and notifies subscribers.
|
||||
*
|
||||
* If the current template ID has changed and is not undefined (which means that it is not a page, post or template), it notifies all subscribers.
|
||||
*/
|
||||
public checkIfTemplateHasChangedAndNotifySubscribers(): void {
|
||||
this.previousTemplateId = this.currentTemplateId;
|
||||
|
||||
const postOrPageId = select( 'core/editor' )?.getCurrentPostId<
|
||||
string | number | undefined
|
||||
>();
|
||||
|
||||
this.isPostOrPage = Boolean( postOrPageId );
|
||||
|
||||
const editedPostId =
|
||||
postOrPageId ||
|
||||
select( 'core/edit-site' )?.getEditedPostId<
|
||||
string | number | undefined
|
||||
>();
|
||||
this.currentTemplateId = this.parseTemplateId( editedPostId );
|
||||
|
||||
const hasChangedTemplate =
|
||||
this.previousTemplateId !== this.currentTemplateId;
|
||||
const hasTemplateId = Boolean( this.currentTemplateId );
|
||||
|
||||
if ( ! hasChangedTemplate || ! hasTemplateId ) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ export const registerBlockSingleProductTemplate = ( {
|
||||
|
||||
// With GB 16.3.0 the return type can be a number: https://github.com/WordPress/gutenberg/issues/53230
|
||||
currentTemplateId = parseTemplateId(
|
||||
store?.getEditedPostId< string | number | undefined >()
|
||||
store?.getEditedPostId() as string | number | undefined
|
||||
);
|
||||
const hasChangedTemplate = previousTemplateId !== currentTemplateId;
|
||||
const hasTemplateId = Boolean( currentTemplateId );
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Button, { ButtonProps } from '..';
|
||||
const availableTypes = [ 'button', 'input', 'submit' ];
|
||||
|
||||
export default {
|
||||
title: 'Base Components/Button',
|
||||
argTypes: {
|
||||
children: {
|
||||
control: 'text',
|
||||
},
|
||||
type: {
|
||||
control: 'radio',
|
||||
options: availableTypes,
|
||||
},
|
||||
},
|
||||
component: Button,
|
||||
} as Meta< ButtonProps >;
|
||||
|
||||
const Template: Story< ButtonProps > = ( args ) => {
|
||||
return <Button { ...args } />;
|
||||
};
|
||||
|
||||
export const Default = Template.bind( {} );
|
||||
Default.args = {
|
||||
children: 'Buy Now',
|
||||
disabled: false,
|
||||
showSpinner: false,
|
||||
type: 'button',
|
||||
};
|
||||
|
||||
export const Disabled = Template.bind( {} );
|
||||
Disabled.args = {
|
||||
...Default.args,
|
||||
disabled: true,
|
||||
};
|
||||
|
||||
export const Loading = Template.bind( {} );
|
||||
Loading.args = {
|
||||
...Default.args,
|
||||
disabled: true,
|
||||
showSpinner: true,
|
||||
};
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { isPostcode } from '@woocommerce/blocks-checkout';
|
||||
import {
|
||||
ValidatedTextInput,
|
||||
isPostcode,
|
||||
type ValidatedTextInputHandle,
|
||||
} from '@woocommerce/blocks-components';
|
||||
} from '@woocommerce/blocks-checkout';
|
||||
import {
|
||||
BillingCountryInput,
|
||||
ShippingCountryInput,
|
||||
@@ -180,7 +180,6 @@ const AddressForm = ( {
|
||||
( fieldsRef.current[ field.key ] = el )
|
||||
}
|
||||
{ ...fieldProps }
|
||||
type={ field.type }
|
||||
value={ values[ field.key ] }
|
||||
onChange={ ( newValue: string ) =>
|
||||
onChange( {
|
||||
|
||||
@@ -34,8 +34,7 @@ table.wc-block-cart-items {
|
||||
}
|
||||
.wc-block-cart-item__quantity {
|
||||
.wc-block-cart-item__remove-link {
|
||||
@include link-button();
|
||||
@include hover-effect();
|
||||
@include link-button;
|
||||
@include font-size( smaller );
|
||||
|
||||
text-transform: none;
|
||||
@@ -76,7 +75,7 @@ table.wc-block-cart-items {
|
||||
.wc-block-cart-item__remove-link {
|
||||
display: none;
|
||||
}
|
||||
&:not(.wc-block-mini-cart-items):not(:last-child) {
|
||||
&:not(.wc-block-mini-cart-items) {
|
||||
.wc-block-cart-items__row {
|
||||
@include with-translucent-border( 0 0 1px );
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useContainerWidthContext } from '@woocommerce/base-context';
|
||||
import { Panel } from '@woocommerce/blocks-components';
|
||||
import { Panel } from '@woocommerce/blocks-checkout';
|
||||
import type { CartItem } from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { ShippingAddress } from '@woocommerce/settings';
|
||||
import { useCustomerData } from '@woocommerce/base-context/hooks';
|
||||
import { dispatch } from '@wordpress/data';
|
||||
import { CART_STORE_KEY, processErrorResponse } from '@woocommerce/block-data';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
|
||||
import { removeNoticesWithContext } from '@woocommerce/base-utils';
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
import classNames from 'classnames';
|
||||
import { _n, sprintf } from '@wordpress/i18n';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
import { Label, Panel } from '@woocommerce/blocks-components';
|
||||
import { Panel } from '@woocommerce/blocks-checkout';
|
||||
import { Label } from '@woocommerce/blocks-components';
|
||||
import { useCallback } from '@wordpress/element';
|
||||
import { useShippingData } from '@woocommerce/base-context/hooks';
|
||||
import { sanitizeHTML } from '@woocommerce/utils';
|
||||
|
||||
@@ -9,11 +9,11 @@ import { withInstanceId } from '@wordpress/compose';
|
||||
import {
|
||||
ValidatedTextInput,
|
||||
ValidationInputError,
|
||||
} from '@woocommerce/blocks-components';
|
||||
} from '@woocommerce/blocks-checkout';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||
import classnames from 'classnames';
|
||||
import type { MouseEvent, MouseEventHandler } from 'react';
|
||||
import type { MouseEvent } from 'react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
@@ -62,18 +62,18 @@ export const TotalsCoupon = ( {
|
||||
validationErrorId: store.getValidationErrorId( textInputId ),
|
||||
};
|
||||
} );
|
||||
const handleCouponAnchorClick: MouseEventHandler< HTMLAnchorElement > = (
|
||||
e: MouseEvent< HTMLAnchorElement >
|
||||
const handleCouponAnchorClick = (
|
||||
e: MouseEvent< HTMLAnchorElement, MouseEvent >
|
||||
) => {
|
||||
e.preventDefault();
|
||||
setIsCouponFormHidden( false );
|
||||
};
|
||||
const handleCouponSubmit: MouseEventHandler< HTMLButtonElement > = (
|
||||
e: MouseEvent< HTMLButtonElement >
|
||||
const handleCouponSubmit = (
|
||||
e: MouseEvent< HTMLButtonElement, MouseEvent >
|
||||
) => {
|
||||
e.preventDefault();
|
||||
if ( typeof onSubmit !== 'undefined' ) {
|
||||
onSubmit( couponValue )?.then( ( result ) => {
|
||||
if ( onSubmit !== undefined ) {
|
||||
onSubmit( couponValue ).then( ( result ) => {
|
||||
if ( result ) {
|
||||
setCouponValue( '' );
|
||||
setIsCouponFormHidden( true );
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useArgs } from '@storybook/client-api';
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
import { INTERACTION_TIMEOUT } from '@woocommerce/storybook-controls';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { TotalsCoupon, TotalsCouponProps } from '..';
|
||||
|
||||
export default {
|
||||
title: 'Base Components/Totals/Coupon',
|
||||
component: TotalsCoupon,
|
||||
args: {
|
||||
initialOpen: true,
|
||||
},
|
||||
} as Meta< TotalsCouponProps >;
|
||||
|
||||
const INVALID_COUPON_ERROR = {
|
||||
hidden: false,
|
||||
message: 'Invalid coupon code',
|
||||
};
|
||||
|
||||
const Template: Story< TotalsCouponProps > = ( args ) => {
|
||||
const [ {}, setArgs ] = useArgs();
|
||||
|
||||
const onSubmit = ( code: string ) => {
|
||||
args.onSubmit?.( code );
|
||||
setArgs( { isLoading: true } );
|
||||
return new Promise( ( resolve ) => {
|
||||
setTimeout( () => {
|
||||
setArgs( { isLoading: false } );
|
||||
resolve( true );
|
||||
}, INTERACTION_TIMEOUT );
|
||||
} );
|
||||
};
|
||||
|
||||
return <TotalsCoupon { ...args } onSubmit={ onSubmit } />;
|
||||
};
|
||||
|
||||
export const Default = Template.bind( {} );
|
||||
Default.args = {};
|
||||
|
||||
export const LoadingState = Template.bind( {} );
|
||||
LoadingState.args = {
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
export const ErrorState: Story< TotalsCouponProps > = ( args ) => {
|
||||
const { setValidationErrors } = useDispatch( VALIDATION_STORE_KEY );
|
||||
|
||||
setValidationErrors( { coupon: INVALID_COUPON_ERROR } );
|
||||
|
||||
return <TotalsCoupon { ...args } />;
|
||||
};
|
||||
|
||||
ErrorState.decorators = [
|
||||
( StoryComponent ) => {
|
||||
return <StoryComponent />;
|
||||
},
|
||||
];
|
||||
@@ -3,8 +3,8 @@
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import LoadingMask from '@woocommerce/base-components/loading-mask';
|
||||
import { RemovableChip, TotalsItem } from '@woocommerce/blocks-components';
|
||||
import { applyCheckoutFilter } from '@woocommerce/blocks-checkout';
|
||||
import { RemovableChip } from '@woocommerce/blocks-components';
|
||||
import { applyCheckoutFilter, TotalsItem } from '@woocommerce/blocks-checkout';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import {
|
||||
CartResponseCouponItemWithLabel,
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useArgs } from '@storybook/client-api';
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
import {
|
||||
currenciesAPIShape as currencies,
|
||||
currencyControl,
|
||||
INTERACTION_TIMEOUT,
|
||||
} from '@woocommerce/storybook-controls';
|
||||
import {
|
||||
CartResponseCouponItemWithLabel,
|
||||
CartTotalsItem,
|
||||
LooselyMustHave,
|
||||
} from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Discount, { TotalsDiscountProps } from '..';
|
||||
|
||||
const EXAMPLE_COUPONS: CartResponseCouponItemWithLabel[] = [
|
||||
{
|
||||
code: 'AWSMSB',
|
||||
discount_type: '',
|
||||
label: 'Awesome Storybook coupon',
|
||||
totals: {
|
||||
...currencies.EUR,
|
||||
total_discount: '5000',
|
||||
total_discount_tax: '250',
|
||||
},
|
||||
},
|
||||
{
|
||||
code: 'STONKS',
|
||||
discount_type: '',
|
||||
label: 'Most valuable coupon',
|
||||
totals: {
|
||||
...currencies.EUR,
|
||||
total_discount: '10000',
|
||||
total_discount_tax: '1000',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function extractValuesFromCoupons(
|
||||
coupons: LooselyMustHave< CartResponseCouponItemWithLabel, 'totals' >[]
|
||||
) {
|
||||
return coupons.reduce(
|
||||
( acc, curr ) => {
|
||||
const totalDiscount =
|
||||
Number( acc.total_discount ) +
|
||||
Number( curr.totals.total_discount );
|
||||
const totalDiscountTax =
|
||||
Number( acc.total_discount_tax ) +
|
||||
Number( curr.totals.total_discount_tax );
|
||||
|
||||
return {
|
||||
total_discount: String( totalDiscount ),
|
||||
total_discount_tax: String( totalDiscountTax ),
|
||||
};
|
||||
},
|
||||
{ total_discount: '0', total_discount_tax: '0' } as LooselyMustHave<
|
||||
CartTotalsItem,
|
||||
'total_discount' | 'total_discount_tax'
|
||||
>
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Base Components/Totals/Discount',
|
||||
component: Discount,
|
||||
argTypes: {
|
||||
currency: currencyControl,
|
||||
removeCoupon: { action: 'Removing coupon with code' },
|
||||
},
|
||||
args: {
|
||||
cartCoupons: EXAMPLE_COUPONS,
|
||||
isRemovingCoupon: false,
|
||||
values: extractValuesFromCoupons( EXAMPLE_COUPONS ),
|
||||
},
|
||||
} as Meta< TotalsDiscountProps >;
|
||||
|
||||
const Template: Story< TotalsDiscountProps > = ( args ) => {
|
||||
const [ {}, setArgs ] = useArgs();
|
||||
|
||||
const removeCoupon = ( code: string ) => {
|
||||
args.removeCoupon( code );
|
||||
setArgs( { isRemovingCoupon: true } );
|
||||
|
||||
const cartCoupons = args.cartCoupons.filter(
|
||||
( coupon ) => coupon.code !== code
|
||||
);
|
||||
|
||||
const values = extractValuesFromCoupons( cartCoupons );
|
||||
|
||||
setTimeout(
|
||||
() => setArgs( { cartCoupons, values, isRemovingCoupon: false } ),
|
||||
INTERACTION_TIMEOUT
|
||||
);
|
||||
};
|
||||
|
||||
return <Discount { ...args } removeCoupon={ removeCoupon } />;
|
||||
};
|
||||
|
||||
export const Default = Template.bind( {} );
|
||||
Default.args = {};
|
||||
|
||||
export const RemovingCoupon = Template.bind( {} );
|
||||
RemovingCoupon.args = {
|
||||
isRemovingCoupon: true,
|
||||
};
|
||||
@@ -4,11 +4,8 @@
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import classNames from 'classnames';
|
||||
import { createInterpolateElement } from '@wordpress/element';
|
||||
import {
|
||||
FormattedMonetaryAmount,
|
||||
TotalsItem,
|
||||
} from '@woocommerce/blocks-components';
|
||||
import { applyCheckoutFilter } from '@woocommerce/blocks-checkout';
|
||||
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';
|
||||
import {
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
import { allSettings } from '@woocommerce/settings';
|
||||
import { Currency } from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import FooterItem, { TotalsFooterItemProps } from '..';
|
||||
|
||||
const NZD: Currency = {
|
||||
code: 'NZD',
|
||||
symbol: '$',
|
||||
thousandSeparator: ' ',
|
||||
decimalSeparator: '.',
|
||||
minorUnit: 2,
|
||||
prefix: '$',
|
||||
suffix: '',
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Base Components/Totals/FooterItem',
|
||||
component: FooterItem,
|
||||
args: {
|
||||
currency: NZD,
|
||||
values: { total_price: '2500', total_tax: '550' },
|
||||
},
|
||||
} as Meta< TotalsFooterItemProps >;
|
||||
|
||||
const Template: Story< TotalsFooterItemProps > = ( args ) => (
|
||||
<FooterItem { ...args } />
|
||||
);
|
||||
|
||||
export const Default = Template.bind( {} );
|
||||
Default.decorators = [
|
||||
( StoryComponent ) => {
|
||||
allSettings.displayCartPricesIncludingTax = false;
|
||||
|
||||
return <StoryComponent />;
|
||||
},
|
||||
];
|
||||
|
||||
export const NoTaxLabel = Template.bind( {} );
|
||||
NoTaxLabel.decorators = [
|
||||
( StoryComponent ) => {
|
||||
allSettings.displayCartPricesIncludingTax = true;
|
||||
|
||||
return <StoryComponent />;
|
||||
},
|
||||
];
|
||||
|
||||
export const SingleTaxLabel = Template.bind( {} );
|
||||
SingleTaxLabel.args = {
|
||||
values: {
|
||||
total_price: '2500',
|
||||
total_tax: '550',
|
||||
tax_lines: [ { name: '10% VAT', price: '550', rate: '10.00' } ],
|
||||
},
|
||||
};
|
||||
SingleTaxLabel.decorators = [
|
||||
( StoryComponent ) => {
|
||||
allSettings.displayCartPricesIncludingTax = true;
|
||||
|
||||
return <StoryComponent />;
|
||||
},
|
||||
];
|
||||
|
||||
export const MultipleTaxLabels = Template.bind( {} );
|
||||
MultipleTaxLabels.args = {
|
||||
values: {
|
||||
total_price: '2500',
|
||||
total_tax: '550',
|
||||
tax_lines: [
|
||||
{ name: '10% VAT', price: '300', rate: '10.00' },
|
||||
{ name: '5% VAT', price: '250', rate: '5.00' },
|
||||
],
|
||||
},
|
||||
};
|
||||
MultipleTaxLabels.decorators = [
|
||||
( StoryComponent ) => {
|
||||
allSettings.displayCartPricesIncludingTax = true;
|
||||
|
||||
return <StoryComponent />;
|
||||
},
|
||||
];
|
||||
@@ -5,8 +5,8 @@ import classnames from 'classnames';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
import { TotalsItem } from '@woocommerce/blocks-components';
|
||||
import type { Currency } from '@woocommerce/types';
|
||||
import { TotalsItem } from '@woocommerce/blocks-checkout';
|
||||
import type { Currency } from '@woocommerce/price-format';
|
||||
import { ShippingVia } from '@woocommerce/base-components/cart-checkout/totals/shipping/shipping-via';
|
||||
import {
|
||||
isAddressComplete,
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { formatShippingAddress } from '@woocommerce/base-utils';
|
||||
import {
|
||||
formatShippingAddress,
|
||||
isAddressComplete,
|
||||
} from '@woocommerce/base-utils';
|
||||
import { useEditorContext } from '@woocommerce/base-context';
|
||||
import { ShippingAddress as ShippingAddressType } from '@woocommerce/settings';
|
||||
import PickupLocation from '@woocommerce/base-components/cart-checkout/pickup-location';
|
||||
@@ -28,19 +31,15 @@ export const ShippingAddress = ( {
|
||||
setIsShippingCalculatorOpen,
|
||||
shippingAddress,
|
||||
}: ShippingAddressProps ): JSX.Element | null => {
|
||||
const addressComplete = isAddressComplete( shippingAddress );
|
||||
const { isEditor } = useEditorContext();
|
||||
const prefersCollection = useSelect( ( select ) =>
|
||||
select( CHECKOUT_STORE_KEY ).prefersCollection()
|
||||
);
|
||||
const hasFormattedAddress = !! formatShippingAddress( shippingAddress );
|
||||
|
||||
// If there is no default customer location set in the store, the customer hasn't provided their address,
|
||||
// but a default shipping method is available for all locations,
|
||||
// then the shipping calculator will be hidden to avoid confusion.
|
||||
if ( ! hasFormattedAddress && ! isEditor ) {
|
||||
// If the address is incomplete, and we're not in the editor, don't show anything.
|
||||
if ( ! addressComplete && ! isEditor ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formattedLocation = formatShippingAddress( shippingAddress );
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -229,11 +229,8 @@ describe( 'TotalsShipping', () => {
|
||||
currency={ {
|
||||
code: 'USD',
|
||||
symbol: '$',
|
||||
minorUnit: 2,
|
||||
decimalSeparator: '.',
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
thousandSeparator: ', ',
|
||||
position: 'left',
|
||||
precision: 2,
|
||||
} }
|
||||
values={ {
|
||||
total_shipping: '0',
|
||||
@@ -277,11 +274,8 @@ describe( 'TotalsShipping', () => {
|
||||
currency={ {
|
||||
code: 'USD',
|
||||
symbol: '$',
|
||||
minorUnit: 2,
|
||||
decimalSeparator: '.',
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
thousandSeparator: ', ',
|
||||
position: 'left',
|
||||
precision: 2,
|
||||
} }
|
||||
values={ {
|
||||
total_shipping: '0',
|
||||
@@ -301,51 +295,4 @@ describe( 'TotalsShipping', () => {
|
||||
screen.queryByText( 'Add an address for shipping options' )
|
||||
).not.toBeInTheDocument();
|
||||
} );
|
||||
it( 'does show the calculator button when default rates are available and has formatted address', () => {
|
||||
baseContextHooks.useStoreCart.mockReturnValue( {
|
||||
cartItems: mockPreviewCart.items,
|
||||
cartTotals: [ mockPreviewCart.totals ],
|
||||
cartCoupons: mockPreviewCart.coupons,
|
||||
cartFees: mockPreviewCart.fees,
|
||||
cartNeedsShipping: mockPreviewCart.needs_shipping,
|
||||
shippingRates: mockPreviewCart.shipping_rates,
|
||||
shippingAddress: {
|
||||
...shippingAddress,
|
||||
city: '',
|
||||
state: 'California',
|
||||
country: 'US',
|
||||
postcode: '',
|
||||
},
|
||||
billingAddress: mockPreviewCart.billing_address,
|
||||
cartHasCalculatedShipping: mockPreviewCart.has_calculated_shipping,
|
||||
isLoadingRates: false,
|
||||
} );
|
||||
render(
|
||||
<SlotFillProvider>
|
||||
<TotalsShipping
|
||||
currency={ {
|
||||
code: 'USD',
|
||||
symbol: '$',
|
||||
minorUnit: 2,
|
||||
decimalSeparator: '.',
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
thousandSeparator: ', ',
|
||||
} }
|
||||
values={ {
|
||||
total_shipping: '0',
|
||||
total_shipping_tax: '0',
|
||||
} }
|
||||
showCalculator={ true }
|
||||
showRateSelector={ true }
|
||||
isCheckout={ false }
|
||||
className={ '' }
|
||||
/>
|
||||
</SlotFillProvider>
|
||||
);
|
||||
expect( screen.queryByText( 'Change address' ) ).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText( 'Add an address for shipping options' )
|
||||
).not.toBeInTheDocument();
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -6,7 +6,7 @@ import { __ } from '@wordpress/i18n';
|
||||
import { useEffect, useRef } from '@wordpress/element';
|
||||
import { withInstanceId } from '@wordpress/compose';
|
||||
import { ComboboxControl } from 'wordpress-components';
|
||||
import { ValidationInputError } from '@woocommerce/blocks-components';
|
||||
import { ValidationInputError } from '@woocommerce/blocks-checkout';
|
||||
import { isObject } from '@woocommerce/types';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||
@@ -121,26 +121,14 @@ const Combobox = ( {
|
||||
// Try to match.
|
||||
const normalizedFilterValue =
|
||||
filterValue.toLocaleUpperCase();
|
||||
|
||||
// Try to find an exact match first using values.
|
||||
const foundValue = options.find(
|
||||
const foundOption = options.find(
|
||||
( option ) =>
|
||||
option.label
|
||||
.toLocaleUpperCase()
|
||||
.startsWith( normalizedFilterValue ) ||
|
||||
option.value.toLocaleUpperCase() ===
|
||||
normalizedFilterValue
|
||||
normalizedFilterValue
|
||||
);
|
||||
|
||||
if ( foundValue ) {
|
||||
onChange( foundValue.value );
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to a label match.
|
||||
const foundOption = options.find( ( option ) =>
|
||||
option.label
|
||||
.toLocaleUpperCase()
|
||||
.startsWith( normalizedFilterValue )
|
||||
);
|
||||
|
||||
if ( foundOption ) {
|
||||
onChange( foundOption.value );
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { useState, useEffect } from '@wordpress/element';
|
||||
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CountryInput, CountryInputWithCountriesProps } from '..';
|
||||
import { countries } from './countries-filler';
|
||||
|
||||
type CountryCode = keyof typeof countries;
|
||||
|
||||
export default {
|
||||
title: 'Base Components/CountryInput',
|
||||
component: CountryInput,
|
||||
args: {
|
||||
countries,
|
||||
autoComplete: 'off',
|
||||
id: 'country',
|
||||
label: 'Countries: ',
|
||||
required: false,
|
||||
},
|
||||
argTypes: {
|
||||
countries: { control: false },
|
||||
options: { table: { disable: true } },
|
||||
value: { control: false },
|
||||
},
|
||||
decorators: [ ( StoryComponent ) => <StoryComponent /> ],
|
||||
} as Meta< CountryInputWithCountriesProps >;
|
||||
|
||||
const Template: Story< CountryInputWithCountriesProps > = ( args ) => {
|
||||
const [ selectedCountry, selectCountry ] = useState< CountryCode | '' >(
|
||||
''
|
||||
);
|
||||
const { clearValidationError, showValidationError } =
|
||||
useDispatch( VALIDATION_STORE_KEY );
|
||||
|
||||
useEffect( () => {
|
||||
showValidationError( 'country' );
|
||||
}, [ showValidationError ] );
|
||||
|
||||
function updateCountry( country: CountryCode ) {
|
||||
clearValidationError( 'country' );
|
||||
selectCountry( country );
|
||||
}
|
||||
|
||||
return (
|
||||
<CountryInput
|
||||
{ ...args }
|
||||
onChange={ ( value ) => updateCountry( value as CountryCode ) }
|
||||
value={ selectedCountry }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = Template.bind( {} );
|
||||
|
||||
export const WithError = Template.bind( {} );
|
||||
WithError.args = {
|
||||
errorId: 'country',
|
||||
errorMessage: 'Please select a country',
|
||||
required: true,
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
import { useState } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import FormTokenField, { Props } from '..';
|
||||
|
||||
export default {
|
||||
title: 'Base Components/FormTokenField',
|
||||
argTypes: {},
|
||||
component: FormTokenField,
|
||||
} as Meta< Props >;
|
||||
|
||||
const Template: Story< Props > = ( args ) => {
|
||||
const [ selected, setSelected ] = useState< string[] >( [] );
|
||||
|
||||
return (
|
||||
<FormTokenField
|
||||
{ ...args }
|
||||
value={ selected }
|
||||
onChange={ ( tokens ) => setSelected( tokens ) }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const suggestions = [ 'foo', 'bar', 'baz' ];
|
||||
|
||||
export const Default = Template.bind( {} );
|
||||
Default.args = {
|
||||
suggestions,
|
||||
};
|
||||
|
||||
export const Disabled = Template.bind( {} );
|
||||
Disabled.args = {
|
||||
...Default.args,
|
||||
disabled: true,
|
||||
};
|
||||
@@ -10,6 +10,7 @@ export * from './filter-reset-button';
|
||||
export * from './filter-submit-button';
|
||||
export * from './form';
|
||||
export * from './form-token-field';
|
||||
export * from './label';
|
||||
export * from './load-more-button';
|
||||
export * from './loading-mask';
|
||||
export * from './noninteractive';
|
||||
@@ -25,6 +26,9 @@ export * from './read-more';
|
||||
export * from './reviews';
|
||||
export * from './sidebar-layout';
|
||||
export * from './snackbar-list';
|
||||
export * from './sort-select';
|
||||
export * from './state-input';
|
||||
export * from './summary';
|
||||
export * from './tabs';
|
||||
export * from './textarea';
|
||||
export * from './title';
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import { Canvas, Meta, ArgTypes, Primary, Source } from '@storybook/blocks';
|
||||
|
||||
import * as NoticeBannerStories from '../stories/index.stories.tsx';
|
||||
|
||||
<Meta name="Docs" of={ NoticeBannerStories } />
|
||||
|
||||
# NoticeBanner
|
||||
|
||||
An informational UI displayed near the top of the store pages.
|
||||
|
||||
<Primary />
|
||||
|
||||
## Design Guidelines
|
||||
|
||||
`NoticeBanner` is an informational UI element displayed near the top of store pages used to indicate the result of an action, or to draw the user's attention to necessary information.
|
||||
|
||||
Notices are color-coded to indicate the type of message being communicated, and also show an icon to reinforce the meaning of the message. The color and icon used for a notice are determined by the `status` prop.
|
||||
|
||||
### Default Notices
|
||||
|
||||
By default, noices are grey and used for less important messaging.
|
||||
|
||||
<Canvas of={ NoticeBannerStories.Default } />
|
||||
|
||||
### Informational Notices
|
||||
|
||||
Blue notices with an info icon are used for general information for buyers, but do not require them to take an action.
|
||||
|
||||
<Canvas of={ NoticeBannerStories.Info } />
|
||||
|
||||
### Error Notices
|
||||
|
||||
Red notices with an alert icon are used to show that an error has occurred and that the user needs to take action.
|
||||
|
||||
<Canvas of={ NoticeBannerStories.Error } />
|
||||
|
||||
### Success Notices
|
||||
|
||||
Green notices with a success icon are used to show an action was successful.
|
||||
|
||||
<Canvas of={ NoticeBannerStories.Success } />
|
||||
|
||||
### Warning Notices
|
||||
|
||||
Yellow notices with an alert icon are used to show that the user may need to take action, or needs to be aware of something important.
|
||||
|
||||
<Canvas of={ NoticeBannerStories.Warning } />
|
||||
|
||||
### Error Summary
|
||||
|
||||
If you provide a `summary` it will be displayed above the notice content. This can be useful for displaying a summary of errors in a list format.
|
||||
|
||||
<Canvas of={ NoticeBannerStories.ErrorSummary } />
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Props
|
||||
|
||||
<ArgTypes />
|
||||
|
||||
### Usage examples
|
||||
|
||||
#### Example: string based notices
|
||||
|
||||
To display a basic notice, pass the notice message as a string:
|
||||
|
||||
```jsx
|
||||
import { NoticeBanner } from '@woocommerce/base-components';
|
||||
|
||||
<NoticeBanner status="info">Your message here</NoticeBanner>;
|
||||
```
|
||||
|
||||
#### Example: components within notices
|
||||
|
||||
For more complex markup, you can wrap any JSX element:
|
||||
|
||||
```jsx
|
||||
import { NoticeBanner } from '@woocommerce/base-components';
|
||||
|
||||
<NoticeBanner status="error">
|
||||
<p>
|
||||
An error occurred: <code>{ errorDetails }</code>.
|
||||
</p>
|
||||
</NoticeBanner>;
|
||||
```
|
||||
|
||||
#### Example: list of notices
|
||||
|
||||
In this example, the summary prop is used to indicate to the user that there are errors in the form submission.
|
||||
|
||||
```typescript
|
||||
import { NoticeBanner } from '@woocommerce/base-components';
|
||||
|
||||
const errorMessages = [
|
||||
'First error message',
|
||||
'Second error message',
|
||||
'Third error message',
|
||||
];
|
||||
|
||||
<NoticeBanner
|
||||
status="error"
|
||||
summary="There are errors in your form submission:"
|
||||
>
|
||||
<ul>
|
||||
{ errorMessages.map( ( message ) => (
|
||||
<li key={ message }>{ message }</li>
|
||||
) ) }
|
||||
</ul>
|
||||
</NoticeBanner>;
|
||||
```
|
||||
|
||||
The list of error messages is rendered within the NoticeBanner component using an unordered list (`<ul>`) and list items (`<li>`). The `status` prop is set to `error` to indicate that the notice represents an error message.
|
||||
@@ -14,20 +14,29 @@ import Button from '../button';
|
||||
import { useSpokenMessage } from '../../hooks';
|
||||
|
||||
export interface NoticeBannerProps {
|
||||
// The displayed message of a notice. Also used as the spoken message for assistive technology, unless `spokenMessage` is provided as an alternative message.
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
isDismissible?: boolean;
|
||||
onRemove?: () => void;
|
||||
politeness?: 'polite' | 'assertive';
|
||||
spokenMessage?: string | React.ReactNode;
|
||||
// Additional class name to give to the notice.
|
||||
className?: string | undefined;
|
||||
// Determines whether the notice can be dismissed by the user.
|
||||
isDismissible?: boolean | undefined;
|
||||
// Function called when dismissing the notice.
|
||||
onRemove?: ( () => void ) | undefined;
|
||||
// Determines the level of politeness for the notice for assistive technology.
|
||||
politeness?: 'polite' | 'assertive' | undefined;
|
||||
// Optionally provided to change the spoken message for assistive technology.
|
||||
spokenMessage?: string | React.ReactNode | undefined;
|
||||
// Status determines the color of the notice and the icon.
|
||||
status: 'success' | 'error' | 'info' | 'warning' | 'default';
|
||||
summary?: string;
|
||||
// Optional summary text shown above notice content, used when several notices are listed together.
|
||||
summary?: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* NoticeBanner component.
|
||||
* NoticeBanner: An informational UI displayed near the top of the store pages.
|
||||
*
|
||||
* An informational UI displayed near the top of the store pages.
|
||||
* Notices are informational UI displayed near the top of store pages. WooCommerce blocks, themes, and plugins all use
|
||||
* notices to indicate the result of an action, or to draw the user’s attention to necessary information.
|
||||
*/
|
||||
const NoticeBanner = ( {
|
||||
className,
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { StoryFn, Meta } from '@storybook/react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import NoticeBanner, { NoticeBannerProps } from '../';
|
||||
const availableStatus = [ 'default', 'success', 'error', 'warning', 'info' ];
|
||||
|
||||
export default {
|
||||
title: 'Base Components/NoticeBanner',
|
||||
argTypes: {
|
||||
status: {
|
||||
control: 'radio',
|
||||
options: availableStatus,
|
||||
description:
|
||||
'Status determines the color of the notice and the icon.',
|
||||
},
|
||||
isDismissible: {
|
||||
control: 'boolean',
|
||||
description:
|
||||
'Determines whether the notice can be dismissed by the user. When set to true, a close icon will be displayed on the banner.',
|
||||
},
|
||||
summary: {
|
||||
description:
|
||||
'Optional summary text shown above notice content, used when several notices are listed together.',
|
||||
control: 'text',
|
||||
},
|
||||
className: {
|
||||
description: 'Additional class name to give to the notice.',
|
||||
control: 'text',
|
||||
},
|
||||
spokenMessage: {
|
||||
description:
|
||||
'Optionally provided to change the spoken message for assistive technology. If not provided, the `children` prop will be used as the spoken message.',
|
||||
control: 'text',
|
||||
},
|
||||
politeness: {
|
||||
control: 'radio',
|
||||
options: [ 'polite', 'assertive' ],
|
||||
description:
|
||||
'Determines the level of politeness for the notice for assistive technology.',
|
||||
},
|
||||
children: {
|
||||
description:
|
||||
'The displayed message of a notice. Also used as the spoken message for assistive technology, unless `spokenMessage` is provided as an alternative message.',
|
||||
disable: true,
|
||||
},
|
||||
onRemove: {
|
||||
description:
|
||||
'Function called when dismissing the notice. When the close icon is clicked or the Escape key is pressed, this function will be called.',
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
component: NoticeBanner,
|
||||
} as Meta< NoticeBannerProps >;
|
||||
|
||||
const Template: StoryFn< NoticeBannerProps > = ( args ) => {
|
||||
return <NoticeBanner { ...args } />;
|
||||
};
|
||||
|
||||
export const Default = Template.bind( {} );
|
||||
Default.args = {
|
||||
children: 'This is a default notice',
|
||||
status: 'default',
|
||||
isDismissible: true,
|
||||
summary: undefined,
|
||||
className: undefined,
|
||||
spokenMessage: undefined,
|
||||
politeness: undefined,
|
||||
};
|
||||
|
||||
export const Error = Template.bind( {} );
|
||||
Error.args = {
|
||||
children: 'This is an error notice',
|
||||
status: 'error',
|
||||
};
|
||||
|
||||
export const Warning = Template.bind( {} );
|
||||
Warning.args = {
|
||||
children: 'This is a warning notice',
|
||||
status: 'warning',
|
||||
};
|
||||
|
||||
export const Info = Template.bind( {} );
|
||||
Info.args = {
|
||||
children: 'This is an informational notice',
|
||||
status: 'info',
|
||||
};
|
||||
|
||||
export const Success = Template.bind( {} );
|
||||
Success.args = {
|
||||
children: 'This is a success notice',
|
||||
status: 'success',
|
||||
};
|
||||
|
||||
export const ErrorSummary = Template.bind( {} );
|
||||
ErrorSummary.args = {
|
||||
summary: 'Please fix the following errors',
|
||||
children: (
|
||||
<ul>
|
||||
<li>This is an error notice</li>
|
||||
<li>This is another error notice</li>
|
||||
</ul>
|
||||
),
|
||||
status: 'error',
|
||||
};
|
||||
@@ -15,10 +15,6 @@
|
||||
padding: 0.3em 0.6em;
|
||||
min-width: 2.2em;
|
||||
|
||||
&:not([disabled]) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@include breakpoint("<782px") {
|
||||
padding: 0.1em 0.2em;
|
||||
min-width: 1.6em;
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { currencies, currencyControl } from '@woocommerce/storybook-controls';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import PriceSlider, { PriceSliderProps } from '..';
|
||||
|
||||
export default {
|
||||
title: 'Base Components/PriceSlider',
|
||||
component: PriceSlider,
|
||||
args: {
|
||||
currency: currencies.USD,
|
||||
maxPrice: 5000,
|
||||
maxConstraint: 5000,
|
||||
minConstraint: 1000,
|
||||
minPrice: 1000,
|
||||
step: 250,
|
||||
},
|
||||
argTypes: {
|
||||
currency: currencyControl,
|
||||
maxPrice: { control: { disable: true } },
|
||||
minPrice: { control: { disable: true } },
|
||||
},
|
||||
} as Meta< PriceSliderProps >;
|
||||
|
||||
const Template: Story< PriceSliderProps > = ( args ) => {
|
||||
const { maxPrice, minPrice, ...props } = args;
|
||||
// PriceSlider expects client to update min & max price, i.e. is a controlled component
|
||||
const [ min, setMin ] = useState( minPrice );
|
||||
const [ max, setMax ] = useState( maxPrice );
|
||||
|
||||
return (
|
||||
<PriceSlider
|
||||
{ ...props }
|
||||
maxPrice={ max }
|
||||
minPrice={ min }
|
||||
onChange={ ( [ newMin, newMax ] ) => {
|
||||
setMin( newMin );
|
||||
setMax( newMax );
|
||||
} }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = Template.bind( {} );
|
||||
|
||||
export const WithoutInputs = Template.bind( {} );
|
||||
WithoutInputs.args = {
|
||||
showInputFields: false,
|
||||
};
|
||||
|
||||
export const WithButton = Template.bind( {} );
|
||||
WithButton.args = {
|
||||
showFilterButton: true,
|
||||
};
|
||||
@@ -2,8 +2,7 @@
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { SortSelect } from '@woocommerce/blocks-components';
|
||||
|
||||
import SortSelect from '@woocommerce/base-components/sort-select';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import ProductName, { ProductNameProps } from '..';
|
||||
|
||||
export default {
|
||||
title: 'Base Components/ProductName',
|
||||
component: ProductName,
|
||||
args: {
|
||||
name: 'Test product',
|
||||
permalink: '#',
|
||||
},
|
||||
} as Meta< ProductNameProps >;
|
||||
|
||||
const Template: Story< ProductNameProps > = ( args ) => (
|
||||
<ProductName { ...args } />
|
||||
);
|
||||
|
||||
export const Default = Template.bind( {} );
|
||||
Default.args = {
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
export const DisabledProduct = Template.bind( {} );
|
||||
DisabledProduct.args = {
|
||||
disabled: true,
|
||||
};
|
||||
@@ -280,7 +280,7 @@ const ProductPrice = ( {
|
||||
console.error( 'Price formats need to include the `<price/>` tag.' );
|
||||
}
|
||||
|
||||
const isDiscounted = regularPrice && price && price < regularPrice;
|
||||
const isDiscounted = regularPrice && price !== regularPrice;
|
||||
let priceComponent = (
|
||||
<span
|
||||
className={ classNames(
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
import { currencyControl } from '@woocommerce/storybook-controls';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import ProductPrice, { ProductPriceProps } from '..';
|
||||
|
||||
const ALLOWED_ALIGN_VALUES = [ 'left', 'center', 'right' ];
|
||||
|
||||
export default {
|
||||
title: 'Base Components/ProductPrice',
|
||||
component: ProductPrice,
|
||||
argTypes: {
|
||||
align: {
|
||||
control: { type: 'radio' },
|
||||
options: ALLOWED_ALIGN_VALUES,
|
||||
},
|
||||
currency: currencyControl,
|
||||
},
|
||||
args: {
|
||||
align: 'left',
|
||||
format: '<price/>',
|
||||
price: 3000,
|
||||
currency: {
|
||||
code: 'USD',
|
||||
symbol: '$',
|
||||
thousandSeparator: ' ',
|
||||
decimalSeparator: '.',
|
||||
minorUnit: 2,
|
||||
prefix: '$',
|
||||
suffix: '',
|
||||
},
|
||||
},
|
||||
} as Meta< ProductPriceProps >;
|
||||
|
||||
const Template: Story< ProductPriceProps > = ( args ) => (
|
||||
<ProductPrice { ...args } />
|
||||
);
|
||||
|
||||
export const Default = Template.bind( {} );
|
||||
Default.args = {};
|
||||
|
||||
export const Sale = Template.bind( {} );
|
||||
Sale.args = {
|
||||
regularPrice: 4500,
|
||||
};
|
||||
|
||||
export const Range = Template.bind( {} );
|
||||
Range.args = {
|
||||
maxPrice: 5000,
|
||||
minPrice: 3000,
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useArgs } from '@storybook/client-api';
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import QuantitySelector, { QuantitySelectorProps } from '..';
|
||||
|
||||
export default {
|
||||
title: 'Base Components/QuantitySelector',
|
||||
component: QuantitySelector,
|
||||
args: {
|
||||
itemName: 'widgets',
|
||||
quantity: 1,
|
||||
},
|
||||
} as Meta< QuantitySelectorProps >;
|
||||
|
||||
const Template: Story< QuantitySelectorProps > = ( args ) => {
|
||||
const [ {}, setArgs ] = useArgs();
|
||||
|
||||
const onChange = ( newVal: number ) => {
|
||||
args.onChange?.( newVal );
|
||||
setArgs( { quantity: newVal } );
|
||||
};
|
||||
|
||||
return <QuantitySelector { ...args } onChange={ onChange } />;
|
||||
};
|
||||
|
||||
export const Default = Template.bind( {} );
|
||||
Default.args = {};
|
||||
|
||||
export const Disabled = Template.bind( {} );
|
||||
Disabled.args = {
|
||||
disabled: true,
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import ReadMore, { defaultProps, ReadMoreProps } from '..';
|
||||
|
||||
export default {
|
||||
title: 'Base Components/ReadMore',
|
||||
component: ReadMore,
|
||||
args: defaultProps,
|
||||
argTypes: {
|
||||
children: { control: { disable: true } },
|
||||
},
|
||||
} as Meta< ReadMoreProps >;
|
||||
|
||||
const LongText = (
|
||||
<>
|
||||
<h1>
|
||||
No! Alderaan is peaceful. We have no weapons. You can't
|
||||
possibly…
|
||||
</h1>
|
||||
<p>
|
||||
As you wish. But with the blast shield down, I can't even see!
|
||||
How am I supposed to fight? Look, I ain't in this for your
|
||||
revolution, and I'm not in it for you, Princess. I expect to be
|
||||
well paid. I'm in it for the money.
|
||||
</p>
|
||||
<p>
|
||||
You mean it controls your actions?
|
||||
<strong>
|
||||
{ ' ' }
|
||||
She must have hidden the plans in the escape pod.
|
||||
</strong>{ ' ' }
|
||||
<em>
|
||||
Send a detachment down to retrieve them, and see to it
|
||||
personally, Commander.
|
||||
</em>
|
||||
There'll be no one to stop us this time!
|
||||
</p>
|
||||
<h2>Escape is not his plan. I must face him, alone.</h2>
|
||||
<ol>
|
||||
<li>Partially, but it also obeys your commands.</li>
|
||||
<li>
|
||||
Leave that to me. Send a distress signal, and inform the Senate
|
||||
that all on board were killed.
|
||||
</li>
|
||||
<li>
|
||||
A tremor in the Force. The last time I felt it was in the
|
||||
presence of my old master.
|
||||
</li>
|
||||
</ol>
|
||||
<aside>
|
||||
<a href="http://fillerama.io">
|
||||
Content from http://fillerama.io "Star Wars"
|
||||
</a>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
|
||||
const Template: Story< ReadMoreProps > = ( args ) => <ReadMore { ...args } />;
|
||||
|
||||
export const Default = Template.bind( {} );
|
||||
Default.args = {
|
||||
children: LongText,
|
||||
maxLines: 6,
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { SortSelect } from '@woocommerce/blocks-components';
|
||||
import SortSelect from '@woocommerce/base-components/sort-select';
|
||||
import type { ChangeEventHandler } from 'react';
|
||||
|
||||
/**
|
||||
@@ -12,7 +12,7 @@ import './style.scss';
|
||||
|
||||
interface ReviewSortSelectProps {
|
||||
onChange: ChangeEventHandler;
|
||||
readOnly?: boolean;
|
||||
readOnly: boolean;
|
||||
value: 'most-recent' | 'highest-rating' | 'lowest-rating';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import { Canvas, Meta, ArgTypes } from '@storybook/blocks';
|
||||
|
||||
import * as SnackbarListStories from '../stories/index.stories.tsx';
|
||||
import * as SnackbarStories from '../stories/snackbar.stories.tsx';
|
||||
|
||||
<Meta name="Docs" of={ SnackbarListStories } />
|
||||
|
||||
# SnackbarList
|
||||
|
||||
A temporary informational UI element displayed at the bottom of store pages.
|
||||
|
||||
<Canvas
|
||||
of={ SnackbarListStories.Default }
|
||||
layout="padded"
|
||||
className={ 'force-canvas-height' }
|
||||
/>
|
||||
|
||||
## Design Guidelines
|
||||
|
||||
The Snackbar is a temporary informational UI element displayed at the bottom of store pages. WooCommerce blocks, themes, and plugins all use snackbar notices to indicate the result of a successful action. For example, adding something to the cart.
|
||||
|
||||
Snackbar notices work in the same way as the NoticeBanner component, and support the same statuses and styles.
|
||||
|
||||
### Default Snackbars
|
||||
|
||||
By default, notices are grey and used for less important messaging.
|
||||
|
||||
<Canvas of={ SnackbarStories.Default } />
|
||||
|
||||
### Informational Snackbars
|
||||
|
||||
Blue notices with an info icon are used for general information for buyers, but do not require action.
|
||||
|
||||
<Canvas of={ SnackbarStories.Info } />
|
||||
|
||||
### Error Snackbars
|
||||
|
||||
Red notices with an alert icon are used to show that an error has occurred and that the user needs to take action.
|
||||
|
||||
<Canvas of={ SnackbarStories.Error } />
|
||||
|
||||
### Success Snackbars
|
||||
|
||||
Green notices with a success icon are used to show an action was successful.
|
||||
|
||||
<Canvas of={ SnackbarStories.Success } />
|
||||
|
||||
### Warning Snackbars
|
||||
|
||||
Yellow notices with an alert icon are used to show that the user may need to take action, or needs to be aware of something important.
|
||||
|
||||
<Canvas of={ SnackbarStories.Warning } />
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
The component consuming `SnackbarList` is responsible for managing the notices state. The `SnackbarList` component will automatically remove notices from the list when they are dismissed by the user using the provided `onRemove` callback, and also when the notice times out after 10000ms.
|
||||
|
||||
### Props
|
||||
|
||||
<ArgTypes />
|
||||
|
||||
### NoticeType
|
||||
|
||||
Each notice can be provided with the following args.
|
||||
|
||||
- The `id` (required) prop is used to identify the notice and should be unique.
|
||||
- The `content` (required) prop is the content to display in the notice.
|
||||
- The `status` prop is used to determine the color of the notice and the icon. Acceptable values are 'success', 'error', 'info', 'warning', and 'default'.
|
||||
- The `isDismissible` prop determines whether the notice can be dismissed by the user.
|
||||
- The `spokenMessage` prop is used to change the spoken message for assistive technology. If not provided, the `content` prop will be used as the spoken message.
|
||||
|
||||
### Usage example
|
||||
|
||||
To display snackbar notices, pass an array of `notices` to the `SnackbarList` component:
|
||||
|
||||
```jsx
|
||||
import { SnackbarList } from '@woocommerce/base-components';
|
||||
|
||||
const notices = [
|
||||
{
|
||||
id: '1',
|
||||
content: 'This is a snackbar notice.',
|
||||
status: 'default',
|
||||
isDismissible: true,
|
||||
spokenMessage: "Hello snackbar!"
|
||||
}
|
||||
];
|
||||
|
||||
<SnackbarList notices={ notices }>;
|
||||
```
|
||||
@@ -2,7 +2,6 @@
|
||||
* External dependencies
|
||||
*/
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import classNames from 'classnames';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
@@ -10,18 +9,15 @@ import classNames from 'classnames';
|
||||
import NoticeBanner, { NoticeBannerProps } from '../notice-banner';
|
||||
import { SNACKBAR_TIMEOUT } from './constants';
|
||||
|
||||
export interface SnackbarProps extends NoticeBannerProps {
|
||||
// A ref to the list that contains the snackbar.
|
||||
listRef?: React.MutableRefObject< HTMLDivElement | null >;
|
||||
}
|
||||
|
||||
export const Snackbar = ( {
|
||||
const Snackbar = ( {
|
||||
onRemove = () => void 0,
|
||||
children,
|
||||
listRef,
|
||||
className,
|
||||
...notice
|
||||
}: SnackbarProps ) => {
|
||||
}: {
|
||||
// A ref to the list that contains the snackbar.
|
||||
listRef?: React.MutableRefObject< HTMLDivElement | null >;
|
||||
} & NoticeBannerProps ) => {
|
||||
// Only set up the timeout dismiss if we're not explicitly dismissing.
|
||||
useEffect( () => {
|
||||
const timeoutHandle = setTimeout( () => {
|
||||
@@ -33,10 +29,6 @@ export const Snackbar = ( {
|
||||
|
||||
return (
|
||||
<NoticeBanner
|
||||
className={ classNames(
|
||||
className,
|
||||
'wc-block-components-notice-snackbar'
|
||||
) }
|
||||
{ ...notice }
|
||||
onRemove={ () => {
|
||||
// Prevent focus loss by moving it to the list element.
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { StoryFn, Meta } from '@storybook/react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import SnackbarList, { SnackbarListProps } from '../';
|
||||
|
||||
export default {
|
||||
title: 'Base Components/SnackbarList',
|
||||
args: {
|
||||
notices: [
|
||||
{
|
||||
id: '1',
|
||||
content: 'This is a snackbar notice.',
|
||||
status: 'success',
|
||||
isDismissible: true,
|
||||
},
|
||||
],
|
||||
className: undefined,
|
||||
onRemove: () => void 0,
|
||||
},
|
||||
argTypes: {
|
||||
className: {
|
||||
description: 'Additional class name to give to the notice.',
|
||||
control: 'text',
|
||||
},
|
||||
notices: {
|
||||
description:
|
||||
'A list of notices to display as snackbars. Each notice must have an `id` and `content` prop.',
|
||||
disable: true,
|
||||
},
|
||||
onRemove: {
|
||||
description:
|
||||
'Function called when dismissing the notice. When the close icon is clicked or the Escape key is pressed, this function will be called. This is also called when the notice times out after 10000ms.',
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
component: SnackbarList,
|
||||
} as Meta< SnackbarListProps >;
|
||||
|
||||
const Template: StoryFn< SnackbarListProps > = ( args ) => {
|
||||
return <SnackbarList { ...args } />;
|
||||
};
|
||||
|
||||
export const Default = Template.bind( {} );
|
||||
Default.args = {
|
||||
notices: [
|
||||
{
|
||||
id: '1',
|
||||
content: 'This is a snackbar notice.',
|
||||
status: 'default',
|
||||
isDismissible: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
content: 'This is an informational snackbar notice.',
|
||||
status: 'info',
|
||||
isDismissible: true,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
content: 'This is a snackbar error notice.',
|
||||
status: 'error',
|
||||
isDismissible: true,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
content: 'This is a snackbar warning notice.',
|
||||
status: 'warning',
|
||||
isDismissible: true,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
content: 'This is a snackbar success notice.',
|
||||
status: 'success',
|
||||
isDismissible: true,
|
||||
},
|
||||
],
|
||||
className: undefined,
|
||||
onRemove: () => void 0,
|
||||
};
|
||||
@@ -1,97 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { StoryFn, Meta } from '@storybook/react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Snackbar, { SnackbarProps } from '../snackbar';
|
||||
const availableStatus = [ 'default', 'success', 'error', 'warning', 'info' ];
|
||||
|
||||
export default {
|
||||
title: 'Base Components/SnackbarList/Snackbar',
|
||||
argTypes: {
|
||||
status: {
|
||||
control: 'radio',
|
||||
options: availableStatus,
|
||||
description:
|
||||
'Status determines the color of the notice and the icon.',
|
||||
},
|
||||
isDismissible: {
|
||||
control: 'boolean',
|
||||
description:
|
||||
'Determines whether the notice can be dismissed by the user. When set to true, a close icon will be displayed on the banner.',
|
||||
},
|
||||
summary: {
|
||||
description:
|
||||
'Optional summary text shown above notice content, used when several notices are listed together.',
|
||||
control: 'text',
|
||||
},
|
||||
className: {
|
||||
description: 'Additional class name to give to the notice.',
|
||||
control: 'text',
|
||||
},
|
||||
spokenMessage: {
|
||||
description:
|
||||
'Optionally provided to change the spoken message for assistive technology. If not provided, the `children` prop will be used as the spoken message.',
|
||||
control: 'text',
|
||||
},
|
||||
politeness: {
|
||||
control: 'radio',
|
||||
options: [ 'polite', 'assertive' ],
|
||||
description:
|
||||
'Determines the level of politeness for the notice for assistive technology.',
|
||||
},
|
||||
children: {
|
||||
description:
|
||||
'The displayed message of a notice. Also used as the spoken message for assistive technology, unless `spokenMessage` is provided as an alternative message.',
|
||||
disable: true,
|
||||
},
|
||||
onRemove: {
|
||||
description:
|
||||
'Function called when dismissing the notice. When the close icon is clicked or the Escape key is pressed, this function will be called.',
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
component: Snackbar,
|
||||
} as Meta< SnackbarProps >;
|
||||
|
||||
const Template: StoryFn< SnackbarProps > = ( args ) => {
|
||||
return <Snackbar { ...args } />;
|
||||
};
|
||||
|
||||
export const Default = Template.bind( {} );
|
||||
Default.args = {
|
||||
children: 'This is a default snackbar notice',
|
||||
status: 'default',
|
||||
isDismissible: true,
|
||||
summary: undefined,
|
||||
className: undefined,
|
||||
spokenMessage: undefined,
|
||||
politeness: undefined,
|
||||
};
|
||||
|
||||
export const Error = Template.bind( {} );
|
||||
Error.args = {
|
||||
children: 'This is an error snackbar notice',
|
||||
status: 'error',
|
||||
};
|
||||
|
||||
export const Warning = Template.bind( {} );
|
||||
Warning.args = {
|
||||
children: 'This is a warning snackbar notice',
|
||||
status: 'warning',
|
||||
};
|
||||
|
||||
export const Info = Template.bind( {} );
|
||||
Info.args = {
|
||||
children: 'This is an informational snackbar notice',
|
||||
status: 'info',
|
||||
};
|
||||
|
||||
export const Success = Template.bind( {} );
|
||||
Success.args = {
|
||||
children: 'This is a success snackbar notice',
|
||||
status: 'success',
|
||||
};
|
||||
@@ -9,32 +9,25 @@
|
||||
bottom: $gap-large;
|
||||
left: $gap-large;
|
||||
right: $gap-large;
|
||||
}
|
||||
|
||||
.wc-block-components-notice-snackbar-list .wc-block-components-notice-banner,
|
||||
.wc-block-components-notice-banner.wc-block-components-notice-snackbar {
|
||||
display: inline-flex;
|
||||
width: auto;
|
||||
max-width: 600px;
|
||||
pointer-events: all;
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
margin: $gap-large $gap 0 0;
|
||||
.wc-block-components-notice-banner {
|
||||
display: inline-flex;
|
||||
width: auto;
|
||||
max-width: 600px;
|
||||
pointer-events: all;
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
margin: $gap-large 0 0;
|
||||
|
||||
&.is-error,
|
||||
&.is-info,
|
||||
&.is-success {
|
||||
border-color: transparent;
|
||||
}
|
||||
&.is-default {
|
||||
border-color: $gray-800;
|
||||
}
|
||||
|
||||
&.is-default {
|
||||
border-color: $gray-800;
|
||||
}
|
||||
|
||||
@include breakpoint("<782px") {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
@include breakpoint("<782px") {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { __ } from '@wordpress/i18n';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
import { useCallback, useMemo, useEffect, useRef } from '@wordpress/element';
|
||||
import classnames from 'classnames';
|
||||
import { ValidatedTextInput } from '@woocommerce/blocks-components';
|
||||
import { ValidatedTextInput } from '@woocommerce/blocks-checkout';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
import { useState } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { __TabsWithoutInstanceId as Tabs, TabsProps } from '..';
|
||||
|
||||
export default {
|
||||
title: 'Base Components/Tabs',
|
||||
component: Tabs,
|
||||
args: {
|
||||
tabs: [
|
||||
{
|
||||
name: 'firstTab',
|
||||
title: 'First Tab',
|
||||
content: <div>Content of the first tab</div>,
|
||||
},
|
||||
{
|
||||
name: 'secondTab',
|
||||
title: 'Second Tab',
|
||||
content: <div>Content of the second tab</div>,
|
||||
},
|
||||
],
|
||||
initialTabName: 'firstTab',
|
||||
},
|
||||
argTypes: {
|
||||
initialTabName: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: [ 'firstTab', 'secondTab' ],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as Meta< TabsProps >;
|
||||
|
||||
const Template: Story< TabsProps > = ( args ) => {
|
||||
const [ initialTab, setInitialTab ] = useState( args.initialTabName );
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
initialTabName={ initialTab }
|
||||
onSelect={ ( newTabName ) => {
|
||||
setInitialTab( newTabName );
|
||||
} }
|
||||
{ ...args }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = Template.bind( {} );
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
PAYMENT_STORE_KEY,
|
||||
CART_STORE_KEY,
|
||||
} from '@woocommerce/block-data';
|
||||
import { ValidationInputError } from '@woocommerce/blocks-components';
|
||||
import { ValidationInputError } from '@woocommerce/blocks-checkout';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
||||
@@ -24,6 +24,8 @@ interface CheckoutAddress {
|
||||
setShippingAddress: ( data: Partial< ShippingAddress > ) => void;
|
||||
setBillingAddress: ( data: Partial< BillingAddress > ) => void;
|
||||
setEmail: ( value: string ) => void;
|
||||
setBillingPhone: ( value: string ) => void;
|
||||
setShippingPhone: ( value: string ) => void;
|
||||
useShippingAsBilling: boolean;
|
||||
setUseShippingAsBilling: ( useShippingAsBilling: boolean ) => void;
|
||||
defaultAddressFields: AddressFields;
|
||||
@@ -57,13 +59,28 @@ export const useCheckoutAddress = (): CheckoutAddress => {
|
||||
} = useCustomerData();
|
||||
|
||||
const setEmail = useCallback(
|
||||
( value: string ) =>
|
||||
( value ) =>
|
||||
void setBillingAddress( {
|
||||
email: value,
|
||||
} ),
|
||||
[ setBillingAddress ]
|
||||
);
|
||||
|
||||
const setBillingPhone = useCallback(
|
||||
( value ) =>
|
||||
void setBillingAddress( {
|
||||
phone: value,
|
||||
} ),
|
||||
[ setBillingAddress ]
|
||||
);
|
||||
|
||||
const setShippingPhone = useCallback(
|
||||
( value ) =>
|
||||
void setShippingAddress( {
|
||||
phone: value,
|
||||
} ),
|
||||
[ setShippingAddress ]
|
||||
);
|
||||
const forcedBillingAddress: boolean = getSetting(
|
||||
'forcedBillingAddress',
|
||||
false
|
||||
@@ -74,6 +91,8 @@ export const useCheckoutAddress = (): CheckoutAddress => {
|
||||
setShippingAddress,
|
||||
setBillingAddress,
|
||||
setEmail,
|
||||
setBillingPhone,
|
||||
setShippingPhone,
|
||||
defaultAddressFields,
|
||||
useShippingAsBilling,
|
||||
setUseShippingAsBilling: __internalSetUseShippingAsBilling,
|
||||
@@ -82,8 +101,8 @@ export const useCheckoutAddress = (): CheckoutAddress => {
|
||||
! forcedBillingAddress && needsShipping && ! prefersCollection,
|
||||
showShippingMethods: needsShipping && ! prefersCollection,
|
||||
showBillingFields:
|
||||
! needsShipping || ! useShippingAsBilling || !! prefersCollection,
|
||||
! needsShipping || ! useShippingAsBilling || prefersCollection,
|
||||
forcedBillingAddress,
|
||||
useBillingAsShipping: forcedBillingAddress || !! prefersCollection,
|
||||
useBillingAsShipping: forcedBillingAddress || prefersCollection,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -35,10 +35,12 @@ describe( 'formatError', () => {
|
||||
const mockResponse = new Response( mockMalformedJson, { status: 400 } );
|
||||
|
||||
const error = await formatError( mockResponse );
|
||||
const expectedError = {
|
||||
message:
|
||||
'invalid json response body at reason: Unexpected end of JSON input',
|
||||
type: 'general',
|
||||
};
|
||||
|
||||
expect( error.message ).toContain(
|
||||
'invalid json response body at reason:'
|
||||
);
|
||||
expect( error.type ).toEqual( 'general' );
|
||||
expect( error ).toEqual( expectedError );
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useExpressPaymentMethods } from '@woocommerce/base-context/hooks';
|
||||
import { noticeContexts } from '@woocommerce/base-context';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
|
||||
import LoadingMask from '@woocommerce/base-components/loading-mask';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { CHECKOUT_STORE_KEY, PAYMENT_STORE_KEY } from '@woocommerce/block-data';
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useEditorContext, noticeContexts } from '@woocommerce/base-context';
|
||||
import { Title, StoreNoticesContainer } from '@woocommerce/blocks-components';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
|
||||
import Title from '@woocommerce/base-components/title';
|
||||
import LoadingMask from '@woocommerce/base-components/loading-mask';
|
||||
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings';
|
||||
import { CHECKOUT_STORE_KEY, PAYMENT_STORE_KEY } from '@woocommerce/block-data';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
|
||||
import { noticeContexts } from '@woocommerce/base-context';
|
||||
import { NoticeType } from '@woocommerce/types';
|
||||
interface PaymentMethodErrorBoundaryProps {
|
||||
|
||||
@@ -14,8 +14,10 @@ import {
|
||||
CartProvider,
|
||||
noticeContexts,
|
||||
} from '@woocommerce/base-context';
|
||||
import { SlotFillProvider } from '@woocommerce/blocks-checkout';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
|
||||
import {
|
||||
SlotFillProvider,
|
||||
StoreNoticesContainer,
|
||||
} from '@woocommerce/blocks-checkout';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { TotalsCoupon } from '@woocommerce/base-components/cart-checkout';
|
||||
import { useStoreCartCoupons } from '@woocommerce/base-context/hooks';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import { TotalsWrapper } from '@woocommerce/blocks-components';
|
||||
import { TotalsWrapper } from '@woocommerce/blocks-checkout';
|
||||
|
||||
const Block = ( { className }: { className: string } ): JSX.Element | null => {
|
||||
const couponsEnabled = getSetting( 'couponsEnabled', true );
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
* External dependencies
|
||||
*/
|
||||
import { TotalsDiscount } from '@woocommerce/base-components/cart-checkout';
|
||||
import { TotalsWrapper } from '@woocommerce/blocks-components';
|
||||
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
|
||||
import {
|
||||
useStoreCartCoupons,
|
||||
useStoreCart,
|
||||
} from '@woocommerce/base-context/hooks';
|
||||
import { ExperimentalDiscountsMeta } from '@woocommerce/blocks-checkout';
|
||||
import {
|
||||
ExperimentalDiscountsMeta,
|
||||
TotalsWrapper,
|
||||
} from '@woocommerce/blocks-checkout';
|
||||
|
||||
const DiscountSlotFill = (): JSX.Element => {
|
||||
// Prepare props to pass to the ExperimentalOrderMeta slot fill. We need to pluck out receiveCart.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { TotalsFees, TotalsWrapper } from '@woocommerce/blocks-components';
|
||||
import { TotalsFees, TotalsWrapper } from '@woocommerce/blocks-checkout';
|
||||
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { TotalsShipping } from '@woocommerce/base-components/cart-checkout';
|
||||
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
import { TotalsWrapper } from '@woocommerce/blocks-components';
|
||||
import { TotalsWrapper } from '@woocommerce/blocks-checkout';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
|
||||
const Block = ( { className }: { className: string } ): JSX.Element | null => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Subtotal, TotalsWrapper } from '@woocommerce/blocks-components';
|
||||
import { Subtotal, TotalsWrapper } from '@woocommerce/blocks-checkout';
|
||||
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { TotalsTaxes, TotalsWrapper } from '@woocommerce/blocks-components';
|
||||
import { TotalsTaxes, TotalsWrapper } from '@woocommerce/blocks-checkout';
|
||||
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
|
||||
@@ -96,9 +96,6 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
table.wc-block-cart-items {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.is-medium,
|
||||
|
||||
@@ -7,7 +7,6 @@ import type {
|
||||
CartShippingAddress,
|
||||
CartBillingAddress,
|
||||
} from '@woocommerce/types';
|
||||
import { AddressFields, AddressField } from '@woocommerce/settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
@@ -18,12 +17,12 @@ const AddressCard = ( {
|
||||
address,
|
||||
onEdit,
|
||||
target,
|
||||
fieldConfig,
|
||||
showPhoneField,
|
||||
}: {
|
||||
address: CartShippingAddress | CartBillingAddress;
|
||||
onEdit: () => void;
|
||||
target: string;
|
||||
fieldConfig: Record< keyof AddressFields, Partial< AddressField > >;
|
||||
showPhoneField: boolean;
|
||||
} ): JSX.Element | null => {
|
||||
return (
|
||||
<div className="wc-block-components-address-card">
|
||||
@@ -34,7 +33,7 @@ const AddressCard = ( {
|
||||
<div className="wc-block-components-address-card__address-section">
|
||||
{ [
|
||||
address.address_1,
|
||||
! fieldConfig.address_2.hidden && address.address_2,
|
||||
address.address_2,
|
||||
address.city,
|
||||
address.state,
|
||||
address.postcode,
|
||||
@@ -47,7 +46,7 @@ const AddressCard = ( {
|
||||
<span key={ `address-` + index }>{ field }</span>
|
||||
) ) }
|
||||
</div>
|
||||
{ address.phone && ! fieldConfig.phone.hidden ? (
|
||||
{ address.phone && showPhoneField ? (
|
||||
<div
|
||||
key={ `address-phone` }
|
||||
className="wc-block-components-address-card__address-section"
|
||||
|
||||
@@ -12,8 +12,10 @@ import { CheckoutProvider, noticeContexts } from '@woocommerce/base-context';
|
||||
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
|
||||
import { SidebarLayout } from '@woocommerce/base-components/sidebar-layout';
|
||||
import { CURRENT_USER_IS_ADMIN, getSetting } from '@woocommerce/settings';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
|
||||
import { SlotFillProvider } from '@woocommerce/blocks-checkout';
|
||||
import {
|
||||
SlotFillProvider,
|
||||
StoreNoticesContainer,
|
||||
} from '@woocommerce/blocks-checkout';
|
||||
import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Title } from '@woocommerce/blocks-components';
|
||||
import Title from '@woocommerce/base-components/title';
|
||||
|
||||
/**
|
||||
* Step Heading Component
|
||||
|
||||
@@ -9,8 +9,10 @@ import {
|
||||
} from '@woocommerce/base-components/cart-checkout';
|
||||
import { useCheckoutSubmit } from '@woocommerce/base-context/hooks';
|
||||
import { noticeContexts } from '@woocommerce/base-context';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
|
||||
import { applyCheckoutFilter } from '@woocommerce/blocks-checkout';
|
||||
import {
|
||||
StoreNoticesContainer,
|
||||
applyCheckoutFilter,
|
||||
} from '@woocommerce/blocks-checkout';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
||||
@@ -14,10 +14,9 @@ import type {
|
||||
AddressField,
|
||||
AddressFields,
|
||||
} from '@woocommerce/settings';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { CART_STORE_KEY } from '@woocommerce/block-data';
|
||||
import isShallowEqual from '@wordpress/is-shallow-equal';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
@@ -30,19 +29,17 @@ const Block = ( {
|
||||
showPhoneField = false,
|
||||
requireCompanyField = false,
|
||||
requirePhoneField = false,
|
||||
forceEditing = false,
|
||||
}: {
|
||||
showCompanyField: boolean;
|
||||
showApartmentField: boolean;
|
||||
showPhoneField: boolean;
|
||||
requireCompanyField: boolean;
|
||||
requirePhoneField: boolean;
|
||||
forceEditing?: boolean;
|
||||
} ): JSX.Element => {
|
||||
const {
|
||||
shippingAddress,
|
||||
billingAddress,
|
||||
setShippingAddress,
|
||||
useBillingAsShipping,
|
||||
} = useCheckoutAddress();
|
||||
const { billingAddress, setShippingAddress, useBillingAsShipping } =
|
||||
useCheckoutAddress();
|
||||
const { isEditor } = useEditorContext();
|
||||
|
||||
// Syncs shipping address with billing address if "Force shipping to the customer billing address" is enabled.
|
||||
@@ -74,17 +71,11 @@ const Block = ( {
|
||||
address_2: {
|
||||
hidden: ! showApartmentField,
|
||||
},
|
||||
phone: {
|
||||
hidden: ! showPhoneField,
|
||||
required: requirePhoneField,
|
||||
},
|
||||
};
|
||||
}, [
|
||||
showCompanyField,
|
||||
requireCompanyField,
|
||||
showApartmentField,
|
||||
showPhoneField,
|
||||
requirePhoneField,
|
||||
] ) as Record< keyof AddressFields, Partial< AddressField > >;
|
||||
|
||||
const WrapperComponent = isEditor ? Noninteractive : Fragment;
|
||||
@@ -98,20 +89,6 @@ const Block = ( {
|
||||
cartDataLoaded: store.hasFinishedResolution( 'getCartData' ),
|
||||
};
|
||||
} );
|
||||
|
||||
// Default editing state for CustomerAddress component comes from the current address and whether or not we're in the editor.
|
||||
const hasAddress = !! (
|
||||
billingAddress.address_1 &&
|
||||
( billingAddress.first_name || billingAddress.last_name )
|
||||
);
|
||||
const { email, ...billingAddressWithoutEmail } = billingAddress;
|
||||
const billingMatchesShipping = isShallowEqual(
|
||||
billingAddressWithoutEmail,
|
||||
shippingAddress
|
||||
);
|
||||
const defaultEditingAddress =
|
||||
isEditor || ! hasAddress || billingMatchesShipping;
|
||||
|
||||
return (
|
||||
<>
|
||||
<StoreNoticesContainer context={ noticeContext } />
|
||||
@@ -119,7 +96,9 @@ const Block = ( {
|
||||
{ cartDataLoaded ? (
|
||||
<CustomerAddress
|
||||
addressFieldsConfig={ addressFieldsConfig }
|
||||
defaultEditing={ defaultEditingAddress }
|
||||
showPhoneField={ showPhoneField }
|
||||
requirePhoneField={ requirePhoneField }
|
||||
forceEditing={ forceEditing }
|
||||
/>
|
||||
) : null }
|
||||
</WrapperComponent>
|
||||
|
||||
@@ -16,24 +16,35 @@ import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||
* Internal dependencies
|
||||
*/
|
||||
import AddressWrapper from '../../address-wrapper';
|
||||
import PhoneNumber from '../../phone-number';
|
||||
import AddressCard from '../../address-card';
|
||||
|
||||
const CustomerAddress = ( {
|
||||
addressFieldsConfig,
|
||||
defaultEditing = false,
|
||||
showPhoneField,
|
||||
requirePhoneField,
|
||||
forceEditing = false,
|
||||
}: {
|
||||
addressFieldsConfig: Record< keyof AddressFields, Partial< AddressField > >;
|
||||
defaultEditing?: boolean;
|
||||
showPhoneField: boolean;
|
||||
requirePhoneField: boolean;
|
||||
forceEditing?: boolean;
|
||||
} ) => {
|
||||
const {
|
||||
defaultAddressFields,
|
||||
billingAddress,
|
||||
setShippingAddress,
|
||||
setBillingAddress,
|
||||
setBillingPhone,
|
||||
setShippingPhone,
|
||||
useBillingAsShipping,
|
||||
} = useCheckoutAddress();
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
const [ editing, setEditing ] = useState( defaultEditing );
|
||||
const hasAddress = !! (
|
||||
billingAddress.address_1 &&
|
||||
( billingAddress.first_name || billingAddress.last_name )
|
||||
);
|
||||
const [ editing, setEditing ] = useState( ! hasAddress || forceEditing );
|
||||
|
||||
// Forces editing state if store has errors.
|
||||
const { hasValidationErrors, invalidProps } = useSelect( ( select ) => {
|
||||
@@ -43,9 +54,8 @@ const CustomerAddress = ( {
|
||||
invalidProps: Object.keys( billingAddress )
|
||||
.filter( ( key ) => {
|
||||
return (
|
||||
key !== 'email' &&
|
||||
store.getValidationError( 'billing_' + key ) !==
|
||||
undefined
|
||||
undefined
|
||||
);
|
||||
} )
|
||||
.filter( Boolean ),
|
||||
@@ -87,10 +97,10 @@ const CustomerAddress = ( {
|
||||
onEdit={ () => {
|
||||
setEditing( true );
|
||||
} }
|
||||
fieldConfig={ addressFieldsConfig }
|
||||
showPhoneField={ showPhoneField }
|
||||
/>
|
||||
),
|
||||
[ billingAddress, addressFieldsConfig ]
|
||||
[ billingAddress, showPhoneField ]
|
||||
);
|
||||
|
||||
const renderAddressFormComponent = useCallback(
|
||||
@@ -104,13 +114,39 @@ const CustomerAddress = ( {
|
||||
fields={ addressFieldKeys }
|
||||
fieldConfig={ addressFieldsConfig }
|
||||
/>
|
||||
{ showPhoneField && (
|
||||
<PhoneNumber
|
||||
id="billing-phone"
|
||||
errorId={ 'billing_phone' }
|
||||
isRequired={ requirePhoneField }
|
||||
value={ billingAddress.phone }
|
||||
onChange={ ( value ) => {
|
||||
setBillingPhone( value );
|
||||
dispatchCheckoutEvent( 'set-phone-number', {
|
||||
step: 'billing',
|
||||
} );
|
||||
if ( useBillingAsShipping ) {
|
||||
setShippingPhone( value );
|
||||
dispatchCheckoutEvent( 'set-phone-number', {
|
||||
step: 'billing',
|
||||
} );
|
||||
}
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
</>
|
||||
),
|
||||
[
|
||||
addressFieldKeys,
|
||||
addressFieldsConfig,
|
||||
billingAddress,
|
||||
dispatchCheckoutEvent,
|
||||
onChangeAddress,
|
||||
requirePhoneField,
|
||||
setBillingPhone,
|
||||
setShippingPhone,
|
||||
showPhoneField,
|
||||
useBillingAsShipping,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { useRef, useEffect } from '@wordpress/element';
|
||||
import { withFilteredAttributes } from '@woocommerce/shared-hocs';
|
||||
import { FormStep } from '@woocommerce/blocks-components';
|
||||
import { useCheckoutAddress } from '@woocommerce/base-context/hooks';
|
||||
@@ -42,8 +43,21 @@ const FrontendBlock = ( {
|
||||
showCompanyField,
|
||||
showPhoneField,
|
||||
} = useCheckoutBlockContext();
|
||||
const { showBillingFields, forcedBillingAddress, useBillingAsShipping } =
|
||||
useCheckoutAddress();
|
||||
const {
|
||||
showBillingFields,
|
||||
forcedBillingAddress,
|
||||
useBillingAsShipping,
|
||||
useShippingAsBilling,
|
||||
} = useCheckoutAddress();
|
||||
|
||||
// If initial state was true, force editing to true so address fields are visible if the useShippingAsBilling option is unchecked.
|
||||
const toggledUseShippingAsBilling = useRef( useShippingAsBilling );
|
||||
|
||||
useEffect( () => {
|
||||
if ( useShippingAsBilling ) {
|
||||
toggledUseShippingAsBilling.current = true;
|
||||
}
|
||||
}, [ useShippingAsBilling ] );
|
||||
|
||||
if ( ! showBillingFields && ! useBillingAsShipping ) {
|
||||
return null;
|
||||
@@ -72,6 +86,7 @@ const FrontendBlock = ( {
|
||||
showCompanyField={ showCompanyField }
|
||||
showPhoneField={ showPhoneField }
|
||||
requirePhoneField={ requirePhoneField }
|
||||
forceEditing={ toggledUseShippingAsBilling.current }
|
||||
/>
|
||||
{ children }
|
||||
</FormStep>
|
||||
|
||||
@@ -8,15 +8,19 @@ import {
|
||||
noticeContexts,
|
||||
} from '@woocommerce/base-context';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import { CheckboxControl } from '@woocommerce/blocks-checkout';
|
||||
import {
|
||||
StoreNoticesContainer,
|
||||
CheckboxControl,
|
||||
ValidatedTextInput,
|
||||
} from '@woocommerce/blocks-components';
|
||||
StoreNoticesContainer,
|
||||
} from '@woocommerce/blocks-checkout';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
|
||||
import { isEmail } from '@wordpress/url';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
|
||||
const Block = (): JSX.Element => {
|
||||
const { customerId, shouldCreateAccount } = useSelect( ( select ) => {
|
||||
const store = select( CHECKOUT_STORE_KEY );
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { Main } from '@woocommerce/base-components/sidebar-layout';
|
||||
import { useStoreEvents } from '@woocommerce/base-context/hooks';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
|
||||
const FrontendBlock = ( {
|
||||
children,
|
||||
@@ -13,14 +11,6 @@ const FrontendBlock = ( {
|
||||
children: JSX.Element;
|
||||
className?: string;
|
||||
} ): JSX.Element => {
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
|
||||
// Ignore changes to dispatchCheckoutEvent callback so this is ran on first mount only.
|
||||
useEffect( () => {
|
||||
dispatchCheckoutEvent( 'render-checkout-form' );
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [] );
|
||||
|
||||
return (
|
||||
<Main className={ classnames( 'wc-block-checkout__main', className ) }>
|
||||
<form className="wc-block-components-form wc-block-checkout__form">
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 0 calc(#{$gap-smaller} * 2); // Required for spacing especially when using flex-grow
|
||||
|
||||
.wc-block-components-text-input,
|
||||
.wc-block-components-country-input,
|
||||
.wc-block-components-state-input {
|
||||
flex: 1 0 calc(50% - #{$gap-smaller}); // "flex-grow = 1" allows the input to grow to fill the space
|
||||
flex: 0 0 calc(50% - #{$gap-smaller});
|
||||
box-sizing: border-box;
|
||||
|
||||
&:nth-of-type(2),
|
||||
@@ -33,8 +33,7 @@
|
||||
|
||||
.wc-block-components-address-form__company,
|
||||
.wc-block-components-address-form__address_1,
|
||||
.wc-block-components-address-form__address_2,
|
||||
.wc-block-components-country-input {
|
||||
.wc-block-components-address-form__address_2 {
|
||||
flex: 0 0 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import { OrderSummary } from '@woocommerce/base-components/cart-checkout';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
import { TotalsWrapper } from '@woocommerce/blocks-components';
|
||||
import { TotalsWrapper } from '@woocommerce/blocks-checkout';
|
||||
|
||||
const Block = ( { className }: { className: string } ): JSX.Element => {
|
||||
const { cartItems } = useStoreCart();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { TotalsCoupon } from '@woocommerce/base-components/cart-checkout';
|
||||
import { useStoreCartCoupons } from '@woocommerce/base-context/hooks';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import { TotalsWrapper } from '@woocommerce/blocks-components';
|
||||
import { TotalsWrapper } from '@woocommerce/blocks-checkout';
|
||||
|
||||
const Block = ( {
|
||||
className = '',
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
* External dependencies
|
||||
*/
|
||||
import { TotalsDiscount } from '@woocommerce/base-components/cart-checkout';
|
||||
import { TotalsWrapper } from '@woocommerce/blocks-components';
|
||||
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
|
||||
import {
|
||||
useStoreCartCoupons,
|
||||
useStoreCart,
|
||||
} from '@woocommerce/base-context/hooks';
|
||||
import { ExperimentalDiscountsMeta } from '@woocommerce/blocks-checkout';
|
||||
import {
|
||||
ExperimentalDiscountsMeta,
|
||||
TotalsWrapper,
|
||||
} from '@woocommerce/blocks-checkout';
|
||||
|
||||
const DiscountSlotFill = (): JSX.Element => {
|
||||
// Prepare props to pass to the ExperimentalOrderMeta slot fill. We need to pluck out receiveCart.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { TotalsFees, TotalsWrapper } from '@woocommerce/blocks-components';
|
||||
import { TotalsFees, TotalsWrapper } from '@woocommerce/blocks-checkout';
|
||||
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Subtotal, TotalsWrapper } from '@woocommerce/blocks-components';
|
||||
import { Subtotal, TotalsWrapper } from '@woocommerce/blocks-checkout';
|
||||
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { TotalsTaxes, TotalsWrapper } from '@woocommerce/blocks-components';
|
||||
import { TotalsTaxes, TotalsWrapper } from '@woocommerce/blocks-checkout';
|
||||
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
|
||||
@@ -4,12 +4,10 @@
|
||||
import classnames from 'classnames';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
import { withFilteredAttributes } from '@woocommerce/shared-hocs';
|
||||
import {
|
||||
FormStep,
|
||||
StoreNoticesContainer,
|
||||
} from '@woocommerce/blocks-components';
|
||||
import { FormStep } from '@woocommerce/blocks-components';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
|
||||
import { noticeContexts } from '@woocommerce/base-context';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,68 +1,66 @@
|
||||
.wc-block-checkout__pickup-options,
|
||||
.wp-block-woocommerce-checkout-pickup-options-block {
|
||||
.wc-block-components-local-pickup-rates-control {
|
||||
.wc-block-components-radio-control__option {
|
||||
@include with-translucent-border(0 0 1px);
|
||||
margin: 0;
|
||||
padding: em($gap-small) 0 em($gap-small) em($gap-largest);
|
||||
.wc-block-components-radio-control__option {
|
||||
@include with-translucent-border(0 0 1px);
|
||||
margin: 0;
|
||||
padding: em($gap-small) 0 em($gap-small) em($gap-largest);
|
||||
}
|
||||
.wc-block-components-shipping-rates-control__no-results-notice {
|
||||
margin: em($gap-small) 0;
|
||||
}
|
||||
.wc-block-components-radio-control .wc-block-components-radio-control__input {
|
||||
top: auto;
|
||||
transform: none;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.wc-block-components-radio-control__option-layout {
|
||||
display: block;
|
||||
}
|
||||
.wc-block-components-radio-control__label-group {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
> :last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
.wc-block-components-shipping-rates-control__no-results-notice {
|
||||
margin: em($gap-small) 0;
|
||||
}
|
||||
.wc-block-components-radio-control .wc-block-components-radio-control__input {
|
||||
top: auto;
|
||||
transform: none;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.wc-block-components-radio-control__option-layout {
|
||||
}
|
||||
.wc-block-components-radio-control__description-group {
|
||||
display: none;
|
||||
}
|
||||
.wc-block-components-radio-control__option-checked {
|
||||
.wc-block-components-radio-control__description-group {
|
||||
display: block;
|
||||
}
|
||||
.wc-block-components-radio-control__label-group {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
.wc-block-components-radio-control__label-group {
|
||||
em {
|
||||
text-transform: uppercase;
|
||||
font-style: inherit;
|
||||
}
|
||||
}
|
||||
.wc-block-components-radio-control__description-group {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background-color: $gray-100;
|
||||
border-radius: $universal-border-radius;
|
||||
padding: 1px em($gap-small);
|
||||
margin-top: em($gap-smaller);
|
||||
@include font-size(regular);
|
||||
}
|
||||
.wc-block-components-radio-control__description,
|
||||
.wc-block-components-radio-control__secondary-description {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
margin: em($gap-small) 0;
|
||||
display: block;
|
||||
}
|
||||
.wc-block-components-radio-control__secondary-description {
|
||||
color: $gray-700;
|
||||
|
||||
> :last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
.wc-block-components-radio-control__description-group {
|
||||
display: none;
|
||||
}
|
||||
.wc-block-components-radio-control__option-checked {
|
||||
.wc-block-components-radio-control__description-group {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.wc-block-components-radio-control__label-group {
|
||||
em {
|
||||
text-transform: uppercase;
|
||||
font-style: inherit;
|
||||
}
|
||||
}
|
||||
.wc-block-components-radio-control__description-group {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background-color: $gray-100;
|
||||
border-radius: $universal-border-radius;
|
||||
padding: 1px em($gap-small);
|
||||
margin-top: em($gap-smaller);
|
||||
@include font-size(regular);
|
||||
}
|
||||
.wc-block-components-radio-control__description,
|
||||
.wc-block-components-radio-control__secondary-description {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
margin: em($gap-small) 0;
|
||||
display: block;
|
||||
}
|
||||
.wc-block-components-radio-control__secondary-description {
|
||||
color: $gray-700;
|
||||
|
||||
> svg {
|
||||
vertical-align: middle;
|
||||
margin-top: -4px;
|
||||
fill: currentColor;
|
||||
}
|
||||
> svg {
|
||||
vertical-align: middle;
|
||||
margin-top: -4px;
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,10 @@ import {
|
||||
useEditorContext,
|
||||
noticeContexts,
|
||||
} from '@woocommerce/base-context';
|
||||
import { CheckboxControl } from '@woocommerce/blocks-checkout';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
|
||||
import {
|
||||
CheckboxControl,
|
||||
StoreNoticesContainer,
|
||||
} from '@woocommerce/blocks-checkout';
|
||||
import Noninteractive from '@woocommerce/base-components/noninteractive';
|
||||
import type {
|
||||
BillingAddress,
|
||||
@@ -80,17 +82,11 @@ const Block = ( {
|
||||
address_2: {
|
||||
hidden: ! showApartmentField,
|
||||
},
|
||||
phone: {
|
||||
hidden: ! showPhoneField,
|
||||
required: requirePhoneField,
|
||||
},
|
||||
};
|
||||
}, [
|
||||
showCompanyField,
|
||||
requireCompanyField,
|
||||
showApartmentField,
|
||||
showPhoneField,
|
||||
requirePhoneField,
|
||||
] ) as Record< keyof AddressFields, Partial< AddressField > >;
|
||||
|
||||
const WrapperComponent = isEditor ? Noninteractive : Fragment;
|
||||
@@ -109,9 +105,6 @@ const Block = ( {
|
||||
};
|
||||
} );
|
||||
|
||||
// Default editing state for CustomerAddress component comes from the current address and whether or not we're in the editor.
|
||||
const defaultEditingAddress = isEditor || ! hasAddress;
|
||||
|
||||
return (
|
||||
<>
|
||||
<StoreNoticesContainer context={ noticeContext } />
|
||||
@@ -119,24 +112,27 @@ const Block = ( {
|
||||
{ cartDataLoaded ? (
|
||||
<CustomerAddress
|
||||
addressFieldsConfig={ addressFieldsConfig }
|
||||
defaultEditing={ defaultEditingAddress }
|
||||
showPhoneField={ showPhoneField }
|
||||
requirePhoneField={ requirePhoneField }
|
||||
/>
|
||||
) : null }
|
||||
</WrapperComponent>
|
||||
<CheckboxControl
|
||||
className="wc-block-checkout__use-address-for-billing"
|
||||
label={ __(
|
||||
'Use same address for billing',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
checked={ useShippingAsBilling }
|
||||
onChange={ ( checked: boolean ) => {
|
||||
setUseShippingAsBilling( checked );
|
||||
if ( checked ) {
|
||||
syncBillingWithShipping();
|
||||
}
|
||||
} }
|
||||
/>
|
||||
{ hasAddress && (
|
||||
<CheckboxControl
|
||||
className="wc-block-checkout__use-address-for-billing"
|
||||
label={ __(
|
||||
'Use same address for billing',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
checked={ useShippingAsBilling }
|
||||
onChange={ ( checked: boolean ) => {
|
||||
setUseShippingAsBilling( checked );
|
||||
if ( checked ) {
|
||||
syncBillingWithShipping();
|
||||
}
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
*/
|
||||
import { useState, useCallback, useEffect } from '@wordpress/element';
|
||||
import { AddressForm } from '@woocommerce/base-components/cart-checkout';
|
||||
import { useCheckoutAddress, useStoreEvents } from '@woocommerce/base-context';
|
||||
import {
|
||||
useCheckoutAddress,
|
||||
useStoreEvents,
|
||||
useEditorContext,
|
||||
} from '@woocommerce/base-context';
|
||||
import type {
|
||||
ShippingAddress,
|
||||
AddressField,
|
||||
@@ -16,24 +20,34 @@ import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||
* Internal dependencies
|
||||
*/
|
||||
import AddressWrapper from '../../address-wrapper';
|
||||
import PhoneNumber from '../../phone-number';
|
||||
import AddressCard from '../../address-card';
|
||||
|
||||
const CustomerAddress = ( {
|
||||
addressFieldsConfig,
|
||||
defaultEditing = false,
|
||||
showPhoneField,
|
||||
requirePhoneField,
|
||||
}: {
|
||||
addressFieldsConfig: Record< keyof AddressFields, Partial< AddressField > >;
|
||||
defaultEditing?: boolean;
|
||||
showPhoneField: boolean;
|
||||
requirePhoneField: boolean;
|
||||
} ) => {
|
||||
const {
|
||||
defaultAddressFields,
|
||||
shippingAddress,
|
||||
setShippingAddress,
|
||||
setBillingAddress,
|
||||
setShippingPhone,
|
||||
setBillingPhone,
|
||||
useShippingAsBilling,
|
||||
} = useCheckoutAddress();
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
const [ editing, setEditing ] = useState( defaultEditing );
|
||||
const { isEditor } = useEditorContext();
|
||||
const hasAddress = !! (
|
||||
shippingAddress.address_1 &&
|
||||
( shippingAddress.first_name || shippingAddress.last_name )
|
||||
);
|
||||
const [ editing, setEditing ] = useState( ! hasAddress || isEditor );
|
||||
|
||||
// Forces editing state if store has errors.
|
||||
const { hasValidationErrors, invalidProps } = useSelect( ( select ) => {
|
||||
@@ -60,11 +74,19 @@ const CustomerAddress = ( {
|
||||
const addressFieldKeys = Object.keys(
|
||||
defaultAddressFields
|
||||
) as ( keyof AddressFields )[];
|
||||
|
||||
const onChangeAddress = useCallback(
|
||||
( values: Partial< ShippingAddress > ) => {
|
||||
setShippingAddress( values );
|
||||
if ( useShippingAsBilling ) {
|
||||
setBillingAddress( values );
|
||||
// Sync billing with shipping. Ensure unwanted properties are omitted.
|
||||
const { ...syncBilling } = values;
|
||||
|
||||
if ( ! showPhoneField ) {
|
||||
delete syncBilling.phone;
|
||||
}
|
||||
|
||||
setBillingAddress( syncBilling );
|
||||
dispatchCheckoutEvent( 'set-billing-address' );
|
||||
}
|
||||
dispatchCheckoutEvent( 'set-shipping-address' );
|
||||
@@ -74,6 +96,7 @@ const CustomerAddress = ( {
|
||||
setBillingAddress,
|
||||
setShippingAddress,
|
||||
useShippingAsBilling,
|
||||
showPhoneField,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -85,28 +108,56 @@ const CustomerAddress = ( {
|
||||
onEdit={ () => {
|
||||
setEditing( true );
|
||||
} }
|
||||
fieldConfig={ addressFieldsConfig }
|
||||
showPhoneField={ showPhoneField }
|
||||
/>
|
||||
),
|
||||
[ shippingAddress, addressFieldsConfig ]
|
||||
[ shippingAddress, showPhoneField ]
|
||||
);
|
||||
|
||||
const renderAddressFormComponent = useCallback(
|
||||
() => (
|
||||
<AddressForm
|
||||
id="shipping"
|
||||
type="shipping"
|
||||
onChange={ onChangeAddress }
|
||||
values={ shippingAddress }
|
||||
fields={ addressFieldKeys }
|
||||
fieldConfig={ addressFieldsConfig }
|
||||
/>
|
||||
<>
|
||||
<AddressForm
|
||||
id="shipping"
|
||||
type="shipping"
|
||||
onChange={ onChangeAddress }
|
||||
values={ shippingAddress }
|
||||
fields={ addressFieldKeys }
|
||||
fieldConfig={ addressFieldsConfig }
|
||||
/>
|
||||
{ showPhoneField && (
|
||||
<PhoneNumber
|
||||
id="shipping-phone"
|
||||
errorId={ 'shipping_phone' }
|
||||
isRequired={ requirePhoneField }
|
||||
value={ shippingAddress.phone }
|
||||
onChange={ ( value ) => {
|
||||
setShippingPhone( value );
|
||||
dispatchCheckoutEvent( 'set-phone-number', {
|
||||
step: 'shipping',
|
||||
} );
|
||||
if ( useShippingAsBilling ) {
|
||||
setBillingPhone( value );
|
||||
dispatchCheckoutEvent( 'set-phone-number', {
|
||||
step: 'billing',
|
||||
} );
|
||||
}
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
</>
|
||||
),
|
||||
[
|
||||
addressFieldKeys,
|
||||
addressFieldsConfig,
|
||||
dispatchCheckoutEvent,
|
||||
onChangeAddress,
|
||||
requirePhoneField,
|
||||
setBillingPhone,
|
||||
setShippingPhone,
|
||||
shippingAddress,
|
||||
showPhoneField,
|
||||
useShippingAsBilling,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -13,11 +13,9 @@ import {
|
||||
isAddressComplete,
|
||||
} from '@woocommerce/base-utils';
|
||||
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
|
||||
import {
|
||||
FormattedMonetaryAmount,
|
||||
StoreNoticesContainer,
|
||||
} from '@woocommerce/blocks-components';
|
||||
import { FormattedMonetaryAmount } from '@woocommerce/blocks-components';
|
||||
import { useEditorContext, noticeContexts } from '@woocommerce/base-context';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { Sidebar } from '@woocommerce/base-components/sidebar-layout';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
|
||||
|
||||
const FrontendBlock = ( {
|
||||
children,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { CheckboxControl } from '@woocommerce/blocks-checkout';
|
||||
import { Textarea } from '@woocommerce/blocks-components';
|
||||
import { Textarea } from '@woocommerce/base-components/textarea';
|
||||
|
||||
interface CheckoutOrderNotesProps {
|
||||
disabled: boolean;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { ValidatedTextInput } from '@woocommerce/blocks-components';
|
||||
import { ValidatedTextInput } from '@woocommerce/blocks-checkout';
|
||||
|
||||
/**
|
||||
* Renders a phone number input.
|
||||
|
||||
@@ -4,30 +4,14 @@
|
||||
"title": "Collection Filters",
|
||||
"description": "A block that adds product filters to the product collection.",
|
||||
"category": "woocommerce",
|
||||
"keywords": [
|
||||
"WooCommerce",
|
||||
"Filters"
|
||||
],
|
||||
"keywords": [ "WooCommerce", "Filters" ],
|
||||
"textdomain": "woocommerce",
|
||||
"supports": {
|
||||
"html": false,
|
||||
"reusable": false
|
||||
},
|
||||
"usesContext": [
|
||||
"query"
|
||||
],
|
||||
"providesContext": {
|
||||
"collectionData": "collectionData"
|
||||
},
|
||||
"ancestor": [
|
||||
"woocommerce/product-collection"
|
||||
],
|
||||
"attributes": {
|
||||
"collectionData": {
|
||||
"type": "object",
|
||||
"default": {}
|
||||
}
|
||||
},
|
||||
"usesContext": [ "query" ],
|
||||
"ancestor": [ "woocommerce/product-collection" ],
|
||||
"apiVersion": 2,
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json"
|
||||
}
|
||||
|
||||
@@ -2,40 +2,11 @@
|
||||
* External dependencies
|
||||
*/
|
||||
import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import { useCollection } from '@woocommerce/base-context/hooks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { formatQuery, getQueryParams } from './utils';
|
||||
import type { EditProps } from './type';
|
||||
|
||||
const Edit = ( { clientId, setAttributes, context }: EditProps ) => {
|
||||
const Edit = () => {
|
||||
const blockProps = useBlockProps();
|
||||
const innerBlockProps = useInnerBlocksProps( blockProps );
|
||||
|
||||
// Get inner blocks by clientId
|
||||
const currentBlock = useSelect( ( select ) => {
|
||||
return select( 'core/block-editor' ).getBlock( clientId );
|
||||
} );
|
||||
|
||||
const { results } = useCollection( {
|
||||
namespace: '/wc/store/v1',
|
||||
resourceName: 'products/collection-data',
|
||||
query: {
|
||||
...formatQuery( context.query ),
|
||||
...getQueryParams( currentBlock ),
|
||||
},
|
||||
} );
|
||||
|
||||
useEffect( () => {
|
||||
setAttributes( {
|
||||
collectionData: results,
|
||||
} );
|
||||
}, [ results, setAttributes ] );
|
||||
|
||||
return <nav { ...innerBlockProps } />;
|
||||
};
|
||||
|
||||
|
||||
@@ -10,22 +10,16 @@
|
||||
],
|
||||
"textdomain": "woocommerce",
|
||||
"apiVersion": 2,
|
||||
"viewScript": [
|
||||
"wc-collection-price-filter-block-frontend"
|
||||
],
|
||||
"ancestor": [
|
||||
"woocommerce/collection-filters"
|
||||
"woocommerce/product-collection"
|
||||
],
|
||||
"supports": {
|
||||
"interactivity": true
|
||||
},
|
||||
"usesContext": [
|
||||
"collectionData"
|
||||
],
|
||||
"attributes": {
|
||||
"queryParam": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"calculate_price_range": "true"
|
||||
}
|
||||
},
|
||||
"showInputFields": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user