From 633bc4805e53650d795fbf7e59676b4ec2d62213 Mon Sep 17 00:00:00 2001 From: fannta1990 Date: Mon, 9 Feb 2026 22:31:44 +0800 Subject: [PATCH] Add review deletion functionality and UI enhancements - Implemented delete functionality for reviews in both the ReviewModal and ReviewsTable components. - Added confirmation prompts for review deletion actions. - Updated ReviewController to allow deletion of reviews by admins or the review owner. - Enhanced event handling to refresh the review list upon deletion. - Improved UI to include delete buttons for admins in the ratings page and reviews table. --- client/components/modals/ReviewModal.vue | 24 +++++++++++++++- client/components/tables/ReviewsTable.vue | 30 ++++++++++++++++++-- client/pages/library/_library/ratings.vue | 22 +++++++++++++-- server/controllers/ReviewController.js | 34 +++++++++++++++++++++++ server/routers/ApiRouter.js | 1 + 5 files changed, 106 insertions(+), 5 deletions(-) diff --git a/client/components/modals/ReviewModal.vue b/client/components/modals/ReviewModal.vue index 5dfa953d6..c236b2c97 100644 --- a/client/components/modals/ReviewModal.vue +++ b/client/components/modals/ReviewModal.vue @@ -23,6 +23,10 @@
+ + delete + {{ $strings.ButtonDelete }} + {{ $strings.ButtonCancel }} {{ $strings.ButtonSubmit }}
@@ -36,13 +40,15 @@ * Managed via the 'globals' Vuex store. * * @emit review-updated - Emits the new/updated review object on the root event bus. + * @emit review-deleted - Emits the libraryItemId of the deleted review on the root event bus. */ export default { data() { return { rating: 0, reviewText: '', - processing: false + processing: false, + processingDelete: false } }, watch: { @@ -78,6 +84,22 @@ export default { } }, methods: { + async deleteReview() { + if (!confirm('Are you sure you want to delete this review?')) return + + this.processingDelete = true + try { + await this.$axios.$delete(`/api/items/${this.libraryItem.id}/review`) + this.$root.$emit('review-deleted', { libraryItemId: this.libraryItem.id, reviewId: this.selectedReviewItem.review.id }) + this.$toast.success('Review deleted') + this.show = false + } catch (error) { + console.error('Failed to delete review', error) + this.$toast.error('Failed to delete review') + } finally { + this.processingDelete = false + } + }, async submit() { if (!this.rating) { this.$toast.error('Please select a rating') diff --git a/client/components/tables/ReviewsTable.vue b/client/components/tables/ReviewsTable.vue index 533559402..a0111e73c 100644 --- a/client/components/tables/ReviewsTable.vue +++ b/client/components/tables/ReviewsTable.vue @@ -31,7 +31,12 @@

{{ review.user.username }}

-

{{ $formatDate(review.createdAt, dateFormat) }}

+
+

{{ $formatDate(review.createdAt, dateFormat) }}

+ +

{{ review.reviewText }}

@@ -44,7 +49,7 @@ diff --git a/client/pages/library/_library/ratings.vue b/client/pages/library/_library/ratings.vue index 760e580c9..635a9369a 100644 --- a/client/pages/library/_library/ratings.vue +++ b/client/pages/library/_library/ratings.vue @@ -101,10 +101,13 @@ -
+
+
@@ -121,7 +124,7 @@ - + @@ -160,6 +163,9 @@ export default { currentUser() { return this.$store.state.user.user }, + isAdmin() { + return this.currentUser.type === 'admin' || this.currentUser.type === 'root' + }, sortItems() { return [ { value: 'newest', text: this.$strings.LabelSortNewestFirst }, @@ -259,6 +265,18 @@ export default { isReviewAuthor(review) { return review.userId === this.currentUser.id }, + async deleteReviewAdmin(review) { + if (!confirm(`Are you sure you want to delete ${review.user?.username || 'this'}'s review?`)) return + + try { + await this.$axios.$delete(`/api/reviews/${review.id}`) + this.fetchReviews() + this.$toast.success('Review deleted') + } catch (error) { + console.error('Failed to delete review', error) + this.$toast.error('Failed to delete review') + } + }, editReview(review) { this.$store.commit('globals/setReviewModal', { libraryItem: review.libraryItem, diff --git a/server/controllers/ReviewController.js b/server/controllers/ReviewController.js index 3d61fced7..78a88fe86 100644 --- a/server/controllers/ReviewController.js +++ b/server/controllers/ReviewController.js @@ -108,6 +108,40 @@ class ReviewController { } } + /** + * DELETE: /api/reviews/:id + * Delete a review by ID. + * Admin or the owner of the review can delete it. + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + async deleteById(req, res) { + if (!Database.serverSettings.enableReviews) { + return res.status(403).send('Review feature is disabled') + } + + const { id } = req.params + + try { + const review = await Database.reviewModel.findByPk(id) + if (!review) { + return res.sendStatus(404) + } + + // Check if user is owner or admin + if (review.userId !== req.user.id && req.user.type !== 'admin' && req.user.type !== 'root') { + return res.status(403).send('Not authorized to delete this review') + } + + await review.destroy() + res.sendStatus(200) + } catch (error) { + Logger.error(`[ReviewController] Failed to delete review ${id}`, error) + res.status(500).send('Failed to delete review') + } + } + /** * GET: /api/me/reviews * Get all reviews by the current user. diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index ae1ab85f3..d798e751a 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -132,6 +132,7 @@ class ApiRouter { this.router.get('/items/:id/reviews', ReviewController.middleware.bind(this), ReviewController.findAllForItem.bind(this)) this.router.post('/items/:id/review', ReviewController.middleware.bind(this), ReviewController.createUpdate.bind(this)) this.router.delete('/items/:id/review', ReviewController.middleware.bind(this), ReviewController.delete.bind(this)) + this.router.delete('/reviews/:id', ReviewController.deleteById.bind(this)) // // User Routes