Skip to content

Commit 4627dfd

Browse files
committed
API: Added comment tree to pages-read endpoint
Includes tests to cover
1 parent fcacf7c commit 4627dfd

File tree

5 files changed

+69
-8
lines changed

5 files changed

+69
-8
lines changed

app/Activity/Controllers/CommentApiController.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@
1818
* scoped to the page which the comment is on. The 'parent_id' is used for replies
1919
* and refers to the 'local_id' of the parent comment on the same page, not the main
2020
* globally unique 'id'.
21+
*
22+
* If you want to get all comments for a page in a tree-like structure, as reflected in
23+
* the UI, then that is provided on pages-read API responses.
2124
*/
2225
class CommentApiController extends ApiController
2326
{
24-
// TODO - Add tree-style comment listing to page-show responses.
25-
2627
protected array $rules = [
2728
'create' => [
2829
'page_id' => ['required', 'integer'],

app/Activity/Tools/CommentTree.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ class CommentTree
1313
* @var CommentTreeNode[]
1414
*/
1515
protected array $tree;
16+
17+
/**
18+
* A linear array of loaded comments.
19+
* @var Comment[]
20+
*/
1621
protected array $comments;
1722

1823
public function __construct(
@@ -39,7 +44,7 @@ public function count(): int
3944

4045
public function getActive(): array
4146
{
42-
return array_filter($this->tree, fn (CommentTreeNode $node) => !$node->comment->archived);
47+
return array_values(array_filter($this->tree, fn (CommentTreeNode $node) => !$node->comment->archived));
4348
}
4449

4550
public function activeThreadCount(): int
@@ -49,7 +54,7 @@ public function activeThreadCount(): int
4954

5055
public function getArchived(): array
5156
{
52-
return array_filter($this->tree, fn (CommentTreeNode $node) => $node->comment->archived);
57+
return array_values(array_filter($this->tree, fn (CommentTreeNode $node) => $node->comment->archived));
5358
}
5459

5560
public function archivedThreadCount(): int
@@ -79,6 +84,14 @@ public function canUpdateAny(): bool
7984
return false;
8085
}
8186

87+
public function loadVisibleHtml(): void
88+
{
89+
foreach ($this->comments as $comment) {
90+
$comment->setAttribute('html', $comment->safeHtml());
91+
$comment->makeVisible('html');
92+
}
93+
}
94+
8295
/**
8396
* @param Comment[] $comments
8497
* @return CommentTreeNode[]
@@ -123,6 +136,9 @@ protected function createTreeNodeForId(int $id, int $depth, array &$byId, array
123136
return new CommentTreeNode($byId[$id], $depth, $children);
124137
}
125138

139+
/**
140+
* @return Comment[]
141+
*/
126142
protected function loadComments(): array
127143
{
128144
if (!$this->enabled()) {

app/Entities/Controllers/PageApiController.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace BookStack\Entities\Controllers;
44

5+
use BookStack\Activity\Tools\CommentTree;
56
use BookStack\Entities\Queries\EntityQueries;
67
use BookStack\Entities\Queries\PageQueries;
78
use BookStack\Entities\Repos\PageRepo;
@@ -88,21 +89,32 @@ public function create(Request $request)
8889
/**
8990
* View the details of a single page.
9091
* Pages will always have HTML content. They may have markdown content
91-
* if the markdown editor was used to last update the page.
92+
* if the Markdown editor was used to last update the page.
9293
*
93-
* The 'html' property is the fully rendered & escaped HTML content that BookStack
94+
* The 'html' property is the fully rendered and escaped HTML content that BookStack
9495
* would show on page view, with page includes handled.
9596
* The 'raw_html' property is the direct database stored HTML content, which would be
9697
* what BookStack shows on page edit.
9798
*
9899
* See the "Content Security" section of these docs for security considerations when using
99100
* the page content returned from this endpoint.
101+
*
102+
* Comments for the page are provided in a tree-structure representing the hierarchy of top-level
103+
* comments and replies, for both archived and active comments.
100104
*/
101105
public function read(string $id)
102106
{
103107
$page = $this->queries->findVisibleByIdOrFail($id);
104108

105-
return response()->json($page->forJsonDisplay());
109+
$page = $page->forJsonDisplay();
110+
$commentTree = (new CommentTree($page));
111+
$commentTree->loadVisibleHtml();
112+
$page->setAttribute('comments', [
113+
'active' => $commentTree->getActive(),
114+
'archived' => $commentTree->getArchived(),
115+
]);
116+
117+
return response()->json($page);
106118
}
107119

108120
/**

database/factories/Activity/Models/CommentFactory.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ class CommentFactory extends Factory
1313
*/
1414
protected $model = \BookStack\Activity\Models\Comment::class;
1515

16+
/**
17+
* A static counter to provide a unique local_id for each comment.
18+
*/
19+
protected static int $nextLocalId = 1000;
20+
1621
/**
1722
* Define the model's default state.
1823
*
@@ -22,11 +27,12 @@ public function definition()
2227
{
2328
$text = $this->faker->paragraph(1);
2429
$html = '<p>' . $text . '</p>';
30+
$nextLocalId = static::$nextLocalId++;
2531

2632
return [
2733
'html' => $html,
2834
'parent_id' => null,
29-
'local_id' => 1,
35+
'local_id' => $nextLocalId,
3036
'content_ref' => '',
3137
'archived' => false,
3238
];

tests/Api/PagesApiTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Tests\Api;
44

5+
use BookStack\Activity\Models\Comment;
56
use BookStack\Entities\Models\Chapter;
67
use BookStack\Entities\Models\Page;
78
use Carbon\Carbon;
@@ -199,6 +200,31 @@ public function test_read_endpoint_returns_not_found()
199200
$this->assertSame(404, $resp->json('error')['code']);
200201
}
201202

203+
public function test_read_endpoint_includes_page_comments_tree_structure()
204+
{
205+
$this->actingAsApiEditor();
206+
$page = $this->entities->page();
207+
$relation = ['commentable_type' => 'page', 'commentable_id' => $page->id];
208+
$active = Comment::factory()->create([...$relation, 'html' => '<p>My active<script>cat</script> comment</p>']);
209+
Comment::factory()->count(5)->create([...$relation, 'parent_id' => $active->local_id]);
210+
$archived = Comment::factory()->create([...$relation, 'archived' => true]);
211+
Comment::factory()->count(2)->create([...$relation, 'parent_id' => $archived->local_id]);
212+
213+
$resp = $this->getJson("{$this->baseEndpoint}/{$page->id}");
214+
$resp->assertOk();
215+
216+
$resp->assertJsonCount(1, 'comments.active');
217+
$resp->assertJsonCount(1, 'comments.archived');
218+
$resp->assertJsonCount(5, 'comments.active.0.children');
219+
$resp->assertJsonCount(2, 'comments.archived.0.children');
220+
221+
$resp->assertJsonFragment([
222+
'id' => $active->id,
223+
'local_id' => $active->local_id,
224+
'html' => '<p>My active comment</p>',
225+
]);
226+
}
227+
202228
public function test_update_endpoint()
203229
{
204230
$this->actingAsApiEditor();

0 commit comments

Comments
 (0)