From 8bdf948743016f0461e589759130cbb50e46ab20 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 28 Apr 2025 15:37:09 +0100 Subject: [PATCH] Comments: Added archive endpoints, messages, Js actions and tests --- app/Activity/CommentRepo.php | 27 +++++++++ .../Controllers/CommentController.php | 36 +++++++++++ lang/en/common.php | 2 + lang/en/entities.php | 2 + resources/icons/archive.svg | 1 + resources/js/components/page-comment.ts | 19 +++++- resources/views/comments/comment.blade.php | 7 +++ routes/web.php | 2 + tests/Entity/CommentTest.php | 60 +++++++++++++++++++ 9 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 resources/icons/archive.svg diff --git a/app/Activity/CommentRepo.php b/app/Activity/CommentRepo.php index c488350ca..866368ee6 100644 --- a/app/Activity/CommentRepo.php +++ b/app/Activity/CommentRepo.php @@ -53,6 +53,33 @@ class CommentRepo return $comment; } + + /** + * Archive an existing comment. + */ + public function archive(Comment $comment): Comment + { + $comment->archived = true; + $comment->save(); + + ActivityService::add(ActivityType::COMMENT_UPDATE, $comment); + + return $comment; + } + + /** + * Un-archive an existing comment. + */ + public function unarchive(Comment $comment): Comment + { + $comment->archived = false; + $comment->save(); + + ActivityService::add(ActivityType::COMMENT_UPDATE, $comment); + + return $comment; + } + /** * Delete a comment from the system. */ diff --git a/app/Activity/Controllers/CommentController.php b/app/Activity/Controllers/CommentController.php index 262080067..7a290ebab 100644 --- a/app/Activity/Controllers/CommentController.php +++ b/app/Activity/Controllers/CommentController.php @@ -75,6 +75,42 @@ class CommentController extends Controller ]); } + /** + * Mark a comment as archived. + */ + public function archive(int $id) + { + $comment = $this->commentRepo->getById($id); + if (!userCan('comment-update', $comment) && !userCan('comment-delete', $comment)) { + $this->showPermissionError(); + } + + $this->commentRepo->archive($comment); + + return view('comments.comment', [ + 'comment' => $comment, + 'readOnly' => false, + ]); + } + + /** + * Unmark a comment as archived. + */ + public function unarchive(int $id) + { + $comment = $this->commentRepo->getById($id); + if (!userCan('comment-update', $comment) && !userCan('comment-delete', $comment)) { + $this->showPermissionError(); + } + + $this->commentRepo->unarchive($comment); + + return view('comments.comment', [ + 'comment' => $comment, + 'readOnly' => false, + ]); + } + /** * Delete a comment from the system. */ diff --git a/lang/en/common.php b/lang/en/common.php index b05169bb2..06a9e855c 100644 --- a/lang/en/common.php +++ b/lang/en/common.php @@ -30,6 +30,8 @@ return [ 'create' => 'Create', 'update' => 'Update', 'edit' => 'Edit', + 'archive' => 'Archive', + 'unarchive' => 'Un-Archive', 'sort' => 'Sort', 'move' => 'Move', 'copy' => 'Copy', diff --git a/lang/en/entities.php b/lang/en/entities.php index f9fab8ebf..141e75b5f 100644 --- a/lang/en/entities.php +++ b/lang/en/entities.php @@ -402,6 +402,8 @@ return [ '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?', diff --git a/resources/icons/archive.svg b/resources/icons/archive.svg new file mode 100644 index 000000000..90a4f35b7 --- /dev/null +++ b/resources/icons/archive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/js/components/page-comment.ts b/resources/js/components/page-comment.ts index 11ad769b1..d2cbd21d1 100644 --- a/resources/js/components/page-comment.ts +++ b/resources/js/components/page-comment.ts @@ -8,6 +8,7 @@ export class PageComment extends Component { protected commentLocalId: string; protected deletedText: string; protected updatedText: string; + protected archiveText: string; protected wysiwygEditor: any = null; protected wysiwygLanguage: string; @@ -20,6 +21,7 @@ export class PageComment extends Component { protected editButton: HTMLElement; protected deleteButton: HTMLElement; protected replyButton: HTMLElement; + protected archiveButton: HTMLElement; protected input: HTMLInputElement; setup() { @@ -27,7 +29,8 @@ export class PageComment extends Component { this.commentId = this.$opts.commentId; this.commentLocalId = this.$opts.commentLocalId; this.deletedText = this.$opts.deletedText; - this.updatedText = this.$opts.updatedText; + this.deletedText = this.$opts.deletedText; + this.archiveText = this.$opts.archiveText; // Editor reference and text options this.wysiwygLanguage = this.$opts.wysiwygLanguage; @@ -41,6 +44,7 @@ export class PageComment extends Component { this.editButton = this.$refs.editButton; this.deleteButton = this.$refs.deleteButton; this.replyButton = this.$refs.replyButton; + this.archiveButton = this.$refs.archiveButton; this.input = this.$refs.input as HTMLInputElement; this.setupListeners(); @@ -63,6 +67,10 @@ export class PageComment extends Component { if (this.deleteButton) { this.deleteButton.addEventListener('click', this.delete.bind(this)); } + + if (this.archiveButton) { + this.archiveButton.addEventListener('click', this.archive.bind(this)); + } } protected toggleEditMode(show: boolean) : void { @@ -126,6 +134,15 @@ export class PageComment extends Component { window.$events.success(this.deletedText); } + protected async archive(): Promise { + this.showLoading(); + const isArchived = this.archiveButton.dataset.isArchived === 'true'; + + await window.$http.put(`/comment/${this.commentId}/${isArchived ? 'unarchive' : 'archive'}`); + this.$emit('archive'); + window.$events.success(this.archiveText); + } + protected showLoading(): HTMLElement { const loading = getLoading(); loading.classList.add('px-l'); diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php index 5310b2fe4..58e057140 100644 --- a/resources/views/comments/comment.blade.php +++ b/resources/views/comments/comment.blade.php @@ -6,6 +6,7 @@ option:page-comment:comment-local-id="{{ $comment->local_id }}" option:page-comment:updated-text="{{ trans('entities.comment_updated_success') }}" option:page-comment:deleted-text="{{ trans('entities.comment_deleted_success') }}" + option:page-comment:archive-text="{{ $comment->archived ? trans('entities.comment_unarchive_success') : trans('entities.comment_archive_success') }}" option:page-comment:wysiwyg-language="{{ $locale->htmlLang() }}" option:page-comment:wysiwyg-text-direction="{{ $locale->htmlDirection() }}" id="comment{{$comment->local_id}}" @@ -37,6 +38,12 @@ @if(userCan('comment-create-all')) @endif + @if(userCan('comment-update', $comment) || userCan('comment-delete', $comment)) + + @endif @if(userCan('comment-update', $comment)) @endif diff --git a/routes/web.php b/routes/web.php index 818472583..ea3efe1ac 100644 --- a/routes/web.php +++ b/routes/web.php @@ -179,6 +179,8 @@ Route::middleware('auth')->group(function () { // Comments Route::post('/comment/{pageId}', [ActivityControllers\CommentController::class, 'savePageComment']); + Route::put('/comment/{id}/archive', [ActivityControllers\CommentController::class, 'archive']); + Route::put('/comment/{id}/unarchive', [ActivityControllers\CommentController::class, 'unarchive']); Route::put('/comment/{id}', [ActivityControllers\CommentController::class, 'update']); Route::delete('/comment/{id}', [ActivityControllers\CommentController::class, 'destroy']); diff --git a/tests/Entity/CommentTest.php b/tests/Entity/CommentTest.php index 973b2b81d..baf0d392b 100644 --- a/tests/Entity/CommentTest.php +++ b/tests/Entity/CommentTest.php @@ -106,6 +106,66 @@ class CommentTest extends TestCase $this->assertActivityExists(ActivityType::COMMENT_DELETE); } + public function test_comment_archive_and_unarchive() + { + $this->asAdmin(); + $page = $this->entities->page(); + + $comment = Comment::factory()->make(); + $page->comments()->save($comment); + $comment->refresh(); + + $this->put("/comment/$comment->id/archive"); + + $this->assertDatabaseHas('comments', [ + 'id' => $comment->id, + 'archived' => true, + ]); + + $this->assertActivityExists(ActivityType::COMMENT_UPDATE); + + $this->put("/comment/$comment->id/unarchive"); + + $this->assertDatabaseHas('comments', [ + 'id' => $comment->id, + 'archived' => false, + ]); + + $this->assertActivityExists(ActivityType::COMMENT_UPDATE); + } + + public function test_archive_endpoints_require_delete_or_edit_permissions() + { + $viewer = $this->users->viewer(); + $page = $this->entities->page(); + + $comment = Comment::factory()->make(); + $page->comments()->save($comment); + $comment->refresh(); + + $endpoints = ["/comment/$comment->id/archive", "/comment/$comment->id/unarchive"]; + + foreach ($endpoints as $endpoint) { + $resp = $this->actingAs($viewer)->put($endpoint); + $this->assertPermissionError($resp); + } + + $this->permissions->grantUserRolePermissions($viewer, ['comment-delete-all']); + + foreach ($endpoints as $endpoint) { + $resp = $this->actingAs($viewer)->put($endpoint); + $resp->assertOk(); + } + + $this->permissions->removeUserRolePermissions($viewer, ['comment-delete-all']); + $this->permissions->grantUserRolePermissions($viewer, ['comment-update-all']); + + foreach ($endpoints as $endpoint) { + $resp = $this->actingAs($viewer)->put($endpoint); + $resp->assertOk(); + } + } + public function test_scripts_cannot_be_injected_via_comment_html() { $page = $this->entities->page();