fix: queue store stability + persistence (#5909)

* fix: queue store stability + persistence

* fix: lint

* feat: set to draft btn

* feat: migrate to indexed db rather than local storage for moderation checklist storage (keep session + perms alone)

* fix: storage cleanup + lint

* fix: invalidation fixes
This commit is contained in:
Calum H.
2026-04-27 17:39:32 +01:00
committed by GitHub
parent a2eed001b2
commit 3f8fd9cb56
19 changed files with 1548 additions and 387 deletions

View File

@@ -754,10 +754,7 @@
},
{
id: 'moderation-checklist',
action: () => {
moderationStore.setSingleProject(project.id)
showModerationChecklist = true
},
action: openModerationChecklistFromMenu,
color: 'orange',
hoverOnly: true,
shown:
@@ -1046,7 +1043,7 @@
>
<ModerationChecklist
:collapsed="collapsedModerationChecklist"
@exit="showModerationChecklist = false"
@exit="setModerationChecklistOpen(false)"
@toggle-collapsed="collapsedModerationChecklist = !collapsedModerationChecklist"
/>
</div>
@@ -1145,7 +1142,11 @@ import { saveFeatureFlags } from '~/composables/featureFlags.ts'
import { STALE_TIME, STALE_TIME_LONG } from '~/composables/queries/project'
import { versionQueryOptions } from '~/composables/queries/version'
import { userCollectProject, userFollowProject } from '~/composables/user.js'
import { useModerationStore } from '~/store/moderation.ts'
import {
loadChecklistOpenState,
saveChecklistOpenState,
} from '~/services/moderation-checklist-storage.ts'
import { useModerationQueue } from '~/services/moderation-queue.ts'
import { getReportPath, reportProject } from '~/utils/report-helpers.ts'
definePageMeta({
@@ -1156,7 +1157,7 @@ const data = useNuxtApp()
const route = useRoute()
const signInRouteObj = computed(() => getSignInRouteObj(route))
const config = useRuntimeConfig()
const moderationStore = useModerationStore()
const moderationQueue = useModerationQueue()
const notifications = injectNotificationManager()
const { addNotification } = notifications
@@ -2514,16 +2515,84 @@ async function copyPermalink() {
const collapsedChecklist = ref(false)
const showModerationChecklist = useLocalStorage(
`show-moderation-checklist-${project.value?.id ?? 'unknown'}`,
false,
)
const showModerationChecklist = ref(false)
const collapsedModerationChecklist = useLocalStorage('collapsed-moderation-checklist', false)
if (import.meta.client && history && history.state && history.state.showChecklist) {
showModerationChecklist.value = true
function consumeShowChecklistHistoryState() {
if (!import.meta.client) return false
if (!window.history?.state?.showChecklist) return false
const state = { ...window.history.state }
delete state.showChecklist
window.history.replaceState(state, '', window.location.href)
return true
}
function setModerationChecklistOpen(open, projectId = project.value?.id) {
showModerationChecklist.value = open
if (projectId) {
void saveChecklistOpenState(projectId, open)
}
}
function isProjectInActiveModerationQueue(projectId = project.value?.id) {
return (
!!projectId &&
moderationQueue.isQueueMode &&
moderationQueue.currentQueue.items.includes(projectId)
)
}
async function openModerationChecklistFromMenu() {
const projectId = project.value?.id
if (!projectId) return
await moderationQueue.ready
if (!isProjectInActiveModerationQueue(projectId)) {
await moderationQueue.setSingleProject(projectId)
}
setModerationChecklistOpen(true)
}
watch(
() => project.value?.id,
async (projectId, _previousProjectId, onCleanup) => {
if (!import.meta.client || !projectId) return
let cancelled = false
onCleanup(() => {
cancelled = true
})
const openedFromNavigation = consumeShowChecklistHistoryState()
await moderationQueue.ready
if (cancelled) return
if (openedFromNavigation) {
setModerationChecklistOpen(true)
return
}
const storedOpen = await loadChecklistOpenState(projectId)
if (cancelled) return
if (storedOpen !== null) {
showModerationChecklist.value = storedOpen
return
}
const shouldRecoverFromQueue =
moderationQueue.isQueueMode && moderationQueue.getCurrentProjectId() === projectId
showModerationChecklist.value = shouldRecoverFromQueue
if (shouldRecoverFromQueue) {
void saveChecklistOpenState(projectId, true)
}
},
{ immediate: true },
)
function closeDownloadModal(event) {
downloadModal.value.hide(event)
userSelectedPlatform.value = null

View File

@@ -93,7 +93,7 @@
:set-status="setStatus"
:current-member="currentMember"
:auth="auth"
@update-thread="(newThread) => (thread = newThread)"
@update-thread="updateThread"
/>
</section>
</div>
@@ -132,6 +132,13 @@ const { data: thread } = useQuery({
enabled: computed(() => !!project.value?.thread_id),
})
function updateThread(newThread) {
const threadId = newThread?.id ?? project.value?.thread_id
if (!threadId) return
queryClient.setQueryData(['thread', threadId], newThread)
}
async function setStatus(status) {
startLoading()

View File

@@ -112,13 +112,13 @@ import ConfettiExplosion from 'vue-confetti-explosion'
import ModerationQueueCard from '~/components/ui/moderation/ModerationQueueCard.vue'
import { enrichProjectBatch, type ModerationProject } from '~/helpers/moderation.ts'
import { useModerationStore } from '~/store/moderation.ts'
import { useModerationQueue } from '~/services/moderation-queue.ts'
useHead({ title: 'Projects queue - Modrinth' })
const { formatMessage } = useVIntl()
const { addNotification } = injectNotificationManager()
const moderationStore = useModerationStore()
const moderationQueue = useModerationQueue()
const route = useRoute()
const router = useRouter()
@@ -331,18 +331,18 @@ function goToPage(page: number) {
async function findFirstUnlockedProject(): Promise<ModerationProject | null> {
let skippedCount = 0
while (moderationStore.hasItems) {
const currentId = moderationStore.getCurrentProjectId()
while (moderationQueue.hasItems) {
const currentId = moderationQueue.getCurrentProjectId()
if (!currentId) return null
const project = filteredProjects.value.find((p) => p.project.id === currentId)
if (!project) {
moderationStore.completeCurrentProject(currentId, 'skipped')
await moderationQueue.completeCurrentProject(currentId, 'skipped')
continue
}
try {
const lockStatus = await moderationStore.checkLock(currentId)
const lockStatus = await moderationQueue.checkLock(currentId)
if (!lockStatus.locked || lockStatus.expired) {
if (skippedCount > 0) {
@@ -356,7 +356,7 @@ async function findFirstUnlockedProject(): Promise<ModerationProject | null> {
}
// Project is locked, skip it
moderationStore.completeCurrentProject(currentId, 'skipped')
await moderationQueue.completeCurrentProject(currentId, 'skipped')
skippedCount++
} catch {
return project
@@ -371,7 +371,7 @@ async function moderateAllInFilter() {
const startIndex = (currentPage.value - 1) * itemsPerPage
const projectsFromCurrentPage = filteredProjects.value.slice(startIndex)
const projectIds = projectsFromCurrentPage.map((queueItem) => queueItem.project.id)
moderationStore.setQueue(projectIds)
await moderationQueue.setQueue(projectIds)
// Find first unlocked project
const targetProject = await findFirstUnlockedProject()
@@ -402,12 +402,12 @@ async function startFromProject(projectId: string) {
const projectIndex = filteredProjects.value.findIndex((p) => p.project.id === projectId)
if (projectIndex === -1) {
// Project not found in filtered list, just moderate it alone
moderationStore.setSingleProject(projectId)
await moderationQueue.setSingleProject(projectId)
} else {
// Start queue from this project onwards
const projectsFromHere = filteredProjects.value.slice(projectIndex)
const projectIds = projectsFromHere.map((queueItem) => queueItem.project.id)
moderationStore.setQueue(projectIds)
await moderationQueue.setQueue(projectIds)
}
// Find first unlocked project

View File

@@ -246,8 +246,14 @@ const reviewItem = computed(() => {
}
})
function handleMarkComplete(_projectId: string) {
queryClient.invalidateQueries({ queryKey: ['tech-reviews'] })
async function handleMarkComplete(projectId: string) {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['tech-reviews'] }),
queryClient.invalidateQueries({ queryKey: ['tech-review-project-report', projectId] }),
queryClient.invalidateQueries({ queryKey: ['project', projectId] }),
queryClient.invalidateQueries({ queryKey: ['project', 'v2', projectId] }),
queryClient.invalidateQueries({ queryKey: ['project', 'v3', projectId] }),
])
}
const maliciousSummaryModalRef = ref<InstanceType<typeof MaliciousSummaryModal>>()