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;
*/
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();
*/
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();
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;
return view('comments.comment-branch', [
'readOnly' => false,
- 'branch' => [
- 'comment' => $comment,
- 'children' => [],
- ]
+ 'branch' => new CommentTreeNode($comment, 0, []),
]);
}
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),
]);
}
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),
]);
}
{
/**
* The built nested tree structure array.
- * @var array{comment: Comment, depth: int, children: array}[]
+ * @var CommentTreeNode[]
*/
protected array $tree;
protected array $comments;
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
/**
* @param Comment[] $comments
+ * @return CommentTreeNode[]
*/
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
--- /dev/null
+<?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;
+ }
+}
'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',
protected async archive(): Promise<void> {
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 {
}
}
+export interface ArchiveEvent extends Event {
+ detail: {
+ new_thread_dom: HTMLElement;
+ }
+}
+
export class PageComments extends Component {
private elem: HTMLElement;
private commentCountBar: HTMLElement;
private commentsTitle: HTMLElement;
private addButtonContainer: HTMLElement;
+ private archiveContainer: HTMLElement;
private replyToRow: HTMLElement;
private formContainer: HTMLElement;
private form: HTMLFormElement;
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;
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));
+{{--
+$branch CommentTreeNode
+--}}
<div class="comment-branch">
<div>
- @include('comments.comment', ['comment' => $branch['comment']])
+ @include('comments.comment', ['comment' => $branch->comment])
</div>
<div class="flex-container-row">
<div class="comment-thread-indicator-parent">
<div class="comment-thread-indicator"></div>
</div>
<div class="comment-branch-children flex">
- @foreach($branch['children'] as $childBranch)
+ @foreach($branch->children as $childBranch)
@include('comments.comment-branch', ['branch' => $childBranch])
@endforeach
</div>
@if(userCan('comment-create-all'))
<button refs="page-comment@reply-button" type="button" class="text-button text-muted hover-underline text-small p-xs">@icon('reply') {{ trans('common.reply') }}</button>
@endif
- @if(userCan('comment-update', $comment) || userCan('comment-delete', $comment))
+ @if(!$comment->parent_id && (userCan('comment-update', $comment) || userCan('comment-delete', $comment)))
<button refs="page-comment@archive-button"
type="button"
data-is-archived="{{ $comment->archived ? 'true' : 'false' }}"
@endif
</div>
- <div refs="page-comments@commentContainer" class="comment-container">
- @foreach($commentTree->get() as $branch)
+ <div refs="page-comments@comment-container" class="comment-container">
+ @foreach($commentTree->getActive() as $branch)
@include('comments.comment-branch', ['branch' => $branch, 'readOnly' => false])
@endforeach
</div>
@if(userCan('comment-create-all'))
@include('comments.create')
@if (!$commentTree->empty())
- <div refs="page-comments@addButtonContainer" class="text-right">
+ <div refs="page-comments@addButtonContainer" class="flex-container-row">
+
+ <button type="button"
+ refs="page-comments@show-archived-button"
+ class="text-button hover-underline">{{ trans_choice('entities.comment_archived', count($commentTree->getArchived())) }}</button>
+
<button type="button"
refs="page-comments@add-comment-button"
- class="button outline">{{ trans('entities.comment_add') }}</button>
+ class="button outline ml-auto">{{ trans('entities.comment_add') }}</button>
</div>
@endif
@endif
+ <div refs="page-comments@archive-container" class="comment-container">
+ @foreach($commentTree->getArchived() as $branch)
+ @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => false])
+ @endforeach
+ </div>
+
@if(userCan('comment-create-all') || $commentTree->canUpdateAny())
@push('body-end')
<script src="{{ versioned_asset('libs/tinymce/tinymce.min.js') }}" nonce="{{ $cspNonce }}" defer></script>