rebase on oct-10-2023
This commit is contained in:
@@ -73,7 +73,7 @@ const ActiveAttributeFilters = ( {
|
||||
const attributeLabel = attributeObject.label;
|
||||
|
||||
const filteringForPhpTemplate = getSettingWithCoercion(
|
||||
'is_rendering_php_template',
|
||||
'isRenderingPhpTemplate',
|
||||
false,
|
||||
isBoolean
|
||||
);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useColorProps } from '@woocommerce/base-hooks';
|
||||
import classnames from 'classnames';
|
||||
import { useStyleProps } from '@woocommerce/base-hooks';
|
||||
import { isString } from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
@@ -11,14 +12,18 @@ import Block from './block';
|
||||
import { parseAttributes } from './utils';
|
||||
|
||||
const BlockWrapper = ( props: Record< string, unknown > ) => {
|
||||
const colorProps = useColorProps( props );
|
||||
const styleProps = useStyleProps( props );
|
||||
const parsedBlockAttributes = parseAttributes( props );
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ isString( props.className ) ? props.className : '' }
|
||||
style={ { ...colorProps.style } }
|
||||
className={ classnames(
|
||||
isString( props.className ) ? props.className : '',
|
||||
styleProps.className
|
||||
) }
|
||||
style={ styleProps.style }
|
||||
>
|
||||
<Block isEditor={ false } attributes={ parseAttributes( props ) } />
|
||||
<Block isEditor={ false } attributes={ parsedBlockAttributes } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useQueryStateByKey } from '@woocommerce/base-context/hooks';
|
||||
import { getSetting, getSettingWithCoercion } from '@woocommerce/settings';
|
||||
import { useMemo, useEffect, useState } from '@wordpress/element';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import Label from '@woocommerce/base-components/label';
|
||||
import {
|
||||
isAttributeQueryCollection,
|
||||
@@ -18,6 +17,7 @@ import {
|
||||
import { getUrlParameter } from '@woocommerce/utils';
|
||||
import FilterTitlePlaceholder from '@woocommerce/base-components/filter-placeholder';
|
||||
import { useIsMounted } from '@woocommerce/base-hooks';
|
||||
import type { BlockAttributes } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
@@ -31,31 +31,35 @@ import {
|
||||
cleanFilterUrl,
|
||||
maybeUrlContainsFilters,
|
||||
urlContainsAttributeFilter,
|
||||
StoreAttributes,
|
||||
} from './utils';
|
||||
import ActiveAttributeFilters from './active-attribute-filters';
|
||||
import FilterPlaceholders from './filter-placeholders';
|
||||
import { Attributes } from './types';
|
||||
import { useSetWraperVisibility } from '../filter-wrapper/context';
|
||||
|
||||
interface ActiveFiltersBlockProps {
|
||||
/**
|
||||
* The attributes for this block.
|
||||
*/
|
||||
attributes: BlockAttributes;
|
||||
/**
|
||||
* Whether it's in the editor or frontend display.
|
||||
*/
|
||||
isEditor: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component displaying active filters.
|
||||
*
|
||||
* @param {Object} props Incoming props for the component.
|
||||
* @param {Object} props.attributes Incoming attributes for the block.
|
||||
* @param {boolean} props.isEditor Whether or not in the editor context.
|
||||
*/
|
||||
const ActiveFiltersBlock = ( {
|
||||
attributes: blockAttributes,
|
||||
isEditor = false,
|
||||
}: {
|
||||
attributes: Attributes;
|
||||
isEditor?: boolean;
|
||||
} ) => {
|
||||
}: ActiveFiltersBlockProps ) => {
|
||||
const setWrapperVisibility = useSetWraperVisibility();
|
||||
const isMounted = useIsMounted();
|
||||
const componentHasMounted = isMounted();
|
||||
const filteringForPhpTemplate = getSettingWithCoercion(
|
||||
'is_rendering_php_template',
|
||||
'isRenderingPhpTemplate',
|
||||
false,
|
||||
isBoolean
|
||||
);
|
||||
@@ -81,7 +85,7 @@ const ActiveFiltersBlock = ( {
|
||||
useQueryStateByKey( 'rating' );
|
||||
|
||||
const STOCK_STATUS_OPTIONS = getSetting( 'stockStatusOptions', [] );
|
||||
const STORE_ATTRIBUTES = getSetting( 'attributes', [] );
|
||||
const STORE_ATTRIBUTES: StoreAttributes[] = getSetting( 'attributes', [] );
|
||||
const activeStockStatusFilters = useMemo( () => {
|
||||
if (
|
||||
shouldShowLoadingPlaceholders ||
|
||||
@@ -319,7 +323,7 @@ const ActiveFiltersBlock = ( {
|
||||
);
|
||||
|
||||
const hasFilterableProducts = getSettingWithCoercion(
|
||||
'has_filterable_products',
|
||||
'hasFilterableProducts',
|
||||
false,
|
||||
isBoolean
|
||||
);
|
||||
@@ -413,15 +417,4 @@ const ActiveFiltersBlock = ( {
|
||||
);
|
||||
};
|
||||
|
||||
ActiveFiltersBlock.propTypes = {
|
||||
/**
|
||||
* The attributes for this block.
|
||||
*/
|
||||
attributes: PropTypes.object.isRequired,
|
||||
/**
|
||||
* Whether it's in the editor or frontend display.
|
||||
*/
|
||||
isEditor: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ActiveFiltersBlock;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import classNames from 'classnames';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { blockAttributes } from './attributes';
|
||||
import metadata from './block.json';
|
||||
import { Attributes } from './types';
|
||||
|
||||
const v1 = {
|
||||
attributes: {
|
||||
...metadata.attributes,
|
||||
...blockAttributes,
|
||||
},
|
||||
save: ( { attributes }: { attributes: Attributes } ) => {
|
||||
const { className, displayStyle, heading, headingLevel } = attributes;
|
||||
const data = {
|
||||
'data-display-style': displayStyle,
|
||||
'data-heading': heading,
|
||||
'data-heading-level': headingLevel,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
{ ...useBlockProps.save( {
|
||||
className: classNames( 'is-loading', className ),
|
||||
} ) }
|
||||
{ ...data }
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="wc-block-active-filters__placeholder"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const deprecated = [ v1 ];
|
||||
|
||||
export default deprecated;
|
||||
@@ -14,6 +14,7 @@ import edit from './edit';
|
||||
import metadata from './block.json';
|
||||
import { blockAttributes } from './attributes';
|
||||
import { Attributes } from './types';
|
||||
import deprecated from './deprecated';
|
||||
|
||||
registerBlockType( metadata, {
|
||||
icon: {
|
||||
@@ -31,19 +32,13 @@ registerBlockType( metadata, {
|
||||
edit,
|
||||
// Save the props to post content.
|
||||
save( { attributes }: { attributes: Attributes } ) {
|
||||
const { className, displayStyle, heading, headingLevel } = attributes;
|
||||
const data = {
|
||||
'data-display-style': displayStyle,
|
||||
'data-heading': heading,
|
||||
'data-heading-level': headingLevel,
|
||||
};
|
||||
const { className } = attributes;
|
||||
|
||||
return (
|
||||
<div
|
||||
{ ...useBlockProps.save( {
|
||||
className: classNames( 'is-loading', className ),
|
||||
} ) }
|
||||
{ ...data }
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
@@ -52,4 +47,5 @@ registerBlockType( metadata, {
|
||||
</div>
|
||||
);
|
||||
},
|
||||
deprecated,
|
||||
} );
|
||||
|
||||
@@ -160,7 +160,6 @@
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
line-height: 16px;
|
||||
padding: 0;
|
||||
margin: 0 0.5em 0 0;
|
||||
color: currentColor;
|
||||
|
||||
|
||||
@@ -248,7 +248,7 @@ export const maybeUrlContainsFilters = (): boolean => {
|
||||
return maybeHasFilter;
|
||||
};
|
||||
|
||||
interface StoreAttributes {
|
||||
export interface StoreAttributes {
|
||||
attribute_id: string;
|
||||
attribute_label: string;
|
||||
attribute_name: string;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useColorProps } from '@woocommerce/base-hooks';
|
||||
import classnames from 'classnames';
|
||||
import { useStyleProps } from '@woocommerce/base-hooks';
|
||||
import { isString } from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
@@ -11,14 +12,18 @@ import Block from './block';
|
||||
import { parseAttributes } from './utils';
|
||||
|
||||
const BlockWrapper = ( props: Record< string, unknown > ) => {
|
||||
const colorProps = useColorProps( props );
|
||||
const styleProps = useStyleProps( props );
|
||||
const parsedBlockAttributes = parseAttributes( props );
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ isString( props.className ) ? props.className : '' }
|
||||
style={ { ...colorProps.style } }
|
||||
className={ classnames(
|
||||
isString( props.className ) ? props.className : '',
|
||||
styleProps.className
|
||||
) }
|
||||
style={ styleProps.style }
|
||||
>
|
||||
<Block isEditor={ false } attributes={ parseAttributes( props ) } />
|
||||
<Block isEditor={ false } attributes={ parsedBlockAttributes } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"showCounts": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
"default": false
|
||||
},
|
||||
"queryType": {
|
||||
"type": "string",
|
||||
|
||||
@@ -2,11 +2,7 @@
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import {
|
||||
usePrevious,
|
||||
useShallowEqual,
|
||||
useBorderProps,
|
||||
} from '@woocommerce/base-hooks';
|
||||
import { usePrevious, useShallowEqual } from '@woocommerce/base-hooks';
|
||||
import {
|
||||
useCollection,
|
||||
useQueryStateByKey,
|
||||
@@ -34,7 +30,6 @@ import {
|
||||
PREFIX_QUERY_ARG_FILTER_TYPE,
|
||||
PREFIX_QUERY_ARG_QUERY_TYPE,
|
||||
} from '@woocommerce/utils';
|
||||
import { difference } from 'lodash';
|
||||
import FormTokenField from '@woocommerce/base-components/form-token-field';
|
||||
import FilterTitlePlaceholder from '@woocommerce/base-components/filter-placeholder';
|
||||
import classnames from 'classnames';
|
||||
@@ -77,27 +72,23 @@ const AttributeFilterBlock = ( {
|
||||
getNotice?: GetNotice;
|
||||
} ) => {
|
||||
const hasFilterableProducts = getSettingWithCoercion(
|
||||
'has_filterable_products',
|
||||
'hasFilterableProducts',
|
||||
false,
|
||||
isBoolean
|
||||
);
|
||||
|
||||
const filteringForPhpTemplate = getSettingWithCoercion(
|
||||
'is_rendering_php_template',
|
||||
'isRenderingPhpTemplate',
|
||||
false,
|
||||
isBoolean
|
||||
);
|
||||
|
||||
const pageUrl = getSettingWithCoercion(
|
||||
'page_url',
|
||||
'pageUrl',
|
||||
window.location.href,
|
||||
isString
|
||||
);
|
||||
|
||||
const productIds = isEditor
|
||||
? []
|
||||
: getSettingWithCoercion( 'product_ids', [], Array.isArray );
|
||||
|
||||
const [ hasSetFilterDefaultsFromUrl, setHasSetFilterDefaultsFromUrl ] =
|
||||
useState( false );
|
||||
|
||||
@@ -128,8 +119,6 @@ const AttributeFilterBlock = ( {
|
||||
: []
|
||||
);
|
||||
|
||||
const borderProps = useBorderProps( blockAttributes );
|
||||
|
||||
const [ queryState ] = useQueryStateByContext();
|
||||
const [ productAttributesQuery, setProductAttributesQuery ] =
|
||||
useQueryStateByKey( 'attributes', [] );
|
||||
@@ -142,9 +131,6 @@ const AttributeFilterBlock = ( {
|
||||
shouldSelect: blockAttributes.attributeId > 0,
|
||||
} );
|
||||
|
||||
const filterAvailableTerms =
|
||||
blockAttributes.displayStyle !== 'dropdown' &&
|
||||
blockAttributes.queryType === 'and';
|
||||
const { results: filteredCounts, isLoading: filteredCountsLoading } =
|
||||
useCollectionData( {
|
||||
queryAttribute: {
|
||||
@@ -153,9 +139,7 @@ const AttributeFilterBlock = ( {
|
||||
},
|
||||
queryState: {
|
||||
...queryState,
|
||||
attributes: filterAvailableTerms ? queryState.attributes : null,
|
||||
},
|
||||
productIds,
|
||||
isEditor,
|
||||
} );
|
||||
|
||||
@@ -556,14 +540,10 @@ const AttributeFilterBlock = ( {
|
||||
<>
|
||||
<FormTokenField
|
||||
key={ remountKey }
|
||||
className={ classnames( borderProps.className, {
|
||||
className={ classnames( {
|
||||
'single-selection': ! multiple,
|
||||
'is-loading': isLoading,
|
||||
} ) }
|
||||
style={ {
|
||||
...borderProps.style,
|
||||
borderStyle: 'none',
|
||||
} }
|
||||
suggestions={ displayedOptions
|
||||
.filter(
|
||||
( option ) =>
|
||||
@@ -595,13 +575,19 @@ const AttributeFilterBlock = ( {
|
||||
: token;
|
||||
} );
|
||||
|
||||
const added = difference( tokens, checked );
|
||||
const added = [ tokens, checked ].reduce(
|
||||
( a, b ) =>
|
||||
a.filter( ( c ) => ! b.includes( c ) )
|
||||
);
|
||||
|
||||
if ( added.length === 1 ) {
|
||||
return onChange( added[ 0 ] );
|
||||
}
|
||||
|
||||
const removed = difference( checked, tokens );
|
||||
const removed = [ checked, tokens ].reduce(
|
||||
( a, b ) =>
|
||||
a.filter( ( c ) => ! b.includes( c ) )
|
||||
);
|
||||
if ( removed.length === 1 ) {
|
||||
onChange( removed[ 0 ] );
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import { isFeaturePluginBuild } from '@woocommerce/block-settings';
|
||||
import classNames from 'classnames';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type { BlockAttributes } from './types';
|
||||
import { blockAttributes } from './attributes';
|
||||
import metadata from './block.json';
|
||||
|
||||
const v1 = {
|
||||
supports: {
|
||||
...metadata.supports,
|
||||
...( isFeaturePluginBuild() && {
|
||||
__experimentalBorder: {
|
||||
radius: false,
|
||||
color: true,
|
||||
width: false,
|
||||
},
|
||||
} ),
|
||||
},
|
||||
attributes: {
|
||||
...metadata.attributes,
|
||||
showCounts: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
...blockAttributes,
|
||||
},
|
||||
save: ( { attributes }: { attributes: BlockAttributes } ) => {
|
||||
const {
|
||||
className,
|
||||
showCounts,
|
||||
queryType,
|
||||
attributeId,
|
||||
heading,
|
||||
headingLevel,
|
||||
displayStyle,
|
||||
showFilterButton,
|
||||
selectType,
|
||||
} = attributes;
|
||||
const data: Record< string, unknown > = {
|
||||
'data-attribute-id': attributeId,
|
||||
'data-show-counts': showCounts,
|
||||
'data-query-type': queryType,
|
||||
'data-heading': heading,
|
||||
'data-heading-level': headingLevel,
|
||||
};
|
||||
if ( displayStyle !== 'list' ) {
|
||||
data[ 'data-display-style' ] = displayStyle;
|
||||
}
|
||||
if ( showFilterButton ) {
|
||||
data[ 'data-show-filter-button' ] = showFilterButton;
|
||||
}
|
||||
if ( selectType === 'single' ) {
|
||||
data[ 'data-select-type' ] = selectType;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
{ ...useBlockProps.save( {
|
||||
className: classNames( 'is-loading', className ),
|
||||
} ) }
|
||||
{ ...data }
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="wc-block-product-attribute-filter__placeholder"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const deprecated = [ v1 ];
|
||||
|
||||
export default deprecated;
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { sortBy } from 'lodash';
|
||||
import { sort } from 'fast-sort';
|
||||
import { __, sprintf, _n } from '@wordpress/i18n';
|
||||
import { useState } from '@wordpress/element';
|
||||
import {
|
||||
@@ -69,7 +69,6 @@ const Edit = ( {
|
||||
}: EditProps ) => {
|
||||
const {
|
||||
attributeId,
|
||||
className,
|
||||
displayStyle,
|
||||
heading,
|
||||
headingLevel,
|
||||
@@ -158,15 +157,14 @@ const Edit = ( {
|
||||
),
|
||||
};
|
||||
|
||||
const list = sortBy(
|
||||
const list = sort(
|
||||
ATTRIBUTES.map( ( item ) => {
|
||||
return {
|
||||
id: parseInt( item.attribute_id, 10 ),
|
||||
name: item.attribute_label,
|
||||
};
|
||||
} ),
|
||||
'name'
|
||||
);
|
||||
} )
|
||||
).asc( 'name' );
|
||||
|
||||
return (
|
||||
<SearchListControl
|
||||
@@ -354,6 +352,7 @@ const Edit = ( {
|
||||
href={ getAdminLink(
|
||||
'edit.php?post_type=product&page=product_attributes'
|
||||
) }
|
||||
target="_top"
|
||||
>
|
||||
{ __( 'Add new attribute', 'woo-gutenberg-products-block' ) +
|
||||
' ' }
|
||||
@@ -363,6 +362,7 @@ const Edit = ( {
|
||||
className="wc-block-attribute-filter__read_more_button"
|
||||
isTertiary
|
||||
href="https://docs.woocommerce.com/document/managing-product-taxonomies/"
|
||||
target="_blank"
|
||||
>
|
||||
{ __( 'Learn more', 'woo-gutenberg-products-block' ) }
|
||||
</Button>
|
||||
@@ -420,12 +420,7 @@ const Edit = ( {
|
||||
{ isEditing ? (
|
||||
renderEditMode()
|
||||
) : (
|
||||
<div
|
||||
className={ classnames(
|
||||
className,
|
||||
'wc-block-attribute-filter'
|
||||
) }
|
||||
>
|
||||
<div className={ classnames( 'wc-block-attribute-filter' ) }>
|
||||
{ heading && (
|
||||
<BlockTitle
|
||||
className="wc-block-attribute-filter__title"
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
*/
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import { isFeaturePluginBuild } from '@woocommerce/block-settings';
|
||||
import { Icon, category } from '@wordpress/icons';
|
||||
import classNames from 'classnames';
|
||||
|
||||
@@ -14,6 +13,7 @@ import edit from './edit';
|
||||
import type { BlockAttributes } from './types';
|
||||
import { blockAttributes } from './attributes';
|
||||
import metadata from './block.json';
|
||||
import deprecated from './deprecated';
|
||||
|
||||
registerBlockType( metadata, {
|
||||
icon: {
|
||||
@@ -26,13 +26,6 @@ registerBlockType( metadata, {
|
||||
},
|
||||
supports: {
|
||||
...metadata.supports,
|
||||
...( isFeaturePluginBuild() && {
|
||||
__experimentalBorder: {
|
||||
radius: false,
|
||||
color: true,
|
||||
width: false,
|
||||
},
|
||||
} ),
|
||||
},
|
||||
attributes: {
|
||||
...metadata.attributes,
|
||||
@@ -41,45 +34,15 @@ registerBlockType( metadata, {
|
||||
edit,
|
||||
// Save the props to post content.
|
||||
save( { attributes }: { attributes: BlockAttributes } ) {
|
||||
const {
|
||||
className,
|
||||
showCounts,
|
||||
queryType,
|
||||
attributeId,
|
||||
heading,
|
||||
headingLevel,
|
||||
displayStyle,
|
||||
showFilterButton,
|
||||
selectType,
|
||||
} = attributes;
|
||||
const data: Record< string, unknown > = {
|
||||
'data-attribute-id': attributeId,
|
||||
'data-show-counts': showCounts,
|
||||
'data-query-type': queryType,
|
||||
'data-heading': heading,
|
||||
'data-heading-level': headingLevel,
|
||||
};
|
||||
if ( displayStyle !== 'list' ) {
|
||||
data[ 'data-display-style' ] = displayStyle;
|
||||
}
|
||||
if ( showFilterButton ) {
|
||||
data[ 'data-show-filter-button' ] = showFilterButton;
|
||||
}
|
||||
if ( selectType === 'single' ) {
|
||||
data[ 'data-select-type' ] = selectType;
|
||||
}
|
||||
const { className } = attributes;
|
||||
|
||||
return (
|
||||
<div
|
||||
{ ...useBlockProps.save( {
|
||||
className: classNames( 'is-loading', className ),
|
||||
} ) }
|
||||
{ ...data }
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="wc-block-product-attribute-filter__placeholder"
|
||||
/>
|
||||
</div>
|
||||
/>
|
||||
);
|
||||
},
|
||||
deprecated,
|
||||
} );
|
||||
|
||||
@@ -127,7 +127,7 @@ export const parseAttributes = ( data: Record< string, unknown > ) => {
|
||||
isString( data?.attributeId ) ? data.attributeId : '0',
|
||||
10
|
||||
),
|
||||
showCounts: data?.showCounts !== 'false',
|
||||
showCounts: data?.showCounts === 'true',
|
||||
queryType:
|
||||
( isString( data?.queryType ) && data.queryType ) ||
|
||||
metadata.attributes.queryType.default,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Icon, queryPagination } from '@wordpress/icons';
|
||||
*/
|
||||
import metadata from './block.json';
|
||||
import edit from './edit';
|
||||
import './style.scss';
|
||||
|
||||
const featurePluginSupport = {
|
||||
...metadata.supports,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
const expressIcon = (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke="#1E1E1E"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M18.25 12a6.25 6.25 0 1 1-12.5 0 6.25 6.25 0 0 1 12.5 0Z"
|
||||
/>
|
||||
<path fill="#1E1E1E" d="M10 3h4v3h-4z" />
|
||||
<rect width="1.5" height="5" x="11.25" y="8" fill="#1E1E1E" rx=".75" />
|
||||
<path
|
||||
fill="#1E1E1E"
|
||||
d="m15.7 4.816 1.66 1.078-1.114 1.718-1.661-1.078z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default expressIcon;
|
||||
@@ -91,7 +91,7 @@ const CheckoutExpressPayment = () => {
|
||||
headingLevel="2"
|
||||
>
|
||||
{ __(
|
||||
'Express checkout',
|
||||
'Express Checkout',
|
||||
'woocommerce'
|
||||
) }
|
||||
</Title>
|
||||
|
||||
@@ -5,18 +5,13 @@ $border-radius: 5px;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
|
||||
// nested class to avoid conflict with .editor-styles-wrapper ul
|
||||
.wc-block-components-express-payment__event-buttons {
|
||||
list-style: none;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(calc(33% - 10px), 1fr));
|
||||
grid-gap: 10px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
|
||||
> li {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
@@ -27,18 +22,23 @@ $border-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint("<782px") {
|
||||
.wc-block-components-express-payment__event-buttons {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-express-payment--checkout {
|
||||
/* stylelint-disable-next-line function-calc-no-unspaced-operator */
|
||||
margin-top: calc($border-radius * 3);
|
||||
|
||||
.wc-block-components-express-payment__event-buttons {
|
||||
list-style: none;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(calc(33% - 10px), 1fr));
|
||||
grid-gap: 10px;
|
||||
|
||||
@include breakpoint("<782px") {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-express-payment__title-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useEditorContext } from '@woocommerce/base-context';
|
||||
import { CheckboxControl } from '@woocommerce/blocks-checkout';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import { CHECKOUT_STORE_KEY, PAYMENT_STORE_KEY } from '@woocommerce/block-data';
|
||||
|
||||
@@ -23,7 +22,14 @@ import PaymentMethodErrorBoundary from './payment-method-error-boundary';
|
||||
*
|
||||
* @return {*} The rendered component.
|
||||
*/
|
||||
const PaymentMethodCard = ( { children, showSaveOption } ) => {
|
||||
interface PaymentMethodCardProps {
|
||||
showSaveOption: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
const PaymentMethodCard = ( {
|
||||
children,
|
||||
showSaveOption,
|
||||
}: PaymentMethodCardProps ) => {
|
||||
const { isEditor } = useEditorContext();
|
||||
const { shouldSavePaymentMethod, customerId } = useSelect( ( select ) => {
|
||||
const paymentMethodStore = select( PAYMENT_STORE_KEY );
|
||||
@@ -44,7 +50,7 @@ const PaymentMethodCard = ( { children, showSaveOption } ) => {
|
||||
className="wc-block-components-payment-methods__save-card-info"
|
||||
label={ __(
|
||||
'Save payment information to my account for future purchases.',
|
||||
'woocommerce'
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
checked={ shouldSavePaymentMethod }
|
||||
onChange={ () =>
|
||||
@@ -58,9 +64,4 @@ const PaymentMethodCard = ( { children, showSaveOption } ) => {
|
||||
);
|
||||
};
|
||||
|
||||
PaymentMethodCard.propTypes = {
|
||||
showSaveOption: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default PaymentMethodCard;
|
||||
@@ -1,68 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Component } from '@wordpress/element';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
|
||||
import { noticeContexts } from '@woocommerce/base-context';
|
||||
|
||||
class PaymentMethodErrorBoundary extends Component {
|
||||
state = { errorMessage: '', hasError: false };
|
||||
|
||||
static getDerivedStateFromError( error ) {
|
||||
return {
|
||||
errorMessage: error.message,
|
||||
hasError: true,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { hasError, errorMessage } = this.state;
|
||||
const { isEditor } = this.props;
|
||||
|
||||
if ( hasError ) {
|
||||
let errorText = __(
|
||||
'We are experiencing difficulties with this payment method. Please contact us for assistance.',
|
||||
'woocommerce'
|
||||
);
|
||||
if ( isEditor || CURRENT_USER_IS_ADMIN ) {
|
||||
if ( errorMessage ) {
|
||||
errorText = errorMessage;
|
||||
} else {
|
||||
errorText = __(
|
||||
"There was an error with this payment method. Please verify it's configured correctly.",
|
||||
'woocommerce'
|
||||
);
|
||||
}
|
||||
}
|
||||
const notices = [
|
||||
{
|
||||
id: '0',
|
||||
content: errorText,
|
||||
isDismissible: false,
|
||||
status: 'error',
|
||||
},
|
||||
];
|
||||
return (
|
||||
<StoreNoticesContainer
|
||||
additionalNotices={ notices }
|
||||
context={ noticeContexts.PAYMENTS }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
PaymentMethodErrorBoundary.propTypes = {
|
||||
isEditor: PropTypes.bool,
|
||||
};
|
||||
|
||||
PaymentMethodErrorBoundary.defaultProps = {
|
||||
isEditor: false,
|
||||
};
|
||||
|
||||
export default PaymentMethodErrorBoundary;
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
|
||||
import { noticeContexts } from '@woocommerce/base-context';
|
||||
import { NoticeType } from '@woocommerce/types';
|
||||
interface PaymentMethodErrorBoundaryProps {
|
||||
isEditor: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
const PaymentMethodErrorBoundary = ( {
|
||||
isEditor,
|
||||
children,
|
||||
}: PaymentMethodErrorBoundaryProps ) => {
|
||||
const [ errorMessage ] = useState( '' );
|
||||
const [ hasError ] = useState( false );
|
||||
if ( hasError ) {
|
||||
let errorText = __(
|
||||
'We are experiencing difficulties with this payment method. Please contact us for assistance.',
|
||||
'woo-gutenberg-products-block'
|
||||
);
|
||||
if ( isEditor || CURRENT_USER_IS_ADMIN ) {
|
||||
if ( errorMessage ) {
|
||||
errorText = errorMessage;
|
||||
} else {
|
||||
errorText = __(
|
||||
"There was an error with this payment method. Please verify it's configured correctly.",
|
||||
'woo-gutenberg-products-block'
|
||||
);
|
||||
}
|
||||
}
|
||||
const notices: NoticeType[] = [
|
||||
{
|
||||
id: '0',
|
||||
content: errorText,
|
||||
isDismissible: false,
|
||||
status: 'error',
|
||||
},
|
||||
];
|
||||
return (
|
||||
<StoreNoticesContainer
|
||||
additionalNotices={ notices }
|
||||
context={ noticeContexts.PAYMENTS }
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <>{ children }</>;
|
||||
};
|
||||
export default PaymentMethodErrorBoundary;
|
||||
@@ -12,6 +12,7 @@ import { PAYMENT_STORE_KEY } from '@woocommerce/block-data';
|
||||
import NoPaymentMethods from './no-payment-methods';
|
||||
import PaymentMethodOptions from './payment-method-options';
|
||||
import SavedPaymentMethodOptions from './saved-payment-method-options';
|
||||
import './style.scss';
|
||||
|
||||
/**
|
||||
* PaymentMethods component.
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useMemo, cloneElement } from '@wordpress/element';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { noticeContexts } from '@woocommerce/base-context';
|
||||
import RadioControl from '@woocommerce/base-components/radio-control';
|
||||
import {
|
||||
usePaymentMethodInterface,
|
||||
useStoreEvents,
|
||||
} from '@woocommerce/base-context/hooks';
|
||||
import { PAYMENT_STORE_KEY } from '@woocommerce/block-data';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { getPaymentMethods } from '@woocommerce/blocks-registry';
|
||||
|
||||
/**
|
||||
* @typedef {import('@woocommerce/type-defs/contexts').CustomerPaymentMethod} CustomerPaymentMethod
|
||||
* @typedef {import('@woocommerce/type-defs/contexts').PaymentStatusDispatch} PaymentStatusDispatch
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the option object for a cc or echeck saved payment method token.
|
||||
*
|
||||
* @param {CustomerPaymentMethod} savedPaymentMethod
|
||||
* @return {string} label
|
||||
*/
|
||||
const getCcOrEcheckLabel = ( { method, expires } ) => {
|
||||
return sprintf(
|
||||
/* translators: %1$s is referring to the payment method brand, %2$s is referring to the last 4 digits of the payment card, %3$s is referring to the expiry date. */
|
||||
__(
|
||||
'%1$s ending in %2$s (expires %3$s)',
|
||||
'woocommerce'
|
||||
),
|
||||
method.brand,
|
||||
method.last4,
|
||||
expires
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the option object for any non specific saved payment method.
|
||||
*
|
||||
* @param {CustomerPaymentMethod} savedPaymentMethod
|
||||
* @return {string} label
|
||||
*/
|
||||
const getDefaultLabel = ( { method } ) => {
|
||||
/* For saved payment methods with brand & last 4 */
|
||||
if ( method.brand && method.last4 ) {
|
||||
return sprintf(
|
||||
/* translators: %1$s is referring to the payment method brand, %2$s is referring to the last 4 digits of the payment card. */
|
||||
__( '%1$s ending in %2$s', 'woocommerce' ),
|
||||
method.brand,
|
||||
method.last4
|
||||
);
|
||||
}
|
||||
|
||||
/* For saved payment methods without brand & last 4 */
|
||||
return sprintf(
|
||||
/* translators: %s is the name of the payment method gateway. */
|
||||
__( 'Saved token for %s', 'woocommerce' ),
|
||||
method.gateway
|
||||
);
|
||||
};
|
||||
|
||||
const SavedPaymentMethodOptions = () => {
|
||||
const { activeSavedToken, activePaymentMethod, savedPaymentMethods } =
|
||||
useSelect( ( select ) => {
|
||||
const store = select( PAYMENT_STORE_KEY );
|
||||
return {
|
||||
activeSavedToken: store.getActiveSavedToken(),
|
||||
activePaymentMethod: store.getActivePaymentMethod(),
|
||||
savedPaymentMethods: store.getSavedPaymentMethods(),
|
||||
};
|
||||
} );
|
||||
const { __internalSetActivePaymentMethod } =
|
||||
useDispatch( PAYMENT_STORE_KEY );
|
||||
const paymentMethods = getPaymentMethods();
|
||||
const paymentMethodInterface = usePaymentMethodInterface();
|
||||
const { removeNotice } = useDispatch( 'core/notices' );
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
|
||||
const options = useMemo( () => {
|
||||
const types = Object.keys( savedPaymentMethods );
|
||||
return types
|
||||
.flatMap( ( type ) => {
|
||||
const typeMethods = savedPaymentMethods[ type ];
|
||||
return typeMethods.map( ( paymentMethod ) => {
|
||||
const isCC = type === 'cc' || type === 'echeck';
|
||||
const paymentMethodSlug = paymentMethod.method.gateway;
|
||||
return {
|
||||
name: `wc-saved-payment-method-token-${ paymentMethodSlug }`,
|
||||
label: isCC
|
||||
? getCcOrEcheckLabel( paymentMethod )
|
||||
: getDefaultLabel( paymentMethod ),
|
||||
value: paymentMethod.tokenId.toString(),
|
||||
onChange: ( token ) => {
|
||||
const savedTokenKey = `wc-${ paymentMethodSlug }-payment-token`;
|
||||
__internalSetActivePaymentMethod(
|
||||
paymentMethodSlug,
|
||||
{
|
||||
token,
|
||||
payment_method: paymentMethodSlug,
|
||||
[ savedTokenKey ]: token.toString(),
|
||||
isSavedToken: true,
|
||||
}
|
||||
);
|
||||
removeNotice(
|
||||
'wc-payment-error',
|
||||
noticeContexts.PAYMENTS
|
||||
);
|
||||
dispatchCheckoutEvent(
|
||||
'set-active-payment-method',
|
||||
{
|
||||
paymentMethodSlug,
|
||||
}
|
||||
);
|
||||
},
|
||||
};
|
||||
} );
|
||||
} )
|
||||
.filter( Boolean );
|
||||
}, [
|
||||
savedPaymentMethods,
|
||||
__internalSetActivePaymentMethod,
|
||||
removeNotice,
|
||||
dispatchCheckoutEvent,
|
||||
] );
|
||||
const savedPaymentMethodHandler =
|
||||
!! activeSavedToken &&
|
||||
paymentMethods[ activePaymentMethod ] &&
|
||||
paymentMethods[ activePaymentMethod ]?.savedTokenComponent
|
||||
? cloneElement(
|
||||
paymentMethods[ activePaymentMethod ]?.savedTokenComponent,
|
||||
{ token: activeSavedToken, ...paymentMethodInterface }
|
||||
)
|
||||
: null;
|
||||
|
||||
return options.length > 0 ? (
|
||||
<>
|
||||
<RadioControl
|
||||
id={ 'wc-payment-method-saved-tokens' }
|
||||
selected={ activeSavedToken }
|
||||
options={ options }
|
||||
onChange={ () => void 0 }
|
||||
/>
|
||||
{ savedPaymentMethodHandler }
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default SavedPaymentMethodOptions;
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useMemo, cloneElement } from '@wordpress/element';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { noticeContexts } from '@woocommerce/base-context';
|
||||
import RadioControl from '@woocommerce/base-components/radio-control';
|
||||
import {
|
||||
usePaymentMethodInterface,
|
||||
useStoreEvents,
|
||||
} from '@woocommerce/base-context/hooks';
|
||||
import { PAYMENT_STORE_KEY } from '@woocommerce/block-data';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { getPaymentMethods } from '@woocommerce/blocks-registry';
|
||||
import { isNull } from '@woocommerce/types';
|
||||
import { RadioControlOption } from '@woocommerce/base-components/radio-control/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getCanMakePaymentArg } from '../../../data/payment/utils/check-payment-methods';
|
||||
import { CustomerPaymentMethodConfiguration } from '../../../data/payment/types';
|
||||
|
||||
/**
|
||||
* Returns the option object for a cc or echeck saved payment method token.
|
||||
*/
|
||||
const getCcOrEcheckLabel = ( {
|
||||
method,
|
||||
expires,
|
||||
}: {
|
||||
method: CustomerPaymentMethodConfiguration;
|
||||
expires: string;
|
||||
} ): string => {
|
||||
return sprintf(
|
||||
/* translators: %1$s is referring to the payment method brand, %2$s is referring to the last 4 digits of the payment card, %3$s is referring to the expiry date. */
|
||||
__(
|
||||
'%1$s ending in %2$s (expires %3$s)',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
method.brand,
|
||||
method.last4,
|
||||
expires
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the option object for any non specific saved payment method.
|
||||
*/
|
||||
const getDefaultLabel = ( {
|
||||
method,
|
||||
}: {
|
||||
method: CustomerPaymentMethodConfiguration;
|
||||
} ): string => {
|
||||
/* For saved payment methods with brand & last 4 */
|
||||
if ( method.brand && method.last4 ) {
|
||||
return sprintf(
|
||||
/* translators: %1$s is referring to the payment method brand, %2$s is referring to the last 4 digits of the payment card. */
|
||||
__( '%1$s ending in %2$s', 'woo-gutenberg-products-block' ),
|
||||
method.brand,
|
||||
method.last4
|
||||
);
|
||||
}
|
||||
|
||||
/* For saved payment methods without brand & last 4 */
|
||||
return sprintf(
|
||||
/* translators: %s is the name of the payment method gateway. */
|
||||
__( 'Saved token for %s', 'woo-gutenberg-products-block' ),
|
||||
method.gateway
|
||||
);
|
||||
};
|
||||
|
||||
const SavedPaymentMethodOptions = () => {
|
||||
const { activeSavedToken, activePaymentMethod, savedPaymentMethods } =
|
||||
useSelect( ( select ) => {
|
||||
const store = select( PAYMENT_STORE_KEY );
|
||||
return {
|
||||
activeSavedToken: store.getActiveSavedToken(),
|
||||
activePaymentMethod: store.getActivePaymentMethod(),
|
||||
savedPaymentMethods: store.getSavedPaymentMethods(),
|
||||
};
|
||||
} );
|
||||
const { __internalSetActivePaymentMethod } =
|
||||
useDispatch( PAYMENT_STORE_KEY );
|
||||
const canMakePaymentArg = getCanMakePaymentArg();
|
||||
const paymentMethods = getPaymentMethods();
|
||||
const paymentMethodInterface = usePaymentMethodInterface();
|
||||
const { removeNotice } = useDispatch( 'core/notices' );
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
|
||||
const options = useMemo< RadioControlOption[] >( () => {
|
||||
const types = Object.keys( savedPaymentMethods );
|
||||
|
||||
// Get individual payment methods from saved payment methods and put them into a unique array.
|
||||
const individualPaymentGateways = new Set(
|
||||
types.flatMap( ( type ) =>
|
||||
savedPaymentMethods[ type ].map(
|
||||
( paymentMethod ) => paymentMethod.method.gateway
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const gatewaysThatCanMakePayment = Array.from(
|
||||
individualPaymentGateways
|
||||
).filter( ( method ) => {
|
||||
return paymentMethods[ method ]?.canMakePayment(
|
||||
canMakePaymentArg
|
||||
);
|
||||
} );
|
||||
|
||||
const mappedOptions = types.flatMap( ( type ) => {
|
||||
const typeMethods = savedPaymentMethods[ type ];
|
||||
return typeMethods.map( ( paymentMethod ) => {
|
||||
const canMakePayment = gatewaysThatCanMakePayment.includes(
|
||||
paymentMethod.method.gateway
|
||||
);
|
||||
if ( ! canMakePayment ) {
|
||||
return void 0;
|
||||
}
|
||||
const isCC = type === 'cc' || type === 'echeck';
|
||||
const paymentMethodSlug = paymentMethod.method.gateway;
|
||||
return {
|
||||
name: `wc-saved-payment-method-token-${ paymentMethodSlug }`,
|
||||
label: isCC
|
||||
? getCcOrEcheckLabel( paymentMethod )
|
||||
: getDefaultLabel( paymentMethod ),
|
||||
value: paymentMethod.tokenId.toString(),
|
||||
onChange: ( token: string ) => {
|
||||
const savedTokenKey = `wc-${ paymentMethodSlug }-payment-token`;
|
||||
__internalSetActivePaymentMethod( paymentMethodSlug, {
|
||||
token,
|
||||
payment_method: paymentMethodSlug,
|
||||
[ savedTokenKey ]: token.toString(),
|
||||
isSavedToken: true,
|
||||
} );
|
||||
removeNotice(
|
||||
'wc-payment-error',
|
||||
noticeContexts.PAYMENTS
|
||||
);
|
||||
dispatchCheckoutEvent( 'set-active-payment-method', {
|
||||
paymentMethodSlug,
|
||||
} );
|
||||
},
|
||||
};
|
||||
} );
|
||||
} );
|
||||
return mappedOptions.filter(
|
||||
( option ) => typeof option !== 'undefined'
|
||||
) as RadioControlOption[];
|
||||
}, [
|
||||
savedPaymentMethods,
|
||||
paymentMethods,
|
||||
__internalSetActivePaymentMethod,
|
||||
removeNotice,
|
||||
dispatchCheckoutEvent,
|
||||
canMakePaymentArg,
|
||||
] );
|
||||
const savedPaymentMethodHandler =
|
||||
!! activeSavedToken &&
|
||||
paymentMethods[ activePaymentMethod ] &&
|
||||
typeof paymentMethods[ activePaymentMethod ]?.savedTokenComponent !==
|
||||
'undefined' &&
|
||||
! isNull( paymentMethods[ activePaymentMethod ].savedTokenComponent )
|
||||
? cloneElement(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore - we know for sure that the savedTokenComponent is not null or undefined at this point.
|
||||
paymentMethods[ activePaymentMethod ].savedTokenComponent,
|
||||
{ token: activeSavedToken, ...paymentMethodInterface }
|
||||
)
|
||||
: null;
|
||||
|
||||
return options.length > 0 ? (
|
||||
<>
|
||||
<RadioControl
|
||||
id={ 'wc-payment-method-saved-tokens' }
|
||||
selected={ activeSavedToken }
|
||||
options={ options }
|
||||
onChange={ () => void 0 }
|
||||
/>
|
||||
{ savedPaymentMethodHandler }
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default SavedPaymentMethodOptions;
|
||||
@@ -59,6 +59,7 @@
|
||||
}
|
||||
|
||||
label {
|
||||
@include reset-color();
|
||||
@include reset-typography();
|
||||
@include font-size(regular);
|
||||
line-height: 1.375; // =22px when font-size is 16px.
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { registerPaymentMethod } from '@woocommerce/blocks-registry';
|
||||
import * as wpData from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import SavedPaymentMethodOptions from '../saved-payment-method-options';
|
||||
|
||||
jest.mock( '@wordpress/data', () => ( {
|
||||
__esModule: true,
|
||||
...jest.requireActual( '@wordpress/data' ),
|
||||
useSelect: jest.fn(),
|
||||
} ) );
|
||||
|
||||
const mockedUseSelect = wpData.useSelect as jest.Mock;
|
||||
// Mock use select so we can override it when wc/store/checkout is accessed, but return the original select function if any other store is accessed.
|
||||
mockedUseSelect.mockImplementation(
|
||||
jest.fn().mockImplementation( ( passedMapSelect ) => {
|
||||
const mockedSelect = jest.fn().mockImplementation( ( storeName ) => {
|
||||
if ( storeName === 'wc/store/payment' ) {
|
||||
return {
|
||||
...jest
|
||||
.requireActual( '@wordpress/data' )
|
||||
.select( storeName ),
|
||||
getActiveSavedToken: () => 1,
|
||||
getSavedPaymentMethods: () => {
|
||||
return {
|
||||
cc: [
|
||||
{
|
||||
tokenId: 1,
|
||||
expires: '1/2099',
|
||||
method: {
|
||||
brand: 'Visa',
|
||||
gateway:
|
||||
'can-pay-true-test-payment-method',
|
||||
last4: '1234',
|
||||
},
|
||||
},
|
||||
{
|
||||
tokenId: 2,
|
||||
expires: '1/2099',
|
||||
method: {
|
||||
brand: 'Visa',
|
||||
gateway:
|
||||
'can-pay-true-test-payment-method',
|
||||
last4: '2345',
|
||||
},
|
||||
},
|
||||
{
|
||||
tokenId: 3,
|
||||
expires: '1/2099',
|
||||
method: {
|
||||
brand: 'Visa',
|
||||
gateway:
|
||||
'can-pay-true-first-false-second-test-payment-method',
|
||||
last4: '3456',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
return jest.requireActual( '@wordpress/data' ).select( storeName );
|
||||
} );
|
||||
return passedMapSelect( mockedSelect, {
|
||||
dispatch: jest.requireActual( '@wordpress/data' ).dispatch,
|
||||
} );
|
||||
} )
|
||||
);
|
||||
|
||||
describe( 'SavedPaymentMethodOptions', () => {
|
||||
it( 'renders saved methods when a registered method exists', () => {
|
||||
registerPaymentMethod( {
|
||||
name: 'can-pay-true-test-payment-method',
|
||||
label: 'Can Pay True Test Payment Method',
|
||||
edit: <div>edit</div>,
|
||||
ariaLabel: 'Can Pay True Test Payment Method',
|
||||
canMakePayment: () => true,
|
||||
content: <div>content</div>,
|
||||
supports: {
|
||||
showSavedCards: true,
|
||||
showSaveOption: true,
|
||||
features: [ 'products' ],
|
||||
},
|
||||
} );
|
||||
render( <SavedPaymentMethodOptions /> );
|
||||
|
||||
// First saved token for can-pay-true-test-payment-method.
|
||||
expect(
|
||||
screen.getByText( 'Visa ending in 1234 (expires 1/2099)' )
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Second saved token for can-pay-true-test-payment-method.
|
||||
expect(
|
||||
screen.getByText( 'Visa ending in 2345 (expires 1/2099)' )
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Third saved token for can-pay-false-test-payment-method - this should not show because the method is not registered.
|
||||
expect(
|
||||
screen.queryByText( 'Visa ending in 3456 (expires 1/2099)' )
|
||||
).not.toBeInTheDocument();
|
||||
} );
|
||||
it( "does not show saved methods when the method's canPay function returns false", () => {
|
||||
registerPaymentMethod( {
|
||||
name: 'can-pay-true-first-false-second-test-payment-method',
|
||||
label: 'Can Pay True First False Second Test Payment Method',
|
||||
edit: <div>edit</div>,
|
||||
ariaLabel: 'Can Pay True First False Second Test Payment Method',
|
||||
// This mock will return true the first time it runs, then false on subsequent calls.
|
||||
canMakePayment: jest
|
||||
.fn()
|
||||
.mockReturnValueOnce( true )
|
||||
.mockReturnValue( false ),
|
||||
content: <div>content</div>,
|
||||
supports: {
|
||||
showSavedCards: true,
|
||||
showSaveOption: true,
|
||||
features: [ 'products' ],
|
||||
},
|
||||
} );
|
||||
const { rerender } = render( <SavedPaymentMethodOptions /> );
|
||||
// Saved token for can-pay-true-first-false-second-test-payment-method - this should show because canPay is true on first call.
|
||||
expect(
|
||||
screen.queryByText( 'Visa ending in 3456 (expires 1/2099)' )
|
||||
).toBeInTheDocument();
|
||||
rerender( <SavedPaymentMethodOptions /> );
|
||||
|
||||
// Saved token for can-pay-true-first-false-second-test-payment-method - this should not show because canPay is false on subsequent calls.
|
||||
expect(
|
||||
screen.queryByText( 'Visa ending in 3456 (expires 1/2099)' )
|
||||
).not.toBeInTheDocument();
|
||||
} );
|
||||
} );
|
||||
@@ -12,10 +12,13 @@ import { CartCheckoutSidebarCompatibilityNotice } from '@woocommerce/editor-comp
|
||||
import { NoPaymentMethodsNotice } from '@woocommerce/editor-components/no-payment-methods-notice';
|
||||
import { PAYMENT_STORE_KEY } from '@woocommerce/block-data';
|
||||
import { DefaultNotice } from '@woocommerce/editor-components/default-notice';
|
||||
import { TemplateNotice } from '@woocommerce/editor-components/template-notice';
|
||||
import { IncompatiblePaymentGatewaysNotice } from '@woocommerce/editor-components/incompatible-payment-gateways-notice';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { CartCheckoutFeedbackPrompt } from '@woocommerce/editor-components/feedback-prompt';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
|
||||
declare module '@wordpress/editor' {
|
||||
let store: StoreDescriptor;
|
||||
}
|
||||
@@ -36,6 +39,8 @@ const withSidebarNotices = createHigherOrderComponent(
|
||||
isSelected: isBlockSelected,
|
||||
} = props;
|
||||
|
||||
const isBlockTheme = getSetting( 'isBlockTheme' );
|
||||
|
||||
const [
|
||||
isIncompatiblePaymentGatewaysNoticeDismissed,
|
||||
setIsIncompatiblePaymentGatewaysNoticeDismissed,
|
||||
@@ -101,15 +106,20 @@ const withSidebarNotices = createHigherOrderComponent(
|
||||
}
|
||||
/>
|
||||
|
||||
{ isBlockTheme ? (
|
||||
<TemplateNotice
|
||||
block={ isCheckout ? 'checkout' : 'cart' }
|
||||
/>
|
||||
) : (
|
||||
<DefaultNotice
|
||||
block={ isCheckout ? 'checkout' : 'cart' }
|
||||
/>
|
||||
) }
|
||||
|
||||
{ isIncompatiblePaymentGatewaysNoticeDismissed ? (
|
||||
<>
|
||||
<DefaultNotice
|
||||
block={ isCheckout ? 'checkout' : 'cart' }
|
||||
/>
|
||||
<CartCheckoutSidebarCompatibilityNotice
|
||||
block={ isCheckout ? 'checkout' : 'cart' }
|
||||
/>
|
||||
</>
|
||||
<CartCheckoutSidebarCompatibilityNotice
|
||||
block={ isCheckout ? 'checkout' : 'cart' }
|
||||
/>
|
||||
) : null }
|
||||
|
||||
{ isPaymentMethodsBlock && ! hasPaymentMethods && (
|
||||
|
||||
@@ -5,11 +5,15 @@ import { __ } from '@wordpress/i18n';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import LoadingMask from '@woocommerce/base-components/loading-mask';
|
||||
import { CartProvider, noticeContexts } from '@woocommerce/base-context';
|
||||
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings';
|
||||
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
|
||||
import { translateJQueryEventToNative } from '@woocommerce/base-utils';
|
||||
import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top';
|
||||
import {
|
||||
CartEventsProvider,
|
||||
CartProvider,
|
||||
noticeContexts,
|
||||
} from '@woocommerce/base-context';
|
||||
import {
|
||||
SlotFillProvider,
|
||||
StoreNoticesContainer,
|
||||
@@ -85,8 +89,10 @@ const Block = ( { attributes, children, scrollToTop } ) => (
|
||||
<StoreNoticesContainer context={ noticeContexts.CART } />
|
||||
<SlotFillProvider>
|
||||
<CartProvider>
|
||||
<Cart attributes={ attributes }>{ children }</Cart>
|
||||
<ScrollOnError scrollToTop={ scrollToTop } />
|
||||
<CartEventsProvider>
|
||||
<Cart attributes={ attributes }>{ children }</Cart>
|
||||
<ScrollOnError scrollToTop={ scrollToTop } />
|
||||
</CartEventsProvider>
|
||||
</CartProvider>
|
||||
</SlotFillProvider>
|
||||
</BlockErrorBoundary>
|
||||
|
||||
@@ -16,7 +16,7 @@ import { Block as ProductRating } from '../../../atomic/blocks/product-elements/
|
||||
import { Block as ProductSaleBadge } from '../../../atomic/blocks/product-elements/sale-badge/block';
|
||||
import { Block as ProductPrice } from '../../../atomic/blocks/product-elements/price/block';
|
||||
import { Block as ProductButton } from '../../../atomic/blocks/product-elements/button/block';
|
||||
import AddToCartButton from '../../../atomic/blocks/product-elements/add-to-cart/block';
|
||||
import { ImageSizing } from '../../../atomic/blocks/product-elements/image/types';
|
||||
|
||||
interface CrossSellsProductProps {
|
||||
product: ProductResponseItem;
|
||||
@@ -44,7 +44,7 @@ const CartCrossSellsProduct = ( {
|
||||
productId={ product.id }
|
||||
showProductLink={ false }
|
||||
saleBadgeAlign={ 'left' }
|
||||
imageSizing={ 'full-size' }
|
||||
imageSizing={ ImageSizing.SINGLE }
|
||||
isDescendentOfQueryLoop={ false }
|
||||
/>
|
||||
<ProductName
|
||||
@@ -59,11 +59,7 @@ const CartCrossSellsProduct = ( {
|
||||
/>
|
||||
<ProductPrice />
|
||||
</div>
|
||||
{ product.is_in_stock ? (
|
||||
<AddToCartButton />
|
||||
) : (
|
||||
<ProductButton />
|
||||
) }
|
||||
<ProductButton />
|
||||
</ProductDataContextProvider>
|
||||
</InnerBlockLayoutContextProvider>
|
||||
</div>
|
||||
|
||||
@@ -42,8 +42,8 @@ export const Edit = ( { attributes, setAttributes }: Props ): JSX.Element => {
|
||||
onChange={ ( value ) =>
|
||||
setAttributes( { columns: value } )
|
||||
}
|
||||
min={ getSetting( 'min_columns', 1 ) }
|
||||
max={ getSetting( 'max_columns', 6 ) }
|
||||
min={ getSetting( 'minColumns', 1 ) }
|
||||
max={ getSetting( 'maxColumns', 6 ) }
|
||||
/>
|
||||
</PanelBody>
|
||||
</InspectorControls>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { registerBlockType } from '@wordpress/blocks';
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Edit, Save } from './edit';
|
||||
import './style.scss';
|
||||
|
||||
registerBlockType( 'woocommerce/cart-cross-sells-products-block', {
|
||||
icon: {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "woocommerce/cart-express-payment-block",
|
||||
"version": "1.0.0",
|
||||
"title": "Express Checkout",
|
||||
"description": "Provide an express payment option for your customers.",
|
||||
"description": "Allow customers to breeze through with quick payment options.",
|
||||
"category": "woocommerce",
|
||||
"supports": {
|
||||
"align": false,
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.wp-block-woocommerce-proceed-to-checkout-block {
|
||||
margin-bottom: 28px;
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.wp-block-woocommerce-checkout-express-payment-block-placeholder {
|
||||
* {
|
||||
pointer-events: all; // Overrides parent disabled component in editor context
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon, payment } from '@wordpress/icons';
|
||||
import { Icon } from '@wordpress/icons';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Edit, Save } from './edit';
|
||||
import expressIcon from '../../../cart-checkout-shared/icon';
|
||||
|
||||
registerBlockType( 'woocommerce/cart-express-payment-block', {
|
||||
icon: {
|
||||
src: (
|
||||
<Icon
|
||||
icon={ payment }
|
||||
style={ { fill: 'none' } } // this is needed for this particular svg
|
||||
icon={ expressIcon }
|
||||
className="wc-block-editor-components-block-icon"
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import Title from '@woocommerce/base-components/title';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const Block = ( {
|
||||
@@ -12,12 +11,11 @@ const Block = ( {
|
||||
content: string;
|
||||
} ): JSX.Element => {
|
||||
return (
|
||||
<Title
|
||||
headingLevel="2"
|
||||
<span
|
||||
className={ classnames( className, 'wc-block-cart__totals-title' ) }
|
||||
>
|
||||
{ content }
|
||||
</Title>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
* External dependencies
|
||||
*/
|
||||
import { PlainText, useBlockProps } from '@wordpress/block-editor';
|
||||
import Title from '@woocommerce/base-components/title';
|
||||
import classnames from 'classnames';
|
||||
|
||||
/**
|
||||
@@ -24,8 +23,7 @@ export const Edit = ( {
|
||||
const blockProps = useBlockProps();
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<Title
|
||||
headingLevel="2"
|
||||
<span
|
||||
className={ classnames(
|
||||
className,
|
||||
'wc-block-cart__totals-title'
|
||||
@@ -39,7 +37,7 @@ export const Edit = ( {
|
||||
}
|
||||
style={ { backgroundColor: 'transparent' } }
|
||||
/>
|
||||
</Title>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { TemplateArray } from '@wordpress/blocks';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
import {
|
||||
useForcedLayout,
|
||||
getAllowedBlocks,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { registerBlockType } from '@wordpress/blocks';
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Edit, Save } from './edit';
|
||||
import './style.scss';
|
||||
|
||||
registerBlockType( 'woocommerce/cart-totals-block', {
|
||||
icon: {
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
useForcedLayout,
|
||||
getAllowedBlocks,
|
||||
} from '../../../cart-checkout-shared';
|
||||
import './style.scss';
|
||||
|
||||
const browseStoreTemplate = SHOP_URL
|
||||
? [
|
||||
|
||||
@@ -9,6 +9,7 @@ import { registerBlockType } from '@wordpress/blocks';
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Edit, Save } from './edit';
|
||||
import './style.scss';
|
||||
|
||||
registerBlockType( 'woocommerce/empty-cart-block', {
|
||||
icon: {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { useState, useEffect } from '@wordpress/element';
|
||||
import { useState, useEffect, useMemo } from '@wordpress/element';
|
||||
import Button from '@woocommerce/base-components/button';
|
||||
import { CHECKOUT_URL } from '@woocommerce/block-settings';
|
||||
import { usePositionRelativeToViewport } from '@woocommerce/base-hooks';
|
||||
@@ -10,11 +10,12 @@ import { getSetting } from '@woocommerce/settings';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { CART_STORE_KEY, CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
|
||||
import { applyCheckoutFilter } from '@woocommerce/blocks-checkout';
|
||||
import { isErrorResponse } from '@woocommerce/base-context';
|
||||
import { useCartEventsContext } from '@woocommerce/base-context/providers';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
import { defaultButtonLabel } from './constants';
|
||||
|
||||
/**
|
||||
@@ -74,18 +75,34 @@ const Block = ( {
|
||||
arg: { cart },
|
||||
} );
|
||||
|
||||
const { dispatchOnProceedToCheckout } = useCartEventsContext();
|
||||
|
||||
const submitContainerContents = (
|
||||
<Button
|
||||
className="wc-block-cart__submit-button"
|
||||
href={ filteredLink }
|
||||
disabled={ isCalculating }
|
||||
onClick={ () => setShowSpinner( true ) }
|
||||
onClick={ ( e ) => {
|
||||
dispatchOnProceedToCheckout().then( ( observerResponses ) => {
|
||||
if ( observerResponses.some( isErrorResponse ) ) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
setShowSpinner( true );
|
||||
} );
|
||||
} }
|
||||
showSpinner={ showSpinner }
|
||||
>
|
||||
{ label }
|
||||
</Button>
|
||||
);
|
||||
|
||||
// Get the body background color to use as the sticky container background color.
|
||||
const backgroundColor = useMemo(
|
||||
() => getComputedStyle( document.body ).backgroundColor,
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={ classnames( 'wc-block-cart__submit', className ) }>
|
||||
{ positionReferenceElement }
|
||||
@@ -95,7 +112,10 @@ const Block = ( {
|
||||
</div>
|
||||
{ /* If the positionReferenceElement is below the viewport, display the sticky container. */ }
|
||||
{ positionRelativeToViewport === 'below' && (
|
||||
<div className="wc-block-cart__submit-container wc-block-cart__submit-container--sticky">
|
||||
<div
|
||||
className="wc-block-cart__submit-container wc-block-cart__submit-container--sticky"
|
||||
style={ { backgroundColor } }
|
||||
>
|
||||
{ submitContainerContents }
|
||||
</div>
|
||||
) }
|
||||
|
||||
@@ -9,6 +9,7 @@ import { registerBlockType } from '@wordpress/blocks';
|
||||
*/
|
||||
import attributes from './attributes';
|
||||
import { Edit, Save } from './edit';
|
||||
import './style.scss';
|
||||
|
||||
registerBlockType( 'woocommerce/proceed-to-checkout-block', {
|
||||
icon: {
|
||||
|
||||
@@ -30,10 +30,11 @@
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 9999;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::before {
|
||||
box-shadow: 0 -10px 20px 10px currentColor;
|
||||
color: color.adjust($gray-400, $alpha: -0.5);
|
||||
color: color.adjust($gray-400, $alpha: -0.7);
|
||||
content: "";
|
||||
height: 100%;
|
||||
left: 0;
|
||||
@@ -43,4 +44,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { registerCheckoutFilters } from '@woocommerce/blocks-checkout';
|
||||
import { useCartEventsContext } from '@woocommerce/base-context';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from '../block';
|
||||
import { CartEventsProvider } from '../../../../../base/context/providers';
|
||||
|
||||
describe( 'Proceed to checkout block', () => {
|
||||
it( 'allows the text to be filtered', () => {
|
||||
@@ -49,4 +53,37 @@ describe( 'Proceed to checkout block', () => {
|
||||
//@todo When https://github.com/WordPress/gutenberg/issues/22850 is complete use that new matcher here for more specific error message assertion.
|
||||
expect( console ).toHaveErrored();
|
||||
} );
|
||||
it( 'dispatches the onProceedToCheckout event when the button is clicked', async () => {
|
||||
const mockObserver = jest.fn().mockReturnValue( { type: 'error' } );
|
||||
const MockObserverComponent = () => {
|
||||
const { onProceedToCheckout } = useCartEventsContext();
|
||||
useEffect( () => {
|
||||
return onProceedToCheckout( mockObserver );
|
||||
}, [ onProceedToCheckout ] );
|
||||
return <div>Mock observer</div>;
|
||||
};
|
||||
|
||||
render(
|
||||
<CartEventsProvider>
|
||||
<div>
|
||||
<MockObserverComponent />
|
||||
<Block
|
||||
buttonLabel={ 'Proceed to Checkout' }
|
||||
checkoutPageId={ 0 }
|
||||
className="test-block"
|
||||
/>
|
||||
</div>
|
||||
</CartEventsProvider>
|
||||
);
|
||||
expect( screen.getByText( 'Mock observer' ) ).toBeInTheDocument();
|
||||
const button = screen.getByText( 'Proceed to Checkout' );
|
||||
|
||||
// Forcibly set the button URL to # to prevent JSDOM error: `["Error: Not implemented: navigation (except hash changes)`
|
||||
button.parentElement?.removeAttribute( 'href' );
|
||||
|
||||
button.click();
|
||||
await waitFor( () => {
|
||||
expect( mockObserver ).toHaveBeenCalled();
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -12,77 +12,17 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table.wc-block-cart-items,
|
||||
table.wc-block-cart-items th,
|
||||
table.wc-block-cart-items td {
|
||||
// Override Storefront theme gray table background.
|
||||
background: none !important;
|
||||
// Remove borders on default themes.
|
||||
border: 0;
|
||||
margin: 0 0 2em;
|
||||
}
|
||||
|
||||
.editor-styles-wrapper table.wc-block-cart-items,
|
||||
table.wc-block-cart-items {
|
||||
width: 100%;
|
||||
|
||||
.wc-block-cart-items__header {
|
||||
@include font-size( smaller );
|
||||
text-transform: uppercase;
|
||||
|
||||
.wc-block-cart-items__header-image {
|
||||
width: 100px;
|
||||
}
|
||||
.wc-block-cart-items__header-product {
|
||||
visibility: hidden;
|
||||
}
|
||||
.wc-block-cart-items__header-total {
|
||||
width: 100px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
.wc-block-cart-items__row {
|
||||
.wc-block-cart-item__image img {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
.wc-block-cart-item__quantity {
|
||||
.wc-block-cart-item__remove-link {
|
||||
@include link-button;
|
||||
@include font-size( smaller );
|
||||
|
||||
text-transform: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
.wc-block-components-product-name {
|
||||
display: block;
|
||||
max-width: max-content;
|
||||
}
|
||||
.wc-block-cart-item__total {
|
||||
@include font-size( regular );
|
||||
text-align: right;
|
||||
line-height: inherit;
|
||||
}
|
||||
.wc-block-components-product-metadata {
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
transition: opacity 200ms ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-cart {
|
||||
.wc-block-components-totals-taxes,
|
||||
.wc-block-components-totals-footer-item {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
table.wc-block-cart-items,
|
||||
table.wc-block-cart-items th,
|
||||
table.wc-block-cart-items td {
|
||||
margin: 0 0 2em;
|
||||
}
|
||||
}
|
||||
|
||||
// Loading placeholder state.
|
||||
@@ -168,80 +108,11 @@ table.wc-block-cart-items {
|
||||
}
|
||||
}
|
||||
}
|
||||
table.wc-block-cart-items {
|
||||
td {
|
||||
padding: 0;
|
||||
}
|
||||
.wc-block-cart-items__header {
|
||||
display: none;
|
||||
}
|
||||
.wc-block-cart-item__remove-link {
|
||||
display: none;
|
||||
}
|
||||
&:not(.wc-block-mini-cart-items) {
|
||||
.wc-block-cart-items__row {
|
||||
@include with-translucent-border( 0 0 1px );
|
||||
}
|
||||
}
|
||||
.wc-block-cart-items__row {
|
||||
display: grid;
|
||||
grid-template-columns: 80px 132px;
|
||||
padding: $gap 0;
|
||||
|
||||
.wc-block-cart-item__image {
|
||||
grid-column-start: 1;
|
||||
grid-row-start: 1;
|
||||
padding-right: $gap;
|
||||
}
|
||||
.wc-block-cart-item__product {
|
||||
grid-column-start: 2;
|
||||
grid-column-end: 4;
|
||||
grid-row-start: 1;
|
||||
justify-self: stretch;
|
||||
padding: 0 $gap $gap 0;
|
||||
}
|
||||
.wc-block-cart-item__quantity {
|
||||
grid-column-start: 1;
|
||||
grid-row-start: 2;
|
||||
vertical-align: bottom;
|
||||
padding-right: $gap;
|
||||
align-self: end;
|
||||
padding-top: $gap;
|
||||
}
|
||||
.wc-block-cart-item__total {
|
||||
grid-row-start: 1;
|
||||
|
||||
.wc-block-components-formatted-money-amount {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-large.wc-block-cart {
|
||||
margin-bottom: 3em;
|
||||
|
||||
.wc-block-cart-items {
|
||||
@include with-translucent-border( 0 0 1px );
|
||||
|
||||
th {
|
||||
padding: 0.25rem $gap 0.25rem 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
td {
|
||||
@include with-translucent-border( 1px 0 0 );
|
||||
padding: $gap 0 $gap $gap;
|
||||
vertical-align: top;
|
||||
}
|
||||
th:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
td:last-child {
|
||||
padding-right: $gap;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-radio-control__input {
|
||||
left: 0;
|
||||
margin: 0;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { totals } from '@woocommerce/icons';
|
||||
*/
|
||||
import metadata from './block.json';
|
||||
import edit from './edit';
|
||||
import './style.scss';
|
||||
|
||||
registerBlockType( metadata, {
|
||||
icon: {
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import classnames from 'classnames';
|
||||
import { createInterpolateElement, useEffect } from '@wordpress/element';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
import {
|
||||
useStoreCart,
|
||||
useShowShippingTotalWarning,
|
||||
} from '@woocommerce/base-context/hooks';
|
||||
import { CheckoutProvider, noticeContexts } from '@woocommerce/base-context';
|
||||
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
|
||||
import { SidebarLayout } from '@woocommerce/base-components/sidebar-layout';
|
||||
@@ -161,6 +164,7 @@ const Block = ( {
|
||||
children: React.ReactChildren;
|
||||
scrollToTop: ( props: Record< string, unknown > ) => void;
|
||||
} ): JSX.Element => {
|
||||
useShowShippingTotalWarning();
|
||||
return (
|
||||
<BlockErrorBoundary
|
||||
header={ __(
|
||||
|
||||
@@ -12,4 +12,8 @@
|
||||
.wc-block-components-checkout-step__title {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
.block-editor-plain-text {
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
import { defaultPlaceOrderButtonLabel } from './constants';
|
||||
import './style.scss';
|
||||
|
||||
const Block = ( {
|
||||
cartPageId,
|
||||
@@ -53,7 +53,10 @@ const Block = ( {
|
||||
link={ getSetting( 'page-' + cartPageId, false ) }
|
||||
/>
|
||||
) }
|
||||
<PlaceOrderButton label={ label } />
|
||||
<PlaceOrderButton
|
||||
label={ label }
|
||||
fullWidth={ ! showReturnToCart }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { useRef } from '@wordpress/element';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
@@ -93,23 +94,35 @@ export const Edit = ( {
|
||||
) }
|
||||
</InspectorControls>
|
||||
<div className="wc-block-checkout__actions">
|
||||
<Noninteractive>
|
||||
{ showReturnToCart && (
|
||||
<ReturnToCartButton
|
||||
link={ getSetting( 'page-' + cartPageId, false ) }
|
||||
/>
|
||||
) }
|
||||
</Noninteractive>
|
||||
<EditableButton
|
||||
className="wc-block-cart__submit-button wc-block-components-checkout-place-order-button"
|
||||
value={ placeOrderButtonLabel }
|
||||
placeholder={ defaultPlaceOrderButtonLabel }
|
||||
onChange={ ( content ) => {
|
||||
setAttributes( {
|
||||
placeOrderButtonLabel: content,
|
||||
} );
|
||||
} }
|
||||
/>
|
||||
<div className="wc-block-checkout__actions_row">
|
||||
<Noninteractive>
|
||||
{ showReturnToCart && (
|
||||
<ReturnToCartButton
|
||||
link={ getSetting(
|
||||
'page-' + cartPageId,
|
||||
false
|
||||
) }
|
||||
/>
|
||||
) }
|
||||
</Noninteractive>
|
||||
<EditableButton
|
||||
className={ classnames(
|
||||
'wc-block-cart__submit-button',
|
||||
'wc-block-components-checkout-place-order-button',
|
||||
{
|
||||
'wc-block-components-checkout-place-order-button--full-width':
|
||||
! showReturnToCart,
|
||||
}
|
||||
) }
|
||||
value={ placeOrderButtonLabel }
|
||||
placeholder={ defaultPlaceOrderButtonLabel }
|
||||
onChange={ ( content ) => {
|
||||
setAttributes( {
|
||||
placeOrderButtonLabel: content,
|
||||
} );
|
||||
} }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { BlockConfiguration } from '@wordpress/blocks';
|
||||
*/
|
||||
import attributes from './attributes';
|
||||
import { Edit, Save } from './edit';
|
||||
import './style.scss';
|
||||
|
||||
const blockConfig: BlockConfiguration = {
|
||||
icon: {
|
||||
|
||||
@@ -9,9 +9,11 @@
|
||||
padding: 1em;
|
||||
height: auto;
|
||||
|
||||
.wc-block-components-button__text {
|
||||
line-height: 24px;
|
||||
&--full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wc-block-components-button__text {
|
||||
> svg {
|
||||
fill: $white;
|
||||
vertical-align: top;
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useMemo, useEffect, Fragment, useState } from '@wordpress/element';
|
||||
import {
|
||||
useMemo,
|
||||
useEffect,
|
||||
Fragment,
|
||||
useState,
|
||||
useCallback,
|
||||
} from '@wordpress/element';
|
||||
import {
|
||||
useCheckoutAddress,
|
||||
useStoreEvents,
|
||||
@@ -87,6 +93,23 @@ const Block = ( {
|
||||
showApartmentField,
|
||||
] ) as Record< keyof AddressFields, Partial< AddressField > >;
|
||||
|
||||
const onChangeAddress = useCallback(
|
||||
( values: Partial< BillingAddress > ) => {
|
||||
setBillingAddress( values );
|
||||
if ( useBillingAsShipping ) {
|
||||
setShippingAddress( values );
|
||||
dispatchCheckoutEvent( 'set-shipping-address' );
|
||||
}
|
||||
dispatchCheckoutEvent( 'set-billing-address' );
|
||||
},
|
||||
[
|
||||
dispatchCheckoutEvent,
|
||||
setBillingAddress,
|
||||
setShippingAddress,
|
||||
useBillingAsShipping,
|
||||
]
|
||||
);
|
||||
|
||||
const AddressFormWrapperComponent = isEditor ? Noninteractive : Fragment;
|
||||
const noticeContext = useBillingAsShipping
|
||||
? [ noticeContexts.BILLING_ADDRESS, noticeContexts.SHIPPING_ADDRESS ]
|
||||
@@ -98,14 +121,7 @@ const Block = ( {
|
||||
<AddressForm
|
||||
id="billing"
|
||||
type="billing"
|
||||
onChange={ ( values: Partial< BillingAddress > ) => {
|
||||
setBillingAddress( values );
|
||||
if ( useBillingAsShipping ) {
|
||||
setShippingAddress( values );
|
||||
dispatchCheckoutEvent( 'set-shipping-address' );
|
||||
}
|
||||
dispatchCheckoutEvent( 'set-billing-address' );
|
||||
} }
|
||||
onChange={ onChangeAddress }
|
||||
values={ billingAddress }
|
||||
fields={
|
||||
Object.keys(
|
||||
|
||||
@@ -42,7 +42,10 @@ export const Edit = ( {
|
||||
>
|
||||
<InspectorControls>
|
||||
<PanelBody
|
||||
title={ __( 'Account', 'woo-gutenberg-products-block' ) }
|
||||
title={ __(
|
||||
'Account creation and guest checkout',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
>
|
||||
<p className="wc-block-checkout__controls-text">
|
||||
{ __(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "woocommerce/checkout-express-payment-block",
|
||||
"version": "1.0.0",
|
||||
"title": "Express Checkout",
|
||||
"description": "Provide an express payment option for your customers.",
|
||||
"description": "Allow customers to breeze through with quick payment options.",
|
||||
"category": "woocommerce",
|
||||
"supports": {
|
||||
"align": false,
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon, payment } from '@wordpress/icons';
|
||||
import { Icon } from '@wordpress/icons';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import expressIcon from '../../../cart-checkout-shared/icon';
|
||||
import { Edit, Save } from './edit';
|
||||
|
||||
registerBlockType( 'woocommerce/checkout-express-payment-block', {
|
||||
icon: {
|
||||
src: (
|
||||
<Icon
|
||||
icon={ payment }
|
||||
style={ { fill: 'none' } } // this is needed for this particular svg
|
||||
icon={ expressIcon }
|
||||
className="wc-block-editor-components-block-icon"
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -4,11 +4,6 @@
|
||||
import classnames from 'classnames';
|
||||
import { Main } from '@woocommerce/base-components/sidebar-layout';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
const FrontendBlock = ( {
|
||||
children,
|
||||
className,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { registerBlockType } from '@wordpress/blocks';
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Edit, Save } from './edit';
|
||||
import './style.scss';
|
||||
|
||||
registerBlockType( 'woocommerce/checkout-fields-block', {
|
||||
icon: {
|
||||
|
||||
@@ -15,23 +15,15 @@
|
||||
.wc-block-checkout__shipping-fields,
|
||||
.wc-block-checkout__billing-fields {
|
||||
.wc-block-components-address-form {
|
||||
margin-left: #{-$gap-small * 0.5};
|
||||
margin-right: #{-$gap-small * 0.5};
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
clear: both;
|
||||
display: block;
|
||||
}
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
|
||||
.wc-block-components-text-input,
|
||||
.wc-block-components-country-input,
|
||||
.wc-block-components-state-input {
|
||||
float: left;
|
||||
margin-left: #{$gap-small * 0.5};
|
||||
margin-right: #{$gap-small * 0.5};
|
||||
position: relative;
|
||||
width: calc(50% - #{$gap-small});
|
||||
flex: 0 0 calc(50% - #{$gap-small});
|
||||
box-sizing: border-box;
|
||||
|
||||
&:nth-of-type(2),
|
||||
&:first-of-type {
|
||||
@@ -42,11 +34,7 @@
|
||||
.wc-block-components-address-form__company,
|
||||
.wc-block-components-address-form__address_1,
|
||||
.wc-block-components-address-form__address_2 {
|
||||
width: calc(100% - #{$gap-small});
|
||||
}
|
||||
|
||||
.wc-block-components-checkbox {
|
||||
clear: both;
|
||||
flex: 0 0 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { registerBlockType } from '@wordpress/blocks';
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Edit, Save } from './edit';
|
||||
import './style.scss';
|
||||
|
||||
registerBlockType( 'woocommerce/checkout-order-note-block', {
|
||||
icon: {
|
||||
|
||||
@@ -17,6 +17,6 @@
|
||||
margin-top: $gap;
|
||||
}
|
||||
|
||||
.wc-block-checkout__order-notes.wc-block-components-checkout-step {
|
||||
.wc-block-components-form .wc-block-checkout__order-notes.wc-block-components-checkout-step {
|
||||
padding-left: 0;
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
|
||||
@@ -12,6 +12,8 @@ import Noninteractive from '@woocommerce/base-components/noninteractive';
|
||||
import { GlobalPaymentMethod } from '@woocommerce/types';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { PAYMENT_STORE_KEY } from '@woocommerce/block-data';
|
||||
import { blocksConfig } from '@woocommerce/block-settings';
|
||||
import { trimCharacters, trimWords } from '@woocommerce/utils';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
@@ -49,6 +51,7 @@ export const Edit = ( {
|
||||
'Incompatible with block-based checkout',
|
||||
'woo-gutenberg-products-block'
|
||||
);
|
||||
const wordCountType = blocksConfig.wordCountType;
|
||||
|
||||
return (
|
||||
<FormStepBlock
|
||||
@@ -77,12 +80,32 @@ export const Edit = ( {
|
||||
const isIncompatible =
|
||||
!! incompatiblePaymentMethods[ method.id ];
|
||||
|
||||
let trimmedDescription;
|
||||
|
||||
if ( wordCountType === 'words' ) {
|
||||
trimmedDescription = trimWords(
|
||||
method.description,
|
||||
30,
|
||||
undefined,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
trimmedDescription = trimCharacters(
|
||||
method.description,
|
||||
30,
|
||||
wordCountType ===
|
||||
'characters_including_spaces',
|
||||
undefined,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ExternalLinkCard
|
||||
key={ method.id }
|
||||
href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=checkout§ion=${ method.id }` }
|
||||
title={ method.title }
|
||||
description={ method.description }
|
||||
description={ trimmedDescription }
|
||||
{ ...( isIncompatible
|
||||
? {
|
||||
warning:
|
||||
|
||||
@@ -26,7 +26,6 @@ import { LocalPickupSelect } from '@woocommerce/base-components/cart-checkout/lo
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
import ShippingRatesControlPackage from '../../../../base/components/cart-checkout/shipping-rates-control-package';
|
||||
|
||||
const getPickupLocation = (
|
||||
|
||||
@@ -9,6 +9,7 @@ import { registerBlockType } from '@wordpress/blocks';
|
||||
*/
|
||||
import { Edit, Save } from './edit';
|
||||
import attributes from './attributes';
|
||||
import './style.scss';
|
||||
|
||||
registerBlockType( 'woocommerce/checkout-pickup-options-block', {
|
||||
icon: {
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useMemo, useEffect, Fragment, useState } from '@wordpress/element';
|
||||
import {
|
||||
useMemo,
|
||||
useEffect,
|
||||
Fragment,
|
||||
useState,
|
||||
useCallback,
|
||||
} from '@wordpress/element';
|
||||
import { AddressForm } from '@woocommerce/base-components/cart-checkout';
|
||||
import {
|
||||
useCheckoutAddress,
|
||||
@@ -45,6 +51,7 @@ const Block = ( {
|
||||
setShippingAddress,
|
||||
setBillingAddress,
|
||||
shippingAddress,
|
||||
billingAddress,
|
||||
setShippingPhone,
|
||||
useShippingAsBilling,
|
||||
setUseShippingAsBilling,
|
||||
@@ -52,6 +59,7 @@ const Block = ( {
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
const { isEditor } = useEditorContext();
|
||||
|
||||
const { email } = billingAddress;
|
||||
// This is used to track whether the "Use shipping as billing" checkbox was checked on first load and if we synced
|
||||
// the shipping address to the billing address if it was. This is not used on further toggles of the checkbox.
|
||||
const [ addressesSynced, setAddressesSynced ] = useState( false );
|
||||
@@ -65,20 +73,25 @@ const Block = ( {
|
||||
|
||||
// Run this on first render to ensure addresses sync if needed, there is no need to re-run this when toggling the
|
||||
// checkbox.
|
||||
useEffect( () => {
|
||||
if ( addressesSynced ) {
|
||||
return;
|
||||
}
|
||||
if ( useShippingAsBilling ) {
|
||||
setBillingAddress( shippingAddress );
|
||||
}
|
||||
setAddressesSynced( true );
|
||||
}, [
|
||||
addressesSynced,
|
||||
setBillingAddress,
|
||||
shippingAddress,
|
||||
useShippingAsBilling,
|
||||
] );
|
||||
useEffect(
|
||||
() => {
|
||||
if ( addressesSynced ) {
|
||||
return;
|
||||
}
|
||||
if ( useShippingAsBilling ) {
|
||||
setBillingAddress( { ...shippingAddress, email } );
|
||||
}
|
||||
setAddressesSynced( true );
|
||||
},
|
||||
// Skip the `email` dependency since we don't want to re-run if that changes, but we do want to sync it on first render.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
addressesSynced,
|
||||
setBillingAddress,
|
||||
shippingAddress,
|
||||
useShippingAsBilling,
|
||||
]
|
||||
);
|
||||
|
||||
const addressFieldsConfig = useMemo( () => {
|
||||
return {
|
||||
@@ -96,6 +109,24 @@ const Block = ( {
|
||||
showApartmentField,
|
||||
] ) as Record< keyof AddressFields, Partial< AddressField > >;
|
||||
|
||||
const onChangeAddress = useCallback(
|
||||
( values: Partial< ShippingAddress > ) => {
|
||||
setShippingAddress( values );
|
||||
if ( useShippingAsBilling ) {
|
||||
setBillingAddress( { ...values, email } );
|
||||
dispatchCheckoutEvent( 'set-billing-address' );
|
||||
}
|
||||
dispatchCheckoutEvent( 'set-shipping-address' );
|
||||
},
|
||||
[
|
||||
dispatchCheckoutEvent,
|
||||
email,
|
||||
setBillingAddress,
|
||||
setShippingAddress,
|
||||
useShippingAsBilling,
|
||||
]
|
||||
);
|
||||
|
||||
const AddressFormWrapperComponent = isEditor ? Noninteractive : Fragment;
|
||||
const noticeContext = useShippingAsBilling
|
||||
? [ noticeContexts.SHIPPING_ADDRESS, noticeContexts.BILLING_ADDRESS ]
|
||||
@@ -108,13 +139,7 @@ const Block = ( {
|
||||
<AddressForm
|
||||
id="shipping"
|
||||
type="shipping"
|
||||
onChange={ ( values: Partial< ShippingAddress > ) => {
|
||||
setShippingAddress( values );
|
||||
if ( useShippingAsBilling ) {
|
||||
setBillingAddress( values );
|
||||
}
|
||||
dispatchCheckoutEvent( 'set-shipping-address' );
|
||||
} }
|
||||
onChange={ onChangeAddress }
|
||||
values={ shippingAddress }
|
||||
fields={
|
||||
Object.keys(
|
||||
|
||||
@@ -10,13 +10,13 @@ import {
|
||||
import classnames from 'classnames';
|
||||
import { Icon, store, shipping } from '@wordpress/icons';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { CART_STORE_KEY, VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { isPackageRateCollectable } from '@woocommerce/base-utils';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
import { RatePrice, getLocalPickupPrices, getShippingPrices } from './shared';
|
||||
import type { minMaxPrices } from './shared';
|
||||
import { defaultLocalPickupText, defaultShippingText } from './constants';
|
||||
@@ -92,8 +92,17 @@ const ShippingSelector = ( {
|
||||
shippingCostRequiresAddress: boolean;
|
||||
toggleText: string;
|
||||
} ) => {
|
||||
const hasShippableRates = useSelect( ( select ) => {
|
||||
const rates = select( CART_STORE_KEY ).getShippingRates();
|
||||
return rates.some(
|
||||
( { shipping_rates: shippingRate } ) =>
|
||||
! shippingRate.every( isPackageRateCollectable )
|
||||
);
|
||||
} );
|
||||
const rateShouldBeHidden =
|
||||
shippingCostRequiresAddress && shippingAddressHasValidationErrors();
|
||||
shippingCostRequiresAddress &&
|
||||
shippingAddressHasValidationErrors() &&
|
||||
! hasShippableRates;
|
||||
const hasShippingPrices = rate.min !== undefined && rate.max !== undefined;
|
||||
const { setValidationErrors, clearValidationError } =
|
||||
useDispatch( VALIDATION_STORE_KEY );
|
||||
|
||||
@@ -9,6 +9,7 @@ import { registerBlockType } from '@wordpress/blocks';
|
||||
*/
|
||||
import { Edit, Save } from './edit';
|
||||
import attributes from './attributes';
|
||||
import './style.scss';
|
||||
|
||||
registerBlockType( 'woocommerce/checkout-shipping-method-block', {
|
||||
icon: {
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useShippingData } from '@woocommerce/base-context/hooks';
|
||||
import {
|
||||
useCustomerData,
|
||||
useShippingData,
|
||||
} from '@woocommerce/base-context/hooks';
|
||||
import { ShippingRatesControl } from '@woocommerce/base-components/cart-checkout';
|
||||
import {
|
||||
getShippingRatesPackageCount,
|
||||
@@ -19,15 +22,8 @@ import type {
|
||||
PackageRateOption,
|
||||
CartShippingPackageShippingRate,
|
||||
} from '@woocommerce/types';
|
||||
import { CART_STORE_KEY } from '@woocommerce/block-data';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import NoticeBanner from '@woocommerce/base-components/notice-banner';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
import { shippingAddressHasValidationErrors } from '../../../../data/cart/utils';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
/**
|
||||
* Renders a shipping rate control option.
|
||||
@@ -54,10 +50,7 @@ const renderShippingRatesControlOption = (
|
||||
};
|
||||
};
|
||||
|
||||
const Block = ( {
|
||||
noShippingPlaceholder = null,
|
||||
shippingCostRequiresAddress = false,
|
||||
} ): React.ReactElement | null => {
|
||||
const Block = ( { noShippingPlaceholder = null } ): ReactElement | null => {
|
||||
const { isEditor } = useEditorContext();
|
||||
|
||||
const {
|
||||
@@ -68,9 +61,7 @@ const Block = ( {
|
||||
isCollectable,
|
||||
} = useShippingData();
|
||||
|
||||
const shippingAddressPushed = useSelect( ( select ) => {
|
||||
return select( CART_STORE_KEY ).getFullShippingAddressPushed();
|
||||
} );
|
||||
const { shippingAddress } = useCustomerData();
|
||||
|
||||
const filteredShippingRates = isCollectable
|
||||
? shippingRates.map( ( shippingRatesPackage ) => {
|
||||
@@ -86,25 +77,14 @@ const Block = ( {
|
||||
} )
|
||||
: shippingRates;
|
||||
|
||||
const shippingAddress = useSelect( ( select ) => {
|
||||
return select( CART_STORE_KEY ).getCustomerData()?.shippingAddress;
|
||||
} );
|
||||
|
||||
if ( ! needsShipping ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const shippingAddressHasErrors = ! shippingAddressHasValidationErrors();
|
||||
const addressComplete = isAddressComplete( shippingAddress );
|
||||
|
||||
const shippingRatesPackageCount =
|
||||
getShippingRatesPackageCount( shippingRates );
|
||||
|
||||
if (
|
||||
( ! hasCalculatedShipping && ! shippingRatesPackageCount ) ||
|
||||
( shippingCostRequiresAddress &&
|
||||
( ! shippingAddressPushed || ! shippingAddressHasErrors ) )
|
||||
) {
|
||||
if ( ! hasCalculatedShipping && ! shippingRatesPackageCount ) {
|
||||
return (
|
||||
<p>
|
||||
{ __(
|
||||
@@ -114,6 +94,7 @@ const Block = ( {
|
||||
</p>
|
||||
);
|
||||
}
|
||||
const addressComplete = isAddressComplete( shippingAddress );
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -20,7 +20,6 @@ const FrontendBlock = ( {
|
||||
showStepNumber,
|
||||
children,
|
||||
className,
|
||||
shippingCostRequiresAddress = false,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
@@ -32,7 +31,6 @@ const FrontendBlock = ( {
|
||||
showStepNumber: boolean;
|
||||
children: JSX.Element;
|
||||
className?: string;
|
||||
shippingCostRequiresAddress: boolean;
|
||||
} ) => {
|
||||
const checkoutIsProcessing = useSelect( ( select ) =>
|
||||
select( CHECKOUT_STORE_KEY ).isProcessing()
|
||||
@@ -55,9 +53,7 @@ const FrontendBlock = ( {
|
||||
description={ description }
|
||||
showStepNumber={ showStepNumber }
|
||||
>
|
||||
<Block
|
||||
shippingCostRequiresAddress={ shippingCostRequiresAddress }
|
||||
/>
|
||||
<Block />
|
||||
{ children }
|
||||
</FormStep>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { registerBlockType } from '@wordpress/blocks';
|
||||
*/
|
||||
import { Edit, Save } from './edit';
|
||||
import attributes from './attributes';
|
||||
import './style.scss';
|
||||
|
||||
registerBlockType( 'woocommerce/checkout-shipping-methods-block', {
|
||||
icon: {
|
||||
|
||||
@@ -14,7 +14,6 @@ import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { termsConsentDefaultText, termsCheckboxDefaultText } from './constants';
|
||||
import './style.scss';
|
||||
|
||||
const FrontendBlock = ( {
|
||||
text,
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
*/
|
||||
import { Icon, customPostType } from '@wordpress/icons';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Edit, Save } from './edit';
|
||||
import './style.scss';
|
||||
|
||||
registerBlockType( 'woocommerce/checkout-terms-block', {
|
||||
icon: {
|
||||
|
||||
@@ -3,11 +3,7 @@
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { Sidebar } from '@woocommerce/base-components/sidebar-layout';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
|
||||
|
||||
const FrontendBlock = ( {
|
||||
children,
|
||||
@@ -20,6 +16,9 @@ const FrontendBlock = ( {
|
||||
<Sidebar
|
||||
className={ classnames( 'wc-block-checkout__sidebar', className ) }
|
||||
>
|
||||
<StoreNoticesContainer
|
||||
context={ 'woocommerce/checkout-totals-block' }
|
||||
/>
|
||||
{ children }
|
||||
</Sidebar>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { registerBlockType } from '@wordpress/blocks';
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Edit, Save } from './edit';
|
||||
import './style.scss';
|
||||
|
||||
registerBlockType( 'woocommerce/checkout-totals-block', {
|
||||
icon: {
|
||||
|
||||
@@ -6,11 +6,6 @@ import { useState } from '@wordpress/element';
|
||||
import { CheckboxControl } from '@woocommerce/blocks-checkout';
|
||||
import { Textarea } from '@woocommerce/base-components/textarea';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
interface CheckoutOrderNotesProps {
|
||||
disabled: boolean;
|
||||
onChange: ( orderNotes: string ) => void;
|
||||
|
||||
@@ -51,17 +51,11 @@ body.wc-lock-selected-block--remove {
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-checkout__controls-text {
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.components-base-control--nested {
|
||||
padding-left: 52px;
|
||||
margin-top: -12px;
|
||||
}
|
||||
|
||||
|
||||
.components-panel__body-title .components-button {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -51,17 +51,21 @@
|
||||
.wc-block-components-panel > h2 {
|
||||
@include font-size(regular);
|
||||
@include reset-box();
|
||||
@include reset-color();
|
||||
@include reset-typography();
|
||||
.wc-block-components-panel__button {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
.wc-block-components-totals-item,
|
||||
.wc-block-components-totals-coupon-link,
|
||||
.wc-block-components-panel {
|
||||
padding-left: $gap;
|
||||
padding-right: $gap;
|
||||
}
|
||||
.wc-block-components-totals-coupon-link {
|
||||
margin-left: $gap;
|
||||
margin-right: $gap;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Skeleton is shown before mobile classes are appended.
|
||||
@@ -78,11 +82,14 @@
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
.wc-block-components-totals-item,
|
||||
.wc-block-components-totals-coupon-link,
|
||||
.wc-block-components-panel {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
.wc-block-components-totals-coupon-link {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,19 +7,18 @@ import {
|
||||
type BlockInstance,
|
||||
} from '@wordpress/blocks';
|
||||
import { isWpVersion } from '@woocommerce/settings';
|
||||
import { isExperimentalBuild } from '@woocommerce/block-settings';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import {
|
||||
INNER_BLOCKS_TEMPLATE as productsInnerBlocksTemplate,
|
||||
QUERY_DEFAULT_ATTRIBUTES as productsQueryDefaultAttributes,
|
||||
PRODUCT_QUERY_VARIATION_NAME as productsVariationName,
|
||||
} from '@woocommerce/blocks/product-query/constants';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
INNER_BLOCKS_TEMPLATE as productsInnerBlocksTemplate,
|
||||
QUERY_DEFAULT_ATTRIBUTES as productsQueryDefaultAttributes,
|
||||
} from '../product-query/constants';
|
||||
import { VARIATION_NAME as productsVariationName } from '../product-query/variations/product-query';
|
||||
import { createArchiveTitleBlock, createRowBlock } from './utils';
|
||||
import { type InheritedAttributes } from './types';
|
||||
import { OnClickCallbackParameter, type InheritedAttributes } from './types';
|
||||
|
||||
const createProductsBlock = ( inheritedAttributes: InheritedAttributes ) =>
|
||||
createBlock(
|
||||
@@ -64,14 +63,14 @@ const getBlockifiedTemplateWithTermDescription = (
|
||||
const isConversionPossible = () => {
|
||||
// Blockification is possible for the WP version 6.1 and above,
|
||||
// which are the versions the Products block supports.
|
||||
return isExperimentalBuild() && isWpVersion( '6.1', '>=' );
|
||||
return isWpVersion( '6.1', '>=' );
|
||||
};
|
||||
|
||||
const getDescriptionAllowingConversion = ( templateTitle: string ) =>
|
||||
sprintf(
|
||||
/* translators: %s is the template title */
|
||||
__(
|
||||
"This block serves as a placeholder for your %s. We recommend upgrading to the Products block for more features to edit your products visually. Don't worry, you can always revert back.",
|
||||
'Transform this template into multiple blocks so you can add, remove, reorder, and customize your %s template.',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
templateTitle
|
||||
@@ -96,18 +95,78 @@ const getDescription = ( templateTitle: string, canConvert: boolean ) => {
|
||||
};
|
||||
|
||||
const getButtonLabel = () =>
|
||||
__( 'Upgrade to Products block', 'woo-gutenberg-products-block' );
|
||||
__( 'Transform into blocks', 'woo-gutenberg-products-block' );
|
||||
|
||||
const onClickCallback = ( {
|
||||
clientId,
|
||||
attributes,
|
||||
getBlocks,
|
||||
replaceBlock,
|
||||
selectBlock,
|
||||
}: OnClickCallbackParameter ) => {
|
||||
replaceBlock( clientId, getBlockifiedTemplate( attributes ) );
|
||||
|
||||
const blocks = getBlocks();
|
||||
|
||||
const groupBlock = blocks.find(
|
||||
( block ) =>
|
||||
block.name === 'core/group' &&
|
||||
block.innerBlocks.some(
|
||||
( innerBlock ) =>
|
||||
innerBlock.name === 'woocommerce/store-notices'
|
||||
)
|
||||
);
|
||||
|
||||
if ( groupBlock ) {
|
||||
selectBlock( groupBlock.clientId );
|
||||
}
|
||||
};
|
||||
|
||||
const onClickCallbackWithTermDescription = ( {
|
||||
clientId,
|
||||
attributes,
|
||||
getBlocks,
|
||||
replaceBlock,
|
||||
selectBlock,
|
||||
}: OnClickCallbackParameter ) => {
|
||||
replaceBlock( clientId, getBlockifiedTemplate( attributes, true ) );
|
||||
|
||||
const blocks = getBlocks();
|
||||
|
||||
const groupBlock = blocks.find(
|
||||
( block ) =>
|
||||
block.name === 'core/group' &&
|
||||
block.innerBlocks.some(
|
||||
( innerBlock ) =>
|
||||
innerBlock.name === 'woocommerce/store-notices'
|
||||
)
|
||||
);
|
||||
|
||||
if ( groupBlock ) {
|
||||
selectBlock( groupBlock.clientId );
|
||||
}
|
||||
};
|
||||
|
||||
const productCatalogBlockifyConfig = {
|
||||
getButtonLabel,
|
||||
onClickCallback,
|
||||
getBlockifiedTemplate,
|
||||
};
|
||||
|
||||
const productTaxonomyBlockifyConfig = {
|
||||
getButtonLabel,
|
||||
onClickCallback: onClickCallbackWithTermDescription,
|
||||
getBlockifiedTemplate: getBlockifiedTemplateWithTermDescription,
|
||||
};
|
||||
|
||||
export const blockifiedProductCatalogConfig = {
|
||||
getBlockifiedTemplate,
|
||||
isConversionPossible,
|
||||
getDescription,
|
||||
getButtonLabel,
|
||||
blockifyConfig: productCatalogBlockifyConfig,
|
||||
};
|
||||
|
||||
export const blockifiedProductTaxonomyConfig = {
|
||||
getBlockifiedTemplate: getBlockifiedTemplateWithTermDescription,
|
||||
isConversionPossible,
|
||||
getDescription,
|
||||
getButtonLabel,
|
||||
blockifyConfig: productTaxonomyBlockifyConfig,
|
||||
};
|
||||
|
||||
@@ -14,10 +14,16 @@ export const TYPES = {
|
||||
productCatalog: 'product-catalog',
|
||||
productTaxonomy: 'product-taxonomy',
|
||||
productSearchResults: 'product-search-results',
|
||||
orderConfirmation: 'order-confirmation',
|
||||
cart: 'cart',
|
||||
checkout: 'checkout',
|
||||
checkoutHeader: 'checkout-header',
|
||||
};
|
||||
export const PLACEHOLDERS = {
|
||||
singleProduct: 'single-product',
|
||||
archiveProduct: 'archive-product',
|
||||
orderConfirmation: 'fallback',
|
||||
checkoutHeader: 'checkout-header',
|
||||
};
|
||||
|
||||
export const TEMPLATES: TemplateDetails = {
|
||||
@@ -61,6 +67,15 @@ export const TEMPLATES: TemplateDetails = {
|
||||
),
|
||||
placeholder: PLACEHOLDERS.archiveProduct,
|
||||
},
|
||||
// Since that it is a fallback value, it has to be the last one.
|
||||
'taxonomy-product': {
|
||||
type: TYPES.productTaxonomy,
|
||||
title: __(
|
||||
"WooCommerce Product's Custom Taxonomy Block",
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
placeholder: PLACEHOLDERS.archiveProduct,
|
||||
},
|
||||
'product-search-results': {
|
||||
type: TYPES.productSearchResults,
|
||||
title: __(
|
||||
@@ -69,4 +84,24 @@ export const TEMPLATES: TemplateDetails = {
|
||||
),
|
||||
placeholder: PLACEHOLDERS.archiveProduct,
|
||||
},
|
||||
cart: {
|
||||
type: TYPES.cart,
|
||||
title: __( 'WooCommerce Cart Block', 'woo-gutenberg-products-block' ),
|
||||
placeholder: 'cart',
|
||||
},
|
||||
checkout: {
|
||||
type: TYPES.checkout,
|
||||
title: __( 'Checkout Block', 'woo-gutenberg-products-block' ),
|
||||
placeholder: 'checkout',
|
||||
},
|
||||
'checkout-header': {
|
||||
type: TYPES.checkoutHeader,
|
||||
title: __( 'Checkout Header', 'woo-gutenberg-products-block' ),
|
||||
placeholder: 'checkout-header',
|
||||
},
|
||||
'order-confirmation': {
|
||||
type: TYPES.orderConfirmation,
|
||||
title: __( 'Order Confirmation Block', 'woo-gutenberg-products-block' ),
|
||||
placeholder: PLACEHOLDERS.orderConfirmation,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,48 +1,88 @@
|
||||
:where(.wp-block-woocommerce-legacy-template) {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 1000px;
|
||||
}
|
||||
|
||||
.wp-block-woocommerce-classic-template__placeholder-copy {
|
||||
max-width: 900px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.wp-block-woocommerce-classic-template__placeholder-warning {
|
||||
border-left: 5px solid #2181d2;
|
||||
padding-left: em(40px);
|
||||
}
|
||||
|
||||
.wp-block-woocommerce-classic-template__placeholder-wireframe {
|
||||
width: 100%;
|
||||
height: 250px;
|
||||
background: #e5e5e5;
|
||||
|
||||
@media only screen and (min-width: 768px) {
|
||||
height: auto;
|
||||
background: transparent;
|
||||
}
|
||||
.wp-block-woocommerce-classic-template__placeholder .components-placeholder__fieldset {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.wp-block-woocommerce-classic-template__placeholder .wp-block-woocommerce-classic-template__placeholder-image {
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
@media only screen and (min-width: 768px) {
|
||||
display: block;
|
||||
}
|
||||
.wp-block-woocommerce-classic-template__placeholder-wireframe,
|
||||
.wp-block-woocommerce-classic-template__placeholder-copy {
|
||||
grid-row-start: 1;
|
||||
grid-column-start: 1;
|
||||
transition: 0.3s all ease;
|
||||
}
|
||||
|
||||
|
||||
.wp-block-woocommerce-classic-template__placeholder-wireframe {
|
||||
.wp-block-woocommerce-classic-template__placeholder-copy {
|
||||
border: 1px solid $gray-900;
|
||||
background-color: #fff;
|
||||
padding: $gap-large $gap-larger;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 900px;
|
||||
width: 400px;
|
||||
margin: auto;
|
||||
opacity: 0;
|
||||
z-index: 10;
|
||||
|
||||
.wp-block-woocommerce-classic-template__placeholder-copy__icon-container {
|
||||
margin: 0 0 $gap;
|
||||
|
||||
span {
|
||||
@include font-size(larger);
|
||||
display: block;
|
||||
}
|
||||
.woo-icon {
|
||||
color: #{$studio-woocommerce-purple};
|
||||
@include font-size(large);
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin: 0 0 $gap;
|
||||
}
|
||||
.wp-block-woocommerce-classic-template__placeholder-migration-button-container {
|
||||
justify-content: center;
|
||||
margin: $gap 0;
|
||||
}
|
||||
}
|
||||
.wp-block-woocommerce-classic-template__placeholder-wireframe {
|
||||
pointer-events: none;
|
||||
|
||||
// Image based placeholders should fill horizontal width.
|
||||
> img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.wp-block-woocommerce-classic-template__placeholder-migration-button-container {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 auto;
|
||||
.wp-block-woocommerce-legacy-template {
|
||||
.components-placeholder {
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
.wp-block-woocommerce-legacy-template.is-selected {
|
||||
.wp-block-woocommerce-classic-template__placeholder-wireframe {
|
||||
filter: blur(3px);
|
||||
opacity: 0.5;
|
||||
|
||||
* {
|
||||
color: $gray-200 !important;
|
||||
border-color: $gray-200 !important;
|
||||
}
|
||||
}
|
||||
.wp-block-woocommerce-classic-template__placeholder-copy {
|
||||
opacity: 1;
|
||||
}
|
||||
.components-placeholder {
|
||||
box-shadow: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,21 +2,36 @@
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
BlockInstance,
|
||||
createBlock,
|
||||
getBlockType,
|
||||
registerBlockType,
|
||||
unregisterBlockType,
|
||||
parse,
|
||||
} from '@wordpress/blocks';
|
||||
import type { BlockEditProps } from '@wordpress/blocks';
|
||||
import { WC_BLOCKS_IMAGE_URL } from '@woocommerce/block-settings';
|
||||
import {
|
||||
isExperimentalBuild,
|
||||
WC_BLOCKS_IMAGE_URL,
|
||||
} from '@woocommerce/block-settings';
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import { Button, Placeholder } from '@wordpress/components';
|
||||
useBlockProps,
|
||||
BlockPreview,
|
||||
store as blockEditorStore,
|
||||
} from '@wordpress/block-editor';
|
||||
import { Button, Placeholder, Popover } from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { box, Icon } from '@wordpress/icons';
|
||||
import { select, useDispatch, subscribe } from '@wordpress/data';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import {
|
||||
useDispatch,
|
||||
subscribe,
|
||||
useSelect,
|
||||
select,
|
||||
dispatch,
|
||||
} from '@wordpress/data';
|
||||
import { useEffect, useState } from '@wordpress/element';
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
import { useEntityRecord } from '@wordpress/core-data';
|
||||
import { debounce } from '@woocommerce/base-utils';
|
||||
import { woo } from '@woocommerce/icons';
|
||||
import { isNumber } from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
@@ -35,6 +50,8 @@ import {
|
||||
} from './archive-product';
|
||||
import * as blockifiedSingleProduct from './single-product';
|
||||
import * as blockifiedProductSearchResults from './product-search-results';
|
||||
import * as blockifiedOrderConfirmation from './order-confirmation';
|
||||
|
||||
import type { BlockifiedTemplateConfig } from './types';
|
||||
|
||||
type Attributes = {
|
||||
@@ -46,7 +63,7 @@ const blockifiedFallbackConfig = {
|
||||
isConversionPossible: () => false,
|
||||
getBlockifiedTemplate: () => [],
|
||||
getDescription: () => '',
|
||||
getButtonLabel: () => '',
|
||||
onClickCallback: () => void 0,
|
||||
};
|
||||
|
||||
const conversionConfig: { [ key: string ]: BlockifiedTemplateConfig } = {
|
||||
@@ -54,22 +71,152 @@ const conversionConfig: { [ key: string ]: BlockifiedTemplateConfig } = {
|
||||
[ TYPES.productTaxonomy ]: blockifiedProductTaxonomyConfig,
|
||||
[ TYPES.singleProduct ]: blockifiedSingleProduct,
|
||||
[ TYPES.productSearchResults ]: blockifiedProductSearchResults,
|
||||
[ TYPES.orderConfirmation ]: blockifiedOrderConfirmation,
|
||||
fallback: blockifiedFallbackConfig,
|
||||
};
|
||||
|
||||
const pickBlockClientIds = ( blocks: Array< BlockInstance > ) =>
|
||||
blocks.reduce< Array< string > >( ( acc, block ) => {
|
||||
if ( block.name === 'core/template-part' ) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return [ ...acc, block.clientId ];
|
||||
}, [] );
|
||||
|
||||
const ConvertTemplate = ( { blockifyConfig, clientId, attributes } ) => {
|
||||
const { getButtonLabel, onClickCallback, getBlockifiedTemplate } =
|
||||
blockifyConfig;
|
||||
|
||||
const [ isPopoverOpen, setIsPopoverOpen ] = useState( false );
|
||||
const { replaceBlock, selectBlock, replaceBlocks } =
|
||||
useDispatch( blockEditorStore );
|
||||
|
||||
const { getBlocks } = useSelect( ( sel ) => {
|
||||
return {
|
||||
getBlocks: sel( blockEditorStore ).getBlocks,
|
||||
};
|
||||
}, [] );
|
||||
|
||||
const { createInfoNotice } = useDispatch( noticesStore );
|
||||
|
||||
return (
|
||||
<div className="wp-block-woocommerce-classic-template__placeholder-migration-button-container">
|
||||
<Button
|
||||
isPrimary
|
||||
onClick={ () => {
|
||||
onClickCallback( {
|
||||
clientId,
|
||||
getBlocks,
|
||||
attributes,
|
||||
replaceBlock,
|
||||
selectBlock,
|
||||
} );
|
||||
createInfoNotice(
|
||||
__(
|
||||
'Template transformed into blocks!',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: __(
|
||||
'Undo',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
onClick: () => {
|
||||
const clientIds = pickBlockClientIds(
|
||||
getBlocks()
|
||||
);
|
||||
|
||||
replaceBlocks(
|
||||
clientIds,
|
||||
createBlock(
|
||||
'core/group',
|
||||
{
|
||||
layout: {
|
||||
inherit: true,
|
||||
type: 'constrained',
|
||||
},
|
||||
},
|
||||
[
|
||||
createBlock(
|
||||
'woocommerce/legacy-template',
|
||||
{
|
||||
template:
|
||||
attributes.template,
|
||||
}
|
||||
),
|
||||
]
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'snackbar',
|
||||
}
|
||||
);
|
||||
} }
|
||||
onMouseEnter={ () => setIsPopoverOpen( true ) }
|
||||
onMouseLeave={ () => setIsPopoverOpen( false ) }
|
||||
text={ getButtonLabel ? getButtonLabel() : '' }
|
||||
>
|
||||
{ isPopoverOpen && (
|
||||
<Popover resize={ false } placement="right-end">
|
||||
<div
|
||||
style={ {
|
||||
minWidth: '250px',
|
||||
width: '250px',
|
||||
maxWidth: '250px',
|
||||
minHeight: '300px',
|
||||
height: '300px',
|
||||
maxHeight: '300px',
|
||||
cursor: 'pointer',
|
||||
} }
|
||||
>
|
||||
<BlockPreview
|
||||
blocks={ getBlockifiedTemplate( attributes ) }
|
||||
viewportWidth={ 1200 }
|
||||
additionalStyles={ [
|
||||
{
|
||||
css: 'body { padding: 20px !important; height: fit-content !important; overflow:hidden}',
|
||||
},
|
||||
] }
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
) }
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Edit = ( {
|
||||
clientId,
|
||||
attributes,
|
||||
setAttributes,
|
||||
}: BlockEditProps< Attributes > ) => {
|
||||
const { replaceBlock } = useDispatch( 'core/block-editor' );
|
||||
|
||||
const blockProps = useBlockProps();
|
||||
const { editedPostId } = useSelect( ( sel ) => {
|
||||
return {
|
||||
editedPostId: sel( 'core/edit-site' ).getEditedPostId(),
|
||||
};
|
||||
}, [] );
|
||||
|
||||
const template = useEntityRecord< {
|
||||
slug: string;
|
||||
title: {
|
||||
rendered?: string;
|
||||
row: string;
|
||||
};
|
||||
} >( 'postType', 'wp_template', editedPostId );
|
||||
|
||||
const templateDetails = getTemplateDetailsBySlug(
|
||||
attributes.template,
|
||||
TEMPLATES
|
||||
);
|
||||
const templateTitle = templateDetails?.title ?? attributes.template;
|
||||
const templateTitle =
|
||||
template.record?.title.rendered?.toLowerCase() ?? attributes.template;
|
||||
const templatePlaceholder = templateDetails?.placeholder ?? 'fallback';
|
||||
const templateType = templateDetails?.type ?? 'fallback';
|
||||
|
||||
@@ -83,45 +230,65 @@ const Edit = ( {
|
||||
);
|
||||
|
||||
const {
|
||||
getBlockifiedTemplate,
|
||||
isConversionPossible,
|
||||
getDescription,
|
||||
getButtonLabel,
|
||||
getSkeleton,
|
||||
blockifyConfig,
|
||||
} = conversionConfig[ templateType ];
|
||||
|
||||
const skeleton = getSkeleton ? (
|
||||
getSkeleton()
|
||||
) : (
|
||||
<img
|
||||
className="wp-block-woocommerce-classic-template__placeholder-image"
|
||||
src={ `${ WC_BLOCKS_IMAGE_URL }template-placeholders/${ templatePlaceholder }.svg` }
|
||||
alt={ templateTitle }
|
||||
/>
|
||||
);
|
||||
|
||||
const canConvert = isConversionPossible();
|
||||
const placeholderDescription = getDescription( templateTitle, canConvert );
|
||||
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<Placeholder
|
||||
icon={ box }
|
||||
label={ templateTitle }
|
||||
className="wp-block-woocommerce-classic-template__placeholder"
|
||||
>
|
||||
<div className="wp-block-woocommerce-classic-template__placeholder-copy">
|
||||
<p>{ placeholderDescription }</p>
|
||||
</div>
|
||||
<Placeholder className="wp-block-woocommerce-classic-template__placeholder">
|
||||
<div className="wp-block-woocommerce-classic-template__placeholder-wireframe">
|
||||
{ canConvert && (
|
||||
<div className="wp-block-woocommerce-classic-template__placeholder-migration-button-container">
|
||||
<Button
|
||||
isPrimary
|
||||
onClick={ () => {
|
||||
replaceBlock(
|
||||
clientId,
|
||||
getBlockifiedTemplate( attributes )
|
||||
);
|
||||
} }
|
||||
text={ getButtonLabel() }
|
||||
/>
|
||||
</div>
|
||||
) }
|
||||
<img
|
||||
className="wp-block-woocommerce-classic-template__placeholder-image"
|
||||
src={ `${ WC_BLOCKS_IMAGE_URL }template-placeholders/${ templatePlaceholder }.svg` }
|
||||
alt={ templateTitle }
|
||||
{ skeleton }
|
||||
</div>
|
||||
<div className="wp-block-woocommerce-classic-template__placeholder-copy">
|
||||
<div className="wp-block-woocommerce-classic-template__placeholder-copy__icon-container">
|
||||
<span className="woo-icon">
|
||||
<Icon icon={ woo } />{ ' ' }
|
||||
{ __(
|
||||
'WooCommerce',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
</span>
|
||||
<span>
|
||||
{ __(
|
||||
'Classic Template Placeholder',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
dangerouslySetInnerHTML={ {
|
||||
__html: placeholderDescription,
|
||||
} }
|
||||
/>
|
||||
<p>
|
||||
{ __(
|
||||
'You cannot edit the content of this block. However, you can move it and place other blocks around it.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
</p>
|
||||
{ canConvert && blockifyConfig && (
|
||||
<ConvertTemplate
|
||||
clientId={ clientId }
|
||||
blockifyConfig={ blockifyConfig }
|
||||
attributes={ attributes }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
</Placeholder>
|
||||
</div>
|
||||
@@ -206,59 +373,102 @@ const registerClassicTemplateBlock = ( {
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempts to recover the Classic Template block if it fails to render on the Single Product template
|
||||
* due to the user resetting customizations without refreshing the page.
|
||||
*
|
||||
* When the Classic Template block fails to render, it is replaced by the 'core/missing' block, which
|
||||
* displays an error message stating that the WooCommerce Classic template block is unsupported.
|
||||
*
|
||||
* This function replaces the 'core/missing' block with the original Classic Template block that failed
|
||||
* to render, allowing the block to be displayed correctly.
|
||||
*
|
||||
* @see {@link https://github.com/woocommerce/woocommerce-blocks/issues/9637|Issue: Block error is displayed on clearing customizations for Woo Templates}
|
||||
*
|
||||
*/
|
||||
const tryToRecoverClassicTemplateBlockWhenItFailsToRender = debounce( () => {
|
||||
const blocks = select( 'core/block-editor' ).getBlocks();
|
||||
const blocksIncludingInnerBlocks = blocks.flatMap( ( block ) => [
|
||||
block,
|
||||
...block.innerBlocks,
|
||||
] );
|
||||
const classicTemplateThatFailedToRender = blocksIncludingInnerBlocks.find(
|
||||
( block ) =>
|
||||
block.name === 'core/missing' &&
|
||||
block.attributes.originalName === BLOCK_SLUG
|
||||
);
|
||||
|
||||
if ( classicTemplateThatFailedToRender ) {
|
||||
const blockToReplaceClassicTemplateBlockThatFailedToRender = parse(
|
||||
classicTemplateThatFailedToRender.attributes.originalContent
|
||||
);
|
||||
if ( blockToReplaceClassicTemplateBlockThatFailedToRender ) {
|
||||
dispatch( 'core/block-editor' ).replaceBlock(
|
||||
classicTemplateThatFailedToRender.clientId,
|
||||
blockToReplaceClassicTemplateBlockThatFailedToRender
|
||||
);
|
||||
}
|
||||
}
|
||||
}, 100 );
|
||||
|
||||
// @todo Refactor when there will be possible to show a block according on a template/post with a Gutenberg API. https://github.com/WordPress/gutenberg/pull/41718
|
||||
|
||||
let currentTemplateId: string | undefined;
|
||||
|
||||
if ( isExperimentalBuild() ) {
|
||||
subscribe( () => {
|
||||
const previousTemplateId = currentTemplateId;
|
||||
const store = select( 'core/edit-site' );
|
||||
currentTemplateId = store?.getEditedPostId() as string | undefined;
|
||||
subscribe( () => {
|
||||
const previousTemplateId = currentTemplateId;
|
||||
const store = select( 'core/edit-site' );
|
||||
// With GB 16.3.0 the return type can be a number: https://github.com/WordPress/gutenberg/issues/53230
|
||||
const editedPostId = store?.getEditedPostId() as
|
||||
| string
|
||||
| number
|
||||
| undefined;
|
||||
|
||||
if ( previousTemplateId === currentTemplateId ) {
|
||||
return;
|
||||
}
|
||||
currentTemplateId = isNumber( editedPostId ) ? undefined : editedPostId;
|
||||
|
||||
const parsedTemplate = currentTemplateId?.split( '//' )[ 1 ];
|
||||
const parsedTemplate = currentTemplateId?.split( '//' )[ 1 ];
|
||||
|
||||
if ( parsedTemplate === null || parsedTemplate === undefined ) {
|
||||
return;
|
||||
}
|
||||
if ( parsedTemplate === null || parsedTemplate === undefined ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const block = getBlockType( BLOCK_SLUG );
|
||||
const block = getBlockType( BLOCK_SLUG );
|
||||
const isBlockRegistered = Boolean( block );
|
||||
|
||||
if (
|
||||
block !== undefined &&
|
||||
( ! hasTemplateSupportForClassicTemplateBlock(
|
||||
parsedTemplate,
|
||||
TEMPLATES
|
||||
) ||
|
||||
isClassicTemplateBlockRegisteredWithAnotherTitle(
|
||||
block,
|
||||
parsedTemplate
|
||||
) )
|
||||
) {
|
||||
unregisterBlockType( BLOCK_SLUG );
|
||||
currentTemplateId = undefined;
|
||||
return;
|
||||
}
|
||||
if (
|
||||
isBlockRegistered &&
|
||||
hasTemplateSupportForClassicTemplateBlock( parsedTemplate, TEMPLATES )
|
||||
) {
|
||||
tryToRecoverClassicTemplateBlockWhenItFailsToRender();
|
||||
}
|
||||
|
||||
if (
|
||||
block === undefined &&
|
||||
hasTemplateSupportForClassicTemplateBlock(
|
||||
parsedTemplate,
|
||||
TEMPLATES
|
||||
)
|
||||
) {
|
||||
registerClassicTemplateBlock( {
|
||||
template: parsedTemplate,
|
||||
inserter: true,
|
||||
} );
|
||||
}
|
||||
} );
|
||||
} else {
|
||||
registerClassicTemplateBlock( {
|
||||
inserter: false,
|
||||
} );
|
||||
}
|
||||
if ( previousTemplateId === currentTemplateId ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
isBlockRegistered &&
|
||||
( ! hasTemplateSupportForClassicTemplateBlock(
|
||||
parsedTemplate,
|
||||
TEMPLATES
|
||||
) ||
|
||||
isClassicTemplateBlockRegisteredWithAnotherTitle(
|
||||
block,
|
||||
parsedTemplate
|
||||
) )
|
||||
) {
|
||||
unregisterBlockType( BLOCK_SLUG );
|
||||
currentTemplateId = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
! isBlockRegistered &&
|
||||
hasTemplateSupportForClassicTemplateBlock( parsedTemplate, TEMPLATES )
|
||||
) {
|
||||
registerClassicTemplateBlock( {
|
||||
template: parsedTemplate,
|
||||
inserter: true,
|
||||
} );
|
||||
}
|
||||
}, 'core/blocks-editor' );
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
const isConversionPossible = () => {
|
||||
return false;
|
||||
};
|
||||
|
||||
const getDescription = () => {
|
||||
return __(
|
||||
'This block represents the classic template used to display the order confirmation. The actual rendered template may appear different from this placeholder.',
|
||||
'woo-gutenberg-products-block'
|
||||
);
|
||||
};
|
||||
|
||||
const getSkeleton = () => {
|
||||
return (
|
||||
<div className="woocommerce-page">
|
||||
<div className="woocommerce-order">
|
||||
<h1>
|
||||
{ __( 'Order received', 'woo-gutenberg-products-block' ) }
|
||||
</h1>
|
||||
<p className="woocommerce-notice woocommerce-notice--success woocommerce-thankyou-order-confirmation">
|
||||
{ __(
|
||||
'Thank you. Your order has been received.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
</p>
|
||||
<ul className="woocommerce-order-overview woocommerce-thankyou-order-details order_details">
|
||||
<li className="woocommerce-order-overview__order order">
|
||||
{ __( 'Order number', 'woo-gutenberg-products-block' ) }
|
||||
: <strong>123</strong>
|
||||
</li>
|
||||
<li className="woocommerce-order-overview__date date">
|
||||
{ __( 'Date', 'woo-gutenberg-products-block' ) }:{ ' ' }
|
||||
<strong>May 25, 2023</strong>
|
||||
</li>
|
||||
<li className="woocommerce-order-overview__email email">
|
||||
{ __( 'Email', 'woo-gutenberg-products-block' ) }:{ ' ' }
|
||||
<strong>shopper@woo.com</strong>
|
||||
</li>
|
||||
<li className="woocommerce-order-overview__total total">
|
||||
{ __( 'Total', 'woo-gutenberg-products-block' ) }:{ ' ' }
|
||||
<strong>$20.00</strong>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<section className="woocommerce-order-details">
|
||||
<h2 className="woocommerce-order-details__title">
|
||||
{ __(
|
||||
'Order details',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
</h2>
|
||||
<table className="woocommerce-table woocommerce-table--order-details shop_table order_details">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="woocommerce-table__product-name product-name">
|
||||
{ __(
|
||||
'Product',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
</th>
|
||||
<th className="woocommerce-table__product-table product-total">
|
||||
{ __(
|
||||
'Total',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="woocommerce-table__line-item order_item">
|
||||
<td className="woocommerce-table__product-name product-name">
|
||||
Sample Product{ ' ' }
|
||||
<strong className="product-quantity">
|
||||
× 2
|
||||
</strong>{ ' ' }
|
||||
</td>
|
||||
|
||||
<td className="woocommerce-table__product-total product-total">
|
||||
$20.00
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
{ __(
|
||||
'Subtotal',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
:
|
||||
</th>
|
||||
<td>$20.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
{ __(
|
||||
'Total',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
:
|
||||
</th>
|
||||
<td>$20.00</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section className="woocommerce-customer-details">
|
||||
<section className="woocommerce-columns woocommerce-columns--2 woocommerce-columns--addresses col2-set addresses">
|
||||
<div className="woocommerce-column woocommerce-column--1 woocommerce-column--billing-address col-1">
|
||||
<h2 className="woocommerce-column__title">
|
||||
{ __(
|
||||
'Billing address',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
</h2>
|
||||
<address>
|
||||
123 Main St
|
||||
<br />
|
||||
New York, NY 10001
|
||||
<br />
|
||||
United States (US)
|
||||
</address>
|
||||
</div>
|
||||
|
||||
<div className="woocommerce-column woocommerce-column--2 woocommerce-column--shipping-address col-2">
|
||||
<h2 className="woocommerce-column__title">
|
||||
{ __(
|
||||
'Shipping address',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
</h2>
|
||||
<address>
|
||||
123 Main St
|
||||
<br />
|
||||
New York, NY 10001
|
||||
<br />
|
||||
United States (US)
|
||||
</address>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { isConversionPossible, getDescription, getSkeleton };
|
||||
@@ -8,19 +8,18 @@ import {
|
||||
type InnerBlockTemplate,
|
||||
} from '@wordpress/blocks';
|
||||
import { isWpVersion } from '@woocommerce/settings';
|
||||
import { isExperimentalBuild } from '@woocommerce/block-settings';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import {
|
||||
INNER_BLOCKS_TEMPLATE as productsInnerBlocksTemplate,
|
||||
QUERY_DEFAULT_ATTRIBUTES as productsQueryDefaultAttributes,
|
||||
PRODUCT_QUERY_VARIATION_NAME as productsVariationName,
|
||||
} from '@woocommerce/blocks/product-query/constants';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
INNER_BLOCKS_TEMPLATE as productsInnerBlocksTemplate,
|
||||
QUERY_DEFAULT_ATTRIBUTES as productsQueryDefaultAttributes,
|
||||
} from '../product-query/constants';
|
||||
import { VARIATION_NAME as productsVariationName } from '../product-query/variations/product-query';
|
||||
import { createArchiveTitleBlock, createRowBlock } from './utils';
|
||||
import { type InheritedAttributes } from './types';
|
||||
import { OnClickCallbackParameter, type InheritedAttributes } from './types';
|
||||
|
||||
const createNoResultsParagraph = () =>
|
||||
createBlock( 'core/paragraph', {
|
||||
@@ -111,14 +110,14 @@ const getBlockifiedTemplate = ( inheritedAttributes: InheritedAttributes ) =>
|
||||
const isConversionPossible = () => {
|
||||
// Blockification is possible for the WP version 6.1 and above,
|
||||
// which are the versions the Products block supports.
|
||||
return isExperimentalBuild() && isWpVersion( '6.1', '>=' );
|
||||
return isWpVersion( '6.1', '>=' );
|
||||
};
|
||||
|
||||
const getDescriptionAllowingConversion = ( templateTitle: string ) =>
|
||||
sprintf(
|
||||
/* translators: %s is the template title */
|
||||
__(
|
||||
"This block serves as a placeholder for your %s. We recommend upgrading to the Products block for more features to edit your products visually. Don't worry, you can always revert back.",
|
||||
'Transform this template into multiple blocks so you can add, remove, reorder, and customize your %s template.',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
templateTitle
|
||||
@@ -142,12 +141,38 @@ const getDescription = ( templateTitle: string, canConvert: boolean ) => {
|
||||
return getDescriptionDisallowingConversion( templateTitle );
|
||||
};
|
||||
|
||||
const getButtonLabel = () =>
|
||||
__( 'Upgrade to Products block', 'woo-gutenberg-products-block' );
|
||||
const onClickCallback = ( {
|
||||
clientId,
|
||||
attributes,
|
||||
getBlocks,
|
||||
replaceBlock,
|
||||
selectBlock,
|
||||
}: OnClickCallbackParameter ) => {
|
||||
replaceBlock( clientId, getBlockifiedTemplate( attributes ) );
|
||||
|
||||
export {
|
||||
getBlockifiedTemplate,
|
||||
isConversionPossible,
|
||||
getDescription,
|
||||
getButtonLabel,
|
||||
const blocks = getBlocks();
|
||||
|
||||
const groupBlock = blocks.find(
|
||||
( block ) =>
|
||||
block.name === 'core/group' &&
|
||||
block.innerBlocks.some(
|
||||
( innerBlock ) =>
|
||||
innerBlock.name === 'woocommerce/store-notices'
|
||||
)
|
||||
);
|
||||
|
||||
if ( groupBlock ) {
|
||||
selectBlock( groupBlock.clientId );
|
||||
}
|
||||
};
|
||||
|
||||
const getButtonLabel = () =>
|
||||
__( 'Transform into blocks', 'woo-gutenberg-products-block' );
|
||||
|
||||
const blockifyConfig = {
|
||||
getButtonLabel,
|
||||
onClickCallback,
|
||||
getBlockifiedTemplate,
|
||||
};
|
||||
|
||||
export { isConversionPossible, getDescription, blockifyConfig };
|
||||
|
||||
@@ -7,6 +7,11 @@ import { BlockInstance, createBlock } from '@wordpress/blocks';
|
||||
import { VARIATION_NAME as PRODUCT_TITLE_VARIATION_NAME } from '@woocommerce/blocks/product-query/variations/elements/product-title';
|
||||
import { VARIATION_NAME as PRODUCT_SUMMARY_VARIATION_NAME } from '@woocommerce/blocks/product-query/variations/elements/product-summary';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { OnClickCallbackParameter } from './types';
|
||||
|
||||
const getBlockifiedTemplate = () =>
|
||||
[
|
||||
createBlock( 'woocommerce/breadcrumbs' ),
|
||||
@@ -22,13 +27,16 @@ const getBlockifiedTemplate = () =>
|
||||
{
|
||||
type: 'constrained',
|
||||
justifyContent: 'right',
|
||||
width: '512px',
|
||||
},
|
||||
[ createBlock( 'woocommerce/product-image-gallery' ) ]
|
||||
),
|
||||
createBlock( 'core/column', {}, [
|
||||
createBlock( 'core/post-title', {
|
||||
__woocommerceNamespace: PRODUCT_TITLE_VARIATION_NAME,
|
||||
level: 1,
|
||||
} ),
|
||||
createBlock( 'woocommerce/product-rating' ),
|
||||
createBlock( 'woocommerce/product-price', {
|
||||
fontSize: 'large',
|
||||
} ),
|
||||
@@ -58,7 +66,7 @@ const getDescriptionAllowingConversion = ( templateTitle: string ) =>
|
||||
sprintf(
|
||||
/* translators: %s is the template title */
|
||||
__(
|
||||
"This block serves as a placeholder for your %s. We recommend upgrading to the Single Products block for more features to edit your products visually. Don't worry, you can always revert back.",
|
||||
'Transform this template into multiple blocks so you can add, remove, reorder, and customize your %s template.',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
templateTitle
|
||||
@@ -83,14 +91,34 @@ const getDescription = ( templateTitle: string, canConvert: boolean ) => {
|
||||
};
|
||||
|
||||
const getButtonLabel = () =>
|
||||
__(
|
||||
'Upgrade to Blockified Single Product template',
|
||||
'woo-gutenberg-products-block'
|
||||
__( 'Transform into blocks', 'woo-gutenberg-products-block' );
|
||||
|
||||
const onClickCallback = ( {
|
||||
clientId,
|
||||
getBlocks,
|
||||
replaceBlock,
|
||||
selectBlock,
|
||||
}: OnClickCallbackParameter ) => {
|
||||
replaceBlock( clientId, getBlockifiedTemplate() );
|
||||
|
||||
const blocks = getBlocks();
|
||||
const groupBlock = blocks.find(
|
||||
( block ) =>
|
||||
block.name === 'core/group' &&
|
||||
block.innerBlocks.some(
|
||||
( innerBlock ) => innerBlock.name === 'woocommerce/breadcrumbs'
|
||||
)
|
||||
);
|
||||
|
||||
export {
|
||||
getBlockifiedTemplate,
|
||||
isConversionPossible,
|
||||
getDescription,
|
||||
getButtonLabel,
|
||||
if ( groupBlock ) {
|
||||
selectBlock( groupBlock.clientId );
|
||||
}
|
||||
};
|
||||
|
||||
const blockifyConfig = {
|
||||
getButtonLabel,
|
||||
onClickCallback,
|
||||
getBlockifiedTemplate,
|
||||
};
|
||||
|
||||
export { isConversionPossible, getDescription, blockifyConfig };
|
||||
|
||||
@@ -1,46 +1,34 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { TEMPLATES } from '../constants';
|
||||
import { getTemplateDetailsBySlug } from '../utils';
|
||||
|
||||
const TEMPLATES = {
|
||||
'single-product': {
|
||||
title: 'Single Product Title',
|
||||
placeholder: 'Single Product Placeholder',
|
||||
},
|
||||
'archive-product': {
|
||||
title: 'Product Archive Title',
|
||||
placeholder: 'Product Archive Placeholder',
|
||||
},
|
||||
'taxonomy-product_cat': {
|
||||
title: 'Product Taxonomy Title',
|
||||
placeholder: 'Product Taxonomy Placeholder',
|
||||
},
|
||||
'taxonomy-product_attribute': {
|
||||
title: 'Product Attribute Title',
|
||||
placeholder: 'Product Attribute Placeholder',
|
||||
},
|
||||
};
|
||||
|
||||
describe( 'getTemplateDetailsBySlug', function () {
|
||||
it( 'should return single-product object when given an exact match', () => {
|
||||
expect(
|
||||
getTemplateDetailsBySlug( 'single-product', TEMPLATES )
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
getTemplateDetailsBySlug( 'single-product', TEMPLATES )
|
||||
).toStrictEqual( TEMPLATES[ 'single-product' ] );
|
||||
} );
|
||||
|
||||
it( 'should return single-product object when given a partial match', () => {
|
||||
expect(
|
||||
getTemplateDetailsBySlug( 'single-product-hoodie', TEMPLATES )
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
getTemplateDetailsBySlug( 'single-product-hoodie', TEMPLATES )
|
||||
).toStrictEqual( TEMPLATES[ 'single-product' ] );
|
||||
} );
|
||||
|
||||
it( 'should return taxonomy-product object when given a partial match', () => {
|
||||
expect(
|
||||
getTemplateDetailsBySlug( 'taxonomy-product_tag', TEMPLATES )
|
||||
).toStrictEqual( TEMPLATES[ 'taxonomy-product_tag' ] );
|
||||
} );
|
||||
|
||||
it( 'should return taxonomy-product object when given an exact match', () => {
|
||||
expect(
|
||||
getTemplateDetailsBySlug( 'taxonomy-product_brands', TEMPLATES )
|
||||
).toStrictEqual( TEMPLATES[ 'taxonomy-product' ] );
|
||||
} );
|
||||
|
||||
it( 'should return null object when given an incorrect match', () => {
|
||||
expect( getTemplateDetailsBySlug( 'void', TEMPLATES ) ).toBeNull();
|
||||
} );
|
||||
|
||||
@@ -3,17 +3,41 @@
|
||||
*/
|
||||
import { type BlockInstance } from '@wordpress/blocks';
|
||||
|
||||
export type TemplateDetails = Record< string, Record< string, string > >;
|
||||
type TemplateDetail = {
|
||||
type: string;
|
||||
title: string;
|
||||
placeholder: string;
|
||||
};
|
||||
|
||||
export type TemplateDetails = Record< string, TemplateDetail >;
|
||||
|
||||
export type InheritedAttributes = {
|
||||
align?: string;
|
||||
};
|
||||
|
||||
export type BlockifiedTemplateConfig = {
|
||||
export type OnClickCallbackParameter = {
|
||||
clientId: string;
|
||||
attributes: Record< string, unknown >;
|
||||
getBlocks: () => BlockInstance[];
|
||||
replaceBlock: ( clientId: string, blocks: BlockInstance[] ) => void;
|
||||
selectBlock: ( clientId: string ) => void;
|
||||
};
|
||||
|
||||
type ConversionConfig = {
|
||||
onClickCallback: ( params: OnClickCallbackParameter ) => void;
|
||||
getButtonLabel: () => string;
|
||||
getBlockifiedTemplate: (
|
||||
inheritedAttributes: InheritedAttributes
|
||||
) => BlockInstance[];
|
||||
isConversionPossible: () => boolean;
|
||||
getDescription: ( templateTitle: string, canConvert: boolean ) => string;
|
||||
getButtonLabel: () => string;
|
||||
};
|
||||
|
||||
export type BlockifiedTemplateConfig = {
|
||||
// Description of the template, shown in the block placeholder.
|
||||
getDescription: ( templateTitle: string, canConvert: boolean ) => string;
|
||||
// Returns the skeleton HTML for the template, or can be left blank to use the default fallback image.
|
||||
getSkeleton?: ( () => JSX.Element ) | undefined;
|
||||
// Is conversion possible for the template?
|
||||
isConversionPossible: () => boolean;
|
||||
// If conversion is possible, returns the config for the template to be blockified.
|
||||
blockifyConfig?: ConversionConfig | undefined;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"title": "Customer account",
|
||||
"description": "A block that allows your customers to log in and out of their accounts in your store.",
|
||||
"category": "woocommerce",
|
||||
"keywords": [ "WooCommerce" ],
|
||||
"keywords": [ "WooCommerce", "My Account" ],
|
||||
"supports": {
|
||||
"align": true,
|
||||
"color": {
|
||||
@@ -13,6 +13,9 @@
|
||||
"typography": {
|
||||
"fontSize": true,
|
||||
"__experimentalFontFamily": true
|
||||
},
|
||||
"spacing": {
|
||||
"margin": true
|
||||
}
|
||||
},
|
||||
"attributes": {
|
||||
|
||||
@@ -20,7 +20,7 @@ const Edit = ( {
|
||||
}: BlockEditProps< Attributes > ) => {
|
||||
const { className } = attributes;
|
||||
const blockProps = useBlockProps( {
|
||||
className: classNames( 'wc-block-customer-account', className ),
|
||||
className: classNames( 'wc-block-editor-customer-account', className ),
|
||||
} );
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,23 @@
|
||||
.wc-block-customer-account__icon-style-toggle {
|
||||
@import "./style";
|
||||
|
||||
.editor-styles-wrapper .is-layout-constrained > .wc-block-editor-customer-account.alignright {
|
||||
@include flex-justify-content(flex-end);
|
||||
}
|
||||
|
||||
.editor-styles-wrapper .is-layout-constrained > .wc-block-editor-customer-account.alignleft {
|
||||
@include flex-justify-content(flex-start);
|
||||
}
|
||||
|
||||
.editor-styles-wrapper .is-layout-constrained > .wc-block-editor-customer-account.aligncenter {
|
||||
@include flex-justify-content(center);
|
||||
}
|
||||
|
||||
.wc-block-editor-customer-account {
|
||||
display: flex;
|
||||
padding: em($gap-smaller) em($gap-smaller);
|
||||
}
|
||||
|
||||
.wc-block-editor-customer-account__icon-style-toggle {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -6,7 +25,3 @@
|
||||
.block-editor-block-card + div > .wc-block-editor-customer-account__link {
|
||||
padding: 0 $gap $gap 52px;
|
||||
}
|
||||
/* In tabbed sidebar (ie: WP >=6.2) */
|
||||
.wc-block-editor-customer-account__link {
|
||||
padding: $gap;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { __ } from '@wordpress/i18n';
|
||||
*/
|
||||
import metadata from './block.json';
|
||||
import edit from './edit';
|
||||
import './style.scss';
|
||||
|
||||
registerBlockType( metadata, {
|
||||
icon: {
|
||||
|
||||
@@ -66,7 +66,9 @@ export const BlockSettings = ( {
|
||||
|
||||
return (
|
||||
<InspectorControls key="inspector">
|
||||
<AccountSettingsLink />
|
||||
<PanelBody>
|
||||
<AccountSettingsLink />
|
||||
</PanelBody>
|
||||
<PanelBody
|
||||
title={ __(
|
||||
'Display settings',
|
||||
@@ -123,7 +125,7 @@ export const BlockSettings = ( {
|
||||
iconStyle: value,
|
||||
} )
|
||||
}
|
||||
className="wc-block-customer-account__icon-style-toggle"
|
||||
className="wc-block-editor-customer-account__icon-style-toggle"
|
||||
>
|
||||
<ToggleGroupControlOption
|
||||
value={ IconStyle.DEFAULT }
|
||||
@@ -132,7 +134,7 @@ export const BlockSettings = ( {
|
||||
icon={ customerAccountStyle }
|
||||
size={ 16 }
|
||||
className={ classNames(
|
||||
'wc-block-customer-account__icon-option',
|
||||
'wc-block-editor-customer-account__icon-option',
|
||||
{
|
||||
active:
|
||||
iconStyle === IconStyle.DEFAULT,
|
||||
@@ -148,7 +150,7 @@ export const BlockSettings = ( {
|
||||
icon={ customerAccountStyleAlt }
|
||||
size={ 20 }
|
||||
className={ classNames(
|
||||
'wc-block-customer-account__icon-option',
|
||||
'wc-block-editor-customer-account__icon-option',
|
||||
{
|
||||
active: iconStyle === IconStyle.ALT,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,23 @@
|
||||
@mixin flex-justify-content($justification) {
|
||||
float: none;
|
||||
justify-content: $justification;
|
||||
}
|
||||
|
||||
.is-layout-constrained > .wp-block-woocommerce-customer-account.alignright {
|
||||
@include flex-justify-content(flex-end);
|
||||
}
|
||||
|
||||
.is-layout-constrained > .wp-block-woocommerce-customer-account.alignleft {
|
||||
@include flex-justify-content(flex-start);
|
||||
}
|
||||
|
||||
.is-layout-constrained > .wp-block-woocommerce-customer-account.aligncenter {
|
||||
@include flex-justify-content(center);
|
||||
}
|
||||
|
||||
.wp-block-woocommerce-customer-account {
|
||||
display: flex;
|
||||
|
||||
a {
|
||||
text-decoration: none !important;
|
||||
align-items: center;
|
||||
@@ -9,11 +28,6 @@
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
.icon + .label,
|
||||
.wc-block-customer-account__account-icon + .label {
|
||||
margin-left: $gap-smaller;
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: em(16px);
|
||||
width: em(16px);
|
||||
@@ -22,6 +36,7 @@
|
||||
.wc-block-customer-account__account-icon {
|
||||
height: em(23px);
|
||||
width: em(23px);
|
||||
padding: em($gap-smaller);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import { ToolbarButton, ToolbarGroup } from '@wordpress/components';
|
||||
import { crop } from '@wordpress/icons';
|
||||
import { WP_REST_API_Category } from 'wp-types';
|
||||
import { ProductResponseItem } from '@woocommerce/types';
|
||||
import TextToolbarButton from '@woocommerce/editor-components/text-toolbar-button';
|
||||
import type { ComponentType, Dispatch, SetStateAction } from 'react';
|
||||
import type { BlockAlignment } from '@wordpress/blocks';
|
||||
|
||||
@@ -112,13 +111,13 @@ export const BlockControls = ( {
|
||||
allowedTypes={ [ 'image' ] }
|
||||
/>
|
||||
{ backgroundImageId && mediaSrc ? (
|
||||
<TextToolbarButton
|
||||
<ToolbarButton
|
||||
onClick={ () =>
|
||||
setAttributes( { mediaId: 0, mediaSrc: '' } )
|
||||
}
|
||||
>
|
||||
{ __( 'Reset', 'woo-gutenberg-products-block' ) }
|
||||
</TextToolbarButton>
|
||||
</ToolbarButton>
|
||||
) : null }
|
||||
</ToolbarGroup>
|
||||
<ToolbarGroup
|
||||
|
||||
@@ -4,11 +4,7 @@
|
||||
import classnames from 'classnames';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { ResizableBox } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useThrottle } from '../../utils/useThrottle';
|
||||
import { useThrottledCallback } from 'use-debounce';
|
||||
|
||||
type ResizeCallback = Exclude< ResizableBox.Props[ 'onResize' ], undefined >;
|
||||
|
||||
@@ -22,7 +18,7 @@ export const ConstrainedResizable = ( {
|
||||
const classNames = classnames( className, {
|
||||
'is-resizing': isResizing,
|
||||
} );
|
||||
const throttledResize = useThrottle< ResizeCallback >(
|
||||
const throttledResize = useThrottledCallback< ResizeCallback >(
|
||||
( event, direction, elt, _delta ) => {
|
||||
if ( ! isResizing ) setIsResizing( true );
|
||||
onResize?.( event, direction, elt, _delta );
|
||||
|
||||
@@ -36,6 +36,10 @@ const CONTENT_CONFIG = {
|
||||
'No product category is selected.',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
noSelectionButtonLabel: __(
|
||||
'Select a category',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
};
|
||||
|
||||
const EDIT_MODE_CONFIG = {
|
||||
|
||||
@@ -36,6 +36,10 @@ const CONTENT_CONFIG = {
|
||||
'No product is selected.',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
noSelectionButtonLabel: __(
|
||||
'Select a product',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
};
|
||||
|
||||
const EDIT_MODE_CONFIG = {
|
||||
|
||||
@@ -64,7 +64,7 @@ export function register(
|
||||
*/
|
||||
minHeight: {
|
||||
type: 'number',
|
||||
default: getSetting( 'default_height', 500 ),
|
||||
default: getSetting( 'defaultHeight', 500 ),
|
||||
},
|
||||
},
|
||||
supports: {
|
||||
@@ -100,7 +100,7 @@ export function register(
|
||||
editMode: false,
|
||||
hasParallax: false,
|
||||
isRepeated: false,
|
||||
height: getSetting( 'default_height', 500 ),
|
||||
height: getSetting( 'defaultHeight', 500 ),
|
||||
mediaSrc: '',
|
||||
overlayColor: '#000000',
|
||||
showDesc: true,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* External dependencies
|
||||
*/
|
||||
import type { Block, BlockEditProps } from '@wordpress/blocks';
|
||||
import { isNumber } from 'lodash';
|
||||
import { isNumber } from '@woocommerce/types';
|
||||
|
||||
export type EditorBlock< T > = Block< T > & BlockEditProps< T >;
|
||||
|
||||
|
||||
@@ -4,14 +4,12 @@
|
||||
* External dependencies
|
||||
*/
|
||||
import type { BlockAlignment } from '@wordpress/blocks';
|
||||
import { ProductResponseItem } from '@woocommerce/types';
|
||||
import { __experimentalGetSpacingClassesAndStyles as getSpacingClassesAndStyles } from '@wordpress/block-editor';
|
||||
import { ProductResponseItem, isEmpty } from '@woocommerce/types';
|
||||
import { Icon, Placeholder, Spinner } from '@wordpress/components';
|
||||
import classnames from 'classnames';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useCallback, useState } from '@wordpress/element';
|
||||
import { WP_REST_API_Category } from 'wp-types';
|
||||
import { useBorderProps } from '@woocommerce/base-hooks';
|
||||
import { useStyleProps } from '@woocommerce/base-hooks';
|
||||
import type { ComponentType, Dispatch, SetStateAction } from 'react';
|
||||
|
||||
/**
|
||||
@@ -29,6 +27,7 @@ import {
|
||||
|
||||
interface WithFeaturedItemConfig extends GenericBlockUIConfig {
|
||||
emptyMessage: string;
|
||||
noSelectionButtonLabel: string;
|
||||
}
|
||||
|
||||
export interface FeaturedItemRequiredAttributes {
|
||||
@@ -46,6 +45,7 @@ export interface FeaturedItemRequiredAttributes {
|
||||
overlayGradient: string;
|
||||
showDesc: boolean;
|
||||
showPrice: boolean;
|
||||
editMode: boolean;
|
||||
}
|
||||
|
||||
interface FeaturedCategoryRequiredAttributes
|
||||
@@ -94,7 +94,12 @@ type FeaturedItemProps< T extends EditorBlock< T > > =
|
||||
| ( T & FeaturedProductProps< T > );
|
||||
|
||||
export const withFeaturedItem =
|
||||
( { emptyMessage, icon, label }: WithFeaturedItemConfig ) =>
|
||||
( {
|
||||
emptyMessage,
|
||||
icon,
|
||||
label,
|
||||
noSelectionButtonLabel,
|
||||
}: WithFeaturedItemConfig ) =>
|
||||
< T extends EditorBlock< T > >( Component: ComponentType< T > ) =>
|
||||
( props: FeaturedItemProps< T > ) => {
|
||||
const [ isEditingImage ] = props.useEditingImage;
|
||||
@@ -142,17 +147,33 @@ export const withFeaturedItem =
|
||||
);
|
||||
};
|
||||
|
||||
const renderNoItemButton = () => {
|
||||
return (
|
||||
<>
|
||||
<p>{ emptyMessage }</p>
|
||||
<div style={ { flexBasis: '100%', height: '0' } }></div>
|
||||
<button
|
||||
type="button"
|
||||
className="components-button is-secondary"
|
||||
onClick={ () => setAttributes( { editMode: true } ) }
|
||||
>
|
||||
{ noSelectionButtonLabel }
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderNoItem = () => (
|
||||
<Placeholder
|
||||
className={ className }
|
||||
icon={ <Icon icon={ icon } /> }
|
||||
label={ label }
|
||||
>
|
||||
{ isLoading ? <Spinner /> : emptyMessage }
|
||||
{ isLoading ? <Spinner /> : renderNoItemButton() }
|
||||
</Placeholder>
|
||||
);
|
||||
|
||||
const borderProps = useBorderProps( attributes );
|
||||
const styleProps = useStyleProps( attributes );
|
||||
|
||||
const renderItem = () => {
|
||||
const {
|
||||
@@ -171,7 +192,7 @@ export const withFeaturedItem =
|
||||
textColor,
|
||||
} = attributes;
|
||||
|
||||
const classes = classnames(
|
||||
const containerClass = classnames(
|
||||
className,
|
||||
{
|
||||
'is-selected':
|
||||
@@ -184,7 +205,8 @@ export const withFeaturedItem =
|
||||
'is-repeated': isRepeated,
|
||||
},
|
||||
dimRatioToClass( dimRatio ),
|
||||
contentAlign !== 'center' && `has-${ contentAlign }-content`
|
||||
contentAlign !== 'center' && `has-${ contentAlign }-content`,
|
||||
styleProps.className
|
||||
);
|
||||
|
||||
const containerStyle = {
|
||||
@@ -193,11 +215,8 @@ export const withFeaturedItem =
|
||||
? `var(--wp--preset--color--${ textColor })`
|
||||
: style?.color?.text,
|
||||
boxSizing: 'border-box',
|
||||
};
|
||||
|
||||
const wrapperStyle = {
|
||||
...getSpacingClassesAndStyles( attributes ).style,
|
||||
minHeight,
|
||||
...styleProps.style,
|
||||
};
|
||||
|
||||
const isImgElement = ! isRepeated && ! hasParallax;
|
||||
@@ -223,14 +242,8 @@ export const withFeaturedItem =
|
||||
showHandle={ isSelected }
|
||||
style={ { minHeight } }
|
||||
/>
|
||||
<div
|
||||
className={ classes }
|
||||
style={ { containerStyle, ...borderProps.style } }
|
||||
>
|
||||
<div
|
||||
className={ `${ className }__wrapper` }
|
||||
style={ wrapperStyle }
|
||||
>
|
||||
<div className={ containerClass } style={ containerStyle }>
|
||||
<div className={ `${ className }__wrapper` }>
|
||||
<div
|
||||
className="background-dim__overlay"
|
||||
style={ overlayStyle }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user