Commit 0b8f21c9ef for woocommerce
commit 0b8f21c9ef7d18e85f726f301e59aec18a3bcec1
Author: Albert Juhé Lluveras <contact@albertjuhe.com>
Date: Mon Feb 16 11:45:55 2026 +0100
Product Reviews block: add support for replies (#63161)
* Product Reviews block: add support for replies
* Add changelog file
* Clean up code
* Remove unnecessary optional chaining
* Fix _embedded type
* Remove unnecessary
* chore: some TS minor cleanup
---------
Co-authored-by: Tung Du <dinhtungdu@gmail.com>
diff --git a/plugins/woocommerce/changelog/fix-product-reviews-add-back-support-for-replies b/plugins/woocommerce/changelog/fix-product-reviews-add-back-support-for-replies
new file mode 100644
index 0000000000..ec43cfb5ff
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-product-reviews-add-back-support-for-replies
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Product Reviews block: add support for replies
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-template/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-template/edit.tsx
index 9d115d9afc..070ae20787 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-template/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-template/edit.tsx
@@ -7,6 +7,7 @@ import { __ } from '@wordpress/i18n';
import { BlockInstance, BlockEditProps } from '@wordpress/blocks';
import { Spinner } from '@wordpress/components';
import { store as coreStore } from '@wordpress/core-data';
+import type { Comment as WPComment } from '@wordpress/core-data';
import {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
@@ -23,10 +24,11 @@ import {
/**
* Internal dependencies
*/
-import { useCommentQueryArgs, useCommentList } from './hooks';
+import { useCommentQueryArgs, useCommentTree } from './hooks';
-interface Comment {
+export interface Comment {
commentId: number;
+ children?: Comment[];
}
interface ReviewTemplateInnerBlocksProps {
@@ -63,7 +65,7 @@ interface ReviewSettings {
const getCommentsPlaceholder = ( {
perPage,
pageComments,
-}: ReviewSettings ) => {
+}: ReviewSettings ): Comment[] => {
const numberOfComments = pageComments ? Math.min( perPage, 3 ) : 3;
return Array.from( { length: numberOfComments }, ( _, i ) => ( {
@@ -129,6 +131,29 @@ const ReviewTemplateInnerBlocks = memo( function ReviewTemplateInnerBlocks( {
comment.commentId === ( activeCommentId || firstCommentId )
}
/>
+ { comment.children && comment.children.length > 0 ? (
+ <ol>
+ { comment.children.map( ( child, index ) => (
+ <BlockContextProvider
+ key={ child.commentId || index }
+ value={ {
+ commentId:
+ child.commentId < 0
+ ? null
+ : child.commentId,
+ } }
+ >
+ <ReviewTemplateInnerBlocks
+ comment={ child }
+ activeCommentId={ activeCommentId }
+ setActiveCommentId={ setActiveCommentId }
+ blocks={ blocks }
+ firstCommentId={ firstCommentId }
+ />
+ </BlockContextProvider>
+ ) ) }
+ </ol>
+ ) : null }
</li>
);
} );
@@ -171,7 +196,14 @@ export default function ReviewTemplateEdit( {
};
return {
topLevelComments: commentQuery
- ? getEntityRecords( 'root', 'comment', commentQuery )
+ ? ( getEntityRecords(
+ 'root',
+ 'comment',
+ commentQuery
+ ) as ( WPComment & {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ _embedded?: { children?: WPComment[][] };
+ } )[] )
: null,
blocks: getBlocks( clientId ),
};
@@ -179,17 +211,30 @@ export default function ReviewTemplateEdit( {
[ clientId, commentQuery ]
);
- let commentTree = useCommentList(
- // Reverse the order of top comments if needed.
- commentOrder === 'desc' && topLevelComments
- ? [
- ...( topLevelComments as Array< {
- id: number;
- } > ),
- ].reverse()
- : ( topLevelComments as Array< {
- id: number;
- } > )
+ let commentTree = useCommentTree(
+ Array.isArray( topLevelComments )
+ ? topLevelComments.map( ( comment ) => {
+ const children = comment._embedded?.children;
+
+ if (
+ Array.isArray( children ) &&
+ children.length >= 1 &&
+ Array.isArray( children[ 0 ] )
+ ) {
+ return {
+ id: comment.id,
+ children: children[ 0 ].map( ( child ) => ( {
+ id: child.id,
+ } ) ),
+ };
+ }
+
+ return {
+ id: comment.id,
+ };
+ } )
+ : [],
+ commentOrder
);
if ( ! topLevelComments ) {
@@ -218,31 +263,25 @@ export default function ReviewTemplateEdit( {
return (
<ol { ...blockProps }>
{ commentTree &&
- commentTree.map(
- (
- {
- commentId,
- }: {
- commentId: number;
- },
- index: number
- ) => (
- <BlockContextProvider
- key={ commentId || index }
- value={ {
- commentId: commentId < 0 ? null : commentId,
- } }
- >
- <ReviewTemplateInnerBlocks
- comment={ { commentId } }
- activeCommentId={ activeCommentId }
- setActiveCommentId={ setActiveCommentId }
- blocks={ blocks }
- firstCommentId={ commentTree[ 0 ]?.commentId }
- />
- </BlockContextProvider>
- )
- ) }
+ commentTree.map( ( comment, index ) => (
+ <BlockContextProvider
+ key={ comment.commentId || index }
+ value={ {
+ commentId:
+ comment.commentId < 0
+ ? null
+ : comment.commentId,
+ } }
+ >
+ <ReviewTemplateInnerBlocks
+ comment={ comment }
+ activeCommentId={ activeCommentId }
+ setActiveCommentId={ setActiveCommentId }
+ blocks={ blocks }
+ firstCommentId={ commentTree[ 0 ]?.commentId }
+ />
+ </BlockContextProvider>
+ ) ) }
</ol>
);
}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-template/hooks.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-template/hooks.tsx
index 431cea1415..04028ce75a 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-template/hooks.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-template/hooks.tsx
@@ -7,6 +7,11 @@ import { store as blockEditorStore } from '@wordpress/block-editor';
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
+/**
+ * Internal dependencies
+ */
+import type { Comment } from './edit';
+
// This is limited by WP REST API
const MAX_COMMENTS_PER_PAGE = 100;
@@ -23,6 +28,8 @@ export const useCommentQueryArgs = ( { postId }: { postId: number } ) => {
context: 'embed',
parent: 0,
type: 'review',
+ // Request embedded children so we can show direct replies.
+ _embed: 'children',
} ),
[]
);
@@ -152,23 +159,33 @@ export const useCommentQueryArgs = ( { postId }: { postId: number } ) => {
};
/**
- * Generate a list of IDs from a list of review entities.
+ * Generate a tree structure of comment IDs from a list of review entities.
*/
-export const useCommentList = (
- // eslint-disable-next-line @typescript-eslint/naming-convention
+export const useCommentTree = (
topLevelComments: Array< {
id: number;
- } >
+ children?: Array< { id: number } >;
+ } >,
+ commentOrder: string
) => {
- const commentList = useMemo(
- () =>
- topLevelComments?.map( ( { id }: { id: number } ) => {
+ const commentTree = useMemo( () => {
+ const comments: Comment[] = topLevelComments.map(
+ ( { id, children } ) => {
return {
commentId: id,
+ children: Array.isArray( children )
+ ? children.map( ( child ) => ( {
+ commentId: child.id,
+ } ) )
+ : [],
};
- } ),
- [ topLevelComments ]
- );
+ }
+ );
+ if ( commentOrder === 'desc' ) {
+ return comments.reverse();
+ }
+ return comments;
+ }, [ topLevelComments, commentOrder ] );
- return commentList;
+ return commentTree;
};
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 871034c4db..e516dfaf8b 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -55077,18 +55077,6 @@ parameters:
count: 1
path: src/Blocks/BlockTypes/Reviews/ProductReviewRating.php
- -
- message: '#^Access to property \$comment_ID on an unknown class Automattic\\WooCommerce\\Blocks\\BlockTypes\\Reviews\\WP_Comment\.$#'
- identifier: class.notFound
- count: 3
- path: src/Blocks/BlockTypes/Reviews/ProductReviewTemplate.php
-
- -
- message: '#^Access to property \$comment_post_ID on an unknown class Automattic\\WooCommerce\\Blocks\\BlockTypes\\Reviews\\WP_Comment\.$#'
- identifier: class.notFound
- count: 1
- path: src/Blocks/BlockTypes/Reviews/ProductReviewTemplate.php
-
-
message: '#^Method Automattic\\WooCommerce\\Blocks\\BlockTypes\\Reviews\\ProductReviewTemplate\:\:render\(\) should return string but empty return statement found\.$#'
identifier: return.empty
@@ -55107,12 +55095,6 @@ parameters:
count: 1
path: src/Blocks/BlockTypes/Reviews/ProductReviewTemplate.php
- -
- message: '#^Parameter \$comments of method Automattic\\WooCommerce\\Blocks\\BlockTypes\\Reviews\\ProductReviewTemplate\:\:block_product_review_template_render_comments\(\) has invalid type Automattic\\WooCommerce\\Blocks\\BlockTypes\\Reviews\\WP_Comment\.$#'
- identifier: class.notFound
- count: 1
- path: src/Blocks/BlockTypes/Reviews/ProductReviewTemplate.php
-
-
message: '#^Access to property \$parsed_block on an unknown class Automattic\\WooCommerce\\Blocks\\BlockTypes\\Reviews\\WP_Block\.$#'
identifier: class.notFound
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/Reviews/ProductReviewTemplate.php b/plugins/woocommerce/src/Blocks/BlockTypes/Reviews/ProductReviewTemplate.php
index 64e40ee65c..7f397bb48d 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/Reviews/ProductReviewTemplate.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/Reviews/ProductReviewTemplate.php
@@ -4,6 +4,8 @@ namespace Automattic\WooCommerce\Blocks\BlockTypes\Reviews;
use Automattic\WooCommerce\Blocks\BlockTypes\AbstractBlock;
use WP_Comment_Query;
use WP_Block;
+use WP_Comment;
+
/**
* ProductReviewTemplate class.
*/
@@ -31,13 +33,19 @@ class ProductReviewTemplate extends AbstractBlock {
*
* @since 6.3.0 Changed render_block_context priority to `1`.
*
- * @param WP_Comment[] $comments The array of comments.
- * @param WP_Block $block Block instance.
+ * @param WP_Comment[] $comments The array of comments.
+ * @param WP_Block $block Block instance.
+ *
* @return string
*/
- protected function block_product_review_template_render_comments( $comments, $block ) {
+ protected function block_product_review_template_render_comments( array $comments, WP_Block $block ): string {
$content = '';
+
foreach ( $comments as $comment ) {
+ if ( ! $comment instanceof WP_Comment ) {
+ continue;
+ }
+
$comment_id = $comment->comment_ID;
$filter_block_context = static function ( $context ) use ( $comment_id ) {
$context['commentId'] = $comment_id;
@@ -45,24 +53,49 @@ class ProductReviewTemplate extends AbstractBlock {
};
/*
- * We set commentId context through the `render_block_context` filter so
- * that dynamically inserted blocks (at `render_block` filter stage)
- * will also receive that context.
- *
- * Use an early priority to so that other 'render_block_context' filters
- * have access to the values.
- */
+ * We set commentId context through the `render_block_context` filter so
+ * that dynamically inserted blocks (at `render_block` filter stage)
+ * will also receive that context.
+ *
+ * Use an early priority so that other 'render_block_context' filters
+ * have access to the values.
+ */
add_filter( 'render_block_context', $filter_block_context, 1 );
/*
- * We construct a new WP_Block instance from the parsed block so that
- * it'll receive any changes made by the `render_block_data` filter.
- */
+ * We construct a new WP_Block instance from the parsed block so that
+ * it'll receive any changes made by the `render_block_data` filter.
+ */
$block_content = ( new WP_Block( $block->parsed_block ) )->render( array( 'dynamic' => false ) );
remove_filter( 'render_block_context', $filter_block_context, 1 );
- $comment_classes = comment_class( '', $comment->comment_ID, $comment->comment_post_ID, false );
+ $children = $comment->get_children();
+
+ /*
+ * We need to create the CSS classes BEFORE recursing into the children.
+ * This is because comment_class() uses globals like `$comment_alt`
+ * and `$comment_thread_alt` which are order-sensitive.
+ *
+ * The `false` parameter at the end means that we do NOT want the function
+ * to `echo` the output but to return a string.
+ * See https://developer.wordpress.org/reference/functions/comment_class/#parameters.
+ */
+ $comment_classes = comment_class(
+ '',
+ (int) $comment->comment_ID,
+ (int) $comment->comment_post_ID,
+ false
+ );
+
+ // If the comment has children, recurse to create the HTML for the nested comments.
+ if ( ! empty( $children ) ) {
+ $inner_content = $this->block_product_review_template_render_comments(
+ $children,
+ $block,
+ );
+ $block_content .= sprintf( '<ol>%1$s</ol>', $inner_content );
+ }
$content .= sprintf( '<li id="comment-%1$s" %2$s>%3$s</li>', $comment->comment_ID, $comment_classes, $block_content );
}