mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-06-19 19:51:21 +08:00
Comments: Started archive display, created mode for tree node
Some checks failed
test-js / build (push) Has been cancelled
analyse-php / build (push) Has been cancelled
lint-php / build (push) Has been cancelled
test-migrations / build (8.2) (push) Has been cancelled
test-migrations / build (8.3) (push) Has been cancelled
test-migrations / build (8.4) (push) Has been cancelled
test-php / build (8.2) (push) Has been cancelled
test-php / build (8.3) (push) Has been cancelled
test-php / build (8.4) (push) Has been cancelled
Some checks failed
test-js / build (push) Has been cancelled
analyse-php / build (push) Has been cancelled
lint-php / build (push) Has been cancelled
test-migrations / build (8.2) (push) Has been cancelled
test-migrations / build (8.3) (push) Has been cancelled
test-migrations / build (8.4) (push) Has been cancelled
test-php / build (8.2) (push) Has been cancelled
test-php / build (8.3) (push) Has been cancelled
test-php / build (8.4) (push) Has been cancelled
This commit is contained in:
@ -4,6 +4,8 @@ namespace BookStack\Activity;
|
|||||||
|
|
||||||
use BookStack\Activity\Models\Comment;
|
use BookStack\Activity\Models\Comment;
|
||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
|
use BookStack\Exceptions\NotifyException;
|
||||||
|
use BookStack\Exceptions\PrettyException;
|
||||||
use BookStack\Facades\Activity as ActivityService;
|
use BookStack\Facades\Activity as ActivityService;
|
||||||
use BookStack\Util\HtmlDescriptionFilter;
|
use BookStack\Util\HtmlDescriptionFilter;
|
||||||
|
|
||||||
@ -59,6 +61,10 @@ class CommentRepo
|
|||||||
*/
|
*/
|
||||||
public function archive(Comment $comment): Comment
|
public function archive(Comment $comment): Comment
|
||||||
{
|
{
|
||||||
|
if ($comment->parent_id) {
|
||||||
|
throw new NotifyException('Only top-level comments can be archived.');
|
||||||
|
}
|
||||||
|
|
||||||
$comment->archived = true;
|
$comment->archived = true;
|
||||||
$comment->save();
|
$comment->save();
|
||||||
|
|
||||||
@ -72,6 +78,10 @@ class CommentRepo
|
|||||||
*/
|
*/
|
||||||
public function unarchive(Comment $comment): Comment
|
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->archived = false;
|
||||||
$comment->save();
|
$comment->save();
|
||||||
|
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
namespace BookStack\Activity\Controllers;
|
namespace BookStack\Activity\Controllers;
|
||||||
|
|
||||||
use BookStack\Activity\CommentRepo;
|
use BookStack\Activity\CommentRepo;
|
||||||
|
use BookStack\Activity\Tools\CommentTree;
|
||||||
|
use BookStack\Activity\Tools\CommentTreeNode;
|
||||||
use BookStack\Entities\Queries\PageQueries;
|
use BookStack\Entities\Queries\PageQueries;
|
||||||
use BookStack\Http\Controller;
|
use BookStack\Http\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@ -45,10 +47,7 @@ class CommentController extends Controller
|
|||||||
|
|
||||||
return view('comments.comment-branch', [
|
return view('comments.comment-branch', [
|
||||||
'readOnly' => false,
|
'readOnly' => false,
|
||||||
'branch' => [
|
'branch' => new CommentTreeNode($comment, 0, []),
|
||||||
'comment' => $comment,
|
|
||||||
'children' => [],
|
|
||||||
]
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,15 +80,17 @@ class CommentController extends Controller
|
|||||||
public function archive(int $id)
|
public function archive(int $id)
|
||||||
{
|
{
|
||||||
$comment = $this->commentRepo->getById($id);
|
$comment = $this->commentRepo->getById($id);
|
||||||
|
$this->checkOwnablePermission('page-view', $comment->entity);
|
||||||
if (!userCan('comment-update', $comment) && !userCan('comment-delete', $comment)) {
|
if (!userCan('comment-update', $comment) && !userCan('comment-delete', $comment)) {
|
||||||
$this->showPermissionError();
|
$this->showPermissionError();
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->commentRepo->archive($comment);
|
$this->commentRepo->archive($comment);
|
||||||
|
|
||||||
return view('comments.comment', [
|
$tree = new CommentTree($comment->entity);
|
||||||
'comment' => $comment,
|
return view('comments.comment-branch', [
|
||||||
'readOnly' => false,
|
'readOnly' => false,
|
||||||
|
'branch' => $tree->getCommentNodeForId($id),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,15 +100,17 @@ class CommentController extends Controller
|
|||||||
public function unarchive(int $id)
|
public function unarchive(int $id)
|
||||||
{
|
{
|
||||||
$comment = $this->commentRepo->getById($id);
|
$comment = $this->commentRepo->getById($id);
|
||||||
|
$this->checkOwnablePermission('page-view', $comment->entity);
|
||||||
if (!userCan('comment-update', $comment) && !userCan('comment-delete', $comment)) {
|
if (!userCan('comment-update', $comment) && !userCan('comment-delete', $comment)) {
|
||||||
$this->showPermissionError();
|
$this->showPermissionError();
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->commentRepo->unarchive($comment);
|
$this->commentRepo->unarchive($comment);
|
||||||
|
|
||||||
return view('comments.comment', [
|
$tree = new CommentTree($comment->entity);
|
||||||
'comment' => $comment,
|
return view('comments.comment-branch', [
|
||||||
'readOnly' => false,
|
'readOnly' => false,
|
||||||
|
'branch' => $tree->getCommentNodeForId($id),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ class CommentTree
|
|||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The built nested tree structure array.
|
* The built nested tree structure array.
|
||||||
* @var array{comment: Comment, depth: int, children: array}[]
|
* @var CommentTreeNode[]
|
||||||
*/
|
*/
|
||||||
protected array $tree;
|
protected array $tree;
|
||||||
protected array $comments;
|
protected array $comments;
|
||||||
@ -36,9 +36,25 @@ class CommentTree
|
|||||||
return count($this->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
|
public function canUpdateAny(): bool
|
||||||
@ -54,6 +70,7 @@ class CommentTree
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Comment[] $comments
|
* @param Comment[] $comments
|
||||||
|
* @return CommentTreeNode[]
|
||||||
*/
|
*/
|
||||||
protected function createTree(array $comments): array
|
protected function createTree(array $comments): array
|
||||||
{
|
{
|
||||||
@ -77,26 +94,22 @@ class CommentTree
|
|||||||
|
|
||||||
$tree = [];
|
$tree = [];
|
||||||
foreach ($childMap[0] ?? [] as $childId) {
|
foreach ($childMap[0] ?? [] as $childId) {
|
||||||
$tree[] = $this->createTreeForId($childId, 0, $byId, $childMap);
|
$tree[] = $this->createTreeNodeForId($childId, 0, $byId, $childMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $tree;
|
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] ?? [];
|
$childIds = $childMap[$id] ?? [];
|
||||||
$children = [];
|
$children = [];
|
||||||
|
|
||||||
foreach ($childIds as $childId) {
|
foreach ($childIds as $childId) {
|
||||||
$children[] = $this->createTreeForId($childId, $depth + 1, $byId, $childMap);
|
$children[] = $this->createTreeNodeForId($childId, $depth + 1, $byId, $childMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return new CommentTreeNode($byId[$id], $depth, $children);
|
||||||
'comment' => $byId[$id],
|
|
||||||
'depth' => $depth,
|
|
||||||
'children' => $children,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function loadComments(): array
|
protected function loadComments(): array
|
||||||
|
23
app/Activity/Tools/CommentTreeNode.php
Normal file
23
app/Activity/Tools/CommentTreeNode.php
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -392,6 +392,7 @@ return [
|
|||||||
'comment' => 'Comment',
|
'comment' => 'Comment',
|
||||||
'comments' => 'Comments',
|
'comments' => 'Comments',
|
||||||
'comment_add' => 'Add Comment',
|
'comment_add' => 'Add Comment',
|
||||||
|
'comment_archived' => ':count Archived Comment|:count Archived Comments',
|
||||||
'comment_placeholder' => 'Leave a comment here',
|
'comment_placeholder' => 'Leave a comment here',
|
||||||
'comment_count' => '{0} No Comments|{1} 1 Comment|[2,*] :count Comments',
|
'comment_count' => '{0} No Comments|{1} 1 Comment|[2,*] :count Comments',
|
||||||
'comment_save' => 'Save Comment',
|
'comment_save' => 'Save Comment',
|
||||||
|
@ -137,10 +137,12 @@ export class PageComment extends Component {
|
|||||||
protected async archive(): Promise<void> {
|
protected async archive(): Promise<void> {
|
||||||
this.showLoading();
|
this.showLoading();
|
||||||
const isArchived = this.archiveButton.dataset.isArchived === 'true';
|
const isArchived = this.archiveButton.dataset.isArchived === 'true';
|
||||||
|
const action = isArchived ? 'unarchive' : 'archive';
|
||||||
|
|
||||||
await window.$http.put(`/comment/${this.commentId}/${isArchived ? 'unarchive' : 'archive'}`);
|
const response = await window.$http.put(`/comment/${this.commentId}/${action}`);
|
||||||
this.$emit('archive');
|
this.$emit(action, {new_thread_dom: htmlToDom(response.data as string)});
|
||||||
window.$events.success(this.archiveText);
|
window.$events.success(this.archiveText);
|
||||||
|
this.container.closest('.comment-branch')?.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected showLoading(): HTMLElement {
|
protected showLoading(): HTMLElement {
|
||||||
|
@ -9,6 +9,12 @@ export interface CommentReplyEvent extends Event {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ArchiveEvent extends Event {
|
||||||
|
detail: {
|
||||||
|
new_thread_dom: HTMLElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class PageComments extends Component {
|
export class PageComments extends Component {
|
||||||
|
|
||||||
private elem: HTMLElement;
|
private elem: HTMLElement;
|
||||||
@ -17,6 +23,7 @@ export class PageComments extends Component {
|
|||||||
private commentCountBar: HTMLElement;
|
private commentCountBar: HTMLElement;
|
||||||
private commentsTitle: HTMLElement;
|
private commentsTitle: HTMLElement;
|
||||||
private addButtonContainer: HTMLElement;
|
private addButtonContainer: HTMLElement;
|
||||||
|
private archiveContainer: HTMLElement;
|
||||||
private replyToRow: HTMLElement;
|
private replyToRow: HTMLElement;
|
||||||
private formContainer: HTMLElement;
|
private formContainer: HTMLElement;
|
||||||
private form: HTMLFormElement;
|
private form: HTMLFormElement;
|
||||||
@ -43,6 +50,7 @@ export class PageComments extends Component {
|
|||||||
this.commentCountBar = this.$refs.commentCountBar;
|
this.commentCountBar = this.$refs.commentCountBar;
|
||||||
this.commentsTitle = this.$refs.commentsTitle;
|
this.commentsTitle = this.$refs.commentsTitle;
|
||||||
this.addButtonContainer = this.$refs.addButtonContainer;
|
this.addButtonContainer = this.$refs.addButtonContainer;
|
||||||
|
this.archiveContainer = this.$refs.archiveContainer;
|
||||||
this.replyToRow = this.$refs.replyToRow;
|
this.replyToRow = this.$refs.replyToRow;
|
||||||
this.formContainer = this.$refs.formContainer;
|
this.formContainer = this.$refs.formContainer;
|
||||||
this.form = this.$refs.form as HTMLFormElement;
|
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.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) {
|
if (this.form) {
|
||||||
this.removeReplyToButton.addEventListener('click', this.removeReplyTo.bind(this));
|
this.removeReplyToButton.addEventListener('click', this.removeReplyTo.bind(this));
|
||||||
this.hideFormButton.addEventListener('click', this.hideForm.bind(this));
|
this.hideFormButton.addEventListener('click', this.hideForm.bind(this));
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
|
{{--
|
||||||
|
$branch CommentTreeNode
|
||||||
|
--}}
|
||||||
<div class="comment-branch">
|
<div class="comment-branch">
|
||||||
<div>
|
<div>
|
||||||
@include('comments.comment', ['comment' => $branch['comment']])
|
@include('comments.comment', ['comment' => $branch->comment])
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-container-row">
|
<div class="flex-container-row">
|
||||||
<div class="comment-thread-indicator-parent">
|
<div class="comment-thread-indicator-parent">
|
||||||
<div class="comment-thread-indicator"></div>
|
<div class="comment-thread-indicator"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="comment-branch-children flex">
|
<div class="comment-branch-children flex">
|
||||||
@foreach($branch['children'] as $childBranch)
|
@foreach($branch->children as $childBranch)
|
||||||
@include('comments.comment-branch', ['branch' => $childBranch])
|
@include('comments.comment-branch', ['branch' => $childBranch])
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
@if(userCan('comment-create-all'))
|
@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>
|
<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
|
@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"
|
<button refs="page-comment@archive-button"
|
||||||
type="button"
|
type="button"
|
||||||
data-is-archived="{{ $comment->archived ? 'true' : 'false' }}"
|
data-is-archived="{{ $comment->archived ? 'true' : 'false' }}"
|
||||||
|
@ -18,8 +18,8 @@
|
|||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div refs="page-comments@commentContainer" class="comment-container">
|
<div refs="page-comments@comment-container" class="comment-container">
|
||||||
@foreach($commentTree->get() as $branch)
|
@foreach($commentTree->getActive() as $branch)
|
||||||
@include('comments.comment-branch', ['branch' => $branch, 'readOnly' => false])
|
@include('comments.comment-branch', ['branch' => $branch, 'readOnly' => false])
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
@ -27,14 +27,25 @@
|
|||||||
@if(userCan('comment-create-all'))
|
@if(userCan('comment-create-all'))
|
||||||
@include('comments.create')
|
@include('comments.create')
|
||||||
@if (!$commentTree->empty())
|
@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"
|
<button type="button"
|
||||||
refs="page-comments@add-comment-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>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@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())
|
@if(userCan('comment-create-all') || $commentTree->canUpdateAny())
|
||||||
@push('body-end')
|
@push('body-end')
|
||||||
<script src="{{ versioned_asset('libs/tinymce/tinymce.min.js') }}" nonce="{{ $cspNonce }}" defer></script>
|
<script src="{{ versioned_asset('libs/tinymce/tinymce.min.js') }}" nonce="{{ $cspNonce }}" defer></script>
|
||||||
|
Reference in New Issue
Block a user