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

code import from pantheon

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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