Skip to content

Content Comments #5584

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
May 22, 2025
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8d159f7
Comments: Started logic for content references
ssddanbrown Apr 18, 2025
add238f
Comments & Pointer: Converted components to typescript
ssddanbrown Apr 18, 2025
5e3c3ad
Comments: Added back-end content reference handling
ssddanbrown Apr 18, 2025
2e7544a
Comments: Converted comment component to TS
ssddanbrown Apr 19, 2025
18ede9b
Comments: Added inline comment marker/highlight logic
ssddanbrown Apr 19, 2025
5bfba28
Comments: Started inline comment display windows
ssddanbrown Apr 21, 2025
f656a82
Comments: Styled content comments & improved interaction
ssddanbrown Apr 24, 2025
ecda4e1
Comments: Added reference marker to comments
ssddanbrown Apr 26, 2025
e8f4418
Comments: Split out page comment reference logic to own component
ssddanbrown Apr 27, 2025
8bdf948
Comments: Added archive endpoints, messages, Js actions and tests
ssddanbrown Apr 28, 2025
099f610
Comments: Started archive display, created mode for tree node
ssddanbrown Apr 28, 2025
e7dcc2d
Comments: Moved to tab UI, Converted tabs component to ts
ssddanbrown Apr 30, 2025
15c79c3
Comments: Addressed a range of edge cases and ux issues for references
ssddanbrown May 1, 2025
c82fa33
Comments: Further range of content reference ux improvements
ssddanbrown May 1, 2025
a27df48
Comments: Fixed display, added archive list support for editor toolbox
ssddanbrown May 9, 2025
f8c0aaf
Comments: Checked content/arhived comment styles in dark mode
ssddanbrown May 9, 2025
62f78f1
Comments: Split tests, added extra archive/reference tests
ssddanbrown May 12, 2025
8f92b6f
Comments: Fixed a range of TS errors + other
ssddanbrown May 12, 2025
32b29fc
Comments: Fixed pointer display, Fixed translation test
ssddanbrown May 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions app/Activity/CommentRepo.php
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@

use BookStack\Activity\Models\Comment;
use BookStack\Entities\Models\Entity;
use BookStack\Exceptions\NotifyException;
use BookStack\Exceptions\PrettyException;
use BookStack\Facades\Activity as ActivityService;
use BookStack\Util\HtmlDescriptionFilter;

@@ -20,7 +22,7 @@ public function getById(int $id): Comment
/**
* Create a new comment on an entity.
*/
public function create(Entity $entity, string $html, ?int $parent_id): Comment
public function create(Entity $entity, string $html, ?int $parentId, string $contentRef): Comment
{
$userId = user()->id;
$comment = new Comment();
@@ -29,7 +31,8 @@ public function create(Entity $entity, string $html, ?int $parent_id): Comment
$comment->created_by = $userId;
$comment->updated_by = $userId;
$comment->local_id = $this->getNextLocalId($entity);
$comment->parent_id = $parent_id;
$comment->parent_id = $parentId;
$comment->content_ref = preg_match('/^bkmrk-(.*?):\d+:(\d*-\d*)?$/', $contentRef) === 1 ? $contentRef : '';

$entity->comments()->save($comment);
ActivityService::add(ActivityType::COMMENT_CREATE, $comment);
@@ -52,6 +55,41 @@ public function update(Comment $comment, string $html): Comment
return $comment;
}


/**
* Archive an existing comment.
*/
public function archive(Comment $comment): Comment
{
if ($comment->parent_id) {
throw new NotifyException('Only top-level comments can be archived.', '/', 400);
}

$comment->archived = true;
$comment->save();

ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);

return $comment;
}

/**
* Un-archive an existing comment.
*/
public function unarchive(Comment $comment): Comment
{
if ($comment->parent_id) {
throw new NotifyException('Only top-level comments can be un-archived.', '/', 400);
}

$comment->archived = false;
$comment->save();

ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);

return $comment;
}

/**
* Delete a comment from the system.
*/
51 changes: 46 additions & 5 deletions app/Activity/Controllers/CommentController.php
Original file line number Diff line number Diff line change
@@ -3,6 +3,8 @@
namespace BookStack\Activity\Controllers;

use BookStack\Activity\CommentRepo;
use BookStack\Activity\Tools\CommentTree;
use BookStack\Activity\Tools\CommentTreeNode;
use BookStack\Entities\Queries\PageQueries;
use BookStack\Http\Controller;
use Illuminate\Http\Request;
@@ -26,6 +28,7 @@ public function savePageComment(Request $request, int $pageId)
$input = $this->validate($request, [
'html' => ['required', 'string'],
'parent_id' => ['nullable', 'integer'],
'content_ref' => ['string'],
]);

$page = $this->pageQueries->findVisibleById($pageId);
@@ -40,14 +43,12 @@ public function savePageComment(Request $request, int $pageId)

// Create a new comment.
$this->checkPermission('comment-create-all');
$comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null);
$contentRef = $input['content_ref'] ?? '';
$comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null, $contentRef);

return view('comments.comment-branch', [
'readOnly' => false,
'branch' => [
'comment' => $comment,
'children' => [],
]
'branch' => new CommentTreeNode($comment, 0, []),
]);
}

@@ -74,6 +75,46 @@ public function update(Request $request, int $commentId)
]);
}

/**
* Mark a comment as archived.
*/
public function archive(int $id)
{
$comment = $this->commentRepo->getById($id);
$this->checkOwnablePermission('page-view', $comment->entity);
if (!userCan('comment-update', $comment) && !userCan('comment-delete', $comment)) {
$this->showPermissionError();
}

$this->commentRepo->archive($comment);

$tree = new CommentTree($comment->entity);
return view('comments.comment-branch', [
'readOnly' => false,
'branch' => $tree->getCommentNodeForId($id),
]);
}

/**
* Unmark a comment as archived.
*/
public function unarchive(int $id)
{
$comment = $this->commentRepo->getById($id);
$this->checkOwnablePermission('page-view', $comment->entity);
if (!userCan('comment-update', $comment) && !userCan('comment-delete', $comment)) {
$this->showPermissionError();
}

$this->commentRepo->unarchive($comment);

$tree = new CommentTree($comment->entity);
return view('comments.comment-branch', [
'readOnly' => false,
'branch' => $tree->getCommentNodeForId($id),
]);
}

/**
* Delete a comment from the system.
*/
2 changes: 2 additions & 0 deletions app/Activity/Models/Comment.php
Original file line number Diff line number Diff line change
@@ -19,6 +19,8 @@
* @property int $entity_id
* @property int $created_by
* @property int $updated_by
* @property string $content_ref
* @property bool $archived
*/
class Comment extends Model implements Loggable
{
47 changes: 35 additions & 12 deletions app/Activity/Tools/CommentTree.php
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ class CommentTree
{
/**
* The built nested tree structure array.
* @var array{comment: Comment, depth: int, children: array}[]
* @var CommentTreeNode[]
*/
protected array $tree;
protected array $comments;
@@ -28,17 +28,43 @@ public function enabled(): bool

public function empty(): bool
{
return count($this->tree) === 0;
return count($this->getActive()) === 0;
}

public function count(): int
{
return count($this->comments);
}

public function get(): array
public function getActive(): array
{
return $this->tree;
return array_filter($this->tree, fn (CommentTreeNode $node) => !$node->comment->archived);
}

public function activeThreadCount(): int
{
return count($this->getActive());
}

public function getArchived(): array
{
return array_filter($this->tree, fn (CommentTreeNode $node) => $node->comment->archived);
}

public function archivedThreadCount(): int
{
return count($this->getArchived());
}

public function getCommentNodeForId(int $commentId): ?CommentTreeNode
{
foreach ($this->tree as $node) {
if ($node->comment->id === $commentId) {
return $node;
}
}

return null;
}

public function canUpdateAny(): bool
@@ -54,6 +80,7 @@ public function canUpdateAny(): bool

/**
* @param Comment[] $comments
* @return CommentTreeNode[]
*/
protected function createTree(array $comments): array
{
@@ -77,26 +104,22 @@ protected function createTree(array $comments): array

$tree = [];
foreach ($childMap[0] ?? [] as $childId) {
$tree[] = $this->createTreeForId($childId, 0, $byId, $childMap);
$tree[] = $this->createTreeNodeForId($childId, 0, $byId, $childMap);
}

return $tree;
}

protected function createTreeForId(int $id, int $depth, array &$byId, array &$childMap): array
protected function createTreeNodeForId(int $id, int $depth, array &$byId, array &$childMap): CommentTreeNode
{
$childIds = $childMap[$id] ?? [];
$children = [];

foreach ($childIds as $childId) {
$children[] = $this->createTreeForId($childId, $depth + 1, $byId, $childMap);
$children[] = $this->createTreeNodeForId($childId, $depth + 1, $byId, $childMap);
}

return [
'comment' => $byId[$id],
'depth' => $depth,
'children' => $children,
];
return new CommentTreeNode($byId[$id], $depth, $children);
}

protected function loadComments(): array
23 changes: 23 additions & 0 deletions app/Activity/Tools/CommentTreeNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace BookStack\Activity\Tools;

use BookStack\Activity\Models\Comment;

class CommentTreeNode
{
public Comment $comment;
public int $depth;

/**
* @var CommentTreeNode[]
*/
public array $children;

public function __construct(Comment $comment, int $depth, array $children)
{
$this->comment = $comment;
$this->depth = $depth;
$this->children = $children;
}
}
2 changes: 2 additions & 0 deletions database/factories/Activity/Models/CommentFactory.php
Original file line number Diff line number Diff line change
@@ -27,6 +27,8 @@ public function definition()
'html' => $html,
'parent_id' => null,
'local_id' => 1,
'content_ref' => '',
'archived' => false,
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('comments', function (Blueprint $table) {
$table->string('content_ref');
$table->boolean('archived')->index();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('comments', function (Blueprint $table) {
$table->dropColumn('content_ref');
$table->dropColumn('archived');
});
}
};
2 changes: 2 additions & 0 deletions lang/en/common.php
Original file line number Diff line number Diff line change
@@ -30,6 +30,8 @@
'create' => 'Create',
'update' => 'Update',
'edit' => 'Edit',
'archive' => 'Archive',
'unarchive' => 'Un-Archive',
'sort' => 'Sort',
'move' => 'Move',
'copy' => 'Copy',
11 changes: 10 additions & 1 deletion lang/en/entities.php
Original file line number Diff line number Diff line change
@@ -392,8 +392,11 @@
'comment' => 'Comment',
'comments' => 'Comments',
'comment_add' => 'Add Comment',
'comment_none' => 'No comments to display',
'comment_placeholder' => 'Leave a comment here',
'comment_count' => '{0} No Comments|{1} 1 Comment|[2,*] :count Comments',
'comment_thread_count' => ':count Comment Thread|:count Comment Threads',
'comment_archived_count' => ':count Archived',
'comment_archived_threads' => 'Archived Threads',
'comment_save' => 'Save Comment',
'comment_new' => 'New Comment',
'comment_created' => 'commented :createDiff',
@@ -402,8 +405,14 @@
'comment_deleted_success' => 'Comment deleted',
'comment_created_success' => 'Comment added',
'comment_updated_success' => 'Comment updated',
'comment_archive_success' => 'Comment archived',
'comment_unarchive_success' => 'Comment un-archived',
'comment_view' => 'View comment',
'comment_jump_to_thread' => 'Jump to thread',
'comment_delete_confirm' => 'Are you sure you want to delete this comment?',
'comment_in_reply_to' => 'In reply to :commentId',
'comment_reference' => 'Reference',
'comment_reference_outdated' => '(Outdated)',
'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',

// Revision
1 change: 1 addition & 0 deletions resources/icons/archive.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions resources/icons/bookmark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading