* Audit atomic block labels

* Add summary, descrtiption, and short description to API and use for summary block

* Add summary to cart item api

* Respect short description

* Tweak label of summary block

* Revert product rating label

* One description vs 3

* Update sample content with more appropriate values, and match in cart items sample

* Move summary generation to class

* Tests

* Prevent extending `ProductSummary`

* Tweak $paragraph handling

* More accurate word counting

* add tear down for filter
This commit is contained in:
Mike Jolley 2020-01-29 10:59:33 +00:00 committed by GitHub
parent b65b93e7e7
commit 29600c501f
9 changed files with 238 additions and 21 deletions

View File

@ -13,7 +13,7 @@ import { ProductButton } from '@woocommerce/atomic-components/product';
import sharedConfig from '../shared-config';
const blockConfig = {
title: __( 'Product Button', 'woo-gutenberg-products-block' ),
title: __( 'Add to Cart Button', 'woo-gutenberg-products-block' ),
description: __(
'Display a call to action button which either adds the product to the cart, or links to the product page.',
'woo-gutenberg-products-block'

View File

@ -14,7 +14,7 @@ import sharedConfig from '../shared-config';
const blockConfig = {
title: __( 'Product Summary', 'woo-gutenberg-products-block' ),
description: __(
'Display the short description of a product.',
'Display a short description about a product.',
'woo-gutenberg-products-block'
),
icon: {

View File

@ -7,7 +7,7 @@ import { useProductLayoutContext } from '@woocommerce/base-context/product-layou
const ProductSummary = ( { className, product } ) => {
const { layoutStyleClassPrefix } = useProductLayoutContext();
if ( ! product.description ) {
if ( ! product.summary ) {
return null;
}
@ -18,7 +18,7 @@ const ProductSummary = ( { className, product } ) => {
`${ layoutStyleClassPrefix }__product-summary`
) }
dangerouslySetInnerHTML={ {
__html: product.description,
__html: product.summary,
} }
/>
);

View File

@ -17,7 +17,13 @@ export const previewCartItems = [
id: 1,
quantity: 2,
name: __( 'Beanie', 'woo-gutenberg-products-block' ),
description: __( 'Warm hat for winter' ),
summary: __( 'Warm hat for winter', 'woo-gutenberg-products-block' ),
short_description: __(
'Warm hat for winter',
'woo-gutenberg-products-block'
),
description:
'Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.',
sku: 'woo-beanie',
permalink: 'https://example.org',
low_stock_remaining: 2,
@ -61,7 +67,16 @@ export const previewCartItems = [
id: 2,
quantity: 1,
name: __( 'Cap', 'woo-gutenberg-products-block' ),
description: __( 'Lightweight baseball cap' ),
summary: __(
'Lightweight baseball cap',
'woo-gutenberg-products-block'
),
short_description: __(
'Lightweight baseball cap',
'woo-gutenberg-products-block'
),
description:
'Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.',
sku: 'woo-cap',
permalink: 'https://example.org',
images: [

View File

@ -8,6 +8,11 @@ import { __ } from '@wordpress/i18n';
*/
import productPicture from './product-image';
const shortDescription = __(
'Fly your WordPress banner with this beauty! Deck out your office space or add it to your kids walls. This banner will spruce up any space its hung!',
'woo-gutenberg-products-block'
);
export const previewProducts = [
{
id: 1,
@ -15,10 +20,10 @@ export const previewProducts = [
variation: '',
permalink: 'https://example.org',
sku: 'wp-pennant',
description: __(
'Fly your WordPress banner with this beauty! Deck out your office space or add it to your kids walls. This banner will spruce up any space its hung!',
'woo-gutenberg-products-block'
),
summary: shortDescription,
short_description: shortDescription,
description:
'Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.',
price: '7.99',
price_html:
'<span class="woocommerce-Price-amount amount"><span class="woocommerce-Price-currencySymbol">$</span>7.99</span>',

View File

@ -10,6 +10,7 @@ namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Schemas;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Blocks\RestApi\Utilities\ProductImages;
use Automattic\WooCommerce\Blocks\RestApi\Utilities\ProductSummary;
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\ReserveStock;
/**
@ -63,11 +64,20 @@ class CartItemSchema extends AbstractSchema {
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'short_description' => [
'description' => __( 'Product short description or excerpt from full description.', 'woo-gutenberg-products-block' ),
'summary' => [
'description' => __( 'A short summary (or excerpt from the full description) for the product in HTML format.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'short_description' => [
'description' => __( 'Product short description in HTML format.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'description' => [
'description' => __( 'Product full description in HTML format.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sku' => [
'description' => __( 'Stock keeping unit, if applicable.', 'woo-gutenberg-products-block' ),
@ -214,17 +224,14 @@ class CartItemSchema extends AbstractSchema {
public function get_item_response( $cart_item ) {
$product = $cart_item['data'];
$short_description = apply_filters( 'woocommerce_short_description', $product->get_short_description() ? $product->get_short_description() : $product->get_description(), 400 );
$short_description = wp_filter_nohtml_kses( $short_description );
$short_description = strip_shortcodes( $short_description );
$short_description = normalize_whitespace( $short_description );
return [
'key' => $cart_item['key'],
'id' => $product->get_id(),
'quantity' => wc_stock_amount( $cart_item['quantity'] ),
'name' => $this->prepare_html_response( $product->get_title() ),
'short_description' => $this->prepare_html_response( $short_description ),
'summary' => $this->prepare_html_response( ( new ProductSummary( $product ) )->get_summary( 150 ) ),
'short_description' => $this->prepare_html_response( wc_format_content( $product->get_short_description() ) ),
'description' => $this->prepare_html_response( wc_format_content( $product->get_description() ) ),
'sku' => $this->prepare_html_response( $product->get_sku() ),
'low_stock_remaining' => $this->get_low_stock_remaining( $product ),
'permalink' => $product->get_permalink(),

View File

@ -10,6 +10,7 @@ namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Schemas;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Blocks\RestApi\Utilities\ProductImages;
use Automattic\WooCommerce\Blocks\RestApi\Utilities\ProductSummary;
/**
* ProductSchema class.
@ -54,8 +55,18 @@ class ProductSchema extends AbstractSchema {
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'summary' => [
'description' => __( 'A short summary (or excerpt from the full description) for the product in HTML format.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'short_description' => [
'description' => __( 'Product short description in HTML format.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'description' => [
'description' => __( 'Short description or excerpt from description.', 'woo-gutenberg-products-block' ),
'description' => __( 'Product full description in HTML format.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
@ -238,7 +249,9 @@ class ProductSchema extends AbstractSchema {
'variation' => $this->prepare_html_response( $product->is_type( 'variation' ) ? wc_get_formatted_variation( $product, true, true, false ) : '' ),
'permalink' => $product->get_permalink(),
'sku' => $this->prepare_html_response( $product->get_sku() ),
'description' => $this->prepare_html_response( apply_filters( 'woocommerce_short_description', $product->get_short_description() ? $product->get_short_description() : wc_trim_string( $product->get_description(), 400 ) ) ),
'summary' => $this->prepare_html_response( ( new ProductSummary( $product ) )->get_summary( 150 ) ),
'short_description' => $this->prepare_html_response( wc_format_content( $product->get_short_description() ) ),
'description' => $this->prepare_html_response( wc_format_content( $product->get_description() ) ),
'on_sale' => $product->is_on_sale(),
'prices' => (object) $this->get_prices( $product ),
'average_rating' => $product->get_average_rating(),

View File

@ -0,0 +1,109 @@
<?php
/**
* Helper class to format a short summary of content for a product.
*
* @package WooCommerce/Blocks
*/
namespace Automattic\WooCommerce\Blocks\RestApi\Utilities;
defined( 'ABSPATH' ) || exit;
/**
* Product Summary class.
*/
final class ProductSummary {
/**
* Reference to product object.
*
* @var \WC_Product
*/
private $product = '';
/**
* Constructor.
*
* @param \WC_Product $product The product to generate a summary for.
*/
public function __construct( \WC_Product $product ) {
$this->product = $product;
}
/**
* Return the formatted summary.
*
* @param int $max_words Word limit for summary.
* @return string
*/
public function get_summary( $max_words = 25 ) {
$summary = $this->get_content_for_summary();
if ( $max_words && $this->get_word_count( $summary ) > $max_words ) {
$summary = $this->generate_summary( $summary, $max_words );
}
return \wc_format_content( $summary );
}
/**
* Get description to base summary on from the product object.
*
* @return string
*/
private function get_content_for_summary() {
$content = $this->product->get_short_description();
if ( ! $content ) {
$content = $this->product->get_description();
}
return $content;
}
/**
* Get the word count. Based on the logic in `wp_trim_words`.
*
* @param string $content HTML Content.
* @return int Length
*/
private function get_word_count( $content ) {
$content = wp_strip_all_tags( $content );
/*
* translators: If your word count is based on single characters (e.g. East Asian characters),
* enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'.
* Do not translate into your own language.
*/
$type = _x( 'words', 'Word count type. Do not translate!' ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
if ( strpos( $type, 'characters' ) === 0 && preg_match( '/^utf\-?8$/i', get_option( 'blog_charset' ) ) ) {
$content = trim( preg_replace( "/[\n\r\t ]+/", ' ', $content ), ' ' );
preg_match_all( '/./u', $content, $words_array );
$words_array = $words_array[0];
} else {
$words_array = preg_split( "/[\n\r\t ]+/", $content );
}
return count( array_filter( $words_array ) );
}
/**
* Get the first paragraph, or a short excerpt, from some content.
*
* @param string $content HTML Content.
* @param int $max_words Maximum allowed words for summary.
* @return string
*/
private function generate_summary( $content, $max_words ) {
$content_p = \wpautop( $content );
// This gets the first paragraph, if <p> tags are found. Otherwise returns the entire string.
$paragraph = \strstr( $content_p, '</p>' ) ? \substr( $content_p, 0, \strpos( $content_p, '</p>' ) + 4 ) : $content;
if ( $this->get_word_count( $paragraph ) > $max_words ) {
return \wp_trim_words( $paragraph, $max_words );
}
return $paragraph;
}
}

View File

@ -0,0 +1,68 @@
<?php
/**
* Utility Tests.
*
* @package WooCommerce\Blocks\Tests
*/
namespace Automattic\WooCommerce\Blocks\Tests\RestApi\Utilities;
use PHPUnit\Framework\TestCase;
use \WC_Helper_Product as ProductHelper;
use Automattic\WooCommerce\Blocks\RestApi\Utilities\ProductSummary;
/**
* ProductSummary Utility Tests.
*/
class ProductSummaryTests extends TestCase {
/**
* Tear down method.
*/
public function tearDown() {
remove_filter( 'gettext_with_context', array( $this, 'gettext_with_context_callback' ), 10, 3 );
parent::tearDown();
}
/**
* Test that stock is reserved for draft orders.
*/
public function test_get_summary() {
$product = new \WC_Product();
$product->set_description( '<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p><p>Ut enim ad minim veniam, quis <strong>nostrud</strong> exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p><p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p><p>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>' );
$class = new ProductSummary( $product );
// 25 word limit should return 1st para.
$this->assertEquals( "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>\n", $class->get_summary() );
// Large limit, should return full description.
$this->assertEquals( "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>\n<p>Ut enim ad minim veniam, quis <strong>nostrud</strong> exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>\n<p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>\n<p>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>\n", $class->get_summary( 1000 ) );
// Should return 3 words.
$this->assertEquals( "<p>Lorem ipsum dolor&hellip;</p>\n", $class->get_summary( 3 ) );
// Should return 1 word.
$this->assertEquals( "<p>Lorem&hellip;</p>\n", $class->get_summary( 1 ) );
// Test for languages where characters are words.
add_filter( 'gettext_with_context', array( $this, 'gettext_with_context_callback' ), 10, 3 );
$product->set_description( '<p>我不知道这是否行得通。</p><p>我是用中文写的说明,因此我们可以测试如何修剪产品摘要中的单词。</p>' );
$this->assertEquals( "<p>我不知道这是否行得通。</p>\n", $class->get_summary() );
$this->assertEquals( "<p>我不知&hellip;</p>\n", $class->get_summary( 3 ) );
}
/**
* Adjusts word count type to character based.
*
* @param string $translated Translated string.
* @param string $text Text to translate.
* @param string $context Translation context.
* @return string
*/
public function gettext_with_context_callback( $translated, $text, $context ) {
if ( $text === 'words' && $context === 'Word count type. Do not translate!' ) {
return 'characters';
}
return $translated;
}
}