mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-11-06 00:50:36 +03:00
API: Added comment tree to pages-read endpoint
Includes tests to cover
This commit is contained in:
@@ -18,11 +18,12 @@ use Illuminate\Http\Response;
|
|||||||
* scoped to the page which the comment is on. The 'parent_id' is used for replies
|
* 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
|
* and refers to the 'local_id' of the parent comment on the same page, not the main
|
||||||
* globally unique 'id'.
|
* 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
|
class CommentApiController extends ApiController
|
||||||
{
|
{
|
||||||
// TODO - Add tree-style comment listing to page-show responses.
|
|
||||||
|
|
||||||
protected array $rules = [
|
protected array $rules = [
|
||||||
'create' => [
|
'create' => [
|
||||||
'page_id' => ['required', 'integer'],
|
'page_id' => ['required', 'integer'],
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ class CommentTree
|
|||||||
* @var CommentTreeNode[]
|
* @var CommentTreeNode[]
|
||||||
*/
|
*/
|
||||||
protected array $tree;
|
protected array $tree;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A linear array of loaded comments.
|
||||||
|
* @var Comment[]
|
||||||
|
*/
|
||||||
protected array $comments;
|
protected array $comments;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -39,7 +44,7 @@ class CommentTree
|
|||||||
|
|
||||||
public function getActive(): array
|
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 activeThreadCount(): int
|
||||||
@@ -49,7 +54,7 @@ class CommentTree
|
|||||||
|
|
||||||
public function getArchived(): array
|
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
|
public function archivedThreadCount(): int
|
||||||
@@ -79,6 +84,14 @@ class CommentTree
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function loadVisibleHtml(): void
|
||||||
|
{
|
||||||
|
foreach ($this->comments as $comment) {
|
||||||
|
$comment->setAttribute('html', $comment->safeHtml());
|
||||||
|
$comment->makeVisible('html');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Comment[] $comments
|
* @param Comment[] $comments
|
||||||
* @return CommentTreeNode[]
|
* @return CommentTreeNode[]
|
||||||
@@ -123,6 +136,9 @@ class CommentTree
|
|||||||
return new CommentTreeNode($byId[$id], $depth, $children);
|
return new CommentTreeNode($byId[$id], $depth, $children);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Comment[]
|
||||||
|
*/
|
||||||
protected function loadComments(): array
|
protected function loadComments(): array
|
||||||
{
|
{
|
||||||
if (!$this->enabled()) {
|
if (!$this->enabled()) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace BookStack\Entities\Controllers;
|
namespace BookStack\Entities\Controllers;
|
||||||
|
|
||||||
|
use BookStack\Activity\Tools\CommentTree;
|
||||||
use BookStack\Entities\Queries\EntityQueries;
|
use BookStack\Entities\Queries\EntityQueries;
|
||||||
use BookStack\Entities\Queries\PageQueries;
|
use BookStack\Entities\Queries\PageQueries;
|
||||||
use BookStack\Entities\Repos\PageRepo;
|
use BookStack\Entities\Repos\PageRepo;
|
||||||
@@ -88,21 +89,32 @@ class PageApiController extends ApiController
|
|||||||
/**
|
/**
|
||||||
* View the details of a single page.
|
* View the details of a single page.
|
||||||
* Pages will always have HTML content. They may have markdown content
|
* 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.
|
* would show on page view, with page includes handled.
|
||||||
* The 'raw_html' property is the direct database stored HTML content, which would be
|
* The 'raw_html' property is the direct database stored HTML content, which would be
|
||||||
* what BookStack shows on page edit.
|
* what BookStack shows on page edit.
|
||||||
*
|
*
|
||||||
* See the "Content Security" section of these docs for security considerations when using
|
* See the "Content Security" section of these docs for security considerations when using
|
||||||
* the page content returned from this endpoint.
|
* 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)
|
public function read(string $id)
|
||||||
{
|
{
|
||||||
$page = $this->queries->findVisibleByIdOrFail($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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ class CommentFactory extends Factory
|
|||||||
*/
|
*/
|
||||||
protected $model = \BookStack\Activity\Models\Comment::class;
|
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.
|
* Define the model's default state.
|
||||||
*
|
*
|
||||||
@@ -22,11 +27,12 @@ class CommentFactory extends Factory
|
|||||||
{
|
{
|
||||||
$text = $this->faker->paragraph(1);
|
$text = $this->faker->paragraph(1);
|
||||||
$html = '<p>' . $text . '</p>';
|
$html = '<p>' . $text . '</p>';
|
||||||
|
$nextLocalId = static::$nextLocalId++;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'html' => $html,
|
'html' => $html,
|
||||||
'parent_id' => null,
|
'parent_id' => null,
|
||||||
'local_id' => 1,
|
'local_id' => $nextLocalId,
|
||||||
'content_ref' => '',
|
'content_ref' => '',
|
||||||
'archived' => false,
|
'archived' => false,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Tests\Api;
|
namespace Tests\Api;
|
||||||
|
|
||||||
|
use BookStack\Activity\Models\Comment;
|
||||||
use BookStack\Entities\Models\Chapter;
|
use BookStack\Entities\Models\Chapter;
|
||||||
use BookStack\Entities\Models\Page;
|
use BookStack\Entities\Models\Page;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
@@ -199,6 +200,31 @@ class PagesApiTest extends TestCase
|
|||||||
$this->assertSame(404, $resp->json('error')['code']);
|
$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()
|
public function test_update_endpoint()
|
||||||
{
|
{
|
||||||
$this->actingAsApiEditor();
|
$this->actingAsApiEditor();
|
||||||
|
|||||||
Reference in New Issue
Block a user