* scoped to the page which the comment is on. The 'parent_id' is used for replies
* and refers to the 'local_id' of the parent comment on the same page, not the main
* globally unique 'id'.
+ *
+ * If you want to get all comments for a page in a tree-like structure, as reflected in
+ * the UI, then that is provided on pages-read API responses.
*/
class CommentApiController extends ApiController
{
- // TODO - Add tree-style comment listing to page-show responses.
-
protected array $rules = [
'create' => [
'page_id' => ['required', 'integer'],
* @var CommentTreeNode[]
*/
protected array $tree;
+
+ /**
+ * A linear array of loaded comments.
+ * @var Comment[]
+ */
protected array $comments;
public function __construct(
public function getActive(): array
{
- return array_filter($this->tree, fn (CommentTreeNode $node) => !$node->comment->archived);
+ return array_values(array_filter($this->tree, fn (CommentTreeNode $node) => !$node->comment->archived));
}
public function activeThreadCount(): int
public function getArchived(): array
{
- return array_filter($this->tree, fn (CommentTreeNode $node) => $node->comment->archived);
+ return array_values(array_filter($this->tree, fn (CommentTreeNode $node) => $node->comment->archived));
}
public function archivedThreadCount(): int
return false;
}
+ public function loadVisibleHtml(): void
+ {
+ foreach ($this->comments as $comment) {
+ $comment->setAttribute('html', $comment->safeHtml());
+ $comment->makeVisible('html');
+ }
+ }
+
/**
* @param Comment[] $comments
* @return CommentTreeNode[]
return new CommentTreeNode($byId[$id], $depth, $children);
}
+ /**
+ * @return Comment[]
+ */
protected function loadComments(): array
{
if (!$this->enabled()) {
namespace BookStack\Entities\Controllers;
+use BookStack\Activity\Tools\CommentTree;
use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Repos\PageRepo;
/**
* View the details of a single page.
* Pages will always have HTML content. They may have markdown content
- * if the markdown editor was used to last update the page.
+ * if the Markdown editor was used to last update the page.
*
- * The 'html' property is the fully rendered & escaped HTML content that BookStack
+ * The 'html' property is the fully rendered and escaped HTML content that BookStack
* would show on page view, with page includes handled.
* The 'raw_html' property is the direct database stored HTML content, which would be
* what BookStack shows on page edit.
*
* See the "Content Security" section of these docs for security considerations when using
* the page content returned from this endpoint.
+ *
+ * Comments for the page are provided in a tree-structure representing the hierarchy of top-level
+ * comments and replies, for both archived and active comments.
*/
public function read(string $id)
{
$page = $this->queries->findVisibleByIdOrFail($id);
- return response()->json($page->forJsonDisplay());
+ $page = $page->forJsonDisplay();
+ $commentTree = (new CommentTree($page));
+ $commentTree->loadVisibleHtml();
+ $page->setAttribute('comments', [
+ 'active' => $commentTree->getActive(),
+ 'archived' => $commentTree->getArchived(),
+ ]);
+
+ return response()->json($page);
}
/**
*/
protected $model = \BookStack\Activity\Models\Comment::class;
+ /**
+ * A static counter to provide a unique local_id for each comment.
+ */
+ protected static int $nextLocalId = 1000;
+
/**
* Define the model's default state.
*
{
$text = $this->faker->paragraph(1);
$html = '<p>' . $text . '</p>';
+ $nextLocalId = static::$nextLocalId++;
return [
'html' => $html,
'parent_id' => null,
- 'local_id' => 1,
+ 'local_id' => $nextLocalId,
'content_ref' => '',
'archived' => false,
];
namespace Tests\Api;
+use BookStack\Activity\Models\Comment;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use Carbon\Carbon;
$this->assertSame(404, $resp->json('error')['code']);
}
+ public function test_read_endpoint_includes_page_comments_tree_structure()
+ {
+ $this->actingAsApiEditor();
+ $page = $this->entities->page();
+ $relation = ['commentable_type' => 'page', 'commentable_id' => $page->id];
+ $active = Comment::factory()->create([...$relation, 'html' => '<p>My active<script>cat</script> comment</p>']);
+ Comment::factory()->count(5)->create([...$relation, 'parent_id' => $active->local_id]);
+ $archived = Comment::factory()->create([...$relation, 'archived' => true]);
+ Comment::factory()->count(2)->create([...$relation, 'parent_id' => $archived->local_id]);
+
+ $resp = $this->getJson("{$this->baseEndpoint}/{$page->id}");
+ $resp->assertOk();
+
+ $resp->assertJsonCount(1, 'comments.active');
+ $resp->assertJsonCount(1, 'comments.archived');
+ $resp->assertJsonCount(5, 'comments.active.0.children');
+ $resp->assertJsonCount(2, 'comments.archived.0.children');
+
+ $resp->assertJsonFragment([
+ 'id' => $active->id,
+ 'local_id' => $active->local_id,
+ 'html' => '<p>My active comment</p>',
+ ]);
+ }
+
public function test_update_endpoint()
{
$this->actingAsApiEditor();