mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-05 00:41:40 +00:00
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.
This commit is contained in:
parent
d2285d952a
commit
633bc4805e
5 changed files with 106 additions and 5 deletions
|
|
@ -23,6 +23,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
|
<ui-btn v-if="selectedReviewItem?.review" color="bg-error" class="mr-auto" :loading="processingDelete" @click="deleteReview">
|
||||||
|
<span class="material-symbols text-base mr-1">delete</span>
|
||||||
|
{{ $strings.ButtonDelete }}
|
||||||
|
</ui-btn>
|
||||||
<ui-btn @click="show = false">{{ $strings.ButtonCancel }}</ui-btn>
|
<ui-btn @click="show = false">{{ $strings.ButtonCancel }}</ui-btn>
|
||||||
<ui-btn color="bg-success" :loading="processing" @click="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
<ui-btn color="bg-success" :loading="processing" @click="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -36,13 +40,15 @@
|
||||||
* Managed via the 'globals' Vuex store.
|
* Managed via the 'globals' Vuex store.
|
||||||
*
|
*
|
||||||
* @emit review-updated - Emits the new/updated review object on the root event bus.
|
* @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 {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
rating: 0,
|
rating: 0,
|
||||||
reviewText: '',
|
reviewText: '',
|
||||||
processing: false
|
processing: false,
|
||||||
|
processingDelete: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
@ -78,6 +84,22 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
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() {
|
async submit() {
|
||||||
if (!this.rating) {
|
if (!this.rating) {
|
||||||
this.$toast.error('Please select a rating')
|
this.$toast.error('Please select a rating')
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,12 @@
|
||||||
<p class="font-semibold text-gray-100 mr-3">{{ review.user.username }}</p>
|
<p class="font-semibold text-gray-100 mr-3">{{ review.user.username }}</p>
|
||||||
<ui-star-rating :value="review.rating" readonly :size="16" />
|
<ui-star-rating :value="review.rating" readonly :size="16" />
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-400">{{ $formatDate(review.createdAt, dateFormat) }}</p>
|
<div class="flex items-center gap-2">
|
||||||
|
<p class="text-xs text-gray-400">{{ $formatDate(review.createdAt, dateFormat) }}</p>
|
||||||
|
<button v-if="isAdmin && review.userId !== user.id" class="p-1 rounded hover:bg-white/10 text-gray-400 hover:text-error transition-colors" title="Delete Review" @click.stop="deleteReviewAdmin(review)">
|
||||||
|
<span class="material-symbols text-base">delete</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="review.reviewText" class="text-gray-200 whitespace-pre-wrap text-sm leading-relaxed">{{ review.reviewText }}</p>
|
<p v-if="review.reviewText" class="text-gray-200 whitespace-pre-wrap text-sm leading-relaxed">{{ review.reviewText }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -44,7 +49,7 @@
|
||||||
<script>
|
<script>
|
||||||
/**
|
/**
|
||||||
* A table component to display reviews for a specific library item.
|
* A table component to display reviews for a specific library item.
|
||||||
* Listens for global 'review-updated' events to refresh the view locally.
|
* Listens for global 'review-updated' and 'review-deleted' events to refresh the view locally.
|
||||||
*/
|
*/
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -65,6 +70,9 @@ export default {
|
||||||
user() {
|
user() {
|
||||||
return this.$store.state.user.user
|
return this.$store.state.user.user
|
||||||
},
|
},
|
||||||
|
isAdmin() {
|
||||||
|
return this.user.type === 'admin' || this.user.type === 'root'
|
||||||
|
},
|
||||||
userReview() {
|
userReview() {
|
||||||
return this.reviews.find((r) => r.userId === this.user.id)
|
return this.reviews.find((r) => r.userId === this.user.id)
|
||||||
},
|
},
|
||||||
|
|
@ -97,6 +105,18 @@ export default {
|
||||||
review: this.userReview
|
review: this.userReview
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
async deleteReviewAdmin(review) {
|
||||||
|
if (!confirm(`Are you sure you want to delete ${review.user.username}'s review?`)) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$axios.$delete(`/api/reviews/${review.id}`)
|
||||||
|
this.reviews = this.reviews.filter(r => r.id !== review.id)
|
||||||
|
this.$toast.success('Review deleted')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete review', error)
|
||||||
|
this.$toast.error('Failed to delete review')
|
||||||
|
}
|
||||||
|
},
|
||||||
onReviewUpdated(review) {
|
onReviewUpdated(review) {
|
||||||
const index = this.reviews.findIndex((r) => r.id === review.id)
|
const index = this.reviews.findIndex((r) => r.id === review.id)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
|
|
@ -113,9 +133,15 @@ export default {
|
||||||
this.onReviewUpdated(review)
|
this.onReviewUpdated(review)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
this.$root.$on('review-deleted', ({ libraryItemId, reviewId }) => {
|
||||||
|
if (libraryItemId === this.libraryItem.id) {
|
||||||
|
this.reviews = this.reviews.filter(r => r.id !== reviewId)
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$root.$off('review-updated')
|
this.$root.$off('review-updated')
|
||||||
|
this.$root.$off('review-deleted')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -101,10 +101,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Edit button -->
|
<!-- Edit button -->
|
||||||
<div class="flex-shrink-0 w-7">
|
<div class="flex-shrink-0 w-7 flex flex-col gap-1">
|
||||||
<button v-if="isReviewAuthor(review)" class="p-0.5 rounded hover:bg-white/10 text-gray-400 hover:text-gray-200" @click.stop="editReview(review)">
|
<button v-if="isReviewAuthor(review)" class="p-0.5 rounded hover:bg-white/10 text-gray-400 hover:text-gray-200" @click.stop="editReview(review)">
|
||||||
<span class="material-symbols text-base">edit</span>
|
<span class="material-symbols text-base">edit</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button v-if="isAdmin && !isReviewAuthor(review)" class="p-0.5 rounded hover:bg-white/10 text-gray-400 hover:text-error transition-colors" title="Delete Review" @click.stop="deleteReviewAdmin(review)">
|
||||||
|
<span class="material-symbols text-base">delete</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -121,7 +124,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modals-review-modal @review-updated="fetchReviews" />
|
<modals-review-modal @review-updated="fetchReviews" @review-deleted="fetchReviews" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -160,6 +163,9 @@ export default {
|
||||||
currentUser() {
|
currentUser() {
|
||||||
return this.$store.state.user.user
|
return this.$store.state.user.user
|
||||||
},
|
},
|
||||||
|
isAdmin() {
|
||||||
|
return this.currentUser.type === 'admin' || this.currentUser.type === 'root'
|
||||||
|
},
|
||||||
sortItems() {
|
sortItems() {
|
||||||
return [
|
return [
|
||||||
{ value: 'newest', text: this.$strings.LabelSortNewestFirst },
|
{ value: 'newest', text: this.$strings.LabelSortNewestFirst },
|
||||||
|
|
@ -259,6 +265,18 @@ export default {
|
||||||
isReviewAuthor(review) {
|
isReviewAuthor(review) {
|
||||||
return review.userId === this.currentUser.id
|
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) {
|
editReview(review) {
|
||||||
this.$store.commit('globals/setReviewModal', {
|
this.$store.commit('globals/setReviewModal', {
|
||||||
libraryItem: review.libraryItem,
|
libraryItem: review.libraryItem,
|
||||||
|
|
|
||||||
|
|
@ -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: /api/me/reviews
|
||||||
* Get all reviews by the current user.
|
* Get all reviews by the current user.
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,7 @@ class ApiRouter {
|
||||||
this.router.get('/items/:id/reviews', ReviewController.middleware.bind(this), ReviewController.findAllForItem.bind(this))
|
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.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('/items/:id/review', ReviewController.middleware.bind(this), ReviewController.delete.bind(this))
|
||||||
|
this.router.delete('/reviews/:id', ReviewController.deleteById.bind(this))
|
||||||
|
|
||||||
//
|
//
|
||||||
// User Routes
|
// User Routes
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue