mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 13:16:38 +00:00
more queue upgrades
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
<script setup lang="ts">
|
||||
import { ScaleIcon, XIcon } from '@modrinth/assets'
|
||||
import { AutoLink, ButtonStyled, injectModrinthClient, NewModal } from '@modrinth/ui'
|
||||
import { ref, useTemplateRef } from 'vue'
|
||||
|
||||
import { useGeneratedState } from '~/composables/generated'
|
||||
import { getProjectTypeForUrlShorthand } from '~/helpers/projects.js'
|
||||
|
||||
const props = defineProps<{
|
||||
completedIds: string[]
|
||||
skippedIds: string[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'review-skipped'): void
|
||||
}>()
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const tags = useGeneratedState()
|
||||
const modalRef = useTemplateRef<InstanceType<typeof NewModal>>('modalRef')
|
||||
|
||||
interface QueueSummaryEntry {
|
||||
id: string
|
||||
title: string
|
||||
link: string
|
||||
}
|
||||
|
||||
const completedEntries = ref<QueueSummaryEntry[]>([])
|
||||
const skippedEntries = ref<QueueSummaryEntry[]>([])
|
||||
|
||||
function toEntries(
|
||||
ids: string[],
|
||||
projectsById: Map<string, { id: string; slug: string; title: string; project_types: string[] }>,
|
||||
): QueueSummaryEntry[] {
|
||||
return ids
|
||||
.map((id) => projectsById.get(id))
|
||||
.filter((project): project is NonNullable<typeof project> => !!project)
|
||||
.map((project) => ({
|
||||
id: project.id,
|
||||
title: project.title,
|
||||
link: `/${getProjectTypeForUrlShorthand(project.project_types[0], [], tags.value)}/${project.slug}`,
|
||||
}))
|
||||
}
|
||||
|
||||
async function show() {
|
||||
const ids = [...new Set([...props.completedIds, ...props.skippedIds])]
|
||||
const projects =
|
||||
ids.length > 0 ? await client.labrinth.projects_v3.getMultiple(ids).catch(() => []) : []
|
||||
const projectsById = new Map(projects.map((project) => [project.id, project]))
|
||||
|
||||
completedEntries.value = toEntries(props.completedIds, projectsById)
|
||||
skippedEntries.value = toEntries(props.skippedIds, projectsById)
|
||||
|
||||
modalRef.value?.show()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
modalRef.value?.hide()
|
||||
}
|
||||
|
||||
function reviewSkipped() {
|
||||
emit('review-skipped')
|
||||
hide()
|
||||
}
|
||||
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NewModal ref="modalRef" header="Queue completed">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div v-if="completedEntries.length > 0" class="flex flex-col gap-2">
|
||||
<span class="font-bold text-contrast">Completed ({{ completedEntries.length }})</span>
|
||||
<ul class="m-0 flex list-none flex-col gap-1 p-0">
|
||||
<li v-for="entry in completedEntries" :key="entry.id">
|
||||
<AutoLink :to="entry.link" class="text-primary hover:underline">{{
|
||||
entry.title
|
||||
}}</AutoLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="skippedEntries.length > 0" class="flex flex-col gap-2">
|
||||
<span class="font-bold text-contrast">Skipped ({{ skippedEntries.length }})</span>
|
||||
<ul class="m-0 flex list-none flex-col gap-1 p-0">
|
||||
<li v-for="entry in skippedEntries" :key="entry.id">
|
||||
<AutoLink :to="entry.link" class="text-primary hover:underline">{{
|
||||
entry.title
|
||||
}}</AutoLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="completedEntries.length === 0 && skippedEntries.length === 0" class="text-secondary">
|
||||
No projects were reviewed during this queue.
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<ButtonStyled v-if="skippedEntries.length > 0" color="orange">
|
||||
<button @click="reviewSkipped">
|
||||
<ScaleIcon />
|
||||
Review skipped ({{ skippedEntries.length }})
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="hide">
|
||||
<XIcon />
|
||||
Close
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
</template>
|
||||
@@ -511,11 +511,10 @@ const alreadyReviewed = ref(false)
|
||||
|
||||
// Prefetch queue for parallel lock checking and instant navigation
|
||||
interface PrefetchedProject {
|
||||
projectId: string
|
||||
project: string
|
||||
slug: string // For canonical URL navigation
|
||||
projectType: string // For canonical URL navigation
|
||||
validatedAt: number
|
||||
skippedIds: string[] // IDs that were locked when this was prefetched
|
||||
}
|
||||
|
||||
const prefetchQueue = ref<PrefetchedProject[]>([])
|
||||
@@ -646,9 +645,9 @@ async function navigateToNextUnlockedProject(): Promise<boolean> {
|
||||
// Quick re-check if close to expiry (last 5 seconds of TTL)
|
||||
if (now - next.validatedAt > PREFETCH_STALE_MS - 5000) {
|
||||
const recheckResults = await batchCheckQueueCandidates(client, moderationQueue, [
|
||||
next.projectId,
|
||||
next.project,
|
||||
])
|
||||
const recheck = recheckResults.get(next.projectId)
|
||||
const recheck = recheckResults.get(next.project)
|
||||
if (!isEligibleQueueCandidate(recheck)) {
|
||||
prefetchQueue.value.shift()
|
||||
return navigateToNextUnlockedProject()
|
||||
@@ -657,17 +656,11 @@ async function navigateToNextUnlockedProject(): Promise<boolean> {
|
||||
|
||||
prefetchQueue.value.shift()
|
||||
|
||||
await Promise.all(
|
||||
next.skippedIds.map((id) => moderationQueue.completeCurrentProject(id, 'skipped')),
|
||||
)
|
||||
|
||||
notifySkippedQueueProjects(next.skippedIds.length)
|
||||
|
||||
maintainPrefetchQueue()
|
||||
|
||||
navigateToQueueProject(
|
||||
{ slug: next.slug, projectType: next.projectType, locked: false, isProcessing: true },
|
||||
next.projectId,
|
||||
next.project,
|
||||
)
|
||||
return true
|
||||
}
|
||||
@@ -702,6 +695,7 @@ function markStageVisited(stageId: string | undefined) {
|
||||
visitedStages: [...visitedStages.value],
|
||||
})
|
||||
}
|
||||
|
||||
const reviewedAnyway = ref(persistedState?.reviewAnyway ?? false)
|
||||
const message = ref<string | null>(persistedState?.message ?? null)
|
||||
const generatedActiveActions = ref<ActiveAction2[] | null>(null)
|
||||
@@ -792,16 +786,6 @@ function reviewAnyway() {
|
||||
maintainPrefetchQueue()
|
||||
}
|
||||
|
||||
function notifySkippedQueueProjects(count: number) {
|
||||
if (count <= 0) return
|
||||
addNotification({
|
||||
title: 'Skipped projects',
|
||||
text: `Skipped ${count} project(s) already moderated or locked by others.`,
|
||||
type: 'info',
|
||||
autoCloseMs: 2000,
|
||||
})
|
||||
}
|
||||
|
||||
function navigateToQueueProject(result: QueueCandidateCheck, projectId: string) {
|
||||
if (result.slug && result.projectType) {
|
||||
const urlType = getProjectTypeForUrlShorthand(result.projectType, [], tags.value)
|
||||
@@ -831,14 +815,14 @@ async function maintainPrefetchQueue() {
|
||||
prefetchQueue.value = prefetchQueue.value.filter((p) => now - p.validatedAt < PREFETCH_STALE_MS)
|
||||
|
||||
if (currentProjectId) {
|
||||
prefetchQueue.value = prefetchQueue.value.filter((p) => p.projectId !== currentProjectId)
|
||||
prefetchQueue.value = prefetchQueue.value.filter((p) => p.project !== currentProjectId)
|
||||
}
|
||||
|
||||
if (prefetchQueue.value.length >= PREFETCH_TARGET_COUNT) {
|
||||
return
|
||||
}
|
||||
|
||||
const prefetchedIds = new Set(prefetchQueue.value.map((p) => p.projectId))
|
||||
const prefetchedIds = new Set(prefetchQueue.value.map((p) => p.project))
|
||||
const queueItems = [...moderationQueue.currentQueue.items]
|
||||
const currentIndex = currentProjectId ? queueItems.indexOf(currentProjectId) : -1
|
||||
const remainingItems =
|
||||
@@ -848,7 +832,6 @@ async function maintainPrefetchQueue() {
|
||||
|
||||
if (candidateIds.length === 0) return
|
||||
|
||||
const skippedIds: string[] = []
|
||||
let checkedCount = 0
|
||||
|
||||
while (
|
||||
@@ -864,16 +847,15 @@ async function maintainPrefetchQueue() {
|
||||
const result = results.get(id)
|
||||
if (isEligibleQueueCandidate(result)) {
|
||||
prefetchQueue.value.push({
|
||||
projectId: id,
|
||||
project: id,
|
||||
slug: result?.slug ?? '',
|
||||
projectType: result?.projectType ?? '',
|
||||
validatedAt: Date.now(),
|
||||
skippedIds: [...skippedIds],
|
||||
})
|
||||
|
||||
if (prefetchQueue.value.length >= PREFETCH_TARGET_COUNT) break
|
||||
} else {
|
||||
skippedIds.push(id)
|
||||
void moderationQueue.excludeProject(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -884,6 +866,21 @@ async function maintainPrefetchQueue() {
|
||||
|
||||
const debouncedPrefetch = useDebounceFn(maintainPrefetchQueue, 300)
|
||||
|
||||
async function goToNextEligibleProject(candidateIds: string[]): Promise<boolean> {
|
||||
if (candidateIds.length === 0) return false
|
||||
|
||||
const next = await findNextEligibleQueueProject(client, moderationQueue, candidateIds)
|
||||
|
||||
if (!next) {
|
||||
await Promise.all(candidateIds.map((id) => moderationQueue.excludeProject(id)))
|
||||
return false
|
||||
}
|
||||
|
||||
await Promise.all(next.excluded.map((id) => moderationQueue.excludeProject(id)))
|
||||
navigateToQueueProject(next.result, next.project)
|
||||
return true
|
||||
}
|
||||
|
||||
async function skipToNextProject() {
|
||||
const currentProjectId = projectV2.value?.id
|
||||
if (!currentProjectId) {
|
||||
@@ -893,7 +890,7 @@ async function skipToNextProject() {
|
||||
debug('[skipToNextProject] Starting. Current project:', currentProjectId)
|
||||
debug('[skipToNextProject] Queue before complete:', [...moderationQueue.currentQueue.items])
|
||||
|
||||
await moderationQueue.completeCurrentProject(currentProjectId, 'skipped')
|
||||
await moderationQueue.deferProject(currentProjectId)
|
||||
|
||||
debug('[skipToNextProject] Queue after complete:', [...moderationQueue.currentQueue.items])
|
||||
debug('[skipToNextProject] hasItems:', moderationQueue.hasItems)
|
||||
@@ -908,20 +905,7 @@ async function skipToNextProject() {
|
||||
const remainingIds = moderationQueue.currentQueue.items.filter((id) => id !== currentProjectId)
|
||||
|
||||
if (remainingIds.length > 0) {
|
||||
const next = await findNextEligibleQueueProject(client, moderationQueue, remainingIds)
|
||||
|
||||
if (next) {
|
||||
await Promise.all(
|
||||
next.skippedIds.map((id) => moderationQueue.completeCurrentProject(id, 'skipped')),
|
||||
)
|
||||
notifySkippedQueueProjects(next.skippedIds.length)
|
||||
navigateToQueueProject(next.result, next.projectId)
|
||||
return
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
remainingIds.map((id) => moderationQueue.completeCurrentProject(id, 'skipped')),
|
||||
)
|
||||
if (await goToNextEligibleProject(remainingIds)) return
|
||||
|
||||
debug('[skipToNextProject] No eligible projects in queue')
|
||||
addNotification({
|
||||
@@ -1531,6 +1515,7 @@ async function generateMessage() {
|
||||
if (loadingMessage.value) return
|
||||
|
||||
loadingMessage.value = true
|
||||
markStageVisited(currentStageObj.value.id)
|
||||
|
||||
router.push(`/${projectUrlType.value}/${projectV2.value.slug}/moderation`)
|
||||
|
||||
@@ -1665,7 +1650,7 @@ async function sendMessage(status: ProjectStatus) {
|
||||
|
||||
await refreshModerationCaches(threadId)
|
||||
|
||||
const willHaveNext = await moderationQueue.completeCurrentProject(projectId, 'completed')
|
||||
const willHaveNext = await moderationQueue.completeProject(projectId)
|
||||
|
||||
await Promise.race([
|
||||
moderationQueue.releaseLock(projectId),
|
||||
@@ -1701,22 +1686,23 @@ async function endChecklist(status?: string) {
|
||||
await clearProjectLocalStorage()
|
||||
|
||||
if (!hasNextProject.value) {
|
||||
const currentProjectId = projectV2.value?.id
|
||||
const isRealQueue =
|
||||
!!currentProjectId &&
|
||||
(moderationQueue.currentQueue.completed.includes(currentProjectId) ||
|
||||
moderationQueue.currentQueue.skipped.includes(currentProjectId))
|
||||
|
||||
await navigateTo({
|
||||
name: 'moderation',
|
||||
state: {
|
||||
confetti: true,
|
||||
queueSummary: isRealQueue,
|
||||
},
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
|
||||
if (moderationQueue.currentQueue.total > 1) {
|
||||
addNotification({
|
||||
title: 'Moderation completed',
|
||||
text: `You have completed the moderation queue.`,
|
||||
type: 'success',
|
||||
})
|
||||
} else {
|
||||
if (!isRealQueue) {
|
||||
addNotification({
|
||||
title: 'Moderation submitted',
|
||||
text: `Project ${status ?? 'completed successfully'}.`,
|
||||
@@ -1730,30 +1716,15 @@ async function endChecklist(status?: string) {
|
||||
(id) => id !== currentProjectId,
|
||||
)
|
||||
|
||||
let foundEligible = false
|
||||
if (remainingIds.length > 0) {
|
||||
const next = await findNextEligibleQueueProject(client, moderationQueue, remainingIds)
|
||||
|
||||
if (next) {
|
||||
await Promise.all(
|
||||
next.skippedIds.map((id) => moderationQueue.completeCurrentProject(id, 'skipped')),
|
||||
)
|
||||
notifySkippedQueueProjects(next.skippedIds.length)
|
||||
navigateToQueueProject(next.result, next.projectId)
|
||||
foundEligible = true
|
||||
} else {
|
||||
await Promise.all(
|
||||
remainingIds.map((id) => moderationQueue.completeCurrentProject(id, 'skipped')),
|
||||
)
|
||||
if (!(await goToNextEligibleProject(remainingIds))) {
|
||||
if (remainingIds.length > 0) {
|
||||
addNotification({
|
||||
title: 'No projects available',
|
||||
text: 'All remaining projects are already moderated or locked by others.',
|
||||
type: 'warning',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundEligible) {
|
||||
await navigateTo({
|
||||
name: 'moderation',
|
||||
})
|
||||
@@ -1778,7 +1749,7 @@ async function skipCurrentProject() {
|
||||
new Promise((r) => setTimeout(r, 2000)),
|
||||
])
|
||||
|
||||
hasNextProject.value = await moderationQueue.completeCurrentProject(projectId, 'skipped')
|
||||
hasNextProject.value = await moderationQueue.deferProject(projectId)
|
||||
|
||||
await endChecklist('skipped')
|
||||
}
|
||||
|
||||
@@ -105,6 +105,12 @@
|
||||
@switch-page="goToPage"
|
||||
/>
|
||||
<ConfettiExplosion v-if="visible" />
|
||||
<QueueSummaryModal
|
||||
ref="queueSummaryModal"
|
||||
:completed-ids="moderationQueue.currentQueue.completed"
|
||||
:skipped-ids="moderationQueue.currentQueue.skipped"
|
||||
@review-skipped="reviewSkippedQueue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
@@ -157,6 +163,7 @@ import { useQuery } from '@tanstack/vue-query'
|
||||
import ConfettiExplosion from 'vue-confetti-explosion'
|
||||
|
||||
import ModerationQueueCard from '~/components/ui/moderation/ModerationQueueCard.vue'
|
||||
import QueueSummaryModal from '~/components/ui/moderation/QueueSummaryModal.vue'
|
||||
import { type ModerationProject, toModerationProjects } from '~/helpers/moderation.ts'
|
||||
import { findNextEligibleQueueProject } from '~/services/moderation/queue-eligibility.ts'
|
||||
import { useModerationQueue } from '~/services/moderation/queue.ts'
|
||||
@@ -170,6 +177,8 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
const client = injectModrinthClient()
|
||||
|
||||
const queueSummaryModal = ref()
|
||||
|
||||
const visible = ref(false)
|
||||
if (import.meta.client && history && history.state && history.state.confetti) {
|
||||
setTimeout(async () => {
|
||||
@@ -182,6 +191,14 @@ if (import.meta.client && history && history.state && history.state.confetti) {
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
if (import.meta.client && history && history.state && history.state.queueSummary) {
|
||||
setTimeout(async () => {
|
||||
history.state.queueSummary = false
|
||||
await nextTick()
|
||||
queueSummaryModal.value?.show()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
moderate: {
|
||||
id: 'moderation.moderate',
|
||||
@@ -496,16 +513,6 @@ function goToPage(page: number) {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
function notifySkippedProjects(skippedCount: number) {
|
||||
if (skippedCount <= 0) return
|
||||
addNotification({
|
||||
title: 'Skipped projects',
|
||||
text: `Skipped ${skippedCount} project(s) already moderated or locked by others.`,
|
||||
type: 'info',
|
||||
autoCloseMs: 2000,
|
||||
})
|
||||
}
|
||||
|
||||
async function findFirstEligibleProject(): Promise<string | null> {
|
||||
const candidateIds = [...moderationQueue.currentQueue.items]
|
||||
if (candidateIds.length === 0) return null
|
||||
@@ -513,18 +520,12 @@ async function findFirstEligibleProject(): Promise<string | null> {
|
||||
const next = await findNextEligibleQueueProject(client, moderationQueue, candidateIds)
|
||||
|
||||
if (!next) {
|
||||
await Promise.all(
|
||||
candidateIds.map((id) => moderationQueue.completeCurrentProject(id, 'skipped')),
|
||||
)
|
||||
notifySkippedProjects(candidateIds.length)
|
||||
await Promise.all(candidateIds.map((id) => moderationQueue.excludeProject(id)))
|
||||
return null
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
next.skippedIds.map((id) => moderationQueue.completeCurrentProject(id, 'skipped')),
|
||||
)
|
||||
notifySkippedProjects(next.skippedIds.length)
|
||||
return next.projectId
|
||||
await Promise.all(next.excluded.map((id) => moderationQueue.excludeProject(id)))
|
||||
return next.project
|
||||
}
|
||||
|
||||
function getProjectRouteParam(projectId: string): string {
|
||||
@@ -577,12 +578,9 @@ async function moderateAllInFilter() {
|
||||
async function startFromProject(projectId: string) {
|
||||
const allFilteredProjectIds = await getFilteredProjectIds()
|
||||
const projectIndex = allFilteredProjectIds.indexOf(projectId)
|
||||
if (projectIndex === -1) {
|
||||
await moderationQueue.setSingleProject(projectId)
|
||||
} else {
|
||||
const projectIds = allFilteredProjectIds.slice(projectIndex)
|
||||
await moderationQueue.setQueue(projectIds)
|
||||
}
|
||||
const projectIds =
|
||||
projectIndex === -1 ? [projectId] : allFilteredProjectIds.slice(projectIndex)
|
||||
await moderationQueue.setQueue(projectIds)
|
||||
|
||||
const targetProjectId = await findFirstEligibleProject()
|
||||
|
||||
@@ -597,4 +595,21 @@ async function startFromProject(projectId: string) {
|
||||
|
||||
await navigateToModerationProject(targetProjectId)
|
||||
}
|
||||
|
||||
async function reviewSkippedQueue() {
|
||||
await moderationQueue.startSkippedReview()
|
||||
|
||||
const targetProjectId = await findFirstEligibleProject()
|
||||
|
||||
if (!targetProjectId) {
|
||||
addNotification({
|
||||
title: 'No projects available',
|
||||
text: 'All previously skipped projects are already moderated or locked by others.',
|
||||
type: 'warning',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await navigateToModerationProject(targetProjectId)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -13,9 +13,9 @@ export interface QueueCandidateCheck {
|
||||
}
|
||||
|
||||
export interface EligibleQueueProject {
|
||||
projectId: string
|
||||
project: string
|
||||
result: QueueCandidateCheck
|
||||
skippedIds: string[]
|
||||
excluded: string[]
|
||||
}
|
||||
|
||||
const BATCH_SIZE = 5
|
||||
@@ -69,7 +69,7 @@ export async function findNextEligibleQueueProject(
|
||||
moderationQueue: ModerationQueueService,
|
||||
candidateIds: string[],
|
||||
): Promise<EligibleQueueProject | null> {
|
||||
const skippedIds: string[] = []
|
||||
const excluded: string[] = []
|
||||
let checkedCount = 0
|
||||
|
||||
while (checkedCount < candidateIds.length) {
|
||||
@@ -81,9 +81,9 @@ export async function findNextEligibleQueueProject(
|
||||
for (const id of batch) {
|
||||
const result = results.get(id)
|
||||
if (isEligibleQueueCandidate(result)) {
|
||||
return { projectId: id, result: result!, skippedIds: [...skippedIds] }
|
||||
return { project: id, result: result!, excluded: [...excluded] }
|
||||
}
|
||||
skippedIds.push(id)
|
||||
excluded.push(id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ export interface PersistedModerationQueueState {
|
||||
savedAt: string
|
||||
currentQueue: {
|
||||
items: string[]
|
||||
skipped?: string[]
|
||||
total: number
|
||||
completed: number
|
||||
skipped: number
|
||||
completed: string[]
|
||||
lastUpdated: string
|
||||
}
|
||||
isQueueMode: boolean
|
||||
@@ -31,9 +31,9 @@ function isPersistedStateCandidate(value: unknown): value is PersistedModeration
|
||||
const queue = candidate.currentQueue
|
||||
if (!queue || typeof queue !== 'object') return false
|
||||
if (!isStringArray(queue.items)) return false
|
||||
if (queue.skipped !== undefined && !isStringArray(queue.skipped)) return false
|
||||
if (typeof queue.total !== 'number' || Number.isNaN(queue.total)) return false
|
||||
if (typeof queue.completed !== 'number' || Number.isNaN(queue.completed)) return false
|
||||
if (typeof queue.skipped !== 'number' || Number.isNaN(queue.skipped)) return false
|
||||
if (!isStringArray(queue.completed)) return false
|
||||
if (typeof queue.lastUpdated !== 'string') return false
|
||||
|
||||
return true
|
||||
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
|
||||
export interface ModerationQueue {
|
||||
items: string[]
|
||||
skipped: string[]
|
||||
total: number
|
||||
completed: number
|
||||
skipped: number
|
||||
completed: string[]
|
||||
lastUpdated: Date
|
||||
}
|
||||
|
||||
@@ -29,11 +29,14 @@ export interface ModerationQueueService {
|
||||
|
||||
queueLength: number
|
||||
hasItems: boolean
|
||||
hasSkipped: boolean
|
||||
progress: number
|
||||
|
||||
setQueue(projectIds: string[]): Promise<void>
|
||||
setSingleProject(projectId: string): Promise<void>
|
||||
completeCurrentProject(projectId: string, status?: 'completed' | 'skipped'): Promise<boolean>
|
||||
completeProject(projectId: string): Promise<boolean>
|
||||
deferProject(projectId: string): Promise<boolean>
|
||||
excludeProject(projectId: string): Promise<boolean>
|
||||
startSkippedReview(): Promise<void>
|
||||
getCurrentProjectId(): string | null
|
||||
resetQueue(): Promise<void>
|
||||
|
||||
@@ -46,31 +49,31 @@ export interface ModerationQueueService {
|
||||
|
||||
const EMPTY_QUEUE: ModerationQueue = {
|
||||
items: [],
|
||||
skipped: [],
|
||||
total: 0,
|
||||
completed: 0,
|
||||
skipped: 0,
|
||||
completed: [],
|
||||
lastUpdated: new Date(),
|
||||
}
|
||||
|
||||
function createEmptyQueue(): ModerationQueue {
|
||||
return { ...EMPTY_QUEUE, lastUpdated: new Date(), items: [] }
|
||||
return { ...EMPTY_QUEUE, lastUpdated: new Date(), items: [], skipped: [], completed: [] }
|
||||
}
|
||||
|
||||
function sanitizeQueue(raw: PersistedModerationQueueState['currentQueue']): ModerationQueue {
|
||||
const lastUpdated = new Date(raw.lastUpdated)
|
||||
const items = raw.items.filter((id): id is string => typeof id === 'string')
|
||||
const completed = Number.isFinite(raw.completed) ? Math.max(Math.trunc(raw.completed), 0) : 0
|
||||
const skipped = Number.isFinite(raw.skipped) ? Math.max(Math.trunc(raw.skipped), 0) : 0
|
||||
const minimumTotal = items.length + completed + skipped
|
||||
const skipped = (raw.skipped ?? []).filter((id): id is string => typeof id === 'string')
|
||||
const completed = (raw.completed ?? []).filter((id): id is string => typeof id === 'string')
|
||||
const minimumTotal = items.length + completed.length
|
||||
const total = Number.isFinite(raw.total)
|
||||
? Math.max(Math.trunc(raw.total), minimumTotal)
|
||||
: minimumTotal
|
||||
|
||||
return {
|
||||
items,
|
||||
skipped,
|
||||
total,
|
||||
completed,
|
||||
skipped,
|
||||
lastUpdated: Number.isNaN(lastUpdated.getTime()) ? new Date() : lastUpdated,
|
||||
}
|
||||
}
|
||||
@@ -84,9 +87,9 @@ function persistedPayload(
|
||||
savedAt: new Date().toISOString(),
|
||||
currentQueue: {
|
||||
items: [...queue.items],
|
||||
skipped: [...queue.skipped],
|
||||
total: queue.total,
|
||||
completed: queue.completed,
|
||||
skipped: queue.skipped,
|
||||
completed: [...queue.completed],
|
||||
lastUpdated: queue.lastUpdated.toISOString(),
|
||||
},
|
||||
isQueueMode,
|
||||
@@ -101,9 +104,10 @@ function createModerationQueueState(client: AbstractModrinthClient = injectModri
|
||||
|
||||
const queueLength = computed(() => currentQueue.value.items.length)
|
||||
const hasItems = computed(() => currentQueue.value.items.length > 0)
|
||||
const hasSkipped = computed(() => currentQueue.value.skipped.length > 0)
|
||||
const progress = computed(() => {
|
||||
if (currentQueue.value.total === 0) return 0
|
||||
return (currentQueue.value.completed + currentQueue.value.skipped) / currentQueue.value.total
|
||||
return (currentQueue.value.total - currentQueue.value.items.length) / currentQueue.value.total
|
||||
})
|
||||
let mutationChain = Promise.resolve()
|
||||
|
||||
@@ -148,42 +152,41 @@ function createModerationQueueState(client: AbstractModrinthClient = injectModri
|
||||
return result
|
||||
}
|
||||
|
||||
function setQueueState(items: string[], mode: boolean) {
|
||||
isQueueMode.value = mode
|
||||
function setQueueState(items: string[]) {
|
||||
isQueueMode.value = true
|
||||
currentQueue.value = {
|
||||
items: [...items],
|
||||
skipped: [],
|
||||
total: items.length,
|
||||
completed: 0,
|
||||
skipped: 0,
|
||||
completed: [],
|
||||
lastUpdated: new Date(),
|
||||
}
|
||||
}
|
||||
|
||||
async function setQueue(projectIds: string[]): Promise<void> {
|
||||
await withMutation(() => {
|
||||
setQueueState(projectIds, true)
|
||||
setQueueState(projectIds)
|
||||
})
|
||||
}
|
||||
|
||||
async function setSingleProject(projectId: string): Promise<void> {
|
||||
await withMutation(() => {
|
||||
setQueueState([projectId], false)
|
||||
})
|
||||
}
|
||||
|
||||
async function completeCurrentProject(
|
||||
projectId: string,
|
||||
status: 'completed' | 'skipped' = 'completed',
|
||||
): Promise<boolean> {
|
||||
async function completeProject(projectId: string): Promise<boolean> {
|
||||
return withMutation(() => {
|
||||
if (!currentQueue.value.items.includes(projectId)) {
|
||||
return currentQueue.value.items.length > 0
|
||||
}
|
||||
|
||||
if (status === 'completed') {
|
||||
currentQueue.value.completed++
|
||||
} else {
|
||||
currentQueue.value.skipped++
|
||||
currentQueue.value.completed = [...currentQueue.value.completed, projectId]
|
||||
currentQueue.value.items = currentQueue.value.items.filter((id) => id !== projectId)
|
||||
currentQueue.value.lastUpdated = new Date()
|
||||
|
||||
return currentQueue.value.items.length > 0
|
||||
})
|
||||
}
|
||||
|
||||
async function excludeProject(projectId: string): Promise<boolean> {
|
||||
return withMutation(() => {
|
||||
if (!currentQueue.value.items.includes(projectId)) {
|
||||
return currentQueue.value.items.length > 0
|
||||
}
|
||||
|
||||
currentQueue.value.items = currentQueue.value.items.filter((id) => id !== projectId)
|
||||
@@ -193,6 +196,26 @@ function createModerationQueueState(client: AbstractModrinthClient = injectModri
|
||||
})
|
||||
}
|
||||
|
||||
async function deferProject(projectId: string): Promise<boolean> {
|
||||
return withMutation(() => {
|
||||
if (!currentQueue.value.items.includes(projectId)) {
|
||||
return currentQueue.value.items.length > 0
|
||||
}
|
||||
|
||||
currentQueue.value.items = currentQueue.value.items.filter((id) => id !== projectId)
|
||||
currentQueue.value.skipped = [...currentQueue.value.skipped, projectId]
|
||||
currentQueue.value.lastUpdated = new Date()
|
||||
|
||||
return currentQueue.value.items.length > 0
|
||||
})
|
||||
}
|
||||
|
||||
async function startSkippedReview(): Promise<void> {
|
||||
await withMutation(() => {
|
||||
setQueueState(currentQueue.value.skipped)
|
||||
})
|
||||
}
|
||||
|
||||
function getCurrentProjectId(): string | null {
|
||||
return currentQueue.value.items[0] || null
|
||||
}
|
||||
@@ -294,11 +317,14 @@ function createModerationQueueState(client: AbstractModrinthClient = injectModri
|
||||
|
||||
queueLength,
|
||||
hasItems,
|
||||
hasSkipped,
|
||||
progress,
|
||||
|
||||
setQueue,
|
||||
setSingleProject,
|
||||
completeCurrentProject,
|
||||
completeProject,
|
||||
deferProject,
|
||||
excludeProject,
|
||||
startSkippedReview,
|
||||
getCurrentProjectId,
|
||||
resetQueue,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user