Skip to content

Commit

Permalink
Comment Template block: Handle nested comments (#36065)
Browse files Browse the repository at this point in the history
* Comments Query: Use discussion settings as a defult for queryPerPage

* Comments Query: Make <ul> into <ol>

* Comment Template: Add styles

* Comment Template: Handle the discussion settings

* Fix whitespace

* Comments template: Stop using comments_per_page

* Comments Template: Update the fixtures

* Comments Template: Update JS-side implementation to fix  bugs with focus and add tests

* Comments Template: Update styles and tests

* Comment Template: Add comments to imports because eslint

* Comments Template: Add better comments

* Coment Template: Refactor JS + add better comments

* Comment Template: Fix JSDoc

* Comment Template: Add nested comments on PHP side

* Comment Template: Add css to block-library CSS file.

* Comment Template: Generate correct attributes

* Just reformat

* Comment Template: Revert useless rename

* Remove unnecesary isset() call

Co-authored-by: Greg Ziółkowski <grzegorz@gziolo.pl>

* Move RenderComments to inside CommentTemplateInnerBlocks

* Fix typo

* Comment Template: Simplify mapping over comments

* Comment Template: Prefix functions with `block_core_comment_template`

* Comment Template: Refactor

Replace the custom convert_to_tree with calls to ::get_children()

* Comment Template: Change how we destructure children in CommentTemplateInnerBlocks

* Comment Template: Add PHP test cases

* Comment Template: Rename RenderComments to CommentList

* Comment Template: Refactor to make ConmmentTemplateInnerBlock more readable

* Comment Template: Handle queryPerPage correctly on the JS side

* Comment Template: Remove the `id` from the comment object

* Comment Template: Remove tearDown() from PHP tests

* Comment Template: Add comment to clarify that build step adds 'gutenberg_*' prefix to functions

* Comment Template: Rename the blockContexts to comments

* Comment Template: Add context `embed`

* Fix code styling issues

* Fix code styling issues

* Improve JSDoc for `convertToTree` function

Co-authored-by: Greg Ziółkowski <grzegorz@gziolo.pl>
  • Loading branch information
michalczaplinski and gziolo committed Dec 6, 2021
1 parent f3f980f commit e629f5b
Show file tree
Hide file tree
Showing 10 changed files with 392 additions and 53 deletions.
3 changes: 2 additions & 1 deletion packages/block-library/src/comment-template/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@
"reusable": false,
"html": false,
"align": true
}
},
"style": "wp-block-comment-template"
}
143 changes: 107 additions & 36 deletions packages/block-library/src/comment-template/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ import {
import { Spinner } from '@wordpress/components';
import { store as coreStore } from '@wordpress/core-data';

/**
* Internal dependencies
*/
import { convertToTree } from './util';

const TEMPLATE = [
[ 'core/comment-author-avatar' ],
[ 'core/comment-author-name' ],
Expand All @@ -23,43 +28,126 @@ const TEMPLATE = [
[ 'core/comment-edit-link' ],
];

function CommentTemplateInnerBlocks() {
const innerBlocksProps = useInnerBlocksProps( {}, { template: TEMPLATE } );
return <li { ...innerBlocksProps } />;
/**
* Component which renders the inner blocks of the Comment Template.
*
* @param {Object} props Component props.
* @param {Array} [props.comment] - A comment object.
* @param {Array} [props.activeComment] - The block that is currently active.
* @param {Array} [props.setActiveComment] - The setter for activeComment.
* @param {Array} [props.firstBlock] - First comment in the array.
* @param {Array} [props.blocks] - Array of blocks returned from
* getBlocks() in parent .
* @return {WPElement} Inner blocks of the Comment Template
*/
function CommentTemplateInnerBlocks( {
comment,
activeComment,
setActiveComment,
firstBlock,
blocks,
} ) {
const { children, ...innerBlocksProps } = useInnerBlocksProps(
{},
{ template: TEMPLATE }
);
return (
<li { ...innerBlocksProps }>
{ comment === ( activeComment || firstBlock ) ? (
children
) : (
<BlockPreview
blocks={ blocks }
__experimentalLive
__experimentalOnClick={ () => setActiveComment( comment ) }
/>
) }
{ comment?.children?.length > 0 ? (
<CommentsList
comments={ comment.children }
activeComment={ activeComment }
setActiveComment={ setActiveComment }
blocks={ blocks }
/>
) : null }
</li>
);
}

/**
* Component that renders a list of (nested) comments. It is called recursively.
*
* @param {Object} props Component props.
* @param {Array} [props.comments] - Array of comment objects.
* @param {Array} [props.blockProps] - Props from parent's `useBlockProps()`.
* @param {Array} [props.activeComment] - The block that is currently active.
* @param {Array} [props.setActiveComment] - The setter for activeComment.
* @param {Array} [props.blocks] - Array of blocks returned from
* getBlocks() in parent .
* @return {WPElement} List of comments.
*/
const CommentsList = ( {
comments,
blockProps,
activeComment,
setActiveComment,
blocks,
} ) => (
<ol { ...blockProps }>
{ comments &&
comments.map( ( comment ) => (
<BlockContextProvider
key={ comment.commentId }
value={ comment }
>
<CommentTemplateInnerBlocks
comment={ comment }
activeComment={ activeComment }
setActiveComment={ setActiveComment }
blocks={ blocks }
firstBlock={ comments[ 0 ] }
/>
</BlockContextProvider>
) ) }
</ol>
);

export default function CommentTemplateEdit( {
clientId,
context: { postId, queryPerPage },
} ) {
const blockProps = useBlockProps();

const [ activeBlockContext, setActiveBlockContext ] = useState();
const [ activeComment, setActiveComment ] = useState();

const { comments, blocks } = useSelect(
const { rawComments, blocks } = useSelect(
( select ) => {
const { getEntityRecords } = select( coreStore );
const { getBlocks } = select( blockEditorStore );

return {
comments: getEntityRecords( 'root', 'comment', {
rawComments: getEntityRecords( 'root', 'comment', {
post: postId,
status: 'approve',
per_page: queryPerPage,
order: 'asc',
context: 'embed',
} ),
blocks: getBlocks( clientId ),
};
},
[ queryPerPage, postId, clientId ]
[ postId, clientId ]
);

const blockContexts = useMemo(
() => comments?.map( ( comment ) => ( { commentId: comment.id } ) ),
[ comments ]
// We convert the flat list of comments to tree.
// Then, we show only a maximum of `queryPerPage` number of comments.
// This is because passing `per_page` to `getEntityRecords()` does not
// take into account nested comments.
const comments = useMemo(
() => convertToTree( rawComments ).slice( 0, queryPerPage ),
[ rawComments, queryPerPage ]
);

if ( ! comments ) {
if ( ! rawComments ) {
return (
<p { ...blockProps }>
<Spinner />
Expand All @@ -72,29 +160,12 @@ export default function CommentTemplateEdit( {
}

return (
<ul { ...blockProps }>
{ blockContexts &&
blockContexts.map( ( blockContext ) => (
<BlockContextProvider
key={ blockContext.commentId }
value={ blockContext }
>
{ blockContext ===
( activeBlockContext || blockContexts[ 0 ] ) ? (
<CommentTemplateInnerBlocks />
) : (
<li>
<BlockPreview
blocks={ blocks }
__experimentalLive
__experimentalOnClick={ () =>
setActiveBlockContext( blockContext )
}
/>
</li>
) }
</BlockContextProvider>
) ) }
</ul>
<CommentsList
comments={ comments }
blockProps={ blockProps }
blocks={ blocks }
activeComment={ activeComment }
setActiveComment={ setActiveComment }
/>
);
}
61 changes: 48 additions & 13 deletions packages/block-library/src/comment-template/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,43 @@
* @package WordPress
*/

/**
* Function that recursively renders a list of nested comments.
*
* @param WP_Comment[] $comments The array of comments.
* @param WP_Block $block Block instance.
* @return string
*/
function block_core_comment_template_render_comments( $comments, $block ) {
$content = '';
foreach ( $comments as $comment ) {

$block_content = ( new WP_Block(
$block->parsed_block,
array(
'commentId' => $comment->comment_ID,
)
) )->render( array( 'dynamic' => false ) );

$children = $comment->get_children();

// If the comment has children, recurse to create the HTML for the nested
// comments.
if ( ! empty( $children ) ) {
$inner_content = block_core_comment_template_render_comments(
$children,
$block
);
$block_content .= sprintf( '<ol>%1$s</ol>', $inner_content );
}

$content .= '<li>' . $block_content . '</li>';
}

return $content;

}

/**
* Renders the `core/comment-template` block on the server.
*
Expand All @@ -27,26 +64,24 @@ function render_block_core_comment_template( $attributes, $content, $block ) {
$number = $block->context['queryPerPage'];

// Get an array of comments for the current post.
$comments = get_approved_comments( $post_id, array( 'number' => $number ) );
$comments = get_approved_comments(
$post_id,
array(
'number' => $number,
'hierarchical' => 'threaded',
)
);

if ( count( $comments ) === 0 ) {
return '';
}

$content = '';
foreach ( $comments as $comment ) {
$block_content = ( new WP_Block(
$block->parsed_block,
array(
'commentId' => $comment->comment_ID,
)
) )->render( array( 'dynamic' => false ) );
$content .= '<li>' . $block_content . '</li>';
}
$wrapper_attributes = get_block_wrapper_attributes();

return sprintf(
'<ul>%1$s</ul>',
$content
'<ol %1$s>%2$s</ol>',
$wrapper_attributes,
block_core_comment_template_render_comments( $comments, $block )
);
}

Expand Down
17 changes: 17 additions & 0 deletions packages/block-library/src/comment-template/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.wp-block-comment-template {
margin-bottom: 0;
max-width: 100%;
list-style: none;
padding: 0;

li {
clear: both;
}

ol {
margin-bottom: 0;
max-width: 100%;
list-style: none;
padding-left: 2rem;
}
}
45 changes: 45 additions & 0 deletions packages/block-library/src/comment-template/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Internal dependencies
*/
import { convertToTree } from '../util';

describe( 'Convert to tree', () => {
it( 'Empty comments', () => {
const comments = convertToTree( [] );

expect( comments ).toEqual( [] );
} );

it( 'Handle comments', () => {
const comments = convertToTree( [
{ id: 1, parent: 0 },
{ id: 2, parent: 0 },
{ id: 3, parent: 2 },
{ id: 4, parent: 2 },
{ id: 5, parent: 4 },
{ id: 6, parent: 1 },
] );

expect( comments ).toEqual( [
{
commentId: 1,
children: [
{
commentId: 6,
children: [],
},
],
},
{
commentId: 2,
children: [
{ commentId: 3, children: [] },
{
commentId: 4,
children: [ { commentId: 5, children: [] } ],
},
],
},
] );
} );
} );
56 changes: 56 additions & 0 deletions packages/block-library/src/comment-template/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
*
* This function converts a flat list of comment objects with a `parent` property
* to a nested list of comment objects with a `children` property. The `children`
* property is itself a list of comment objects.
*
* @example
* ```
* const comments = [
* { id: 1, parent: 0 },
* { id: 2, parent: 1 },
* { id: 3, parent: 2 },
* { id: 4, parent: 1 },
* ];
* expect( convertToTree( comments ) ).toEqual( [
* {
* commentId: 1,
* children: [
* { commentId: 2, children: [ { commentId: 3, children: [] } ] },
* { commentId: 4, children: [] },
* ],
* },
* ] );
* ```
* @typedef {{id: number, parent: number}} Comment
* @param {Comment[]} data - List of comment objects.
*
* @return {Object[]} Nested list of comment objects with a `children` property.
*/
export const convertToTree = ( data ) => {
const table = {};
if ( ! data ) return [];

// First create a hash table of { [id]: { ...comment, children: [] }}
data.forEach( ( item ) => {
table[ item.id ] = { commentId: item.id, children: [] };
} );

const tree = [];

// Iterate over the original comments again
data.forEach( ( item ) => {
if ( item.parent ) {
// If the comment has a "parent", then find that parent in the table that
// we have created above and push the current comment to the array of its
// children.
table[ item.parent ].children.push( table[ item.id ] );
} else {
// Otherwise, if the comment has no parent (also works if parent is 0)
// that means that it's a top-level comment so we can find it in the table
// and push it to the final tree.
tree.push( table[ item.id ] );
}
} );
return tree;
};
Loading

0 comments on commit e629f5b

Please sign in to comment.