fix: optimistically updating on moderation mutations (#5200)
* fix: moderation optimistically update * fix: gallery settings
This commit is contained in:
@@ -1658,14 +1658,18 @@ const patchProjectMutation = useMutation({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onMutate: async ({ projectId, data }) => {
|
onMutate: async ({ projectId, data }) => {
|
||||||
// Cancel outgoing refetches
|
// Cancel outgoing refetches for both slug-based and ID-based cache keys
|
||||||
await queryClient.cancelQueries({ queryKey: ['project', 'v2', projectId] })
|
// The query may be keyed by slug (routeProjectId) but we also have the actual UUID (projectId)
|
||||||
|
await queryClient.cancelQueries({ queryKey: ['project', 'v2', routeProjectId.value] })
|
||||||
|
if (routeProjectId.value !== projectId) {
|
||||||
|
await queryClient.cancelQueries({ queryKey: ['project', 'v2', projectId] })
|
||||||
|
}
|
||||||
|
|
||||||
// Snapshot previous value
|
// Snapshot previous value from the active query (uses route param as key)
|
||||||
const previousProject = queryClient.getQueryData(['project', 'v2', projectId])
|
const previousProject = queryClient.getQueryData(['project', 'v2', routeProjectId.value])
|
||||||
|
|
||||||
// Optimistic update
|
// Optimistic update on the active query key
|
||||||
queryClient.setQueryData(['project', 'v2', projectId], (old) => {
|
queryClient.setQueryData(['project', 'v2', routeProjectId.value], (old) => {
|
||||||
if (!old) return old
|
if (!old) return old
|
||||||
return { ...old, ...data }
|
return { ...old, ...data }
|
||||||
})
|
})
|
||||||
@@ -1673,10 +1677,10 @@ const patchProjectMutation = useMutation({
|
|||||||
return { previousProject }
|
return { previousProject }
|
||||||
},
|
},
|
||||||
|
|
||||||
onError: (err, { projectId }, context) => {
|
onError: (err, _variables, context) => {
|
||||||
// Rollback on error
|
// Rollback on error using the active query key
|
||||||
if (context?.previousProject) {
|
if (context?.previousProject) {
|
||||||
queryClient.setQueryData(['project', 'v2', projectId], context.previousProject)
|
queryClient.setQueryData(['project', 'v2', routeProjectId.value], context.previousProject)
|
||||||
}
|
}
|
||||||
addNotification({
|
addNotification({
|
||||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||||
@@ -1687,8 +1691,11 @@ const patchProjectMutation = useMutation({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onSettled: async (_data, _error, { projectId }) => {
|
onSettled: async (_data, _error, { projectId }) => {
|
||||||
// Always refetch to ensure consistency
|
// Invalidate both slug-based and ID-based cache keys to ensure consistency
|
||||||
await queryClient.invalidateQueries({ queryKey: ['project', 'v2', projectId] })
|
await queryClient.invalidateQueries({ queryKey: ['project', 'v2', routeProjectId.value] })
|
||||||
|
if (routeProjectId.value !== projectId) {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['project', 'v2', projectId] })
|
||||||
|
}
|
||||||
await queryClient.invalidateQueries({ queryKey: ['project', 'v3', projectId] })
|
await queryClient.invalidateQueries({ queryKey: ['project', 'v3', projectId] })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -1703,11 +1710,17 @@ const patchStatusMutation = useMutation({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onMutate: async ({ projectId, status }) => {
|
onMutate: async ({ projectId, status }) => {
|
||||||
await queryClient.cancelQueries({ queryKey: ['project', 'v2', projectId] })
|
// Cancel outgoing refetches for both slug-based and ID-based cache keys
|
||||||
const previousProject = queryClient.getQueryData(['project', 'v2', projectId])
|
await queryClient.cancelQueries({ queryKey: ['project', 'v2', routeProjectId.value] })
|
||||||
|
if (routeProjectId.value !== projectId) {
|
||||||
|
await queryClient.cancelQueries({ queryKey: ['project', 'v2', projectId] })
|
||||||
|
}
|
||||||
|
|
||||||
// Optimistic update
|
// Snapshot previous value from the active query (uses route param as key)
|
||||||
queryClient.setQueryData(['project', 'v2', projectId], (old) => {
|
const previousProject = queryClient.getQueryData(['project', 'v2', routeProjectId.value])
|
||||||
|
|
||||||
|
// Optimistic update on the active query key
|
||||||
|
queryClient.setQueryData(['project', 'v2', routeProjectId.value], (old) => {
|
||||||
if (!old) return old
|
if (!old) return old
|
||||||
return { ...old, status }
|
return { ...old, status }
|
||||||
})
|
})
|
||||||
@@ -1715,9 +1728,10 @@ const patchStatusMutation = useMutation({
|
|||||||
return { previousProject }
|
return { previousProject }
|
||||||
},
|
},
|
||||||
|
|
||||||
onError: (err, { projectId }, context) => {
|
onError: (err, _variables, context) => {
|
||||||
|
// Rollback on error using the active query key
|
||||||
if (context?.previousProject) {
|
if (context?.previousProject) {
|
||||||
queryClient.setQueryData(['project', 'v2', projectId], context.previousProject)
|
queryClient.setQueryData(['project', 'v2', routeProjectId.value], context.previousProject)
|
||||||
}
|
}
|
||||||
addNotification({
|
addNotification({
|
||||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||||
@@ -1727,7 +1741,11 @@ const patchStatusMutation = useMutation({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onSettled: async (_data, _error, { projectId }) => {
|
onSettled: async (_data, _error, { projectId }) => {
|
||||||
await queryClient.invalidateQueries({ queryKey: ['project', 'v2', projectId] })
|
// Invalidate both slug-based and ID-based cache keys to ensure consistency
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['project', 'v2', routeProjectId.value] })
|
||||||
|
if (routeProjectId.value !== projectId) {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['project', 'v2', projectId] })
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1902,6 +1920,9 @@ watch(downloadModal, (modal) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function setProcessing() {
|
async function setProcessing() {
|
||||||
|
// Guard against multiple submissions while mutation is pending
|
||||||
|
if (patchStatusMutation.isPending.value) return
|
||||||
|
|
||||||
startLoading()
|
startLoading()
|
||||||
patchStatusMutation.mutate(
|
patchStatusMutation.mutate(
|
||||||
{ projectId: project.value.id, status: 'processing' },
|
{ projectId: project.value.id, status: 'processing' },
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
Unfeature image
|
Unfeature image
|
||||||
</button>
|
</button>
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<button class="iconified-button" @click="$refs.modal_edit_item.hide()">
|
<button class="iconified-button" @click="modal_edit_item.hide()">
|
||||||
<XIcon aria-hidden="true" />
|
<XIcon aria-hidden="true" />
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@@ -250,7 +250,7 @@
|
|||||||
editDescription = item.description
|
editDescription = item.description
|
||||||
editFeatured = item.featured
|
editFeatured = item.featured
|
||||||
editOrder = item.ordering
|
editOrder = item.ordering
|
||||||
$refs.modal_edit_item.show()
|
modal_edit_item.show()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@@ -262,7 +262,7 @@
|
|||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
deleteIndex = index
|
deleteIndex = index
|
||||||
$refs.modal_confirm.show()
|
modal_confirm.show()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@@ -306,7 +306,8 @@ import {
|
|||||||
|
|
||||||
import { isPermission } from '~/utils/permissions.ts'
|
import { isPermission } from '~/utils/permissions.ts'
|
||||||
|
|
||||||
const { projectV2: project, currentMember } = injectProjectPageContext()
|
const { addNotification } = injectNotificationManager()
|
||||||
|
const { projectV2: project, currentMember, refreshProject } = injectProjectPageContext()
|
||||||
|
|
||||||
const title = `${project.value.title} - Gallery`
|
const title = `${project.value.title} - Gallery`
|
||||||
const description = `View ${project.value.gallery?.length ?? 0} images of ${project.value.title} on Modrinth.`
|
const description = `View ${project.value.gallery?.length ?? 0} images of ${project.value.title} on Modrinth.`
|
||||||
@@ -317,205 +318,199 @@ useSeoMeta({
|
|||||||
ogTitle: title,
|
ogTitle: title,
|
||||||
ogDescription: description,
|
ogDescription: description,
|
||||||
})
|
})
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
const modal_edit_item = ref(null)
|
||||||
export default defineNuxtComponent({
|
const modal_confirm = ref(null)
|
||||||
setup() {
|
|
||||||
const { addNotification } = injectNotificationManager()
|
|
||||||
const { projectV2: project, refreshProject } = injectProjectPageContext()
|
|
||||||
|
|
||||||
return {
|
const expandedGalleryItem = ref(null)
|
||||||
addNotification,
|
const expandedGalleryIndex = ref(0)
|
||||||
project,
|
const zoomedIn = ref(false)
|
||||||
refreshProject,
|
|
||||||
|
const deleteIndex = ref(-1)
|
||||||
|
|
||||||
|
const editIndex = ref(-1)
|
||||||
|
const editTitle = ref('')
|
||||||
|
const editDescription = ref('')
|
||||||
|
const editFeatured = ref(false)
|
||||||
|
const editOrder = ref(null)
|
||||||
|
const editFile = ref(null)
|
||||||
|
const previewImage = ref(null)
|
||||||
|
const shouldPreventActions = ref(false)
|
||||||
|
|
||||||
|
const acceptFileTypes = 'image/png,image/jpeg,image/gif,image/webp,.png,.jpeg,.gif,.webp'
|
||||||
|
|
||||||
|
const nextImage = () => {
|
||||||
|
expandedGalleryIndex.value++
|
||||||
|
if (expandedGalleryIndex.value >= project.value.gallery.length) {
|
||||||
|
expandedGalleryIndex.value = 0
|
||||||
|
}
|
||||||
|
expandedGalleryItem.value = project.value.gallery[expandedGalleryIndex.value]
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousImage = () => {
|
||||||
|
expandedGalleryIndex.value--
|
||||||
|
if (expandedGalleryIndex.value < 0) {
|
||||||
|
expandedGalleryIndex.value = project.value.gallery.length - 1
|
||||||
|
}
|
||||||
|
expandedGalleryItem.value = project.value.gallery[expandedGalleryIndex.value]
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandImage = (item, index) => {
|
||||||
|
expandedGalleryItem.value = item
|
||||||
|
expandedGalleryIndex.value = index
|
||||||
|
zoomedIn.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetEdit = () => {
|
||||||
|
editIndex.value = -1
|
||||||
|
editTitle.value = ''
|
||||||
|
editDescription.value = ''
|
||||||
|
editFeatured.value = false
|
||||||
|
editOrder.value = null
|
||||||
|
editFile.value = null
|
||||||
|
previewImage.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFiles = (files) => {
|
||||||
|
resetEdit()
|
||||||
|
editFile.value = files[0]
|
||||||
|
|
||||||
|
showPreviewImage()
|
||||||
|
modal_edit_item.value.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const showPreviewImage = () => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
if (editFile.value instanceof Blob) {
|
||||||
|
reader.readAsDataURL(editFile.value)
|
||||||
|
reader.onload = (event) => {
|
||||||
|
previewImage.value = event.target.result
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
data() {
|
}
|
||||||
return {
|
|
||||||
expandedGalleryItem: null,
|
|
||||||
expandedGalleryIndex: 0,
|
|
||||||
zoomedIn: false,
|
|
||||||
|
|
||||||
deleteIndex: -1,
|
const createGalleryItem = async () => {
|
||||||
|
shouldPreventActions.value = true
|
||||||
|
startLoading()
|
||||||
|
|
||||||
editIndex: -1,
|
try {
|
||||||
editTitle: '',
|
let url = `project/${project.value.id}/gallery?ext=${
|
||||||
editDescription: '',
|
editFile.value
|
||||||
editFeatured: false,
|
? editFile.value.type.split('/')[editFile.value.type.split('/').length - 1]
|
||||||
editOrder: null,
|
: null
|
||||||
editFile: null,
|
}&featured=${editFeatured.value}`
|
||||||
previewImage: null,
|
|
||||||
shouldPreventActions: false,
|
if (editTitle.value) {
|
||||||
|
url += `&title=${encodeURIComponent(editTitle.value)}`
|
||||||
}
|
}
|
||||||
},
|
if (editDescription.value) {
|
||||||
computed: {
|
url += `&description=${encodeURIComponent(editDescription.value)}`
|
||||||
acceptFileTypes() {
|
}
|
||||||
return 'image/png,image/jpeg,image/gif,image/webp,.png,.jpeg,.gif,.webp'
|
if (editOrder.value) {
|
||||||
},
|
url += `&ordering=${editOrder.value}`
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this._keyListener = function (e) {
|
|
||||||
if (this.expandedGalleryItem) {
|
|
||||||
e.preventDefault()
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
this.expandedGalleryItem = null
|
|
||||||
} else if (e.key === 'ArrowLeft') {
|
|
||||||
e.stopPropagation()
|
|
||||||
this.previousImage()
|
|
||||||
} else if (e.key === 'ArrowRight') {
|
|
||||||
e.stopPropagation()
|
|
||||||
this.nextImage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('keydown', this._keyListener.bind(this))
|
await useBaseFetch(url, {
|
||||||
},
|
method: 'POST',
|
||||||
methods: {
|
body: editFile.value,
|
||||||
nextImage() {
|
})
|
||||||
this.expandedGalleryIndex++
|
await refreshProject()
|
||||||
if (this.expandedGalleryIndex >= this.project.gallery.length) {
|
|
||||||
this.expandedGalleryIndex = 0
|
|
||||||
}
|
|
||||||
this.expandedGalleryItem = this.project.gallery[this.expandedGalleryIndex]
|
|
||||||
},
|
|
||||||
previousImage() {
|
|
||||||
this.expandedGalleryIndex--
|
|
||||||
if (this.expandedGalleryIndex < 0) {
|
|
||||||
this.expandedGalleryIndex = this.project.gallery.length - 1
|
|
||||||
}
|
|
||||||
this.expandedGalleryItem = this.project.gallery[this.expandedGalleryIndex]
|
|
||||||
},
|
|
||||||
expandImage(item, index) {
|
|
||||||
this.expandedGalleryItem = item
|
|
||||||
this.expandedGalleryIndex = index
|
|
||||||
this.zoomedIn = false
|
|
||||||
},
|
|
||||||
resetEdit() {
|
|
||||||
this.editIndex = -1
|
|
||||||
this.editTitle = ''
|
|
||||||
this.editDescription = ''
|
|
||||||
this.editFeatured = false
|
|
||||||
this.editOrder = null
|
|
||||||
this.editFile = null
|
|
||||||
this.previewImage = null
|
|
||||||
},
|
|
||||||
handleFiles(files) {
|
|
||||||
this.resetEdit()
|
|
||||||
this.editFile = files[0]
|
|
||||||
|
|
||||||
this.showPreviewImage()
|
modal_edit_item.value.hide()
|
||||||
this.$refs.modal_edit_item.show()
|
} catch (err) {
|
||||||
},
|
addNotification({
|
||||||
showPreviewImage() {
|
title: 'An error occurred',
|
||||||
const reader = new FileReader()
|
text: err.data ? err.data.description : err,
|
||||||
if (this.editFile instanceof Blob) {
|
type: 'error',
|
||||||
reader.readAsDataURL(this.editFile)
|
})
|
||||||
reader.onload = (event) => {
|
}
|
||||||
this.previewImage = event.target.result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async createGalleryItem() {
|
|
||||||
this.shouldPreventActions = true
|
|
||||||
startLoading()
|
|
||||||
|
|
||||||
try {
|
stopLoading()
|
||||||
let url = `project/${this.project.id}/gallery?ext=${
|
shouldPreventActions.value = false
|
||||||
this.editFile
|
}
|
||||||
? this.editFile.type.split('/')[this.editFile.type.split('/').length - 1]
|
|
||||||
: null
|
|
||||||
}&featured=${this.editFeatured}`
|
|
||||||
|
|
||||||
if (this.editTitle) {
|
const editGalleryItem = async () => {
|
||||||
url += `&title=${encodeURIComponent(this.editTitle)}`
|
shouldPreventActions.value = true
|
||||||
}
|
startLoading()
|
||||||
if (this.editDescription) {
|
try {
|
||||||
url += `&description=${encodeURIComponent(this.editDescription)}`
|
let url = `project/${project.value.id}/gallery?url=${encodeURIComponent(
|
||||||
}
|
project.value.gallery[editIndex.value].url,
|
||||||
if (this.editOrder) {
|
)}&featured=${editFeatured.value}`
|
||||||
url += `&ordering=${this.editOrder}`
|
|
||||||
}
|
|
||||||
|
|
||||||
await useBaseFetch(url, {
|
if (editTitle.value) {
|
||||||
method: 'POST',
|
url += `&title=${encodeURIComponent(editTitle.value)}`
|
||||||
body: this.editFile,
|
}
|
||||||
})
|
if (editDescription.value) {
|
||||||
await this.refreshProject()
|
url += `&description=${encodeURIComponent(editDescription.value)}`
|
||||||
|
}
|
||||||
|
if (editOrder.value) {
|
||||||
|
url += `&ordering=${editOrder.value}`
|
||||||
|
}
|
||||||
|
|
||||||
this.$refs.modal_edit_item.hide()
|
await useBaseFetch(url, {
|
||||||
} catch (err) {
|
method: 'PATCH',
|
||||||
this.addNotification({
|
})
|
||||||
title: 'An error occurred',
|
|
||||||
text: err.data ? err.data.description : err,
|
|
||||||
type: 'error',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
stopLoading()
|
await refreshProject()
|
||||||
this.shouldPreventActions = false
|
modal_edit_item.value.hide()
|
||||||
},
|
} catch (err) {
|
||||||
async editGalleryItem() {
|
addNotification({
|
||||||
this.shouldPreventActions = true
|
title: 'An error occurred',
|
||||||
startLoading()
|
text: err.data ? err.data.description : err,
|
||||||
try {
|
type: 'error',
|
||||||
let url = `project/${this.project.id}/gallery?url=${encodeURIComponent(
|
})
|
||||||
this.project.gallery[this.editIndex].url,
|
}
|
||||||
)}&featured=${this.editFeatured}`
|
|
||||||
|
|
||||||
if (this.editTitle) {
|
stopLoading()
|
||||||
url += `&title=${encodeURIComponent(this.editTitle)}`
|
shouldPreventActions.value = false
|
||||||
}
|
}
|
||||||
if (this.editDescription) {
|
|
||||||
url += `&description=${encodeURIComponent(this.editDescription)}`
|
|
||||||
}
|
|
||||||
if (this.editOrder) {
|
|
||||||
url += `&ordering=${this.editOrder}`
|
|
||||||
}
|
|
||||||
|
|
||||||
await useBaseFetch(url, {
|
const deleteGalleryImage = async () => {
|
||||||
method: 'PATCH',
|
startLoading()
|
||||||
})
|
|
||||||
|
|
||||||
await this.refreshProject()
|
try {
|
||||||
this.$refs.modal_edit_item.hide()
|
await useBaseFetch(
|
||||||
} catch (err) {
|
`project/${project.value.id}/gallery?url=${encodeURIComponent(
|
||||||
this.addNotification({
|
project.value.gallery[deleteIndex.value].url,
|
||||||
title: 'An error occurred',
|
)}`,
|
||||||
text: err.data ? err.data.description : err,
|
{
|
||||||
type: 'error',
|
method: 'DELETE',
|
||||||
})
|
},
|
||||||
}
|
)
|
||||||
|
|
||||||
stopLoading()
|
await refreshProject()
|
||||||
this.shouldPreventActions = false
|
} catch (err) {
|
||||||
},
|
addNotification({
|
||||||
async deleteGalleryImage() {
|
title: 'An error occurred',
|
||||||
startLoading()
|
text: err.data ? err.data.description : err,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
stopLoading()
|
||||||
await useBaseFetch(
|
}
|
||||||
`project/${this.project.id}/gallery?url=${encodeURIComponent(
|
|
||||||
this.project.gallery[this.deleteIndex].url,
|
|
||||||
)}`,
|
|
||||||
{
|
|
||||||
method: 'DELETE',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
await this.refreshProject()
|
const handleKeydown = (e) => {
|
||||||
} catch (err) {
|
if (expandedGalleryItem.value) {
|
||||||
this.addNotification({
|
e.preventDefault()
|
||||||
title: 'An error occurred',
|
if (e.key === 'Escape') {
|
||||||
text: err.data ? err.data.description : err,
|
expandedGalleryItem.value = null
|
||||||
type: 'error',
|
} else if (e.key === 'ArrowLeft') {
|
||||||
})
|
e.stopPropagation()
|
||||||
}
|
previousImage()
|
||||||
|
} else if (e.key === 'ArrowRight') {
|
||||||
|
e.stopPropagation()
|
||||||
|
nextImage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stopLoading()
|
onMounted(() => {
|
||||||
},
|
document.addEventListener('keydown', handleKeydown)
|
||||||
},
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('keydown', handleKeydown)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user