diff --git a/app/Activity/CommentRepo.php b/app/Activity/CommentRepo.php index 866368ee6..bf162f68a 100644 --- a/app/Activity/CommentRepo.php +++ b/app/Activity/CommentRepo.php @@ -4,6 +4,8 @@ namespace BookStack\Activity; 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; @@ -59,6 +61,10 @@ class CommentRepo */ public function archive(Comment $comment): Comment { + if ($comment->parent_id) { + throw new NotifyException('Only top-level comments can be archived.'); + } + $comment->archived = true; $comment->save(); @@ -72,6 +78,10 @@ class CommentRepo */ public function unarchive(Comment $comment): Comment { + if ($comment->parent_id) { + throw new NotifyException('Only top-level comments can be un-archived.'); + } + $comment->archived = false; $comment->save(); diff --git a/app/Activity/Controllers/CommentController.php b/app/Activity/Controllers/CommentController.php index 7a290ebab..7f16c17ff 100644 --- a/app/Activity/Controllers/CommentController.php +++ b/app/Activity/Controllers/CommentController.php @@ -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; @@ -45,10 +47,7 @@ class CommentController extends Controller return view('comments.comment-branch', [ 'readOnly' => false, - 'branch' => [ - 'comment' => $comment, - 'children' => [], - ] + 'branch' => new CommentTreeNode($comment, 0, []), ]); } @@ -81,15 +80,17 @@ class CommentController extends Controller 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); - return view('comments.comment', [ - 'comment' => $comment, + $tree = new CommentTree($comment->entity); + return view('comments.comment-branch', [ 'readOnly' => false, + 'branch' => $tree->getCommentNodeForId($id), ]); } @@ -99,15 +100,17 @@ class CommentController extends Controller 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); - return view('comments.comment', [ - 'comment' => $comment, + $tree = new CommentTree($comment->entity); + return view('comments.comment-branch', [ 'readOnly' => false, + 'branch' => $tree->getCommentNodeForId($id), ]); } diff --git a/app/Activity/Tools/CommentTree.php b/app/Activity/Tools/CommentTree.php index 16f6804ea..13afc9252 100644 --- a/app/Activity/Tools/CommentTree.php +++ b/app/Activity/Tools/CommentTree.php @@ -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; @@ -36,9 +36,25 @@ class CommentTree 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 getArchived(): array + { + return array_filter($this->tree, fn (CommentTreeNode $node) => $node->comment->archived); + } + + 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 +70,7 @@ class CommentTree /** * @param Comment[] $comments + * @return CommentTreeNode[] */ protected function createTree(array $comments): array { @@ -77,26 +94,22 @@ class CommentTree $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 diff --git a/app/Activity/Tools/CommentTreeNode.php b/app/Activity/Tools/CommentTreeNode.php new file mode 100644 index 000000000..7b280bd2d --- /dev/null +++ b/app/Activity/Tools/CommentTreeNode.php @@ -0,0 +1,23 @@ +comment = $comment; + $this->depth = $depth; + $this->children = $children; + } +} diff --git a/lang/en/entities.php b/lang/en/entities.php index 141e75b5f..cda58e65b 100644 --- a/lang/en/entities.php +++ b/lang/en/entities.php @@ -392,6 +392,7 @@ return [ 'comment' => 'Comment', 'comments' => 'Comments', 'comment_add' => 'Add Comment', + 'comment_archived' => ':count Archived Comment|:count Archived Comments', 'comment_placeholder' => 'Leave a comment here', 'comment_count' => '{0} No Comments|{1} 1 Comment|[2,*] :count Comments', 'comment_save' => 'Save Comment', diff --git a/resources/js/components/page-comment.ts b/resources/js/components/page-comment.ts index d2cbd21d1..82cb95f13 100644 --- a/resources/js/components/page-comment.ts +++ b/resources/js/components/page-comment.ts @@ -137,10 +137,12 @@ export class PageComment extends Component { protected async archive(): Promise { this.showLoading(); const isArchived = this.archiveButton.dataset.isArchived === 'true'; + const action = isArchived ? 'unarchive' : 'archive'; - await window.$http.put(`/comment/${this.commentId}/${isArchived ? 'unarchive' : 'archive'}`); - this.$emit('archive'); + const response = await window.$http.put(`/comment/${this.commentId}/${action}`); + this.$emit(action, {new_thread_dom: htmlToDom(response.data as string)}); window.$events.success(this.archiveText); + this.container.closest('.comment-branch')?.remove(); } protected showLoading(): HTMLElement { diff --git a/resources/js/components/page-comments.ts b/resources/js/components/page-comments.ts index 45f8d6a9f..083919b82 100644 --- a/resources/js/components/page-comments.ts +++ b/resources/js/components/page-comments.ts @@ -9,6 +9,12 @@ export interface CommentReplyEvent extends Event { } } +export interface ArchiveEvent extends Event { + detail: { + new_thread_dom: HTMLElement; + } +} + export class PageComments extends Component { private elem: HTMLElement; @@ -17,6 +23,7 @@ export class PageComments extends Component { private commentCountBar: HTMLElement; private commentsTitle: HTMLElement; private addButtonContainer: HTMLElement; + private archiveContainer: HTMLElement; private replyToRow: HTMLElement; private formContainer: HTMLElement; private form: HTMLFormElement; @@ -43,6 +50,7 @@ export class PageComments extends Component { this.commentCountBar = this.$refs.commentCountBar; this.commentsTitle = this.$refs.commentsTitle; this.addButtonContainer = this.$refs.addButtonContainer; + this.archiveContainer = this.$refs.archiveContainer; this.replyToRow = this.$refs.replyToRow; this.formContainer = this.$refs.formContainer; this.form = this.$refs.form as HTMLFormElement; @@ -75,6 +83,14 @@ export class PageComments extends Component { this.setReply(event.detail.id, event.detail.element); }); + this.elem.addEventListener('page-comment-archive', (event: ArchiveEvent) => { + this.archiveContainer.append(event.detail.new_thread_dom); + }); + + this.elem.addEventListener('page-comment-unarchive', (event: ArchiveEvent) => { + this.container.append(event.detail.new_thread_dom) + }); + if (this.form) { this.removeReplyToButton.addEventListener('click', this.removeReplyTo.bind(this)); this.hideFormButton.addEventListener('click', this.hideForm.bind(this)); diff --git a/resources/views/comments/comment-branch.blade.php b/resources/views/comments/comment-branch.blade.php index 83fa4b5c5..658c33219 100644 --- a/resources/views/comments/comment-branch.blade.php +++ b/resources/views/comments/comment-branch.blade.php @@ -1,13 +1,16 @@ +{{-- +$branch CommentTreeNode +--}}
- @include('comments.comment', ['comment' => $branch['comment']]) + @include('comments.comment', ['comment' => $branch->comment])
- @foreach($branch['children'] as $childBranch) + @foreach($branch->children as $childBranch) @include('comments.comment-branch', ['branch' => $childBranch]) @endforeach
diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php index 58e057140..fe61bf1a4 100644 --- a/resources/views/comments/comment.blade.php +++ b/resources/views/comments/comment.blade.php @@ -38,7 +38,7 @@ @if(userCan('comment-create-all')) @endif - @if(userCan('comment-update', $comment) || userCan('comment-delete', $comment)) + @if(!$comment->parent_id && (userCan('comment-update', $comment) || userCan('comment-delete', $comment))) + + class="button outline ml-auto">{{ trans('entities.comment_add') }}
@endif @endif +
+ @foreach($commentTree->getArchived() as $branch) + @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => false]) + @endforeach +
+ @if(userCan('comment-create-all') || $commentTree->canUpdateAny()) @push('body-end')